Open
Description
Describe the bug
In wallets with slightly larger derivation gap, outputs spent at a higher derivation index are recognized inconsistently. After loading the wallet from sqlite, a spent output may be incorrectly reported as unspent leaving the wallet in unusable state. This appears to happen because KeychainTxOutIndex
isn't consistently persisted/restored from the sqlite backend.
To Reproduce
Tricky to reproduce! Here's a test using the higher level Wallet
which consists of two transactions:
tx1
creates some outputs with different gapstx2
spends an output with a larger gap.- Initially, wallet ignores the output with the larger gap, and it doesn't track its spend in tx2.
- After calling
Wallet::load
, the wallet recognizes that output (it didn't previously). It's also incorrectly marked as unspent (because it didn't initially track the spend in tx2).
#[test]
fn test_inconsistent_last_revealed_index_after_reload() {
let mut conn = Connection::open_in_memory().expect("memory db");
let mut wallet = Wallet::create(
"tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)",
"tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)",
)
.network(Network::Regtest)
.create_wallet(&mut conn)
.expect("wallet");
// Create some outputs with different gaps
let funding_tx = Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: LockTime::from_consensus(1),
input: vec![],
output: vec![
TxOut {
value: Amount::default(),
script_pubkey: wallet.peek_address(KeychainKind::External, 1).script_pubkey(),
},
TxOut {
value: Amount::default(),
script_pubkey: wallet.peek_address(KeychainKind::External, 45).script_pubkey(),
},
TxOut {
value: Amount::default(),
script_pubkey: wallet.peek_address(KeychainKind::External, 6).script_pubkey(),
},
TxOut {
value: Amount::default(),
script_pubkey: wallet.peek_address(KeychainKind::External, 20).script_pubkey(),
},
],
};
// Spend second output
let spending_tx = Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: LockTime::from_consensus(1),
input: vec![TxIn {
previous_output: OutPoint {
txid: funding_tx.compute_txid(),
vout: 1,
},
..Default::default()
}],
output: vec![TxOut::NULL],
};
let block = Block {
header: Header {
version: Default::default(),
prev_blockhash: genesis_block(¶ms::REGTEST).block_hash(),
merkle_root: TxMerkleNode::all_zeros(),
time: 0,
bits: Default::default(),
nonce: 0,
},
txdata: vec![funding_tx, spending_tx],
};
wallet.apply_block(&block, 1).expect("apply block");
wallet.persist(&mut conn).expect("persist");
let unspent_before = wallet.list_unspent().count();
println!("last revealed index before: {:?}", wallet.spk_index().last_revealed_index(KeychainKind::External)) ;
// Reload wallet
let wallet = Wallet::load()
.load_wallet(&mut conn)
.expect("wallet found")
.expect("load wallet");
let unspent_after = wallet.list_unspent().count();
println!("last revealed index after: {:?}", wallet.spk_index().last_revealed_index(KeychainKind::External)) ;
assert_eq!(
unspent_before,
unspent_after,
"Unspent output count should be the same before and after reload"
);
}
Output
last revealed index before: Some(20)
last revealed index after: Some(45)
Unspent output count should be the same before and after reload
Left: 3
Right: 4
Expected behavior
As the test shows, the wallet should at least consistently ignore it. If it's not tracking it initially, it shouldn't track it after reload.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status
Discussion