fix(lite-backend): record the mined height of mempool-first transactions
When the mempool monitor saw a transaction first, its wallet entry kept the monitor's height (often 0) after the transaction was mined, because scanning the block reused the entry without updating its block. A later reorg between the two heights then left a phantom spend of our own send and a change note with an empty witness list until a rescan. Scanning a block now sets the entry's block to the block height. This applies to every monitor-first transaction, including payments received from others: their reported block height, the verified/spendable timing that depends on it, and the wallet birthday now use the real mined height instead of the monitor's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+38
@@ -917,6 +917,44 @@ fn test_self_txns_ttoz_nomemo() {
|
||||
}
|
||||
}
|
||||
|
||||
/// R3 (regression test). The mempool monitor (scan_full_mempool_tx) records our send in `txs` at the height
|
||||
/// the server reports. The block that mines it must become its height: a reorg of that block removes the
|
||||
/// send, un-spends its note and drops its change. Before the fix the entry kept the monitor's height.
|
||||
#[test]
|
||||
fn test_r3_mempool_seen_send_takes_mined_height() {
|
||||
const AMOUNT1: u64 = 50000;
|
||||
let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); // note at block 0, tip 1
|
||||
let fvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[1u8; 32]));
|
||||
let ext_address = encode_payment_address(wallet.config.hrp_sapling_address(), &fvk.default_address().unwrap().1);
|
||||
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
||||
let (ss, so) = get_sapling_params().unwrap();
|
||||
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
||||
vec![(&ext_address, 20, None)], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
||||
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
||||
let sent_txid = sent_tx.txid();
|
||||
|
||||
wallet.scan_full_mempool_tx(&sent_tx, 1, 0, true); // decrypts the change: entry at the server height 1
|
||||
assert_eq!(wallet.txs.read().unwrap().get(&sent_txid).map(|t| t.block), Some(1), "setup: monitor entry");
|
||||
|
||||
let cb2 = FakeCompactBlock::new(2, block_hash); // not in block 2
|
||||
wallet.scan_block(&cb2.as_bytes()).unwrap();
|
||||
let mut cb3 = FakeCompactBlock::new(3, cb2.hash()); // mined in block 3
|
||||
cb3.add_tx(&sent_tx);
|
||||
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
||||
assert_eq!(wallet.txs.read().unwrap()[&sent_txid].block, 3,
|
||||
"R3 (regression): mined at 3, but the WalletTx keeps the mempool monitor's height 1");
|
||||
|
||||
assert_eq!(wallet.invalidate_block(3), 1);
|
||||
{
|
||||
let txs = wallet.txs.read().unwrap();
|
||||
assert!(txs.get(&sent_txid).is_none(), "R3 (regression): a reorg of its mining block kept the send");
|
||||
assert_eq!(txs[&txid1].notes[0].spent, None, "R3 (regression): phantom spend");
|
||||
assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None);
|
||||
assert!(!txs[&txid1].notes[0].witnesses.is_empty());
|
||||
}
|
||||
assert_eq!(wallet.zbalance(None), AMOUNT1); // guard dropped first: zbalance takes txs.read itself
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_self_txns_ztoz() {
|
||||
const AMOUNT1: u64 = 50000;
|
||||
|
||||
Reference in New Issue
Block a user