From 7e9463c23de5743d74e894e6a9ba019dc4a123ed Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Sun, 1 Oct 2023 17:39:56 +0200 Subject: [PATCH 1/4] Harden the wallet main UTXO lookup mechanism The current version of the main UTXO lookup mechanism looks just on the last five confirmed transactions targeting the wallet public key hash to determine the plain text main UTXO of the wallet. This mechanism is not ideal as it doesn't recognize the difference between true wallet transactions and arbitrary transfers to the wallet public key hash that can be made by anyone. That means it is enough to craft several arbitrary transfers to block the main UTXO lookup and prevent the given wallet to perform actions requested by the wallet coordinator. To address that problem, we are improving the mechanism to take the full transaction history into account. To make it efficient, we are taking just transaction hashes first and fetch full transaction data only for the latest transactions, where the chance to find the wallet UTXO is the highest. --- pkg/bitcoin/chain.go | 14 ++++- pkg/bitcoin/chain_test.go | 6 ++ pkg/bitcoin/electrum/electrum.go | 58 +++++++++++++------ .../electrum/electrum_integration_test.go | 33 +++++++++++ pkg/maintainer/btcdiff/bitcoin_chain_test.go | 6 ++ pkg/maintainer/spv/bitcoin_chain_test.go | 6 ++ pkg/maintainer/wallet/bitcoin_chain_test.go | 6 ++ pkg/tbtc/bitcoin_chain_test.go | 31 ++++++++++ pkg/tbtc/wallet.go | 27 +++++---- pkg/tbtc/wallet_test.go | 8 +-- 10 files changed, 163 insertions(+), 32 deletions(-) diff --git a/pkg/bitcoin/chain.go b/pkg/bitcoin/chain.go index d3795107c3..43a1c26b3e 100644 --- a/pkg/bitcoin/chain.go +++ b/pkg/bitcoin/chain.go @@ -44,12 +44,24 @@ type Chain interface { // not contain unconfirmed transactions living in the mempool at the moment // of request. The returned transactions list can be limited using the // `limit` parameter. For example, if `limit` is set to `5`, only the - // latest five transactions will be returned. + // latest five transactions will be returned. Note that taking an unlimited + // transaction history may be time-consuming as this function fetches + // complete transactions with all necessary data. GetTransactionsForPublicKeyHash( publicKeyHash [20]byte, limit int, ) ([]*Transaction, error) + // GetTxHashesForPublicKeyHash gets hashes of confirmed transactions that pays + // the given public key hash using either a P2PKH or P2WPKH script. The returned + // transactions hashes are ordered by block height in the ascending order, i.e. + // the latest transaction hash is at the end of the list. The returned list does + // not contain unconfirmed transactions hashes living in the mempool at the + // moment of request. + GetTxHashesForPublicKeyHash( + publicKeyHash [20]byte, + ) ([]Hash, error) + // GetMempoolForPublicKeyHash gets the unconfirmed mempool transactions // that pays the given public key hash using either a P2PKH or P2WPKH script. // The returned transactions are in an indefinite order. diff --git a/pkg/bitcoin/chain_test.go b/pkg/bitcoin/chain_test.go index 74872d80e8..df31a2224a 100644 --- a/pkg/bitcoin/chain_test.go +++ b/pkg/bitcoin/chain_test.go @@ -116,6 +116,12 @@ func (lc *localChain) GetTransactionsForPublicKeyHash( panic("not implemented") } +func (lc *localChain) GetTxHashesForPublicKeyHash( + publicKeyHash [20]byte, +) ([]Hash, error) { + panic("unsupported") +} + func (lc *localChain) GetMempoolForPublicKeyHash( publicKeyHash [20]byte, ) ([]*Transaction, error) { diff --git a/pkg/bitcoin/electrum/electrum.go b/pkg/bitcoin/electrum/electrum.go index 15d2acf0ce..641b667678 100644 --- a/pkg/bitcoin/electrum/electrum.go +++ b/pkg/bitcoin/electrum/electrum.go @@ -348,11 +348,47 @@ func (c *Connection) GetTransactionMerkleProof( // not contain unconfirmed transactions living in the mempool at the moment // of request. The returned transactions list can be limited using the // `limit` parameter. For example, if `limit` is set to `5`, only the -// latest five transactions will be returned. +// latest five transactions will be returned. Note that taking an unlimited +// transaction history may be time-consuming as this function fetches +// complete transactions with all necessary data. func (c *Connection) GetTransactionsForPublicKeyHash( publicKeyHash [20]byte, limit int, ) ([]*bitcoin.Transaction, error) { + txHashes, err := c.GetTxHashesForPublicKeyHash(publicKeyHash) + if err != nil { + return nil, err + } + + var selectedTxHashes []bitcoin.Hash + if len(txHashes) > limit { + selectedTxHashes = txHashes[len(txHashes)-limit:] + } else { + selectedTxHashes = txHashes + } + + transactions := make([]*bitcoin.Transaction, len(selectedTxHashes)) + for i, txHash := range selectedTxHashes { + transaction, err := c.GetTransaction(txHash) + if err != nil { + return nil, fmt.Errorf("cannot get transaction: [%v]", err) + } + + transactions[i] = transaction + } + + return transactions, nil +} + +// GetTxHashesForPublicKeyHash gets hashes of confirmed transactions that pays +// the given public key hash using either a P2PKH or P2WPKH script. The returned +// transactions hashes are ordered by block height in the ascending order, i.e. +// the latest transaction hash is at the end of the list. The returned list does +// not contain unconfirmed transactions hashes living in the mempool at the +// moment of request. +func (c *Connection) GetTxHashesForPublicKeyHash( + publicKeyHash [20]byte, +) ([]bitcoin.Hash, error) { p2pkh, err := bitcoin.PayToPublicKeyHash(publicKeyHash) if err != nil { return nil, fmt.Errorf( @@ -398,24 +434,12 @@ func (c *Connection) GetTransactionsForPublicKeyHash( }, ) - var selectedItems []*scriptHistoryItem - if len(items) > limit { - selectedItems = items[len(items)-limit:] - } else { - selectedItems = items - } - - transactions := make([]*bitcoin.Transaction, len(selectedItems)) - for i, item := range selectedItems { - transaction, err := c.GetTransaction(item.txHash) - if err != nil { - return nil, fmt.Errorf("cannot get transaction: [%v]", err) - } - - transactions[i] = transaction + txHashes := make([]bitcoin.Hash, len(items)) + for i, item := range items { + txHashes[i] = item.txHash } - return transactions, nil + return txHashes, nil } type scriptHistoryItem struct { diff --git a/pkg/bitcoin/electrum/electrum_integration_test.go b/pkg/bitcoin/electrum/electrum_integration_test.go index c5b80c8710..b054cc5e42 100644 --- a/pkg/bitcoin/electrum/electrum_integration_test.go +++ b/pkg/bitcoin/electrum/electrum_integration_test.go @@ -442,6 +442,39 @@ func TestGetTransactionsForPublicKeyHash_Integration(t *testing.T) { }) } +func TestGetTxHashesForPublicKeyHash_Integration(t *testing.T) { + runParallel(t, func(t *testing.T, testConfig testConfig) { + electrum, cancelCtx := newTestConnection(t, testConfig.clientConfig) + defer cancelCtx() + + data, ok := testData.TransactionsForPublicKeyHash[testConfig.network] + if !ok { + t.Fatalf( + "transactions for public key hash data not defined for network %s", + testConfig.network, + ) + } + + publicKeyHash := (*[20]byte)(data.PublicKeyHash) + expectedHashes := data.Transactions + + actualHashes, err := electrum.GetTxHashesForPublicKeyHash(*publicKeyHash) + if err != nil { + t.Fatal(err) + } + + // If the actual hashes set is greater than the expected set, we need + // to adjust them to the same length to make a comparison that makes sense. + if len(actualHashes) > len(expectedHashes) { + actualHashes = actualHashes[len(actualHashes)-len(expectedHashes):] + } + + if diff := deep.Equal(actualHashes, expectedHashes); diff != nil { + t.Errorf("compare failed: %v", diff) + } + }) +} + func TestEstimateSatPerVByteFee_Integration(t *testing.T) { runParallel(t, func(t *testing.T, testConfig testConfig) { electrum, cancelCtx := newTestConnection(t, testConfig.clientConfig) diff --git a/pkg/maintainer/btcdiff/bitcoin_chain_test.go b/pkg/maintainer/btcdiff/bitcoin_chain_test.go index 2704e38b1b..717a95e2eb 100644 --- a/pkg/maintainer/btcdiff/bitcoin_chain_test.go +++ b/pkg/maintainer/btcdiff/bitcoin_chain_test.go @@ -90,6 +90,12 @@ func (lbc *localBitcoinChain) GetTransactionsForPublicKeyHash( panic("unsupported") } +func (lbc *localBitcoinChain) GetTxHashesForPublicKeyHash( + publicKeyHash [20]byte, +) ([]bitcoin.Hash, error) { + panic("unsupported") +} + func (lbc *localBitcoinChain) GetMempoolForPublicKeyHash( publicKeyHash [20]byte, ) ([]*bitcoin.Transaction, error) { diff --git a/pkg/maintainer/spv/bitcoin_chain_test.go b/pkg/maintainer/spv/bitcoin_chain_test.go index 673bc919e8..a692af7aa3 100644 --- a/pkg/maintainer/spv/bitcoin_chain_test.go +++ b/pkg/maintainer/spv/bitcoin_chain_test.go @@ -148,6 +148,12 @@ func (lbc *localBitcoinChain) GetTransactionsForPublicKeyHash( return matchingTransactions, nil } +func (lbc *localBitcoinChain) GetTxHashesForPublicKeyHash( + publicKeyHash [20]byte, +) ([]bitcoin.Hash, error) { + panic("unsupported") +} + func (lbc *localBitcoinChain) GetMempoolForPublicKeyHash(publicKeyHash [20]byte) ( []*bitcoin.Transaction, error, diff --git a/pkg/maintainer/wallet/bitcoin_chain_test.go b/pkg/maintainer/wallet/bitcoin_chain_test.go index 2a250a8ed3..1fbaa646fc 100644 --- a/pkg/maintainer/wallet/bitcoin_chain_test.go +++ b/pkg/maintainer/wallet/bitcoin_chain_test.go @@ -99,6 +99,12 @@ func (lbc *LocalBitcoinChain) GetTransactionsForPublicKeyHash( panic("unsupported") } +func (lbc *LocalBitcoinChain) GetTxHashesForPublicKeyHash( + publicKeyHash [20]byte, +) ([]bitcoin.Hash, error) { + panic("unsupported") +} + func (lbc *LocalBitcoinChain) GetMempoolForPublicKeyHash( publicKeyHash [20]byte, ) ([]*bitcoin.Transaction, error) { diff --git a/pkg/tbtc/bitcoin_chain_test.go b/pkg/tbtc/bitcoin_chain_test.go index 6cdfd8ff50..ff225e8cb9 100644 --- a/pkg/tbtc/bitcoin_chain_test.go +++ b/pkg/tbtc/bitcoin_chain_test.go @@ -123,6 +123,37 @@ func (lbc *localBitcoinChain) GetTransactionsForPublicKeyHash( return matchingTransactions, nil } +func (lbc *localBitcoinChain) GetTxHashesForPublicKeyHash( + publicKeyHash [20]byte, +) ([]bitcoin.Hash, error) { + lbc.transactionsMutex.Lock() + defer lbc.transactionsMutex.Unlock() + + p2pkh, err := bitcoin.PayToPublicKeyHash(publicKeyHash) + if err != nil { + return nil, err + } + + p2wpkh, err := bitcoin.PayToWitnessPublicKeyHash(publicKeyHash) + if err != nil { + return nil, err + } + + matchingTxHashes := make([]bitcoin.Hash, 0) + + for _, transaction := range lbc.transactions { + for _, output := range transaction.Outputs { + script := output.PublicKeyScript + if bytes.Equal(script, p2pkh) || bytes.Equal(script, p2wpkh) { + matchingTxHashes = append(matchingTxHashes, transaction.Hash()) + break + } + } + } + + return matchingTxHashes, nil +} + func (lbc *localBitcoinChain) GetMempoolForPublicKeyHash( publicKeyHash [20]byte, ) ([]*bitcoin.Transaction, error) { diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index 6d39ff0cba..4d3b476cca 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -384,11 +384,6 @@ func (w *wallet) String() string { // currently registered in the Bridge on-chain contract. The returned // main UTXO can be nil if the wallet does not have a main UTXO registered // in the Bridge at the moment. -// -// WARNING: THIS FUNCTION CANNOT DETERMINE THE MAIN UTXO IF IT COMES FROM A -// BITCOIN TRANSACTION THAT IS NOT ONE OF THE LATEST FIVE TRANSACTIONS -// TARGETING THE GIVEN WALLET PUBLIC KEY HASH. HOWEVER, SUCH A CASE IS -// VERY UNLIKELY. func DetermineWalletMainUtxo( walletPublicKeyHash [20]byte, bridgeChain BridgeChain, @@ -412,9 +407,12 @@ func DetermineWalletMainUtxo( // to the second last BTC transaction. In theory, such a gap between // the actual latest BTC transaction and the registered main UTXO in // the Bridge may be even wider. To cover the worst possible cases, we - // always take the five latest transactions made by the wallet for - // consideration. - transactions, err := btcChain.GetTransactionsForPublicKeyHash(walletPublicKeyHash, 5) + // must rely on the full transaction history. Due to performance reasons, + // we are first taking just the transactions hashes (fast call) and then + // fetch full transaction data (time-consuming calls) starting from + // the most recent transactions as there is a high chance the main UTXO + // comes from there. + txHashes, err := btcChain.GetTxHashesForPublicKeyHash(walletPublicKeyHash) if err != nil { return nil, fmt.Errorf("cannot get transactions history for wallet: [%v]", err) } @@ -430,8 +428,17 @@ func DetermineWalletMainUtxo( // Start iterating from the latest transaction as the chance it matches // the wallet main UTXO is the highest. - for i := len(transactions) - 1; i >= 0; i-- { - transaction := transactions[i] + for i := len(txHashes) - 1; i >= 0; i-- { + txHash := txHashes[i] + + transaction, err := btcChain.GetTransaction(txHash) + if err != nil { + return nil, fmt.Errorf( + "cannot get transaction with hash [%s]: [%v]", + txHash.String(), + err, + ) + } // Iterate over transaction's outputs and find the one that targets // the wallet public key hash. diff --git a/pkg/tbtc/wallet_test.go b/pkg/tbtc/wallet_test.go index 977f491d7a..ec46fb0a3e 100644 --- a/pkg/tbtc/wallet_test.go +++ b/pkg/tbtc/wallet_test.go @@ -213,11 +213,11 @@ func TestDetermineWalletMainUtxo(t *testing.T) { expectedMainUtxo: nil, expectedErr: nil, }, - "wallet main UTXO comes from a too old transaction": { - mainUtxoHash: chain.ComputeMainUtxoHash(walletUtxoFrom(transactions[0])), - expectedErr: fmt.Errorf("main UTXO not found"), + "wallet main UTXO comes from the oldest transaction": { + mainUtxoHash: chain.ComputeMainUtxoHash(walletUtxoFrom(transactions[0])), + expectedMainUtxo: walletUtxoFrom(transactions[0]), }, - "wallet main UTXO comes from the oldest acceptable transaction": { + "wallet main UTXO comes from the middle transaction": { mainUtxoHash: chain.ComputeMainUtxoHash(walletUtxoFrom(transactions[1])), expectedMainUtxo: walletUtxoFrom(transactions[1]), }, From 018d03f98587cdaee11faef275d09cf85e30021e Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Mon, 2 Oct 2023 14:44:37 +0200 Subject: [PATCH 2/4] Harden the wallet sync check mechanism The goal of this mechanism is to ensure the previous wallet transaction on Bitcoin chain was properly proved to the Bridge contract. This must be ensured before initiating new wallet transactions in order to maintain proper Bitcoin transaction ordering enforced by the Bridge contract. The current version of this mechanism was a naive implementation that checked whether the wallet main UTXO comes from the latest Bitcoin transaction or, if there was no main UTXO, the wallet doesn't have a transaction history. Additionally, this implementation required the mempool to be empty for both cases. This logic is prone to spam transactions sending funds to wallet addresses which cause the wallet to abandon all actions proposed by the coordinator. Here we fix that by using a more sophisticated mechanism: For wallets having a registered main UTXO, it is enough to check whether their registered UTXO is still among the confirmed unspent outputs from the Bitcoin network standpoint. In order to do that check, we are leveraging ElectrumX `listunspent` method that returns outputs not used as inputs by any (either confirmed or mempool) transaction. If a wallet uses their main UTXO to produce another transaction, `listunspent` will not show it and `EnsureWalletSyncedBetweenChain` will detect this state drift preventing to start another action. For fresh wallets which don't have main UTXO yet, the situation is more complicated. In that case, we are additionally taking mempool UTXOs into account. If there are no UTXOs at all, that implies the wallet has not produced any (either confirmed or mempool) Bitcoin transaction so far. If some UTXOs targets the wallet, we need to check whether they are spam or actually result of proper wallet transaction. We do this by checking the first input of each transaction. Very first transactions of wallets are always deposit sweeps and all their inputs must point to revealed deposit. If the first input refers to a deposit in that case, that means the wallet already produced their first transaction on Bitcoin and no other action should be taken until the corresponding SPV proof is submitted to the Bridge. Otherwise, such a transaction is spam. If all transactions are spam, the wallet can safely start the given action. --- internal/testdata/bitcoin/transaction.go | 17 ++ pkg/bitcoin/chain.go | 21 ++ pkg/bitcoin/chain_test.go | 12 + pkg/bitcoin/electrum/electrum.go | 236 ++++++++++++++++++ .../electrum/electrum_integration_test.go | 50 +++- pkg/maintainer/btcdiff/bitcoin_chain_test.go | 12 + pkg/maintainer/spv/bitcoin_chain_test.go | 12 + pkg/maintainer/wallet/bitcoin_chain_test.go | 12 + pkg/tbtc/bitcoin_chain_test.go | 42 ++++ pkg/tbtc/chain.go | 14 ++ pkg/tbtc/chain_test.go | 7 + pkg/tbtc/deposit_sweep.go | 1 + pkg/tbtc/redemption.go | 1 + pkg/tbtc/wallet.go | 160 +++++++----- 14 files changed, 534 insertions(+), 63 deletions(-) diff --git a/internal/testdata/bitcoin/transaction.go b/internal/testdata/bitcoin/transaction.go index f936a01ea6..5a2e57fce3 100644 --- a/internal/testdata/bitcoin/transaction.go +++ b/internal/testdata/bitcoin/transaction.go @@ -270,6 +270,7 @@ var Transactions = map[bitcoin.Network]map[string]struct { var TransactionsForPublicKeyHash = map[bitcoin.Network]struct { PublicKeyHash []byte Transactions []bitcoin.Hash + Utxos []string // txHash:outputIndex:value sorted asc }{ bitcoin.Testnet: { PublicKeyHash: decodeString("e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0"), @@ -288,6 +289,19 @@ var TransactionsForPublicKeyHash = map[bitcoin.Network]struct { hashFromString("605edd75ae0b4fa7cfc7aae8f1399119e9d7ecc212e6253156b60d60f4925d44"), hashFromString("4f9affc5b418385d5aa61e23caa0b55156bf0682d5fedf2d905446f3f88aec6c"), }, + Utxos: []string{ + "00cc0cd13fc4de7a15cb41ab6d58f8b31c75b6b9b4194958c381441a67d09b08:1:1099200", + "05dabb0291c0a6aa522de5ded5cb6d14ee2159e7ff109d3ef0f21de128b56b94:1:1099200", + "2724545276df61f43f1e92c4b9f1dd3c9109595c022dbd9dc003efbad8ded38b:1:191169", + "3ca4ae3f8ee3b48949192bc7a146c8d9862267816258c85e02a44678364551e1:1:299200", + "44863a79ce2b8fec9792403d5048506e50ffa7338191db0e6c30d3d3358ea2f6:1:299200", + "4c6b33b7c0550e0e536a5d119ac7189d71e1296fcb0c258e0c115356895bc0e6:1:299200", + "4f9affc5b418385d5aa61e23caa0b55156bf0682d5fedf2d905446f3f88aec6c:0:100000", + "605edd75ae0b4fa7cfc7aae8f1399119e9d7ecc212e6253156b60d60f4925d44:1:299200", + "e648838e528ca0666e2612e18634fe86cb7a40fb3c594a444a58c810dd08977b:1:299200", + "ea374ab6842723c647c3fc0ab281ca0641eaa768576cf9df695ca5b827140214:0:10000", + "f65bc5029251f0042aedb37f90dbb2bfb63a2e81694beef9cae5ec62e954c22e:1:299200", + }, }, bitcoin.Mainnet: { PublicKeyHash: decodeString("c3ac203924063c91e70a43c7b97c70745a7635c6"), @@ -298,6 +312,9 @@ var TransactionsForPublicKeyHash = map[bitcoin.Network]struct { hashFromString("d71c0f1ce9c0aa6fe8fed1e0ebb52227b2c8c042e1d27818298a255f94562972"), hashFromString("c7248847ddbcbe4a8b0404ef7e372afff49dc04f26d3f4a27a40cd4a07565ac1"), }, + Utxos: []string{ + "c7248847ddbcbe4a8b0404ef7e372afff49dc04f26d3f4a27a40cd4a07565ac1:0:6411167363", + }, }, } diff --git a/pkg/bitcoin/chain.go b/pkg/bitcoin/chain.go index 43a1c26b3e..f68b3419d6 100644 --- a/pkg/bitcoin/chain.go +++ b/pkg/bitcoin/chain.go @@ -67,6 +67,27 @@ type Chain interface { // The returned transactions are in an indefinite order. GetMempoolForPublicKeyHash(publicKeyHash [20]byte) ([]*Transaction, error) + // GetUtxosForPublicKeyHash gets unspent outputs of confirmed transactions that + // are controlled by the given public key hash (either a P2PKH or P2WPKH script). + // The returned UTXOs are ordered by block height in the ascending order, i.e. + // the latest UTXO is at the end of the list. The returned list does not contain + // unspent outputs of unconfirmed transactions living in the mempool at the + // moment of request. Outputs used as inputs of confirmed or mempool + // transactions are not returned as well because they are no longer UTXOs. + GetUtxosForPublicKeyHash( + publicKeyHash [20]byte, + ) ([]*UnspentTransactionOutput, error) + + // GetMempoolUtxosForPublicKeyHash gets unspent outputs of unconfirmed transactions + // that are controlled by the given public key hash (either a P2PKH or P2WPKH script). + // The returned UTXOs are in an indefinite order. The returned list does not + // contain unspent outputs of confirmed transactions. Outputs used as inputs of + // confirmed or mempool transactions are not returned as well because they are + // no longer UTXOs. + GetMempoolUtxosForPublicKeyHash( + publicKeyHash [20]byte, + ) ([]*UnspentTransactionOutput, error) + // EstimateSatPerVByteFee returns the estimated sat/vbyte fee for a // transaction to be confirmed within the given number of blocks. EstimateSatPerVByteFee(blocks uint32) (int64, error) diff --git a/pkg/bitcoin/chain_test.go b/pkg/bitcoin/chain_test.go index df31a2224a..00b4f9e21e 100644 --- a/pkg/bitcoin/chain_test.go +++ b/pkg/bitcoin/chain_test.go @@ -128,6 +128,18 @@ func (lc *localChain) GetMempoolForPublicKeyHash( panic("not implemented") } +func (lc *localChain) GetUtxosForPublicKeyHash( + publicKeyHash [20]byte, +) ([]*UnspentTransactionOutput, error) { + panic("unsupported") +} + +func (lc *localChain) GetMempoolUtxosForPublicKeyHash( + publicKeyHash [20]byte, +) ([]*UnspentTransactionOutput, error) { + panic("unsupported") +} + func (lc *localChain) EstimateSatPerVByteFee( blocks uint32, ) (int64, error) { diff --git a/pkg/bitcoin/electrum/electrum.go b/pkg/bitcoin/electrum/electrum.go index 641b667678..6d3eeb8352 100644 --- a/pkg/bitcoin/electrum/electrum.go +++ b/pkg/bitcoin/electrum/electrum.go @@ -634,6 +634,242 @@ func (c *Connection) getScriptMempool( return convertedItems, nil } +// GetUtxosForPublicKeyHash gets unspent outputs of confirmed transactions that +// are controlled by the given public key hash (either a P2PKH or P2WPKH script). +// The returned UTXOs are ordered by block height in the ascending order, i.e. +// the latest UTXO is at the end of the list. The returned list does not contain +// unspent outputs of unconfirmed transactions living in the mempool at the +// moment of request. Outputs used as inputs of confirmed or mempool +// transactions are not returned as well because they are no longer UTXOs. +func (c *Connection) GetUtxosForPublicKeyHash( + publicKeyHash [20]byte, +) ([]*bitcoin.UnspentTransactionOutput, error) { + p2pkh, err := bitcoin.PayToPublicKeyHash(publicKeyHash) + if err != nil { + return nil, fmt.Errorf( + "cannot build P2PKH for public key hash [0x%x]: [%v]", + publicKeyHash, + err, + ) + } + + p2wpkh, err := bitcoin.PayToWitnessPublicKeyHash(publicKeyHash) + if err != nil { + return nil, fmt.Errorf( + "cannot build P2WPKH for public key hash [0x%x]: [%v]", + publicKeyHash, + err, + ) + } + + p2pkhItems, err := c.getScriptUtxos(p2pkh, true) + if err != nil { + return nil, fmt.Errorf( + "cannot get P2PKH UTXOs for public key hash [0x%x]: [%v]", + publicKeyHash, + err, + ) + } + + p2wpkhItems, err := c.getScriptUtxos(p2wpkh, true) + if err != nil { + return nil, fmt.Errorf( + "cannot get P2WPKH UTXOs for public key hash [0x%x]: [%v]", + publicKeyHash, + err, + ) + } + + items := append(p2pkhItems, p2wpkhItems...) + + sort.SliceStable( + items, + func(i, j int) bool { + return items[i].blockHeight < items[j].blockHeight + }, + ) + + utxos := make([]*bitcoin.UnspentTransactionOutput, len(items)) + for i, item := range items { + utxos[i] = &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: item.txHash, + OutputIndex: item.outputIndex, + }, + Value: int64(item.value), + } + } + + return utxos, nil +} + +// GetMempoolUtxosForPublicKeyHash gets unspent outputs of unconfirmed transactions +// that are controlled by the given public key hash (either a P2PKH or P2WPKH script). +// The returned UTXOs are in an indefinite order. The returned list does not +// contain unspent outputs of confirmed transactions. Outputs used as inputs of +// confirmed or mempool transactions are not returned as well because they are +// no longer UTXOs. +func (c *Connection) GetMempoolUtxosForPublicKeyHash( + publicKeyHash [20]byte, +) ([]*bitcoin.UnspentTransactionOutput, error) { + p2pkh, err := bitcoin.PayToPublicKeyHash(publicKeyHash) + if err != nil { + return nil, fmt.Errorf( + "cannot build P2PKH for public key hash [0x%x]: [%v]", + publicKeyHash, + err, + ) + } + + p2wpkh, err := bitcoin.PayToWitnessPublicKeyHash(publicKeyHash) + if err != nil { + return nil, fmt.Errorf( + "cannot build P2WPKH for public key hash [0x%x]: [%v]", + publicKeyHash, + err, + ) + } + + p2pkhItems, err := c.getScriptUtxos(p2pkh, false) + if err != nil { + return nil, fmt.Errorf( + "cannot get P2PKH UTXOs for public key hash [0x%x]: [%v]", + publicKeyHash, + err, + ) + } + + p2wpkhItems, err := c.getScriptUtxos(p2wpkh, false) + if err != nil { + return nil, fmt.Errorf( + "cannot get P2WPKH UTXOs for public key hash [0x%x]: [%v]", + publicKeyHash, + err, + ) + } + + items := append(p2pkhItems, p2wpkhItems...) + + utxos := make([]*bitcoin.UnspentTransactionOutput, len(items)) + for i, item := range items { + utxos[i] = &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: item.txHash, + OutputIndex: item.outputIndex, + }, + Value: int64(item.value), + } + } + + return utxos, nil +} + +type scriptUtxoItem struct { + txHash bitcoin.Hash + outputIndex uint32 + value uint64 + blockHeight uint32 +} + +// getScriptUtxos returns unspent outputs of confirmed/unconfirmed transactions +// that are locked using the given script (P2PKH, P2WPKH, P2SH, P2WSH, etc.). +// +// If the `confirmed` flag is true, the returned list contains unspent outputs +// of confirmed transactions, sorted by the block height in the ascending order, +// i.e. the latest UTXO is at the end of the list. The resulting list does not +// contain unspent outputs of unconfirmed transactions living in the mempool +// at the moment of request. +// +// If the `confirmed` flag is false, the returned list contains unspent outputs +// of unconfirmed transactions, in an indefinite order. The resulting list +// does not contain unspent outputs of confirmed transactions. +// +// In both cases, the resulted list DOES NOT CONTAIN outputs already used as +// inputs of confirmed or mempool transactions because they are no longer UTXOs. +func (c *Connection) getScriptUtxos( + script []byte, + confirmed bool, +) ([]*scriptUtxoItem, error) { + scriptHash := sha256.Sum256(script) + reversedScriptHash := byteutils.Reverse(scriptHash[:]) + reversedScriptHashString := hex.EncodeToString(reversedScriptHash) + + items, err := requestWithRetry( + c, + func( + ctx context.Context, + client *electrum.Client, + ) ([]*electrum.ListUnspentResult, error) { + return client.ListUnspent(ctx, reversedScriptHashString) + }, + "ListUnspent", + ) + if err != nil { + return nil, fmt.Errorf( + "failed to get UTXOs for script [0x%x]: [%v]", + script, + err, + ) + } + + // According to https://electrumx.readthedocs.io/en/stable/protocol-methods.html#blockchain-scripthash-listunspent + // unconfirmed items living in the mempool are appended at the end of the + // returned list and their height value is either -1 or 0. That means + // we need to take all items with height >0 to obtain confirmed UTXO + // items and <=0 if we want to take the unconfirmed ones. + var filterFn func(item *electrum.ListUnspentResult) bool + if confirmed { + filterFn = func(item *electrum.ListUnspentResult) bool { + return item.Height > 0 + } + } else { + filterFn = func(item *electrum.ListUnspentResult) bool { + return item.Height <= 0 + } + } + + filteredItems := make([]*scriptUtxoItem, 0) + for _, item := range items { + if filterFn(item) { + txHash, err := bitcoin.NewHashFromString( + item.Hash, + bitcoin.ReversedByteOrder, + ) + if err != nil { + return nil, fmt.Errorf( + "cannot parse hash [%s]: [%v]", + item.Hash, + err, + ) + } + + filteredItems = append( + filteredItems, &scriptUtxoItem{ + txHash: txHash, + outputIndex: item.Position, + value: item.Value, + blockHeight: item.Height, + }, + ) + } + } + + if confirmed { + // The list returned from client.ListUnspent is sorted by the block height + // in the ascending order though we are sorting it again just in case + // (e.g. API contract changes). Sorting makes sense only for confirmed + // items as unconfirmed ones have a block height of -1 or 0. + sort.SliceStable( + filteredItems, + func(i, j int) bool { + return filteredItems[i].blockHeight < filteredItems[j].blockHeight + }, + ) + } + + return filteredItems, nil +} + // EstimateSatPerVByteFee returns the estimated sat/vbyte fee for a // transaction to be confirmed within the given number of blocks. func (c *Connection) EstimateSatPerVByteFee(blocks uint32) (int64, error) { diff --git a/pkg/bitcoin/electrum/electrum_integration_test.go b/pkg/bitcoin/electrum/electrum_integration_test.go index b054cc5e42..f14dd58029 100644 --- a/pkg/bitcoin/electrum/electrum_integration_test.go +++ b/pkg/bitcoin/electrum/electrum_integration_test.go @@ -1,5 +1,3 @@ -//go:build integration - package electrum_test import ( @@ -7,6 +5,7 @@ import ( "encoding/json" "fmt" "math" + "sort" "strings" "testing" "time" @@ -475,6 +474,53 @@ func TestGetTxHashesForPublicKeyHash_Integration(t *testing.T) { }) } +func TestGetUtxosForPublicKeyHash_Integration(t *testing.T) { + runParallel(t, func(t *testing.T, testConfig testConfig) { + electrum, cancelCtx := newTestConnection(t, testConfig.clientConfig) + defer cancelCtx() + + data, ok := testData.TransactionsForPublicKeyHash[testConfig.network] + if !ok { + t.Fatalf( + "transactions for public key hash data not defined for network %s", + testConfig.network, + ) + } + + publicKeyHash := (*[20]byte)(data.PublicKeyHash) + expectedUtxos := data.Utxos + + utxos, err := electrum.GetUtxosForPublicKeyHash(*publicKeyHash) + if err != nil { + t.Fatal(err) + } + + actualUtxos := make([]string, len(utxos)) + for i, utxo := range utxos { + actualUtxos[i] = fmt.Sprintf("%v:%v:%v", + utxo.Outpoint.TransactionHash.Hex(bitcoin.ReversedByteOrder), + utxo.Outpoint.OutputIndex, + utxo.Value, + ) + } + + // Some UTXOs in the test data come from the same block and their + // position is sometimes switched. Let's use another sort criteria + // to achieve a predictable order, i.e. sort the whole UTXO string + // (txHash:outputIndex:value) in the ascending order. + sort.SliceStable( + actualUtxos, + func(i, j int) bool { + return actualUtxos[i] < actualUtxos[j] + }, + ) + + if diff := deep.Equal(actualUtxos, expectedUtxos); diff != nil { + t.Errorf("compare failed: %v", diff) + } + }) +} + func TestEstimateSatPerVByteFee_Integration(t *testing.T) { runParallel(t, func(t *testing.T, testConfig testConfig) { electrum, cancelCtx := newTestConnection(t, testConfig.clientConfig) diff --git a/pkg/maintainer/btcdiff/bitcoin_chain_test.go b/pkg/maintainer/btcdiff/bitcoin_chain_test.go index 717a95e2eb..3dbf262153 100644 --- a/pkg/maintainer/btcdiff/bitcoin_chain_test.go +++ b/pkg/maintainer/btcdiff/bitcoin_chain_test.go @@ -102,6 +102,18 @@ func (lbc *localBitcoinChain) GetMempoolForPublicKeyHash( panic("unsupported") } +func (lbc *localBitcoinChain) GetUtxosForPublicKeyHash( + publicKeyHash [20]byte, +) ([]*bitcoin.UnspentTransactionOutput, error) { + panic("unsupported") +} + +func (lbc *localBitcoinChain) GetMempoolUtxosForPublicKeyHash( + publicKeyHash [20]byte, +) ([]*bitcoin.UnspentTransactionOutput, error) { + panic("unsupported") +} + // SetBlockHeaders sets internal headers for testing purposes. func (lbc *localBitcoinChain) SetBlockHeaders( blockHeaders map[uint]*bitcoin.BlockHeader, diff --git a/pkg/maintainer/spv/bitcoin_chain_test.go b/pkg/maintainer/spv/bitcoin_chain_test.go index a692af7aa3..ed9465a051 100644 --- a/pkg/maintainer/spv/bitcoin_chain_test.go +++ b/pkg/maintainer/spv/bitcoin_chain_test.go @@ -161,6 +161,18 @@ func (lbc *localBitcoinChain) GetMempoolForPublicKeyHash(publicKeyHash [20]byte) panic("unsupported") } +func (lbc *localBitcoinChain) GetUtxosForPublicKeyHash( + publicKeyHash [20]byte, +) ([]*bitcoin.UnspentTransactionOutput, error) { + panic("unsupported") +} + +func (lbc *localBitcoinChain) GetMempoolUtxosForPublicKeyHash( + publicKeyHash [20]byte, +) ([]*bitcoin.UnspentTransactionOutput, error) { + panic("unsupported") +} + func (lbc *localBitcoinChain) EstimateSatPerVByteFee(blocks uint32) ( int64, error, diff --git a/pkg/maintainer/wallet/bitcoin_chain_test.go b/pkg/maintainer/wallet/bitcoin_chain_test.go index 1fbaa646fc..eb608482f2 100644 --- a/pkg/maintainer/wallet/bitcoin_chain_test.go +++ b/pkg/maintainer/wallet/bitcoin_chain_test.go @@ -111,6 +111,18 @@ func (lbc *LocalBitcoinChain) GetMempoolForPublicKeyHash( panic("unsupported") } +func (lbc *LocalBitcoinChain) GetUtxosForPublicKeyHash( + publicKeyHash [20]byte, +) ([]*bitcoin.UnspentTransactionOutput, error) { + panic("unsupported") +} + +func (lbc *LocalBitcoinChain) GetMempoolUtxosForPublicKeyHash( + publicKeyHash [20]byte, +) ([]*bitcoin.UnspentTransactionOutput, error) { + panic("unsupported") +} + func (lbc *LocalBitcoinChain) EstimateSatPerVByteFee( blocks uint32, ) (int64, error) { diff --git a/pkg/tbtc/bitcoin_chain_test.go b/pkg/tbtc/bitcoin_chain_test.go index ff225e8cb9..7efabc9398 100644 --- a/pkg/tbtc/bitcoin_chain_test.go +++ b/pkg/tbtc/bitcoin_chain_test.go @@ -185,6 +185,48 @@ func (lbc *localBitcoinChain) GetMempoolForPublicKeyHash( return matchingTransactions, nil } +func (lbc *localBitcoinChain) GetUtxosForPublicKeyHash( + publicKeyHash [20]byte, +) ([]*bitcoin.UnspentTransactionOutput, error) { + lbc.transactionsMutex.Lock() + defer lbc.transactionsMutex.Unlock() + + p2pkh, err := bitcoin.PayToPublicKeyHash(publicKeyHash) + if err != nil { + return nil, err + } + + p2wpkh, err := bitcoin.PayToWitnessPublicKeyHash(publicKeyHash) + if err != nil { + return nil, err + } + + matchingUtxos := make([]*bitcoin.UnspentTransactionOutput, 0) + + for _, transaction := range lbc.transactions { + for i, output := range transaction.Outputs { + script := output.PublicKeyScript + if bytes.Equal(script, p2pkh) || bytes.Equal(script, p2wpkh) { + matchingUtxos = append(matchingUtxos, &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: transaction.Hash(), + OutputIndex: uint32(i), + }, + Value: output.Value, + }) + } + } + } + + return matchingUtxos, nil +} + +func (lbc *localBitcoinChain) GetMempoolUtxosForPublicKeyHash( + publicKeyHash [20]byte, +) ([]*bitcoin.UnspentTransactionOutput, error) { + return nil, nil +} + func (lbc *localBitcoinChain) EstimateSatPerVByteFee( blocks uint32, ) (int64, error) { diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index efc3ef0f8b..533b5ab925 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -207,6 +207,20 @@ type BridgeChain interface { walletPublicKeyHash [20]byte, redeemerOutputScript bitcoin.Script, ) (*RedemptionRequest, bool, 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) } // NewWalletRegisteredEvent represents a new wallet registered event. diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 07ff55ecb3..4035ec7f16 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -543,6 +543,13 @@ 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, diff --git a/pkg/tbtc/deposit_sweep.go b/pkg/tbtc/deposit_sweep.go index 68e0785fbb..902ca3722d 100644 --- a/pkg/tbtc/deposit_sweep.go +++ b/pkg/tbtc/deposit_sweep.go @@ -131,6 +131,7 @@ func (dsa *depositSweepAction) execute() error { err = EnsureWalletSyncedBetweenChains( walletPublicKeyHash, walletMainUtxo, + dsa.chain, dsa.btcChain, ) if err != nil { diff --git a/pkg/tbtc/redemption.go b/pkg/tbtc/redemption.go index 56d4e52801..472966bf6f 100644 --- a/pkg/tbtc/redemption.go +++ b/pkg/tbtc/redemption.go @@ -173,6 +173,7 @@ func (ra *redemptionAction) execute() error { err = EnsureWalletSyncedBetweenChains( walletPublicKeyHash, walletMainUtxo, + ra.chain, ra.btcChain, ) if err != nil { diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index 4d3b476cca..0a1ff1b67e 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -471,89 +471,127 @@ func DetermineWalletMainUtxo( } // EnsureWalletSyncedBetweenChains makes sure all actions taken by the wallet -// on the Bitcoin chain are reflected in the host chain Bridge. This translates -// to two conditions that must be met: -// - The wallet main UTXO registered in the host chain Bridge comes from the -// latest BTC transaction OR wallet main UTXO is unset and wallet's BTC -// transaction history is empty. This condition ensures that all expected SPV -// proofs of confirmed BTC transactions were submitted to the host chain Bridge -// thus the wallet state held known to the Bridge matches the actual state -// on the BTC chain. -// - There are no pending BTC transactions in the mempool. This condition -// ensures the wallet doesn't currently perform any action on the BTC chain. -// Such a transactions indicate a possible state change in the future -// but their outcome cannot be determined at this stage so, the wallet -// should not perform new actions at the moment. +// on the Bitcoin chain are reflected in the host chain Bridge. func EnsureWalletSyncedBetweenChains( walletPublicKeyHash [20]byte, walletMainUtxo *bitcoin.UnspentTransactionOutput, + bridgeChain BridgeChain, btcChain bitcoin.Chain, ) error { - // Take the recent transactions history for the wallet. - history, err := btcChain.GetTransactionsForPublicKeyHash(walletPublicKeyHash, 5) + // Take UTXOs controlled by the wallet on Bitcoin chain. Those are outputs + // coming from confirmed transactions, ready to be spent right now, and + // not used as inputs of other (either confirmed or mempool) transactions. + confirmedUtxos, err := btcChain.GetUtxosForPublicKeyHash(walletPublicKeyHash) if err != nil { - return fmt.Errorf("cannot get transactions history: [%v]", err) + return fmt.Errorf("cannot get confirmed UTXOs: [%v]", err) } if walletMainUtxo != nil { - // If the wallet main UTXO exists, the transaction history must + // If the wallet main UTXO exists, the UTXOs set must // contain at least one item. If it is empty, something went // really wrong. This should never happen but check this scenario // just in case. - if len(history) == 0 { + if len(confirmedUtxos) == 0 { return fmt.Errorf( - "wallet main UTXO exists but there are no BTC " + - "transactions produced by the wallet", + "wallet main UTXO exists but there are no " + + "UTXOs controlled by the wallet on Bitcoin chain", ) } - // The transaction history is not empty for sure. Take the latest BTC - // transaction from the history. - latestTransaction := history[len(history)-1] - - // Make sure the wallet main UTXO comes from the latest transaction. - // That means all expected SPV proofs were submitted to the Bridge. - // If the wallet main UTXO transaction hash doesn't match the latest - // transaction, that means the SPV proof for the latest transaction was - // not submitted to the Bridge yet. - // - // Note that it is enough to check that the wallet main UTXO transaction - // hash matches the latest transaction hash. There is no way the main - // UTXO changes and the transaction hash stays the same. The Bridge - // enforces that all wallet transactions form a sequence and refer - // each other. - if walletMainUtxo.Outpoint.TransactionHash != latestTransaction.Hash() { - return fmt.Errorf( - "wallet main UTXO doesn't come from the latest BTC transaction", - ) + // Start iterating from the latest UTXO as the chance it matches + // the wallet main UTXO is the highest. + for i := len(confirmedUtxos) - 1; i >= 0; i-- { + utxo := confirmedUtxos[i] + + // If the wallet main UTXO is among the UTXOs returned by Bitcoin + // client, that means the wallet has not spent it by creating + // a Bitcoin transaction. That implies the wallet is not doing + // any action on Bitcoin right now and their state here is synced + // with the host chain Bridge. + if walletMainUtxo.Outpoint.TransactionHash == utxo.Outpoint.TransactionHash && + walletMainUtxo.Outpoint.OutputIndex == utxo.Outpoint.OutputIndex && + walletMainUtxo.Value == utxo.Value { + return nil + } } + + return fmt.Errorf("wallet main UTXO registered in the " + + "host chain Bridge is actually spent on Bitcoin; " + + "Bridge is probably awaiting some SPV proofs", + ) } else { - // If the wallet main UTXO doesn't exist, the transaction history must - // be empty. If it is not, that could mean there is a Bitcoin transaction - // produced by the wallet whose SPV proof was not submitted to - // the Bridge yet. - if len(history) != 0 { - return fmt.Errorf( - "wallet main UTXO doesn't exist but there are BTC " + - "transactions produced by the wallet", - ) + // Otherwise, the wallet is a fresh one and requires special + // treatment. We need to minimize the chance the wallet is + // currently doing their first Bitcoin transaction but, in the same + // time, we cannot just assume their transaction history must be + // empty as there can be spam transactions which arbitrarily send BTC + // to the wallet address. We need to look at the confirmed and mempool + // UTXOs and make sure there are no transactions produced by the wallet + // there. + mempoolUtxos, err := btcChain.GetMempoolUtxosForPublicKeyHash(walletPublicKeyHash) + if err != nil { + return fmt.Errorf("cannot get mempool UTXOs: [%v]", err) } - } - // Regardless of the main UTXO state, we need to make sure that - // no pending wallet transactions exist in the mempool. That way, - // we are handling a plenty of corner cases like transactions races - // that could potentially lead to fraudulent transactions and funds loss. - mempool, err := btcChain.GetMempoolForPublicKeyHash(walletPublicKeyHash) - if err != nil { - return fmt.Errorf("cannot get mempool: [%v]", err) - } + allUtxos := append(confirmedUtxos, mempoolUtxos...) + if len(allUtxos) == 0 { + // Wallet have not produced any transactions - we are good. + return nil + } - if len(mempool) != 0 { - return fmt.Errorf("unconfirmed transactions exist in the mempool") - } + for _, utxo := range allUtxos { + // We know that valid first transaction of the wallet always + // have just one output. Any utxos with output index other + // than 0 are certainly not produced by the wallet and, we should + // not take them into account. + if utxo.Outpoint.OutputIndex != 0 { + continue + } - return nil + transaction, err := btcChain.GetTransaction(utxo.Outpoint.TransactionHash) + if err != nil { + return fmt.Errorf( + "cannot get transaction with hash [%s]: [%v]", + utxo.Outpoint.TransactionHash.String(), + err, + ) + } + + // We know that valid first transaction of the wallet have all their + // inputs referring to revealed deposits. We need to check just + // one input. If it points to a revealed deposit, that means + // the given transaction is produced by our wallet. Otherwise, + // such a transaction is a spam. + input := transaction.Inputs[0] + _, isDeposit, err := bridgeChain.GetDepositRequest( + input.Outpoint.TransactionHash, + input.Outpoint.OutputIndex, + ) + if err != nil { + return fmt.Errorf( + "cannot get deposit request for hash [%s] "+ + "and output index [%v]: [%v]", + input.Outpoint.TransactionHash.String(), + input.Outpoint.OutputIndex, + err, + ) + } + + if isDeposit { + // If that's the case, the wallet was already done their + // first Bitcoin transaction and the Bridge is awaiting the + // SPV proof. + return fmt.Errorf("wallet already produced their first " + + "Bitcoin transaction; Bridge is probably awaiting the SPV proof", + ) + } + + // If the transaction does not refer revealed deposits, it is + // a spam, and we go to the next one. + } + + return nil + } } // signer represents a threshold signer of a tBTC wallet. A signer holds From 5cf18875413964f8ae67ce914fa3734ceb980f61 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Mon, 2 Oct 2023 15:32:19 +0200 Subject: [PATCH 3/4] Bring back electrum integration build tag --- pkg/bitcoin/electrum/electrum_integration_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/bitcoin/electrum/electrum_integration_test.go b/pkg/bitcoin/electrum/electrum_integration_test.go index f14dd58029..93eb7f3473 100644 --- a/pkg/bitcoin/electrum/electrum_integration_test.go +++ b/pkg/bitcoin/electrum/electrum_integration_test.go @@ -1,3 +1,5 @@ +//go:build integration + package electrum_test import ( From 22d0be819e3d2bcde7def6861fc9cae6e5cea586 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Mon, 2 Oct 2023 17:58:22 +0200 Subject: [PATCH 4/4] Use another public key hash for Electrum integration tests on mainnet --- internal/testdata/bitcoin/transaction.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/testdata/bitcoin/transaction.go b/internal/testdata/bitcoin/transaction.go index 5a2e57fce3..e4e2c6e2ed 100644 --- a/internal/testdata/bitcoin/transaction.go +++ b/internal/testdata/bitcoin/transaction.go @@ -304,16 +304,16 @@ var TransactionsForPublicKeyHash = map[bitcoin.Network]struct { }, }, bitcoin.Mainnet: { - PublicKeyHash: decodeString("c3ac203924063c91e70a43c7b97c70745a7635c6"), + PublicKeyHash: decodeString("b0ba76edfe18e81365bddd1d46511a57a4ff8dce"), Transactions: []bitcoin.Hash{ - hashFromString("546c6d90285334a2b84c412c2d541db1f96bb22df6dc9f674c6df8a15506df02"), - hashFromString("948d9b3a1f35c2bcf39f1c08c7df8c3e0b4a9331985a8890c9ba1e1d66b05f8b"), - hashFromString("fbe0689ea2ff2e89c978406819b16e119a9842d9b11bb7d19b31c38693d2db11"), - hashFromString("d71c0f1ce9c0aa6fe8fed1e0ebb52227b2c8c042e1d27818298a255f94562972"), - hashFromString("c7248847ddbcbe4a8b0404ef7e372afff49dc04f26d3f4a27a40cd4a07565ac1"), + hashFromString("4099f8d3e7dcbf3e4df50daae839cace2630b653175289448bcd2cc3b796149c"), + hashFromString("58fe99a67333f88db25d991eefd27589c3866f45308c2f325ee0ef80d6a164bc"), + hashFromString("d48507610c55a33c9c72d8e055a880c7ee4e9b1e1d22c6c7cd7595efef90ad44"), + hashFromString("f649c502e875b7b51bb68206ae8e655c86cccc4b13979aaf241b63ba617c40e4"), + hashFromString("42eae14e7234004c48f335baf7d38e799d7a44bf7a6203aaadb1f558e4e74f7f"), }, Utxos: []string{ - "c7248847ddbcbe4a8b0404ef7e372afff49dc04f26d3f4a27a40cd4a07565ac1:0:6411167363", + "42eae14e7234004c48f335baf7d38e799d7a44bf7a6203aaadb1f558e4e74f7f:0:302376", }, }, }