Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client/asset/btc: SPV Wallet Transaction History #2550

Merged
merged 6 commits into from
Nov 5, 2023

Conversation

martonp
Copy link
Contributor

@martonp martonp commented Sep 30, 2023

This diff implements TransactionHistory for btc SPV wallets. A database is created to store all the transactions related to the wallet. Although the SPV wallet stores some transaction information already, the list transactions output is unreliable and confusing, so it is better to not parse it every time the tx history is requested. Also, it is not possible to differentiate a send and a swap transaction.

Incoming transactions are discovered by querying ListTransactionsSinceBlock every block. There is also special handling for bond creation transactions which are not immediately submitted.

Comment on lines 1493 to 1503
btc.txHistoryDB.Store(txHistoryDB)
if err != nil {
return nil, fmt.Errorf("failed to create tx history db: %v", err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you store on err here on purpose?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope.

Comment on lines 3987 to 3989
copyWt := *wt
copyWt.Submitted = true
btc.pendingTxs[hash] = &copyWt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should work?

Suggested change
copyWt := *wt
copyWt.Submitted = true
btc.pendingTxs[hash] = &copyWt
wt.Submitted = true

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a mutex.

Comment on lines +6366 to +6180
{
const blockQueryBuffer = 3
var blockToQuery uint64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the new block of code to scope these variables?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I just put it there so it could be collapsed in the editor.

}
recentTxs, err := btc.node.listTransactionsSinceBlock(int32(blockToQuery))
if err != nil {
btc.log.Errorf("Error listing transactions since block %d: %v", tip-6, err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
btc.log.Errorf("Error listing transactions since block %d: %v", tip-6, err)
btc.log.Errorf("Error listing transactions since block %d: %v", blockToQuery, err)


var fee uint64
if tx.Fee != nil {
fee = toSatoshi(-*tx.Fee)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess fee will always be reported as positive? I think dcrwallet returns a negative for fees. I have not checked yet just thinking out loud.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fee is also negative in btcwallet. That's why I'm negating it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh ok.

Comment on lines 6482 to 6307
if tx.BlockNumber > 0 && tip > tx.BlockNumber {
confs = tip - tx.BlockNumber + 1
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if tx.BlockNumber > 0 && tip > tx.BlockNumber {
confs = tip - tx.BlockNumber + 1
}
if tx.BlockNumber > 0 && tip >= tx.BlockNumber {
confs = tip - tx.BlockNumber + 1
}

Comment on lines +6476 to +6302
} else if gtr.BlockHash == "" && tx.BlockNumber != 0 {
tx.BlockNumber = 0
updated = true
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this indicate the tx had confs and now does not? So a reorg? If so the final if updated should not remove this one from pendingTxs?

Comment on lines 6503 to 6504
// TxHistory returns all the transactions the wallet has made. This
// includes the ETH wallet and all token wallets. If refID is nil, then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// TxHistory returns all the transactions the wallet has made. This
// includes the ETH wallet and all token wallets. If refID is nil, then
// TxHistory returns all the transactions the wallet has made. If refID is nil, then

Comment on lines +322 to +328
if !wt.Confirmed {
txs = append(txs, &wt)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't they all be unconfirmed if in the pending txn bucket?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't just look at the pending transactions. The elements with blockPrefix come before the elements with pendingPrefix. This looks at those as well.

it := txn.NewIterator(opts)
defer it.Close()

for it.Seek(maxPendingKey); it.Valid(); it.Next() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for it.Seek(maxPendingKey); it.Valid(); it.Next() {
for it.Seek(maxPendingKey); it.ValidForPrefix(pendingPrefix); it.Next() {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This search checks both pendingPrefix and blockPrefix.

This diff implements `TransactionHistory` for btc SPV wallets. A database
is created to store all the transactions related to the wallet. Although
the SPV wallet stores some transaction information already, the list
transactions output is unreliable and confusing, so it is better to not
parse it every time the tx history is requested. Also, it is not possible
to differentiate a send and a swap transaction.

Incoming transactions are discovered by querying
`ListTransactionsSinceBlock` every block. There is also special handling
for bond creation transactions which are not immediately submitted.
@martonp martonp force-pushed the btcTxHistory branch 2 times, most recently from b30df47 to 02ed2dd Compare October 6, 2023 08:31
@@ -450,7 +450,8 @@ type BTCCloneCFG struct {
// OmitRPCOptionsArg is for clones that don't take an options argument.
OmitRPCOptionsArg bool
// AssetID is the asset ID of the clone.
AssetID uint32
AssetID uint32
SupportTxHistory bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This setting is only used internally. It's also curious, since we have a WalletHistorian interface, so we should just implement that on the appropriate wallets.

How about buck54321/dcrdex@2acb714...buck54321:dcrdex:btctxhistorianinterface ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I was just putting off testing with LTC and BCH.. I've tested now though and they are both working as well.

// If past is true, the transactions prior to the refID are returned, otherwise
// the transactions after the refID are returned. n is the number of
// transactions to return. If n is <= 0, all the transactions will be returned.
func (btc *intermediaryWallet) TxHistory(n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just make this func (btc *ExchangeWalletSPV), no?

Comment on lines 638 to 642
func (wc *electrumWallet) listTransactionsSinceBlock(blockHeight int32) ([]btcjson.ListTransactionsResult, error) {
return nil, fmt.Errorf("not supported")
}

// part of btc.Wallet interface
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer to use a custom interface.

buck54321/dcrdex@5beef90...buck54321:dcrdex:txlister

Comment on lines 171 to 175
if currBlockKey[0] == pendingPrefix[0] {
if tx.BlockNumber > 0 {
needNewBlockKey = true
}
} else if currBlockKey[0] == blockPrefix[0] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a helper function?

func hasPrefix(b, prefix []byte) bool {
	if len(b) < len(prefix) {
		return false
	}
	return bytes.Equal(b[:len(prefix)], prefix)
}

return
}

var hash chainhash.Hash
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

txHash

Comment on lines +4035 to +4039
txHistoryDB := btc.txDB()
if txHistoryDB == nil {
return
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we want to add the tx to the pendingTxs map?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this isn't like ETH where we're using the pendingTxs to calculate a pending balance. pendingTxs is just now getting added in this PR. It's just used to keep track of which transactions in the db may need to be updated whenever there's a new block.


for _, tx := range recentTxs {
if tx.Category == "receive" {
hash, err := chainhash.NewHashFromStr(tx.TxID)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

txHash

type txDB interface {
storeTx(tx *extendedWalletTx) error
markTxAsSubmitted(txID dex.Bytes) error
getTxs(n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document getTxs and other complex areas of code. I see a line like this in checkPendingTxs

_, err = txHistoryDB.getTxs(1, &txID, false)

and it raises a bunch of questions. Does the past argument matter if n is 1? Is refID just a starting point for listing txs, or does that exact key have to be in the db? If you are just checking for the presence of a tx in the db, wouldn't a getTx method be appropriate?

I have to look at the implementation for review anyway, but we don't need to doom others to the same fate for eternity. Good docs save man hours.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's just getting a single transaction, and you're right, a getTx method would be nicer. I'll add some docs.

log: log}, nil
}

func (db *badgerTxDB) findFreeBlockKey(txn *badger.Txn, blockNumber uint64) ([]byte, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could have sworn I already commented on this, but apparently not.

Why not just use the tx's block index?

buck54321/dcrdex@0cc91cc...buck54321:dcrdex:indexsequence

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the change, then I realized that btcwallet is not returning the block index. I guess we could update btcwallet, ltcwallet, and bchwallet, but is it really worth it? We have to do the this free key finding loop for pending transactions anyways.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof. No. Just use findFreeBlockKey.

@@ -5690,11 +5858,16 @@ func (btc *baseWallet) MakeBondTx(ver uint16, amt, feeRate uint64, lockTime time
return nil, nil, fmt.Errorf("failed to fund bond tx: %w", err)
}

var txIDToRemoveFromHistory *chainhash.Hash // will be non-nil if tx was added to history
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting that a tx hash != a tx ID. A tx ID is a byte-reversed string of the tx hash.


pendingTxsCopy := make(map[chainhash.Hash]*extendedWalletTx, len(btc.pendingTxs))
btc.pendingTxsMtx.RLock()
for hash, tx := range btc.pendingTxs {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not going to hold this up, but please don't use hash as a variable. Use h when it unambiguous what kind of hash it is, as it is here, or use txHash or blockHash anywhere. Not only is hash ambiguous in many contexts, it's also the name of a standard library Go package.

@buck54321 buck54321 merged commit c4431ff into decred:master Nov 5, 2023
5 checks passed
@martonp martonp deleted the btcTxHistory branch January 20, 2024 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants