Skip to content

Commit

Permalink
Added function for getting transaction Merkle branch to Electrum client
Browse files Browse the repository at this point in the history
  • Loading branch information
tomaszslabon committed May 19, 2023
1 parent 0a6a7ae commit fd1e44a
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 11 deletions.
8 changes: 8 additions & 0 deletions pkg/bitcoin/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions pkg/bitcoin/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
34 changes: 33 additions & 1 deletion pkg/bitcoin/electrum/electrum.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 91 additions & 0 deletions pkg/bitcoin/electrum/electrum_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
14 changes: 13 additions & 1 deletion pkg/bitcoin/electrum/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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),
}
}
33 changes: 25 additions & 8 deletions pkg/bitcoin/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package bitcoin
import (
"bytes"
"encoding/binary"

"github.com/btcsuite/btcd/wire"
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
7 changes: 7 additions & 0 deletions pkg/maintainer/bitcoin_chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 9 additions & 1 deletion pkg/tbtc/bitcoin_chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit fd1e44a

Please sign in to comment.