Skip to content

Commit

Permalink
Hardened wallet actions (#3723)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tomaszslabon authored Oct 2, 2023
2 parents 94ed595 + 22d0be8 commit 742e373
Show file tree
Hide file tree
Showing 15 changed files with 703 additions and 99 deletions.
29 changes: 23 additions & 6 deletions internal/testdata/bitcoin/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -288,15 +289,31 @@ 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"),
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{
"42eae14e7234004c48f335baf7d38e799d7a44bf7a6203aaadb1f558e4e74f7f:0:302376",
},
},
}
Expand Down
35 changes: 34 additions & 1 deletion pkg/bitcoin/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,50 @@ 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.
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)
Expand Down
18 changes: 18 additions & 0 deletions pkg/bitcoin/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,30 @@ 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) {
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) {
Expand Down
Loading

0 comments on commit 742e373

Please sign in to comment.