diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 8e096384c9..e8db1004ff 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -1111,6 +1111,11 @@ func (tc *TbtcChain) GetDepositRequest( vault = &v } + var extraData *[32]byte + if depositRequest.ExtraData != [32]byte{} { + extraData = &depositRequest.ExtraData + } + return &tbtc.DepositChainRequest{ Depositor: chain.Address(depositRequest.Depositor.Hex()), Amount: depositRequest.Amount, @@ -1118,6 +1123,7 @@ func (tc *TbtcChain) GetDepositRequest( Vault: vault, TreasuryFee: depositRequest.TreasuryFee, SweptAt: time.Unix(int64(depositRequest.SweptAt), 0), + ExtraData: extraData, }, true, nil } diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index cbad8737d1..85d7675236 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -261,7 +261,7 @@ type DepositRevealedEvent struct { BlockNumber uint64 } -func (dre *DepositRevealedEvent) unpack() *Deposit { +func (dre *DepositRevealedEvent) unpack(extraData *[32]byte) *Deposit { return &Deposit{ Utxo: &bitcoin.UnspentTransactionOutput{ Outpoint: &bitcoin.TransactionOutpoint{ @@ -276,6 +276,7 @@ func (dre *DepositRevealedEvent) unpack() *Deposit { RefundPublicKeyHash: dre.RefundPublicKeyHash, RefundLocktime: dre.RefundLocktime, Vault: dre.Vault, + ExtraData: extraData, } } @@ -303,6 +304,7 @@ type DepositChainRequest struct { Vault *chain.Address TreasuryFee uint64 SweptAt time.Time + ExtraData *[32]byte } // WalletChainData represents wallet data stored on-chain. diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 2a56530018..69b9fdc450 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -68,6 +68,9 @@ type localChain struct { heartbeatProposalValidationsMutex sync.Mutex heartbeatProposalValidations map[[16]byte]bool + depositRequestsMutex sync.Mutex + depositRequests map[[32]byte]*DepositChainRequest + blockCounter chain.BlockCounter operatorPrivateKey *operator.PrivateKey } @@ -582,13 +585,6 @@ func (lc *localChain) GetPendingRedemptionRequest( return request, true, nil } -func (lc *localChain) GetDepositRequest( - fundingTxHash bitcoin.Hash, - fundingOutputIndex uint32, -) (*DepositChainRequest, bool, error) { - panic("not supported") -} - func (lc *localChain) setPendingRedemptionRequest( walletPublicKeyHash [20]byte, request *RedemptionRequest, @@ -611,6 +607,46 @@ func buildRedemptionRequestKey( return sha256.Sum256(append(walletPublicKeyHash[:], redeemerOutputScript...)) } +func (lc *localChain) GetDepositRequest( + fundingTxHash bitcoin.Hash, + fundingOutputIndex uint32, +) (*DepositChainRequest, bool, error) { + lc.depositRequestsMutex.Lock() + defer lc.depositRequestsMutex.Unlock() + + requestKey := buildDepositRequestKey(fundingTxHash, fundingOutputIndex) + + request, ok := lc.depositRequests[requestKey] + if !ok { + return nil, false, nil + } + + return request, true, nil +} + +func (lc *localChain) setDepositRequest( + fundingTxHash bitcoin.Hash, + fundingOutputIndex uint32, + request *DepositChainRequest, +) { + lc.depositRequestsMutex.Lock() + defer lc.depositRequestsMutex.Unlock() + + requestKey := buildDepositRequestKey(fundingTxHash, fundingOutputIndex) + + lc.depositRequests[requestKey] = request +} + +func buildDepositRequestKey( + fundingTxHash bitcoin.Hash, + fundingOutputIndex uint32, +) [32]byte { + buffer := make([]byte, 4) + binary.BigEndian.PutUint32(buffer[:], fundingOutputIndex) + + return sha256.Sum256(append(fundingTxHash[:], buffer...)) +} + func (lc *localChain) GetWallet(walletPublicKeyHash [20]byte) ( *WalletChainData, error, @@ -891,6 +927,7 @@ func ConnectWithKey( pendingRedemptionRequests: make(map[[32]byte]*RedemptionRequest), redemptionProposalValidations: make(map[[32]byte]bool), heartbeatProposalValidations: make(map[[16]byte]bool), + depositRequests: make(map[[32]byte]*DepositChainRequest), blockCounter: blockCounter, operatorPrivateKey: operatorPrivateKey, } diff --git a/pkg/tbtc/deposit.go b/pkg/tbtc/deposit.go index c0a360e7da..361ed38eb5 100644 --- a/pkg/tbtc/deposit.go +++ b/pkg/tbtc/deposit.go @@ -3,17 +3,25 @@ package tbtc import ( "encoding/hex" "fmt" + "strings" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" - "strings" ) // depositScriptFormat is the format of the deposit P2(W)SH Bitcoin script // The specific placeholders are: depositor, blindingFactor, walletPublicKeyHash, // refundPublicKeyHash and refundLocktime. For reference see: -// https://github.com/keep-network/tbtc-v2/blob/83310bdc9ed934e286bc9ea5091cc16979950134/solidity/contracts/bridge/Deposit.sol#L172 +// https://github.com/keep-network/tbtc-v2/blob/4b6143974b43297e69a45191f0e2b6a25561e72b/solidity/contracts/bridge/Deposit.sol#L217 const depositScriptFormat = "14%v7508%v7576a914%v8763ac6776a914%v8804%vb175ac68" +// depositWithExtraDataScriptFormat is the format of the deposit P2(W)SH Bitcoin +// script with optional 32-byte extra data included. The specific placeholders +// are: depositor, extraData, blindingFactor, walletPublicKeyHash, refundPublicKeyHash, +// and refundLocktime. For reference see: +// https://github.com/keep-network/tbtc-v2/blob/4b6143974b43297e69a45191f0e2b6a25561e72b/solidity/contracts/bridge/Deposit.sol#L246 +const depositWithExtraDataScriptFormat = "14%v7520%v7508%v7576a914%v8763ac6776a914%v8804%vb175ac68" + // Deposit represents a tBTC deposit. type Deposit struct { // Utxo is the unspent output of the deposit funding transaction that @@ -33,6 +41,9 @@ type Deposit struct { // Vault is an optional field that holds the host chain address of the // target vault. Vault *chain.Address + // ExtraData is an optional field that holds 32 bytes of extra data + // embedded in the deposit script. + ExtraData *[32]byte } // Script constructs the deposit P2(W)SH Bitcoin script. This function @@ -48,14 +59,28 @@ func (d *Deposit) Script() ([]byte, error) { return nil, fmt.Errorf("wrong byte length of depositor field") } - script := fmt.Sprintf( - depositScriptFormat, - hex.EncodeToString(depositorBytes), - hex.EncodeToString(d.BlindingFactor[:]), - hex.EncodeToString(d.WalletPublicKeyHash[:]), - hex.EncodeToString(d.RefundPublicKeyHash[:]), - hex.EncodeToString(d.RefundLocktime[:]), - ) + var script string + + if d.ExtraData != nil { + script = fmt.Sprintf( + depositWithExtraDataScriptFormat, + hex.EncodeToString(depositorBytes), + hex.EncodeToString(d.ExtraData[:]), + hex.EncodeToString(d.BlindingFactor[:]), + hex.EncodeToString(d.WalletPublicKeyHash[:]), + hex.EncodeToString(d.RefundPublicKeyHash[:]), + hex.EncodeToString(d.RefundLocktime[:]), + ) + } else { + script = fmt.Sprintf( + depositScriptFormat, + hex.EncodeToString(depositorBytes), + hex.EncodeToString(d.BlindingFactor[:]), + hex.EncodeToString(d.WalletPublicKeyHash[:]), + hex.EncodeToString(d.RefundPublicKeyHash[:]), + hex.EncodeToString(d.RefundLocktime[:]), + ) + } return hex.DecodeString(script) } diff --git a/pkg/tbtc/deposit_sweep.go b/pkg/tbtc/deposit_sweep.go index 008e9ebde2..ca29d63b2e 100644 --- a/pkg/tbtc/deposit_sweep.go +++ b/pkg/tbtc/deposit_sweep.go @@ -247,6 +247,20 @@ func ValidateDepositSweepProposal( FundingTx *bitcoin.Transaction }, ) error + + // GetDepositRequest gets the on-chain deposit request for the given + // funding transaction hash and output index.The returned values represent: + // - deposit request which is non-nil only when the deposit request was + // found, + // - boolean value which is true if the deposit request was found, false + // otherwise, + // - error which is non-nil only when the function execution failed. It will + // be nil if the deposit request was not found, but the function execution + // succeeded. + GetDepositRequest( + fundingTxHash bitcoin.Hash, + fundingOutputIndex uint32, + ) (*DepositChainRequest, bool, error) }, btcChain bitcoin.Chain, ) ([]*Deposit, error) { @@ -352,11 +366,29 @@ func ValidateDepositSweepProposal( ) } + depositRequest, found, err := chain.GetDepositRequest( + depositKey.FundingTxHash, + depositKey.FundingOutputIndex, + ) + if err != nil { + return nil, fmt.Errorf( + "cannot get request data for deposit [%v]: [%v]", + depositDisplayIndex, + err, + ) + } + if !found { + return nil, fmt.Errorf( + "request data not found for deposit [%v]", + depositDisplayIndex, + ) + } + depositExtraInfo[i] = struct { *Deposit FundingTx *bitcoin.Transaction }{ - Deposit: matchingEvent.unpack(), + Deposit: matchingEvent.unpack(depositRequest.ExtraData), FundingTx: fundingTx, } } diff --git a/pkg/tbtc/deposit_sweep_test.go b/pkg/tbtc/deposit_sweep_test.go index d831d2525e..c98f75a3c0 100644 --- a/pkg/tbtc/deposit_sweep_test.go +++ b/pkg/tbtc/deposit_sweep_test.go @@ -119,6 +119,18 @@ func TestDepositSweepAction_Execute(t *testing.T) { if err != nil { t.Fatal(err) } + + hostChain.setDepositRequest( + fundingTxHash, + fundingOutputIndex, + &DepositChainRequest{ + // Set only relevant fields. + Depositor: deposit.Depositor, + Amount: uint64(deposit.Utxo.Value), + Vault: deposit.Vault, + ExtraData: deposit.ExtraData, + }, + ) } // Build the sweep proposal based on the scenario data. @@ -252,6 +264,7 @@ func TestAssembleDepositSweepTransaction(t *testing.T) { RefundPublicKeyHash: d.RefundPublicKeyHash, RefundLocktime: d.RefundLocktime, Vault: d.Vault, + ExtraData: d.ExtraData, } } diff --git a/pkg/tbtc/deposit_test.go b/pkg/tbtc/deposit_test.go index 4a7fc12e9a..72b86344cd 100644 --- a/pkg/tbtc/deposit_test.go +++ b/pkg/tbtc/deposit_test.go @@ -4,6 +4,8 @@ import ( "encoding/hex" "testing" + "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/internal/testutils" ) @@ -16,24 +18,67 @@ func TestDeposit_Script(t *testing.T) { return bytes } - // Fill only the fields relevant for script computation. - d := new(Deposit) - d.Depositor = "934b98637ca318a4d6e7ca6ffd1690b8e77df637" - copy(d.BlindingFactor[:], hexToSlice("f9f0c90d00039523")) - copy(d.WalletPublicKeyHash[:], hexToSlice("8db50eb52063ea9d98b3eac91489a90f738986f6")) - copy(d.RefundPublicKeyHash[:], hexToSlice("28e081f285138ccbe389c1eb8985716230129f89")) - copy(d.RefundLocktime[:], hexToSlice("60bcea61")) - - script, err := d.Script() - if err != nil { - t.Fatal(err) + var tests = map[string]struct { + depositor string + blindingFactor string + walletPublicKeyHash string + refundPublicKeyHash string + refundLocktime string + extraData string + expectedScript string + }{ + "no extra data": { + depositor: "934b98637ca318a4d6e7ca6ffd1690b8e77df637", + blindingFactor: "f9f0c90d00039523", + walletPublicKeyHash: "8db50eb52063ea9d98b3eac91489a90f738986f6", + refundPublicKeyHash: "28e081f285138ccbe389c1eb8985716230129f89", + refundLocktime: "60bcea61", + extraData: "", + expectedScript: "14934b98637ca318a4d6e7ca6ffd1690b8e77df637750" + + "8f9f0c90d000395237576a9148db50eb52063ea9d98b3eac91489a90f" + + "738986f68763ac6776a91428e081f285138ccbe389c1eb89857162301" + + "29f89880460bcea61b175ac68", + }, + "with extra data": { + depositor: "934b98637ca318a4d6e7ca6ffd1690b8e77df637", + blindingFactor: "f9f0c90d00039523", + walletPublicKeyHash: "8db50eb52063ea9d98b3eac91489a90f738986f6", + refundPublicKeyHash: "28e081f285138ccbe389c1eb8985716230129f89", + refundLocktime: "60bcea61", + extraData: "a9b38ea6435c8941d6eda6a46b68e3e2117196995bd154ab55" + + "196396b03d9bda", + expectedScript: "14934b98637ca318a4d6e7ca6ffd1690b8e77df637752" + + "0a9b38ea6435c8941d6eda6a46b68e3e2117196995bd154ab55196396" + + "b03d9bda7508f9f0c90d000395237576a9148db50eb52063ea9d98b3e" + + "ac91489a90f738986f68763ac6776a91428e081f285138ccbe389c1eb" + + "8985716230129f89880460bcea61b175ac68", + }, } - expectedScript := hexToSlice( - "14934b98637ca318a4d6e7ca6ffd1690b8e77df6377508f9f0c90d0003" + - "95237576a9148db50eb52063ea9d98b3eac91489a90f738986f68763ac6776a" + - "91428e081f285138ccbe389c1eb8985716230129f89880460bcea61b175ac68", - ) + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + // Fill only the fields relevant for script computation. + d := new(Deposit) + d.Depositor = chain.Address(test.depositor) + copy(d.BlindingFactor[:], hexToSlice(test.blindingFactor)) + copy(d.WalletPublicKeyHash[:], hexToSlice(test.walletPublicKeyHash)) + copy(d.RefundPublicKeyHash[:], hexToSlice(test.refundPublicKeyHash)) + copy(d.RefundLocktime[:], hexToSlice(test.refundLocktime)) + + if len(test.extraData) > 0 { + var extraData [32]byte + copy(extraData[:], hexToSlice(test.extraData)) + d.ExtraData = &extraData + } + + script, err := d.Script() + if err != nil { + t.Fatal(err) + } - testutils.AssertBytesEqual(t, expectedScript, script) + expectedScript := hexToSlice(test.expectedScript) + + testutils.AssertBytesEqual(t, expectedScript, script) + }) + } } diff --git a/pkg/tbtc/internal/test/marshaling.go b/pkg/tbtc/internal/test/marshaling.go index a21c556a08..e95fa13973 100644 --- a/pkg/tbtc/internal/test/marshaling.go +++ b/pkg/tbtc/internal/test/marshaling.go @@ -29,6 +29,7 @@ func (dsts *DepositSweepTestScenario) UnmarshalJSON(data []byte) error { RefundPublicKeyHash string RefundLocktime string Vault string + ExtraData string } InputTransactions []string Fee int64 @@ -87,6 +88,12 @@ func (dsts *DepositSweepTestScenario) UnmarshalJSON(data []byte) error { } d.Vault = vault + if len(deposit.ExtraData) > 0 { + var extraData [32]byte + copy(extraData[:], hexToSlice(deposit.ExtraData)) + d.ExtraData = &extraData + } + dsts.Deposits = append(dsts.Deposits, d) } diff --git a/pkg/tbtc/internal/test/tbtctest.go b/pkg/tbtc/internal/test/tbtctest.go index 3f176c7033..316e475552 100644 --- a/pkg/tbtc/internal/test/tbtctest.go +++ b/pkg/tbtc/internal/test/tbtctest.go @@ -84,6 +84,7 @@ type Deposit struct { RefundPublicKeyHash [20]byte RefundLocktime [4]byte Vault *chain.Address + ExtraData *[32]byte } // DepositSweepTestScenario represents a deposit sweep test scenario. diff --git a/pkg/tbtc/internal/test/testdata/deposit_sweep_scenario_4.json b/pkg/tbtc/internal/test/testdata/deposit_sweep_scenario_4.json new file mode 100644 index 0000000000..bfcd7595b1 --- /dev/null +++ b/pkg/tbtc/internal/test/testdata/deposit_sweep_scenario_4.json @@ -0,0 +1,52 @@ +{ + "Title": "witness main UTXO with witness deposit containing 32-byte extra data", + "WalletPublicKey": "04989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d9d218b65e7d91c752f7b22eaceb771a9af3a6f3d3f010a5d471a1aeef7d7713af", + "WalletPrivateKey": "7c246a5d2fcf476fd6f805cb8174b1cf441b13ea414e5560ca2bdc963aeb7d0c", + "WalletMainUtxo": { + "Outpoint": { + "TransactionHash": "22ec31f33cb402284c6a6227faebf885103166247b1d9b093d667430bf372d18", + "OutputIndex": 0 + }, + "Value": 1990000 + }, + "Deposits": [ + { + "Utxo": { + "Outpoint": { + "TransactionHash": "b3873bc49a80027bc7c5e0dddf3bb1bb1e88869f66244709a7c87e240a1daf1c", + "OutputIndex": 0 + }, + "Value": 90000 + }, + "Depositor": "934b98637ca318a4d6e7ca6ffd1690b8e77df637", + "BlindingFactor": "f9f0c90d00039523", + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "RefundPublicKeyHash": "28e081f285138ccbe389c1eb8985716230129f89", + "RefundLocktime": "60bcea61", + "Vault": "", + "ExtraData": "a9b38ea6435c8941d6eda6a46b68e3e2117196995bd154ab55196396b03d9bda" + } + ], + "InputTransactions": [ + "0100000000010130c1b2951a7019b8d384179b40108d27c02a0be746106dd9aad3532d053a5e650000000000ffffffff01705d1e00000000001600148db50eb52063ea9d98b3eac91489a90f738986f603483045022100c415b9adb4ee0290ba707f5ddd79a9279e2fd54baf973bed5b2482b89a60f082022051e33af64d9958a2de1d52eaa13fe120b5d804f91d770d543afcd7d54bc874c1012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d95c14a0ee7a142d267c1f36714e4a8f75612f20a7972075083543d400d3653e327576a9148db50eb52063ea9d98b3eac91489a90f738986f68763ac6776a91402551415aa56699043cbc8ab9c8b63efe04a2c0488046f231267b175ac6800000000", + "01000000000101352669959363c5bc6248c6fb3c16a1aa5ec630d66672ebb3df00a3c485d5c0a50000000000ffffffff01905f010000000000220020bfaeddba12b0de6feeb649af76376876bc1feb6c2248fbfef9293ba3ac51bb4a02483045022100cf4651094fc0faaf27cd0b7e2f00969960d2b8e65c7211641728d6fa4df203b302206f4da3a78578f5dce5dfc39a57625c472bc72875ed5cfdcaea23a483ff72884e012102ee067a0273f2e3ba88d23140a24fdb290f27bbcd0f94117a9c65be3911c5c04e00000000" + ], + "Fee": 10000, + "Signatures": [ + { + "R": "7a2c2165f22082cd35b78b606ccd9882c0d0b90eca4fc93cb4488875aadb1dd9", + "S": "63f77b701286714c392627e9cc69238f8157e56dca4e24e397769c40ed1453d2" + }, + { + "R": "f7133a975f9728091fba874f3d6ff23d3c63c084cbb5e3e344c59a7910f96368", + "S": "3006f9ea5a0904c9a53cd3e54cfd36c52d7e2132dedd37601e7e8926e80c7d42" + } + ], + "ExpectedSigHashes": [ + "1b80a53fd3db13ebea43631419f38cc5e1c842fe6156793d9452c7bb6eeb5fbb", + "59aef86e6236d611e409ecbd1875d1f6bc33b03fac420501fce9d33b3509c87c" + ], + "ExpectedSweepTransaction": "01000000000102182d37bf3074663d099b1d7b2466311085f8ebfa27626a4c2802b43cf331ec220000000000ffffffff1caf1d0a247ec8a7094724669f86881ebbb13bdfdde0c5c77b02809ac43b87b30000000000ffffffff01f0951f00000000001600148db50eb52063ea9d98b3eac91489a90f738986f60247304402207a2c2165f22082cd35b78b606ccd9882c0d0b90eca4fc93cb4488875aadb1dd9022063f77b701286714c392627e9cc69238f8157e56dca4e24e397769c40ed1453d2012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d903483045022100f7133a975f9728091fba874f3d6ff23d3c63c084cbb5e3e344c59a7910f9636802203006f9ea5a0904c9a53cd3e54cfd36c52d7e2132dedd37601e7e8926e80c7d42012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d97e14934b98637ca318a4d6e7ca6ffd1690b8e77df6377520a9b38ea6435c8941d6eda6a46b68e3e2117196995bd154ab55196396b03d9bda7508f9f0c90d000395237576a9148db50eb52063ea9d98b3eac91489a90f738986f68763ac6776a91428e081f285138ccbe389c1eb8985716230129f89880460bcea61b175ac6800000000", + "ExpectedSweepTransactionHash": "d1ae6e4627aadcab4148469e88d43c65d15702b97890a9ae7dc03b06c8c677b7", + "ExpectedSweepTransactionWitnessHash": "eda66565064202484aaa10228ebc67a270ea51f6042b56ed8e028684a4271eb2" +} \ No newline at end of file diff --git a/pkg/tbtcpg/deposit_sweep_test.go b/pkg/tbtcpg/deposit_sweep_test.go index 7695878adf..7cbbe6d9a7 100644 --- a/pkg/tbtcpg/deposit_sweep_test.go +++ b/pkg/tbtcpg/deposit_sweep_test.go @@ -1,15 +1,15 @@ package tbtcpg_test import ( - "github.com/keep-network/keep-core/internal/testutils" - "github.com/keep-network/keep-core/pkg/tbtcpg" "reflect" "testing" "github.com/go-test/deep" "github.com/ipfs/go-log" + "github.com/keep-network/keep-core/internal/testutils" "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/tbtc" + "github.com/keep-network/keep-core/pkg/tbtcpg" "github.com/keep-network/keep-core/pkg/tbtcpg/internal/test" ) @@ -115,6 +115,15 @@ func TestDepositSweepTask_ProposeDepositsSweep(t *testing.T) { t.Fatal(err) } + tbtcChain.SetDepositRequest( + deposit.FundingTxHash, + deposit.FundingOutputIndex, + &tbtc.DepositChainRequest{ + // Set only relevant fields. + ExtraData: nil, + }, + ) + btcChain.SetTransaction(deposit.FundingTxHash, &bitcoin.Transaction{}) btcChain.SetTransactionConfirmations(deposit.FundingTxHash, tbtc.DepositSweepRequiredFundingTxConfirmations) }