From fd1e44a89bd993dfc6c22ea9c724ce5b7a211561 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 19 May 2023 10:53:11 +0200 Subject: [PATCH] Added function for getting transaction Merkle branch to Electrum client --- pkg/bitcoin/chain.go | 8 ++ pkg/bitcoin/chain_test.go | 7 ++ pkg/bitcoin/electrum/electrum.go | 34 ++++++- .../electrum/electrum_integration_test.go | 91 +++++++++++++++++++ pkg/bitcoin/electrum/transaction.go | 14 ++- pkg/bitcoin/transaction.go | 33 +++++-- pkg/maintainer/bitcoin_chain_test.go | 7 ++ pkg/tbtc/bitcoin_chain_test.go | 10 +- 8 files changed, 193 insertions(+), 11 deletions(-) diff --git a/pkg/bitcoin/chain.go b/pkg/bitcoin/chain.go index 24e58993dc..d99516cc9b 100644 --- a/pkg/bitcoin/chain.go +++ b/pkg/bitcoin/chain.go @@ -29,6 +29,14 @@ type Chain interface { // returns an error. GetBlockHeader(blockHeight uint) (*BlockHeader, error) + // GetTransactionMerkle gets the Merkle branch for a given transaction. + // The transaction's hash and the block the transaction was included in the + // blockchain need to be provided. + GetTransactionMerkle( + transactionHash Hash, + blockHeight uint, + ) (*TransactionMerkleBranch, error) + // GetTransactionsForPublicKeyHash gets the confirmed transactions that pays the // given public key hash using either a P2PKH or P2WPKH script. The returned // transactions are ordered by block height in the ascending order, i.e. diff --git a/pkg/bitcoin/chain_test.go b/pkg/bitcoin/chain_test.go index 9f7f0c6830..cccfacd53e 100644 --- a/pkg/bitcoin/chain_test.go +++ b/pkg/bitcoin/chain_test.go @@ -44,6 +44,13 @@ func (lc *localChain) GetBlockHeader( panic("not implemented") } +func (lc *localChain) GetTransactionMerkle( + transactionHash Hash, + blockHeight uint, +) (*TransactionMerkleBranch, error) { + panic("not implemented") +} + func (lc *localChain) GetTransactionsForPublicKeyHash( publicKeyHash [20]byte, limit int, diff --git a/pkg/bitcoin/electrum/electrum.go b/pkg/bitcoin/electrum/electrum.go index c85ef2833f..a2e7a75a4b 100644 --- a/pkg/bitcoin/electrum/electrum.go +++ b/pkg/bitcoin/electrum/electrum.go @@ -301,7 +301,39 @@ func (c *Connection) GetBlockHeader( return blockHeader, nil } -// GetTransactionsForPublicKeyHash get confirmed transactions that pays the +// GetTransactionMerkle gets the Merkle branch for a given transaction. +// The transaction's hash and the block the transaction was included in the +// blockchain need to be provided. +func (c *Connection) GetTransactionMerkle( + transactionHash bitcoin.Hash, + blockHeight uint, +) (*bitcoin.TransactionMerkleBranch, error) { + getMerkleProofResult, err := requestWithRetry( + c, + func( + ctx context.Context, + client *electrum.Client, + ) (*electrum.GetMerkleProofResult, error) { + return client.GetMerkleProof( + ctx, + transactionHash.String(), + uint32(blockHeight), + ) + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to get merkle proof: [%w]", err) + } + + transactionMerkle := convertMerkleProof(getMerkleProofResult) + if err != nil { + return nil, fmt.Errorf("failed to convert merkle proof: %w", err) + } + + return transactionMerkle, nil +} + +// GetTransactionsForPublicKeyHash gets confirmed transactions that pays the // given public key hash using either a P2PKH or P2WPKH script. The returned // transactions are ordered by block height in the ascending order, i.e. // the latest transaction is at the end of the list. The returned list does diff --git a/pkg/bitcoin/electrum/electrum_integration_test.go b/pkg/bitcoin/electrum/electrum_integration_test.go index 58ca90a5d0..d3ec209fa5 100644 --- a/pkg/bitcoin/electrum/electrum_integration_test.go +++ b/pkg/bitcoin/electrum/electrum_integration_test.go @@ -292,6 +292,97 @@ func TestGetBlockHeader_Negative_Integration(t *testing.T) { } } +func TestGetTransactionMerkle_Integration(t *testing.T) { + transactionHash, err := bitcoin.NewHashFromString( + "72e7fd57c2adb1ed2305c4247486ff79aec363296f02ec65be141904f80d214e", + bitcoin.InternalByteOrder, + ) + if err != nil { + t.Fatal(err) + } + + blockHeight := uint(1569342) + + expectedResult := &bitcoin.TransactionMerkleBranch{ + BlockHeight: 1569342, + Merkle: []string{ + "8b5bbb5bdf6727bf70fad4f46fe4eaab04c98119ffbd2d95c29adf32d26f8452", + "53637bacb07965e4a8220836861d1b16c6da29f10ea9ab53fc4eca73074f98b9", + "0267e738108d094ceb05217e2942e9c2a4c6389ac47f476f572c9a319ce4dfbc", + "34e00deec50c48d99678ca2b52b82d6d5432326159c69e7233d0dde0924874b4", + "7a53435e6c86a3620cdbae510901f17958f0540314214379197874ed8ed7a913", + "6315dbb7ce350ceaa16cd4c35c5a147005e8b38ca1e9531bd7320629e8d17f5b", + "40380cdadc0206646208871e952af9dcfdff2f104305ce463aed5eeaf7725d2f", + "5d74bae6a71fd1cff2416865460583319a40343650bd4bb89de0a6ae82097037", + "296ddccfc659e0009aad117c8ed15fb6ff81c2bade73fbc89666a22708d233f9", + }, + Position: 176, + } + + for testName, config := range configs { + t.Run(testName, func(t *testing.T) { + electrum := newTestConnection(t, config) + + result, err := electrum.GetTransactionMerkle( + transactionHash, + blockHeight, + ) + if err != nil { + t.Fatal(err) + } + + if diff := deep.Equal(result, expectedResult); diff != nil { + t.Errorf("compare failed: %v", diff) + } + }) + } +} + +func TestGetTransactionMerkle_Negative_Integration(t *testing.T) { + replaceErrorMsgForTests := []string{"electrumx ssl", "fulcrum ssl"} + + for testName, config := range configs { + t.Run(testName, func(t *testing.T) { + electrum := newTestConnection(t, config) + + expectedErrorMsg := fmt.Sprintf( + "failed to get merkle proof: [retry timeout [%s] exceeded; most recent error: [request failed: [tx not found or is unconfirmed]]]", + config.RequestRetryTimeout, + ) + + // As a workaround for the problem described in https://github.com/checksum0/go-electrum/issues/5 + // we use an alternative expected error message for servers + // that are not correctly supported by the electrum client. + if slices.Contains(replaceErrorMsgForTests, testName) { + expectedErrorMsg = fmt.Sprintf( + "failed to get merkle proof: [retry timeout [%s] exceeded; most recent error: [request failed: [Unmarshal received message failed: json: cannot unmarshal object into Go struct field response.error of type string]]]", + config.RequestRetryTimeout, + ) + } + + transactionHash, err := bitcoin.NewHashFromString( + // use incorrect hash + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + bitcoin.InternalByteOrder, + ) + if err != nil { + t.Fatal(err) + } + + blockHeight := uint(math.MaxUint32) // use incorrect height + + _, err = electrum.GetTransactionMerkle(transactionHash, blockHeight) + if err.Error() != expectedErrorMsg { + t.Errorf( + "invalid error\nexpected: %v\nactual: %v", + expectedErrorMsg, + err, + ) + } + }) + } +} + func TestGetTransactionsForPublicKeyHash_Integration(t *testing.T) { var publicKeyHash [20]byte publicKeyHashBytes, err := hex.DecodeString("e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0") diff --git a/pkg/bitcoin/electrum/transaction.go b/pkg/bitcoin/electrum/transaction.go index d83fe655af..4d38c3ae75 100644 --- a/pkg/bitcoin/electrum/transaction.go +++ b/pkg/bitcoin/electrum/transaction.go @@ -6,7 +6,7 @@ import ( "fmt" "github.com/btcsuite/btcd/v2/wire" - + "github.com/checksum0/go-electrum/electrum" "github.com/keep-network/keep-core/pkg/bitcoin" ) @@ -66,3 +66,15 @@ func convertRawTransaction(rawTx string) (*bitcoin.Transaction, error) { return result, nil } + +// convertMerkleProof transforms a MerkleProof returned from Electrum protocol +// to the format expected by the bitcoin.Chain interface. +func convertMerkleProof( + electrumResult *electrum.GetMerkleProofResult, +) *bitcoin.TransactionMerkleBranch { + return &bitcoin.TransactionMerkleBranch{ + BlockHeight: uint(electrumResult.Height), + Merkle: electrumResult.Merkle, + Position: uint(electrumResult.Position), + } +} diff --git a/pkg/bitcoin/transaction.go b/pkg/bitcoin/transaction.go index f75f6e3b5f..0a13a83a55 100644 --- a/pkg/bitcoin/transaction.go +++ b/pkg/bitcoin/transaction.go @@ -3,6 +3,7 @@ package bitcoin import ( "bytes" "encoding/binary" + "github.com/btcsuite/btcd/wire" ) @@ -42,17 +43,17 @@ type Transaction struct { // as described below. // // If the transaction CONTAINS witness inputs and Serialize is called with: -// - Standard serialization format, the result is actually in the Standard -// format and does not include witness data referring to the witness inputs -// - Witness serialization format, the result is actually in the Witness -// format and includes witness data referring to the witness inputs +// - Standard serialization format, the result is actually in the Standard +// format and does not include witness data referring to the witness inputs +// - Witness serialization format, the result is actually in the Witness +// format and includes witness data referring to the witness inputs // // If the transaction DOES NOT CONTAIN witness inputs and Serialize is // called with: -// - Standard serialization format, the result is actually in the Standard -// format -// - Witness serialization format, the result is actually in the Standard -// format because there are no witness inputs whose data can be included +// - Standard serialization format, the result is actually in the Standard +// format +// - Witness serialization format, the result is actually in the Standard +// format because there are no witness inputs whose data can be included // // By default, the Witness format is used and that can be changed using the // optional format argument. The Witness format is used by default as it @@ -238,3 +239,19 @@ type UnspentTransactionOutput struct { // Value denotes the number of unspent satoshis. Value int64 } + +// TransactionMerkleBranch holds information about the merkle branch to +// a confirmed transaction. +type TransactionMerkleBranch struct { + //BlockHeight is the height of the block the transaction was confirmed in. + BlockHeight uint + + //Merkle is a list of transaction hashes the current hash is paired with, + //recursively, in order to trace up to obtain the merkle root of the + //including block, deepest pairing first. Each hash is an unprefixed hex + //string. + Merkle []string + + // Position is the 0-based index of the transaction's position in the block. + Position uint +} diff --git a/pkg/maintainer/bitcoin_chain_test.go b/pkg/maintainer/bitcoin_chain_test.go index 950a8fc563..0dff6ba7e4 100644 --- a/pkg/maintainer/bitcoin_chain_test.go +++ b/pkg/maintainer/bitcoin_chain_test.go @@ -76,6 +76,13 @@ func (lc *localBitcoinChain) GetBlockHeader( return blockHeader, nil } +func (lc *localBitcoinChain) GetTransactionMerkle( + transactionHash bitcoin.Hash, + blockHeight uint, +) (*bitcoin.TransactionMerkleBranch, error) { + panic("unsupported") +} + func (lc *localBitcoinChain) GetTransactionsForPublicKeyHash( publicKeyHash [20]byte, limit int, diff --git a/pkg/tbtc/bitcoin_chain_test.go b/pkg/tbtc/bitcoin_chain_test.go index 548e084625..8866461bea 100644 --- a/pkg/tbtc/bitcoin_chain_test.go +++ b/pkg/tbtc/bitcoin_chain_test.go @@ -3,8 +3,9 @@ package tbtc import ( "bytes" "fmt" - "github.com/keep-network/keep-core/pkg/bitcoin" "sync" + + "github.com/keep-network/keep-core/pkg/bitcoin" ) type localBitcoinChain struct { @@ -79,6 +80,13 @@ func (lbc *localBitcoinChain) GetBlockHeader( panic("not implemented") } +func (lbc *localBitcoinChain) GetTransactionMerkle( + transactionHash bitcoin.Hash, + blockHeight uint, +) (*bitcoin.TransactionMerkleBranch, error) { + panic("not implemented") +} + func (lbc *localBitcoinChain) GetTransactionsForPublicKeyHash( publicKeyHash [20]byte, limit int,