Skip to content

Commit

Permalink
Added GetTransactionMerkle to Electrum client (#3578)
Browse files Browse the repository at this point in the history
  • Loading branch information
nkuba authored May 23, 2023
2 parents d4e4b75 + 4c22e08 commit fe9cea4
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 11 deletions.
29 changes: 29 additions & 0 deletions internal/testdata/bitcoin/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,35 @@ var Transactions = map[string]struct {
},
}

// TxMerkleProof holds details of the transaction Merkle proof data used as a
// test vector.
var TxMerkleProof = struct {
TxHash bitcoin.Hash
BlockHeigh uint
MerkleProof bitcoin.TransactionMerkleProof
}{
// https://blockstream.info/testnet/api/tx/72e7fd57c2adb1ed2305c4247486ff79aec363296f02ec65be141904f80d214e
TxHash: hashFromString(
"72e7fd57c2adb1ed2305c4247486ff79aec363296f02ec65be141904f80d214e",
),
BlockHeigh: 1569342,
MerkleProof: bitcoin.TransactionMerkleProof{
BlockHeight: 1569342,
MerkleNodes: []string{
"8b5bbb5bdf6727bf70fad4f46fe4eaab04c98119ffbd2d95c29adf32d26f8452",
"53637bacb07965e4a8220836861d1b16c6da29f10ea9ab53fc4eca73074f98b9",
"0267e738108d094ceb05217e2942e9c2a4c6389ac47f476f572c9a319ce4dfbc",
"34e00deec50c48d99678ca2b52b82d6d5432326159c69e7233d0dde0924874b4",
"7a53435e6c86a3620cdbae510901f17958f0540314214379197874ed8ed7a913",
"6315dbb7ce350ceaa16cd4c35c5a147005e8b38ca1e9531bd7320629e8d17f5b",
"40380cdadc0206646208871e952af9dcfdff2f104305ce463aed5eeaf7725d2f",
"5d74bae6a71fd1cff2416865460583319a40343650bd4bb89de0a6ae82097037",
"296ddccfc659e0009aad117c8ed15fb6ff81c2bade73fbc89666a22708d233f9",
},
Position: 176,
},
}

func hashFromString(s string) bitcoin.Hash {
hash, err := bitcoin.NewHashFromString(
s,
Expand Down
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)

// GetTransactionMerkleProof gets the Merkle proof for a given transaction.
// The transaction's hash and the block the transaction was included in the
// blockchain need to be provided.
GetTransactionMerkleProof(
transactionHash Hash,
blockHeight uint,
) (*TransactionMerkleProof, 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 @@ -54,6 +54,13 @@ func (lc *localChain) GetBlockHeader(
panic("not implemented")
}

func (lc *localChain) GetTransactionMerkleProof(
transactionHash Hash,
blockHeight uint,
) (*TransactionMerkleProof, error) {
panic("not implemented")
}

func (lc *localChain) GetTransactionsForPublicKeyHash(
publicKeyHash [20]byte,
limit int,
Expand Down
31 changes: 30 additions & 1 deletion pkg/bitcoin/electrum/electrum.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,36 @@ func (c *Connection) GetBlockHeader(
return blockHeader, nil
}

// GetTransactionsForPublicKeyHash get confirmed transactions that pays the
// GetTransactionMerkleProof gets the Merkle proof 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) GetTransactionMerkleProof(
transactionHash bitcoin.Hash,
blockHeight uint,
) (*bitcoin.TransactionMerkleProof, error) {
txID := transactionHash.Hex(bitcoin.ReversedByteOrder)

getMerkleProofResult, err := requestWithRetry(
c,
func(
ctx context.Context,
client *electrum.Client,
) (*electrum.GetMerkleProofResult, error) {
return client.GetMerkleProof(
ctx,
txID,
uint32(blockHeight),
)
},
)
if err != nil {
return nil, fmt.Errorf("failed to get merkle proof: [%w]", err)
}

return convertMerkleProof(getMerkleProofResult), 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
67 changes: 67 additions & 0 deletions pkg/bitcoin/electrum/electrum_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ var (
esploraElectrs: "errNo: 0, errMsg: missing transaction",
}

missingTransactionInBlockMsgs = map[serverImplementation]string{
electrumX: "errNo: 1, errMsg: tx aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa not in block at height 123,456",
fulcrum: "errNo: 1, errMsg: No transaction matching the requested hash found at height 123456",
esploraElectrs: "errNo: 0, errMsg: tx not found or is unconfirmed",
}

missingHeaderServerMsgs = map[serverImplementation]string{
electrumX: "errNo: 1, errMsg: height 4,294,967,295 out of range",
fulcrum: "errNo: 1, errMsg: Invalid height",
Expand Down Expand Up @@ -295,6 +301,67 @@ func TestGetBlockHeader_Negative_Integration(t *testing.T) {
}
}

func TestGetTransactionMerkleProof_Integration(t *testing.T) {
transactionHash := testData.TxMerkleProof.TxHash
blockHeight := testData.TxMerkleProof.BlockHeigh

expectedResult := &testData.TxMerkleProof.MerkleProof

for testName, config := range testConfigs {
t.Run(testName, func(t *testing.T) {
electrum := newTestConnection(t, config.clientConfig)

result, err := electrum.GetTransactionMerkleProof(
transactionHash,
blockHeight,
)
if err != nil {
t.Fatal(err)
}

if diff := deep.Equal(result, expectedResult); diff != nil {
t.Errorf("compare failed: %v", diff)
}
})
}
}

func TestGetTransactionMerkleProof_Negative_Integration(t *testing.T) {
incorrectTransactionHash, err := bitcoin.NewHashFromString(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
bitcoin.ReversedByteOrder,
)
if err != nil {
t.Fatal(err)
}

blockHeight := uint(123456)

for testName, config := range testConfigs {
t.Run(testName, func(t *testing.T) {
electrum := newTestConnection(t, config.clientConfig)

expectedErrorMsg := fmt.Sprintf(
"failed to get merkle proof: [retry timeout [%s] exceeded; most recent error: [request failed: [%s]]]",
config.clientConfig.RequestRetryTimeout,
missingTransactionInBlockMsgs[config.serverImplementation],
)

_, err = electrum.GetTransactionMerkleProof(
incorrectTransactionHash,
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.TransactionMerkleProof {
return &bitcoin.TransactionMerkleProof{
BlockHeight: uint(electrumResult.Height),
MerkleNodes: 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
}

// TransactionMerkleProof holds information about the merkle branch to a
// confirmed transaction.
type TransactionMerkleProof struct {
// BlockHeight is the height of the block the transaction was confirmed in.
BlockHeight uint

// MerkleNodes 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.
MerkleNodes []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 @@ -75,6 +75,13 @@ func (lc *localBitcoinChain) GetBlockHeader(
return blockHeader, nil
}

func (lc *localBitcoinChain) GetTransactionMerkleProof(
transactionHash bitcoin.Hash,
blockHeight uint,
) (*bitcoin.TransactionMerkleProof, 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) GetTransactionMerkleProof(
transactionHash bitcoin.Hash,
blockHeight uint,
) (*bitcoin.TransactionMerkleProof, error) {
panic("not implemented")
}

func (lbc *localBitcoinChain) GetTransactionsForPublicKeyHash(
publicKeyHash [20]byte,
limit int,
Expand Down

0 comments on commit fe9cea4

Please sign in to comment.