Skip to content

Commit

Permalink
Redemptions prototype: Assemble BTC redemption transaction (#3607)
Browse files Browse the repository at this point in the history
Refs #3609 

Here we introduce the `assembleRedemptionTransaction` function that
allows constructing an unsigned Bitcoin redemption transaction based on
provided input arguments.
  • Loading branch information
pdyraga authored Jun 12, 2023
2 parents 1dc0731 + 1bd586a commit aad25d6
Show file tree
Hide file tree
Showing 11 changed files with 754 additions and 7 deletions.
118 changes: 117 additions & 1 deletion pkg/internal/tbtctest/marshaling.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import (
"crypto/elliptic"
"encoding/hex"
"encoding/json"
"math/big"
"time"

"github.com/keep-network/keep-core/pkg/bitcoin"
"github.com/keep-network/keep-core/pkg/chain"
"github.com/keep-network/keep-core/pkg/tecdsa"
"math/big"
)

// UnmarshalJSON implements a custom JSON unmarshaling logic to produce a
Expand Down Expand Up @@ -148,6 +150,120 @@ func (dsts *DepositSweepTestScenario) UnmarshalJSON(data []byte) error {
return nil
}

// UnmarshalJSON implements a custom JSON unmarshaling logic to produce a
// proper RedemptionTestScenario.
func (rts *RedemptionTestScenario) UnmarshalJSON(data []byte) error {
type redemptionTestScenario struct {
Title string
WalletPublicKey string
WalletPrivateKey string
WalletMainUtxo *utxo
RedemptionRequests []struct {
Redeemer string
RedeemerOutputScript string
RequestedAmount uint64
TreasuryFee uint64
TxMaxFee uint64
RequestedAt int64
}
InputTransaction string
FeeShares []int64
Signature signature
ExpectedSigHash string
ExpectedRedemptionTransaction string
ExpectedRedemptionTransactionHash string
ExpectedRedemptionTransactionWitnessHash string
}

var unmarshaled redemptionTestScenario

err := json.Unmarshal(data, &unmarshaled)
if err != nil {
return err
}

// Unmarshal title.
rts.Title = unmarshaled.Title

// Unmarshal wallet public key.
x, y := elliptic.Unmarshal(
tecdsa.Curve,
hexToSlice(unmarshaled.WalletPublicKey),
)
rts.WalletPublicKey = &ecdsa.PublicKey{
Curve: tecdsa.Curve,
X: x,
Y: y,
}

// Unmarshal wallet private key.
rts.WalletPrivateKey = new(big.Int).SetBytes(
hexToSlice(unmarshaled.WalletPrivateKey),
)

// Unmarshal wallet main UTXO.
rts.WalletMainUtxo = unmarshaled.WalletMainUtxo.convert()

// Unmarshal redemption requests.
for _, request := range unmarshaled.RedemptionRequests {
r := new(RedemptionRequest)

r.Redeemer = chain.Address(request.Redeemer)
r.RedeemerOutputScript = hexToSlice(request.RedeemerOutputScript)
r.RequestedAmount = request.RequestedAmount
r.TreasuryFee = request.TreasuryFee
r.TxMaxFee = request.TxMaxFee
r.RequestedAt = time.Unix(request.RequestedAt, 0)

rts.RedemptionRequests = append(rts.RedemptionRequests, r)
}

// Unmarshal input transaction.
rts.InputTransaction = new(bitcoin.Transaction)
err = rts.InputTransaction.Deserialize(hexToSlice(unmarshaled.InputTransaction))
if err != nil {
return err
}

// Unmarshal fee shares.
rts.FeeShares = append(rts.FeeShares, unmarshaled.FeeShares...)

// Unmarshal signature.
rts.Signature = unmarshaled.Signature.convert(rts.WalletPublicKey)

// Unmarshal expected signature hash.
rts.ExpectedSigHash = new(big.Int).SetBytes(hexToSlice(unmarshaled.ExpectedSigHash))

// Unmarshal expected redemption transaction.
rts.ExpectedRedemptionTransaction = new(bitcoin.Transaction)
err = rts.ExpectedRedemptionTransaction.Deserialize(
hexToSlice(unmarshaled.ExpectedRedemptionTransaction),
)
if err != nil {
return err
}

// Unmarshal expected redemption transaction hash.
rts.ExpectedRedemptionTransactionHash, err = bitcoin.NewHashFromString(
unmarshaled.ExpectedRedemptionTransactionHash,
bitcoin.ReversedByteOrder,
)
if err != nil {
return err
}

// Unmarshal expected redemption transaction witness hash.
rts.ExpectedRedemptionTransactionWitnessHash, err = bitcoin.NewHashFromString(
unmarshaled.ExpectedRedemptionTransactionWitnessHash,
bitcoin.ReversedByteOrder,
)
if err != nil {
return err
}

return nil
}

// utxo is a helper type used for unmarshal UTXO encoded as JSON.
type utxo struct {
Outpoint struct {
Expand Down
77 changes: 73 additions & 4 deletions pkg/internal/tbtctest/tbtctest.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,37 @@
// one input (a P2WSH deposit) was swept into a P2WPKH main UTXO.
// For reference see:
// https://live.blockcypher.com/btc-testnet/tx/9efc9d555233e12e06378a35a7b988d54f7043b5c3156adc79c7af0a0fd6f1a0
//
// - redemption_scenario_0.json: Bitcoin redemption transaction that uses a
// single P2WPKH input to pay a single P2PKH redeemer script and a P2WPKH change.
// For reference see:
// https://live.blockcypher.com/btc-testnet/tx/c437f1117db977682334b53a71fbe63a42aab42f6e0976c35b69977f86308c20
//
// - redemption_scenario_1.json: Bitcoin redemption transaction that uses a
// single P2WPKH input to pay a single P2WPKH redeemer script and a P2WPKH change.
// For reference see:
// https://live.blockcypher.com/btc-testnet/tx/925e61dc31396e7f2cbcc8bc9b4009b4f24ba679257762df078b7e9b875ea110
//
// - redemption_scenario_2.json: Bitcoin redemption transaction that uses a
// single P2WPKH input to pay a single P2SH redeemer script and a P2WPKH change.
// For reference see:
// https://live.blockcypher.com/btc-testnet/tx/ef25c9c8f4df673def035c0c1880278c90030b3c94a56668109001a591c2c521
//
// - redemption_scenario_3.json: Bitcoin redemption transaction that uses a
// single P2WPKH input to pay a single P2WSH redeemer script and a P2WPKH change.
// For reference see:
// https://live.blockcypher.com/btc-testnet/tx/3d28bb5bf73379da51bc683f4d0ed31d7b024466c619d80ebd9378077d900be3
//
// - redemption_scenario_4.json: Bitcoin redemption transaction that uses a
// single P2WPKH input to pay redeemer scripts (P2PKH, P2WPKH, P2SH and P2WSH)
// and a P2WPKH change.
// For reference see:
// https://live.blockcypher.com/btc-testnet/tx/f70ff89fd2b6226183e4b8143cc5f0f457f05dd1dca0c6151ab66f4523d972b7
//
// - redemption_scenario_5.json: Bitcoin redemption transaction that uses a
// single P2WPKH input to pay redeemer scripts (P2PKH, P2WPKH) without a change.
// For reference see:
// https://live.blockcypher.com/btc-testnet/tx/afcdf8f91273b73abc40018873978c22bbb7c3d8d669ef2faffa0c4b0898c8eb
package tbtctest

import (
Expand All @@ -34,11 +65,13 @@ import (
"path/filepath"
"runtime"
"strings"
"time"
)

const (
testDataDirFormat = "%s/testdata"
depositSweepTestDataFilePrefix = "deposit_sweep_scenario"
redemptionTestDataFilePrefix = "redemption_scenario"
)

// Deposit holds the deposit data in the given test scenario.
Expand Down Expand Up @@ -71,15 +104,51 @@ type DepositSweepTestScenario struct {

// LoadDepositSweepTestScenarios loads all scenarios related with deposit sweep.
func LoadDepositSweepTestScenarios() ([]*DepositSweepTestScenario, error) {
filePaths, err := detectTestDataFiles(depositSweepTestDataFilePrefix)
return loadTestScenarios[*DepositSweepTestScenario](depositSweepTestDataFilePrefix)
}

// RedemptionRequest holds the redemption request data in the given test scenario.
type RedemptionRequest struct {
Redeemer chain.Address
RedeemerOutputScript []byte
RequestedAmount uint64
TreasuryFee uint64
TxMaxFee uint64
RequestedAt time.Time
}

// RedemptionTestScenario represents a redemption test scenario.
type RedemptionTestScenario struct {
Title string
WalletPublicKey *ecdsa.PublicKey
WalletPrivateKey *big.Int
WalletMainUtxo *bitcoin.UnspentTransactionOutput
RedemptionRequests []*RedemptionRequest
InputTransaction *bitcoin.Transaction
FeeShares []int64
Signature *bitcoin.SignatureContainer

ExpectedSigHash *big.Int
ExpectedRedemptionTransaction *bitcoin.Transaction
ExpectedRedemptionTransactionHash bitcoin.Hash
ExpectedRedemptionTransactionWitnessHash bitcoin.Hash
}

// LoadRedemptionTestScenarios loads all scenarios related with redemption.
func LoadRedemptionTestScenarios() ([]*RedemptionTestScenario, error) {
return loadTestScenarios[*RedemptionTestScenario](redemptionTestDataFilePrefix)
}

func loadTestScenarios[T json.Unmarshaler](testDataFilePrefix string) ([]T, error) {
filePaths, err := detectTestDataFiles(testDataFilePrefix)
if err != nil {
return nil, fmt.Errorf(
"cannot detect test data files: [%v]",
err,
)
}

scenarios := make([]*DepositSweepTestScenario, 0)
scenarios := make([]T, 0)

for _, filePath := range filePaths {
// #nosec G304 (file path provided as taint input)
Expand All @@ -94,7 +163,7 @@ func LoadDepositSweepTestScenarios() ([]*DepositSweepTestScenario, error) {
)
}

var scenario DepositSweepTestScenario
var scenario T
if err = json.Unmarshal(fileBytes, &scenario); err != nil {
return nil, fmt.Errorf(
"cannot unmarshal scenario for file [%v]: [%v]",
Expand All @@ -103,7 +172,7 @@ func LoadDepositSweepTestScenarios() ([]*DepositSweepTestScenario, error) {
)
}

scenarios = append(scenarios, &scenario)
scenarios = append(scenarios, scenario)
}

return scenarios, nil
Expand Down
32 changes: 32 additions & 0 deletions pkg/internal/tbtctest/testdata/redemption_scenario_0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"Title": "single non-witness public key hash redemption with witness change",
"WalletPublicKey": "04989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d9d218b65e7d91c752f7b22eaceb771a9af3a6f3d3f010a5d471a1aeef7d7713af",
"WalletPrivateKey": "7c246a5d2fcf476fd6f805cb8174b1cf441b13ea414e5560ca2bdc963aeb7d0c",
"WalletMainUtxo": {
"Outpoint": {
"TransactionHash": "523e4bfb71804e5ed3b76c8933d733339563e560311c1bf835934ee7aae5db20",
"OutputIndex": 1
},
"Value": 1481680
},
"RedemptionRequests": [
{
"Redeemer": "82883a4c7a8dd73ef165deb402d432613615ced4",
"RedeemerOutputScript": "76a9144130879211c54df460e484ddf9aac009cb38ee7488ac",
"RequestedAmount": 10000,
"TreasuryFee": 1000,
"TxMaxFee": 1600,
"RequestedAt": 1650623240
}
],
"InputTransaction": "0100000000010160d264b34e51e6567254bcaf4cc67e1e069483f4249dc50784eae682645fd11d0100000000ffffffff02d84000000000000022002086a303cdd2e2eab1d1679f1a813835dc5a1b65321077cdccaf08f98cbf04ca96d09b1600000000001600148db50eb52063ea9d98b3eac91489a90f738986f602483045022100ed5fa06ea5e9d4a9f0cf0df86a2cd473f693e5bda3d808ba82b04ee26d72b73f0220648f4d7bb25be781922349d382cf0f32ffcbbf89c483776472c2d15644a48d67012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d900000000",
"FeeShares": [1600],
"Signature": {
"R": "e1bcecbf3c6fc9a4ce2fc8029264d98a1bef4ff3d590816532097fbb93b7fdfb",
"S": "6bca6c7af1db4c70d4d2c819eeb4c8430a291f5fe874c73c8f44acdd06c25d33"
},
"ExpectedSigHash": "d1a6e27780b22b6d266f0a73f4cf6a7c67f00dce65b59c8508afad1aadda2489",
"ExpectedRedemptionTransaction": "0100000000010120dbe5aae74e9335f81b1c3160e563953333d733896cb7d35e4e8071fb4b3e520100000000ffffffff02e81c0000000000001976a9144130879211c54df460e484ddf9aac009cb38ee7488aca8781600000000001600148db50eb52063ea9d98b3eac91489a90f738986f602483045022100e1bcecbf3c6fc9a4ce2fc8029264d98a1bef4ff3d590816532097fbb93b7fdfb02206bca6c7af1db4c70d4d2c819eeb4c8430a291f5fe874c73c8f44acdd06c25d33012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d900000000",
"ExpectedRedemptionTransactionHash": "c437f1117db977682334b53a71fbe63a42aab42f6e0976c35b69977f86308c20",
"ExpectedRedemptionTransactionWitnessHash": "27819cb4d51cf2c2bcae7bd9765f259e4bb8d7a62c40ca8aef6df8c16ff2dc14"
}
32 changes: 32 additions & 0 deletions pkg/internal/tbtctest/testdata/redemption_scenario_1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"Title": "single witness public key hash redemption with witness change",
"WalletPublicKey": "04989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d9d218b65e7d91c752f7b22eaceb771a9af3a6f3d3f010a5d471a1aeef7d7713af",
"WalletPrivateKey": "7c246a5d2fcf476fd6f805cb8174b1cf441b13ea414e5560ca2bdc963aeb7d0c",
"WalletMainUtxo": {
"Outpoint": {
"TransactionHash": "c437f1117db977682334b53a71fbe63a42aab42f6e0976c35b69977f86308c20",
"OutputIndex": 1
},
"Value": 1472680
},
"RedemptionRequests": [
{
"Redeemer": "82883a4c7a8dd73ef165deb402d432613615ced4",
"RedeemerOutputScript": "00144130879211c54df460e484ddf9aac009cb38ee74",
"RequestedAmount": 15000,
"TreasuryFee": 1100,
"TxMaxFee": 1700,
"RequestedAt": 1650623240
}
],
"InputTransaction": "0100000000010120dbe5aae74e9335f81b1c3160e563953333d733896cb7d35e4e8071fb4b3e520100000000ffffffff02e81c0000000000001976a9144130879211c54df460e484ddf9aac009cb38ee7488aca8781600000000001600148db50eb52063ea9d98b3eac91489a90f738986f602483045022100e1bcecbf3c6fc9a4ce2fc8029264d98a1bef4ff3d590816532097fbb93b7fdfb02206bca6c7af1db4c70d4d2c819eeb4c8430a291f5fe874c73c8f44acdd06c25d33012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d900000000",
"FeeShares": [1700],
"Signature": {
"R": "ee8273dd93e85e8a0e0055498803335a370e3d25c51ad2890f0b61294e884e87",
"S": "4ebf3e04161b8172fbdf6070f7b1f22097f3d87c0bd32bc53a786971776e7b45"
},
"ExpectedSigHash": "a95ed4632fbe50a632d9301b7be5165caf54ea92e85925ccbeda9fbe1b64a0ad",
"ExpectedRedemptionTransaction": "01000000000101208c30867f97695bc376096e2fb4aa423ae6fb713ab534236877b97d11f137c40100000000ffffffff02a82f0000000000001600144130879211c54df460e484ddf9aac009cb38ee745c421600000000001600148db50eb52063ea9d98b3eac91489a90f738986f602483045022100ee8273dd93e85e8a0e0055498803335a370e3d25c51ad2890f0b61294e884e8702204ebf3e04161b8172fbdf6070f7b1f22097f3d87c0bd32bc53a786971776e7b45012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d900000000",
"ExpectedRedemptionTransactionHash": "925e61dc31396e7f2cbcc8bc9b4009b4f24ba679257762df078b7e9b875ea110",
"ExpectedRedemptionTransactionWitnessHash": "c14c41854ab88c617cd23c9cda711bc3027846fb9fe39c0a62663f2ead7048c7"
}
32 changes: 32 additions & 0 deletions pkg/internal/tbtctest/testdata/redemption_scenario_2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"Title": "single non-witness script hash redemption with witness change",
"WalletPublicKey": "04989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d9d218b65e7d91c752f7b22eaceb771a9af3a6f3d3f010a5d471a1aeef7d7713af",
"WalletPrivateKey": "7c246a5d2fcf476fd6f805cb8174b1cf441b13ea414e5560ca2bdc963aeb7d0c",
"WalletMainUtxo": {
"Outpoint": {
"TransactionHash": "925e61dc31396e7f2cbcc8bc9b4009b4f24ba679257762df078b7e9b875ea110",
"OutputIndex": 1
},
"Value": 1458780
},
"RedemptionRequests": [
{
"Redeemer": "82883a4c7a8dd73ef165deb402d432613615ced4",
"RedeemerOutputScript": "a9143ec459d0f3c29286ae5df5fcc421e2786024277e87",
"RequestedAmount": 13000,
"TreasuryFee": 800,
"TxMaxFee": 1700,
"RequestedAt": 1650623240
}
],
"InputTransaction": "01000000000101208c30867f97695bc376096e2fb4aa423ae6fb713ab534236877b97d11f137c40100000000ffffffff02a82f0000000000001600144130879211c54df460e484ddf9aac009cb38ee745c421600000000001600148db50eb52063ea9d98b3eac91489a90f738986f602483045022100ee8273dd93e85e8a0e0055498803335a370e3d25c51ad2890f0b61294e884e8702204ebf3e04161b8172fbdf6070f7b1f22097f3d87c0bd32bc53a786971776e7b45012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d900000000",
"FeeShares": [1700],
"Signature": {
"R": "9740ad12d2e74c00ccb4741d533d2ecd6902289144c4626508afb61eed790c97",
"S": "06e67179e8e2a63dc4f1ab758867d8bbfe0a2b67682be6dadfa8e07d3b7ba04d"
},
"ExpectedSigHash": "f3900855bdfd64e6c9a1ed8dfbb899a7a46c034b88c21fb2db74f2194eee8b93",
"ExpectedRedemptionTransaction": "0100000000010110a15e879b7e8b07df62772579a64bf2b409409bbcc8bc2c7f6e3931dc615e920100000000ffffffff02042900000000000017a9143ec459d0f3c29286ae5df5fcc421e2786024277e87b4121600000000001600148db50eb52063ea9d98b3eac91489a90f738986f6024830450221009740ad12d2e74c00ccb4741d533d2ecd6902289144c4626508afb61eed790c97022006e67179e8e2a63dc4f1ab758867d8bbfe0a2b67682be6dadfa8e07d3b7ba04d012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d900000000",
"ExpectedRedemptionTransactionHash": "ef25c9c8f4df673def035c0c1880278c90030b3c94a56668109001a591c2c521",
"ExpectedRedemptionTransactionWitnessHash": "ffab4704d49dce95698491ecc9957fceb87c9c811d43891661f57e6415826313"
}
32 changes: 32 additions & 0 deletions pkg/internal/tbtctest/testdata/redemption_scenario_3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"Title": "single witness script hash redemption with witness change",
"WalletPublicKey": "04989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d9d218b65e7d91c752f7b22eaceb771a9af3a6f3d3f010a5d471a1aeef7d7713af",
"WalletPrivateKey": "7c246a5d2fcf476fd6f805cb8174b1cf441b13ea414e5560ca2bdc963aeb7d0c",
"WalletMainUtxo": {
"Outpoint": {
"TransactionHash": "ef25c9c8f4df673def035c0c1880278c90030b3c94a56668109001a591c2c521",
"OutputIndex": 1
},
"Value": 1446580
},
"RedemptionRequests": [
{
"Redeemer": "82883a4c7a8dd73ef165deb402d432613615ced4",
"RedeemerOutputScript": "002086a303cdd2e2eab1d1679f1a813835dc5a1b65321077cdccaf08f98cbf04ca96",
"RequestedAmount": 18000,
"TreasuryFee": 1000,
"TxMaxFee": 1400,
"RequestedAt": 1650623240
}
],
"InputTransaction": "0100000000010110a15e879b7e8b07df62772579a64bf2b409409bbcc8bc2c7f6e3931dc615e920100000000ffffffff02042900000000000017a9143ec459d0f3c29286ae5df5fcc421e2786024277e87b4121600000000001600148db50eb52063ea9d98b3eac91489a90f738986f6024830450221009740ad12d2e74c00ccb4741d533d2ecd6902289144c4626508afb61eed790c97022006e67179e8e2a63dc4f1ab758867d8bbfe0a2b67682be6dadfa8e07d3b7ba04d012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d900000000",
"FeeShares": [1400],
"Signature": {
"R": "bef6177f72f434248271cf5d18c1ce6add52dcf533ddda215240a858cb63cd07",
"S": "016a68c457f84f01108e1b001e8f81a9b073a3e08511265614318fa0d395ef4d"
},
"ExpectedSigHash": "5402b1b44ab04b4223377904c44e1d10c4686f9dac5b89d96b907a53a71f098d",
"ExpectedRedemptionTransaction": "0100000000010121c5c291a50190106866a5943c0b03908c2780180c5c03ef3d67dff4c8c925ef0100000000ffffffff02f03c00000000000022002086a303cdd2e2eab1d1679f1a813835dc5a1b65321077cdccaf08f98cbf04ca964cd01500000000001600148db50eb52063ea9d98b3eac91489a90f738986f602483045022100bef6177f72f434248271cf5d18c1ce6add52dcf533ddda215240a858cb63cd070220016a68c457f84f01108e1b001e8f81a9b073a3e08511265614318fa0d395ef4d012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d900000000",
"ExpectedRedemptionTransactionHash": "3d28bb5bf73379da51bc683f4d0ed31d7b024466c619d80ebd9378077d900be3",
"ExpectedRedemptionTransactionWitnessHash": "777729dcef49a094322c9e7735e22b193656eac4c16937ed9bb6e856570be7bb"
}
Loading

0 comments on commit aad25d6

Please sign in to comment.