Skip to content

Commit 742e373

Browse files
authored
Hardened wallet actions (#3723)
Here we introduce some improvements regarding two mechanisms important for wallet actions execution: ### Harden the wallet main UTXO lookup mechanism The goal of that mechanism is to determine the plain-text wallet main UTXO whose hash is registered in the `Bridge` contract. The plain-text version is necessary to construct wallet transactions on Bitcoin. The current version of the main UTXO lookup mechanism looks just at 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 from performing 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 fetching full transaction data only for the latest transactions, where the chance to find the wallet UTXO is the highest. ### 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. (see #3559 for further reference) 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 arbitrarily. Such spam transactions can 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.
2 parents 94ed595 + 22d0be8 commit 742e373

File tree

15 files changed

+703
-99
lines changed

15 files changed

+703
-99
lines changed

internal/testdata/bitcoin/transaction.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ var Transactions = map[bitcoin.Network]map[string]struct {
270270
var TransactionsForPublicKeyHash = map[bitcoin.Network]struct {
271271
PublicKeyHash []byte
272272
Transactions []bitcoin.Hash
273+
Utxos []string // txHash:outputIndex:value sorted asc
273274
}{
274275
bitcoin.Testnet: {
275276
PublicKeyHash: decodeString("e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0"),
@@ -288,15 +289,31 @@ var TransactionsForPublicKeyHash = map[bitcoin.Network]struct {
288289
hashFromString("605edd75ae0b4fa7cfc7aae8f1399119e9d7ecc212e6253156b60d60f4925d44"),
289290
hashFromString("4f9affc5b418385d5aa61e23caa0b55156bf0682d5fedf2d905446f3f88aec6c"),
290291
},
292+
Utxos: []string{
293+
"00cc0cd13fc4de7a15cb41ab6d58f8b31c75b6b9b4194958c381441a67d09b08:1:1099200",
294+
"05dabb0291c0a6aa522de5ded5cb6d14ee2159e7ff109d3ef0f21de128b56b94:1:1099200",
295+
"2724545276df61f43f1e92c4b9f1dd3c9109595c022dbd9dc003efbad8ded38b:1:191169",
296+
"3ca4ae3f8ee3b48949192bc7a146c8d9862267816258c85e02a44678364551e1:1:299200",
297+
"44863a79ce2b8fec9792403d5048506e50ffa7338191db0e6c30d3d3358ea2f6:1:299200",
298+
"4c6b33b7c0550e0e536a5d119ac7189d71e1296fcb0c258e0c115356895bc0e6:1:299200",
299+
"4f9affc5b418385d5aa61e23caa0b55156bf0682d5fedf2d905446f3f88aec6c:0:100000",
300+
"605edd75ae0b4fa7cfc7aae8f1399119e9d7ecc212e6253156b60d60f4925d44:1:299200",
301+
"e648838e528ca0666e2612e18634fe86cb7a40fb3c594a444a58c810dd08977b:1:299200",
302+
"ea374ab6842723c647c3fc0ab281ca0641eaa768576cf9df695ca5b827140214:0:10000",
303+
"f65bc5029251f0042aedb37f90dbb2bfb63a2e81694beef9cae5ec62e954c22e:1:299200",
304+
},
291305
},
292306
bitcoin.Mainnet: {
293-
PublicKeyHash: decodeString("c3ac203924063c91e70a43c7b97c70745a7635c6"),
307+
PublicKeyHash: decodeString("b0ba76edfe18e81365bddd1d46511a57a4ff8dce"),
294308
Transactions: []bitcoin.Hash{
295-
hashFromString("546c6d90285334a2b84c412c2d541db1f96bb22df6dc9f674c6df8a15506df02"),
296-
hashFromString("948d9b3a1f35c2bcf39f1c08c7df8c3e0b4a9331985a8890c9ba1e1d66b05f8b"),
297-
hashFromString("fbe0689ea2ff2e89c978406819b16e119a9842d9b11bb7d19b31c38693d2db11"),
298-
hashFromString("d71c0f1ce9c0aa6fe8fed1e0ebb52227b2c8c042e1d27818298a255f94562972"),
299-
hashFromString("c7248847ddbcbe4a8b0404ef7e372afff49dc04f26d3f4a27a40cd4a07565ac1"),
309+
hashFromString("4099f8d3e7dcbf3e4df50daae839cace2630b653175289448bcd2cc3b796149c"),
310+
hashFromString("58fe99a67333f88db25d991eefd27589c3866f45308c2f325ee0ef80d6a164bc"),
311+
hashFromString("d48507610c55a33c9c72d8e055a880c7ee4e9b1e1d22c6c7cd7595efef90ad44"),
312+
hashFromString("f649c502e875b7b51bb68206ae8e655c86cccc4b13979aaf241b63ba617c40e4"),
313+
hashFromString("42eae14e7234004c48f335baf7d38e799d7a44bf7a6203aaadb1f558e4e74f7f"),
314+
},
315+
Utxos: []string{
316+
"42eae14e7234004c48f335baf7d38e799d7a44bf7a6203aaadb1f558e4e74f7f:0:302376",
300317
},
301318
},
302319
}

pkg/bitcoin/chain.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,50 @@ type Chain interface {
4444
// not contain unconfirmed transactions living in the mempool at the moment
4545
// of request. The returned transactions list can be limited using the
4646
// `limit` parameter. For example, if `limit` is set to `5`, only the
47-
// latest five transactions will be returned.
47+
// latest five transactions will be returned. Note that taking an unlimited
48+
// transaction history may be time-consuming as this function fetches
49+
// complete transactions with all necessary data.
4850
GetTransactionsForPublicKeyHash(
4951
publicKeyHash [20]byte,
5052
limit int,
5153
) ([]*Transaction, error)
5254

55+
// GetTxHashesForPublicKeyHash gets hashes of confirmed transactions that pays
56+
// the given public key hash using either a P2PKH or P2WPKH script. The returned
57+
// transactions hashes are ordered by block height in the ascending order, i.e.
58+
// the latest transaction hash is at the end of the list. The returned list does
59+
// not contain unconfirmed transactions hashes living in the mempool at the
60+
// moment of request.
61+
GetTxHashesForPublicKeyHash(
62+
publicKeyHash [20]byte,
63+
) ([]Hash, error)
64+
5365
// GetMempoolForPublicKeyHash gets the unconfirmed mempool transactions
5466
// that pays the given public key hash using either a P2PKH or P2WPKH script.
5567
// The returned transactions are in an indefinite order.
5668
GetMempoolForPublicKeyHash(publicKeyHash [20]byte) ([]*Transaction, error)
5769

70+
// GetUtxosForPublicKeyHash gets unspent outputs of confirmed transactions that
71+
// are controlled by the given public key hash (either a P2PKH or P2WPKH script).
72+
// The returned UTXOs are ordered by block height in the ascending order, i.e.
73+
// the latest UTXO is at the end of the list. The returned list does not contain
74+
// unspent outputs of unconfirmed transactions living in the mempool at the
75+
// moment of request. Outputs used as inputs of confirmed or mempool
76+
// transactions are not returned as well because they are no longer UTXOs.
77+
GetUtxosForPublicKeyHash(
78+
publicKeyHash [20]byte,
79+
) ([]*UnspentTransactionOutput, error)
80+
81+
// GetMempoolUtxosForPublicKeyHash gets unspent outputs of unconfirmed transactions
82+
// that are controlled by the given public key hash (either a P2PKH or P2WPKH script).
83+
// The returned UTXOs are in an indefinite order. The returned list does not
84+
// contain unspent outputs of confirmed transactions. Outputs used as inputs of
85+
// confirmed or mempool transactions are not returned as well because they are
86+
// no longer UTXOs.
87+
GetMempoolUtxosForPublicKeyHash(
88+
publicKeyHash [20]byte,
89+
) ([]*UnspentTransactionOutput, error)
90+
5891
// EstimateSatPerVByteFee returns the estimated sat/vbyte fee for a
5992
// transaction to be confirmed within the given number of blocks.
6093
EstimateSatPerVByteFee(blocks uint32) (int64, error)

pkg/bitcoin/chain_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,30 @@ func (lc *localChain) GetTransactionsForPublicKeyHash(
116116
panic("not implemented")
117117
}
118118

119+
func (lc *localChain) GetTxHashesForPublicKeyHash(
120+
publicKeyHash [20]byte,
121+
) ([]Hash, error) {
122+
panic("unsupported")
123+
}
124+
119125
func (lc *localChain) GetMempoolForPublicKeyHash(
120126
publicKeyHash [20]byte,
121127
) ([]*Transaction, error) {
122128
panic("not implemented")
123129
}
124130

131+
func (lc *localChain) GetUtxosForPublicKeyHash(
132+
publicKeyHash [20]byte,
133+
) ([]*UnspentTransactionOutput, error) {
134+
panic("unsupported")
135+
}
136+
137+
func (lc *localChain) GetMempoolUtxosForPublicKeyHash(
138+
publicKeyHash [20]byte,
139+
) ([]*UnspentTransactionOutput, error) {
140+
panic("unsupported")
141+
}
142+
125143
func (lc *localChain) EstimateSatPerVByteFee(
126144
blocks uint32,
127145
) (int64, error) {

0 commit comments

Comments
 (0)