fix(lite-backend): don't panic on a reorg while a send is pending
invalidate_block unwrapped a note's `spent` field for notes that only had `unconfirmed_spent` set, so any reorg while one of our sends was still in the mempool panicked the backend (and poisoned the wallet lock). The branch could only panic or do nothing; it is gone, and a reorg now leaves `unconfirmed_spent` as it is: the send can still be mined on the new branch, because the chain checks the anchor's root, not its height. The oracle no longer steers reorgs around pending sends: it now reorgs above, across and below the sent tip, including popping the anchor block, and the reproducer runs by default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1867,9 +1867,9 @@ pub fn scan_full_mempool_tx(&self, tx: &Transaction, height: i32, _datetime: u64
|
|||||||
nd.spent = None;
|
nd.spent = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if nd.unconfirmed_spent.is_some() && txids_to_remove.contains(&nd.spent.unwrap()) {
|
// unconfirmed_spent is left as it is: a pending send can still be mined on the
|
||||||
nd.unconfirmed_spent = None;
|
// new branch. Only a block that spends the note (scan_block) or a reload clears
|
||||||
}
|
// it, the same as for a send that expires unmined.
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+342
-95
@@ -33,16 +33,19 @@
|
|||||||
//! signature. The first send of a run also checks that the verifier rejects a wrong anchor
|
//! signature. The first send of a run also checks that the verifier rejects a wrong anchor
|
||||||
//! and a wrong nullifier.
|
//! and a wrong nullifier.
|
||||||
//!
|
//!
|
||||||
//! Pre-existing bugs the green runs avoid, each with an `#[ignore]` reproducer below:
|
//! Pending sends: reorgs of any depth happen while a send is pending, above its sent tip T,
|
||||||
//! - F5a: `invalidate_block` panics while a send is pending (`nd.spent.unwrap()` on a note that
|
//! across it (A < at <= T, A = its anchor height) or popping its anchor block (at <= A), in one
|
||||||
//! only has `unconfirmed_spent`). So no reorg happens while a send is pending.
|
//! call or block by block. The wallet keeps `unconfirmed_spent` until a block spends the note or
|
||||||
//! - F5b: a send that never mines leaves its notes with a stale witness (pending notes are not
|
//! a reload drops it (F5a, `oracle_f5a_*`). The chain checks an anchor root, not a height, so a
|
||||||
//! advanced); a reload drops `unconfirmed_spent` and exposes it as spendable. So no reload or
|
//! send whose anchor block was popped may still be mined: the reference orphans it (never mined,
|
||||||
//! throwaway copy happens while a send is pending, and a run that leaves a send unmined only
|
//! its notes stay pending) when the reorg removed one of its notes, else with probability 1/2.
|
||||||
//! mines blocks afterwards.
|
//! The fake chain does not validate anchors. A run that leaves a send unmined orphans it at once.
|
||||||
//! - F5b via a reorg (same root cause): once a send is mined its notes are spent, but a reorg
|
//!
|
||||||
//! that undoes the mining block back to a height >= sent tip + 2 revives them with the stale
|
//! Pre-existing bug the green runs avoid, with `#[ignore]` reproducers below:
|
||||||
//! entries of the pending blocks. So reorg depths are adjusted to stay out of that window.
|
//! - F5b: pending notes are not advanced, so their witnesses go stale. A reload drops
|
||||||
|
//! `unconfirmed_spent` and exposes such a note as spendable; so does a reorg that undoes the
|
||||||
|
//! block that mined the send back into the stale blocks. So no reload or throwaway copy happens
|
||||||
|
//! while a note is pending, and reorg depths are adjusted to stay out of that window.
|
||||||
//!
|
//!
|
||||||
//! Sizes: `oracle_default_*` (4 x 5 seeds x 300 blocks, three proving sends per seed);
|
//! Sizes: `oracle_default_*` (4 x 5 seeds x 300 blocks, three proving sends per seed);
|
||||||
//! `#[ignore]` `oracle_full_*` (8 x 125 seeds x 400 blocks, one proving send per seed) and
|
//! `#[ignore]` `oracle_full_*` (8 x 125 seeds x 400 blocks, one proving send per seed) and
|
||||||
@@ -339,7 +342,9 @@ impl RefNote {
|
|||||||
struct Pending {
|
struct Pending {
|
||||||
gtx: GenTx,
|
gtx: GenTx,
|
||||||
txid: TxId,
|
txid: TxId,
|
||||||
sent_tip: i32,
|
/// The wallet's entries above this height may be stale (F5b): the sent tip, lowered to
|
||||||
|
/// at - 1 by a reorg at `at` while pending.
|
||||||
|
stale_from: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
@@ -366,6 +371,13 @@ struct Stats {
|
|||||||
requeued: usize,
|
requeued: usize,
|
||||||
remined: usize,
|
remined: usize,
|
||||||
full_checks: usize,
|
full_checks: usize,
|
||||||
|
/// Reorgs while a note is pending, by `at`: above the sent tip, across it (anchor kept),
|
||||||
|
/// popping the anchor block.
|
||||||
|
pending_reorgs: [usize; 3],
|
||||||
|
/// Pending sends that will never be mined (anchor popped, or a run that leaves it unmined).
|
||||||
|
orphaned: usize,
|
||||||
|
/// Pending sends whose anchor block was popped but that are still mined.
|
||||||
|
anchor_root_reuse: usize,
|
||||||
t_gen: f64,
|
t_gen: f64,
|
||||||
t_scan: f64,
|
t_scan: f64,
|
||||||
t_ref: f64,
|
t_ref: f64,
|
||||||
@@ -397,10 +409,12 @@ struct Sim {
|
|||||||
/// Our sends in the wallet's mempool: txid -> tip when sent.
|
/// Our sends in the wallet's mempool: txid -> tip when sent.
|
||||||
mempool: HashMap<TxId, i32>,
|
mempool: HashMap<TxId, i32>,
|
||||||
pending: Option<Pending>,
|
pending: Option<Pending>,
|
||||||
/// A send was left unmined: from here on only blocks are mined (F5a/F5b).
|
/// (txid, tip when sent, anchor height) of the last real send.
|
||||||
frozen: bool,
|
last_send: Option<(TxId, i32, i32)>,
|
||||||
/// (tip when sent, block that first mined it) of our mined sends: a reorg to a height in
|
/// Overrides the coin flip that orphans a pending send whose anchor block was popped.
|
||||||
/// (sent + 1, mined] would revive their notes with stale witnesses (F5b via a reorg).
|
force_orphan: Option<bool>,
|
||||||
|
/// (stale_from, block that first mined it) of our mined sends: a reorg to a height in
|
||||||
|
/// (stale_from + 1, mined] would revive their notes with stale witnesses (F5b via a reorg).
|
||||||
pending_windows: Vec<(i32, i32)>,
|
pending_windows: Vec<(i32, i32)>,
|
||||||
/// Blocks still to be paid to us (setup helper).
|
/// Blocks still to be paid to us (setup helper).
|
||||||
force_pay: usize,
|
force_pay: usize,
|
||||||
@@ -482,7 +496,8 @@ impl Sim {
|
|||||||
requeue: vec![],
|
requeue: vec![],
|
||||||
mempool: HashMap::new(),
|
mempool: HashMap::new(),
|
||||||
pending: None,
|
pending: None,
|
||||||
frozen: false,
|
last_send: None,
|
||||||
|
force_orphan: None,
|
||||||
pending_windows: vec![],
|
pending_windows: vec![],
|
||||||
force_pay: 0,
|
force_pay: 0,
|
||||||
force_output: false,
|
force_output: false,
|
||||||
@@ -911,10 +926,15 @@ impl Sim {
|
|||||||
/// Reorg: pop the newest k blocks (one invalidate_block call, or one call per block like
|
/// Reorg: pop the newest k blocks (one invalidate_block call, or one call per block like
|
||||||
/// the sync loop does), then let the new branch be generated.
|
/// the sync loop does), then let the new branch be generated.
|
||||||
fn reorg(&mut self, k: usize, stepwise: bool) {
|
fn reorg(&mut self, k: usize, stepwise: bool) {
|
||||||
assert!(self.pending.is_none() && !self.frozen, "{} reorg with a pending send (F5a)", self.ctx());
|
|
||||||
assert!(k >= 1 && k < self.win.len(), "{} bad reorg depth {}", self.ctx(), k);
|
assert!(k >= 1 && k < self.win.len(), "{} bad reorg depth {}", self.ctx(), k);
|
||||||
self.op = "reorg";
|
self.op = "reorg";
|
||||||
let at = self.tip() - k as i32 + 1;
|
let at = self.tip() - k as i32 + 1;
|
||||||
|
if let Some((_, t, a)) = self.last_send {
|
||||||
|
if self.any_pending() {
|
||||||
|
let class = if at > t { 0 } else if at > a { 1 } else { 2 };
|
||||||
|
self.stats.pending_reorgs[class] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
let n = if stepwise {
|
let n = if stepwise {
|
||||||
(0..k).map(|_| self.w.invalidate_block(self.w.last_scanned_height())).sum::<u64>()
|
(0..k).map(|_| self.w.invalidate_block(self.w.last_scanned_height())).sum::<u64>()
|
||||||
} else {
|
} else {
|
||||||
@@ -940,6 +960,29 @@ impl Sim {
|
|||||||
assert_eq!(popped, k, "{} window pop count", self.ctx());
|
assert_eq!(popped, k, "{} window pop count", self.ctx());
|
||||||
assert!(!self.win.is_empty());
|
assert!(!self.win.is_empty());
|
||||||
|
|
||||||
|
// The wallet leaves pending notes pending (n.pending is kept). A pending send whose anchor
|
||||||
|
// block was popped can still be mined if the new branch has the same anchor root, so it is
|
||||||
|
// orphaned when one of its notes was removed, else by a coin flip.
|
||||||
|
let anchor_popped = self.pending.as_ref()
|
||||||
|
.map_or(false, |p| p.gtx.anchor_height.map_or(false, |a| a >= at));
|
||||||
|
if anchor_popped {
|
||||||
|
let nfs = self.pending.as_ref().unwrap().gtx.nfs();
|
||||||
|
let removed = self.notes.iter().any(|n| n.recv >= at && nfs.contains(&n.nf));
|
||||||
|
let orphan = removed || match self.force_orphan {
|
||||||
|
Some(o) => o,
|
||||||
|
None => self.rng.gen_bool(0.5),
|
||||||
|
};
|
||||||
|
if orphan {
|
||||||
|
self.pending = None;
|
||||||
|
self.stats.orphaned += 1;
|
||||||
|
} else {
|
||||||
|
self.stats.anchor_root_reuse += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(p) = self.pending.as_mut() {
|
||||||
|
p.stale_from = cmp::min(p.stale_from, at - 1);
|
||||||
|
}
|
||||||
|
|
||||||
self.notes.retain(|n| n.recv < at);
|
self.notes.retain(|n| n.recv < at);
|
||||||
let ctx = self.ctx();
|
let ctx = self.ctx();
|
||||||
for n in self.notes.iter_mut() {
|
for n in self.notes.iter_mut() {
|
||||||
@@ -1007,8 +1050,55 @@ impl Sim {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Some note is pending: a send is in flight, or an orphaned one locked its notes until reload.
|
||||||
|
fn any_pending(&self) -> bool {
|
||||||
|
self.notes.iter().any(|n| n.pending.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reorg depth for a reorg while a send is pending. The class of `at` (above the sent tip T,
|
||||||
|
/// across it with the anchor A kept, or popping A; every range capped at the current tip, which
|
||||||
|
/// an earlier reorg may have lowered) is drawn from the non-empty ones, with weights 1, 2 and 3
|
||||||
|
/// (random reorgs mostly hit the first). Half of the popping ones stay within 3 blocks of the
|
||||||
|
/// top of their range, so they rarely remove one of the send's notes and the send may still be
|
||||||
|
/// mined (anchor-root reuse).
|
||||||
|
fn pending_reorg_depth(&mut self) -> Option<usize> {
|
||||||
|
let (_, t, a) = self.last_send?;
|
||||||
|
let tip = self.tip();
|
||||||
|
let lo = *self.win.front().unwrap() + 1;
|
||||||
|
let classes: Vec<(u32, i32, i32)> = [
|
||||||
|
(1, cmp::max(t + 1, lo), tip),
|
||||||
|
(2, cmp::max(a + 1, lo), cmp::min(t, tip)),
|
||||||
|
(3, lo, cmp::min(a, tip)),
|
||||||
|
].iter().cloned().filter(|(_, l, h)| l <= h).collect();
|
||||||
|
if classes.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut r = self.rng.gen_range(0, classes.iter().map(|c| c.0).sum::<u32>());
|
||||||
|
let mut pick = classes[0];
|
||||||
|
for c in classes.iter() {
|
||||||
|
if r < c.0 {
|
||||||
|
pick = *c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
r -= c.0;
|
||||||
|
}
|
||||||
|
let (w, mut l, h) = pick;
|
||||||
|
if w == 3 && self.rng.gen_bool(0.5) {
|
||||||
|
l = cmp::max(l, h - 3);
|
||||||
|
}
|
||||||
|
let at = self.rng.gen_range(l, h + 1);
|
||||||
|
Some((tip - at + 1) as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Orphan the pending send: it is never mined, and its notes stay pending until a reload.
|
||||||
|
fn orphan_pending(&mut self) {
|
||||||
|
if self.pending.take().is_some() {
|
||||||
|
self.stats.orphaned += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn reload(&mut self) {
|
fn reload(&mut self) {
|
||||||
assert!(self.pending.is_none() && !self.frozen, "{} reload with a pending send (F5b)", self.ctx());
|
assert!(!self.any_pending(), "{} reload with a pending note (F5b)", self.ctx());
|
||||||
self.op = "reload";
|
self.op = "reload";
|
||||||
self.reload_raw();
|
self.reload_raw();
|
||||||
self.stats.reloads += 1;
|
self.stats.reloads += 1;
|
||||||
@@ -1030,6 +1120,7 @@ impl Sim {
|
|||||||
w3.write(&mut b3).unwrap();
|
w3.write(&mut b3).unwrap();
|
||||||
assert!(b2 == b3, "{} write(read(x)) is not idempotent", self.ctx());
|
assert!(b2 == b3, "{} write(read(x)) is not idempotent", self.ctx());
|
||||||
self.w = w3;
|
self.w = w3;
|
||||||
|
self.mempool.clear(); // mempool_txs is not saved (an orphaned send may still be in it)
|
||||||
if self.win.len() > MAX_REORG {
|
if self.win.len() > MAX_REORG {
|
||||||
self.clear_bound = cmp::max(self.clear_bound, *self.win.front().unwrap());
|
self.clear_bound = cmp::max(self.clear_bound, *self.win.front().unwrap());
|
||||||
}
|
}
|
||||||
@@ -1039,7 +1130,14 @@ impl Sim {
|
|||||||
/// wallet itself, which leaves it pending. Returns whether a proving send was made (false
|
/// wallet itself, which leaves it pending. Returns whether a proving send was made (false
|
||||||
/// when there were not enough eligible funds; the error path is checked instead).
|
/// when there were not enough eligible funds; the error path is checked instead).
|
||||||
fn send(&mut self, v: &Verifier, kind: SendKind) -> bool {
|
fn send(&mut self, v: &Verifier, kind: SendKind) -> bool {
|
||||||
assert!(self.pending.is_none() && !self.frozen, "{} send with a pending send", self.ctx());
|
self.send_with(v, kind, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `send` with an optional fixed amount (one recipient). With a fixed amount the spent notes
|
||||||
|
/// must be exactly the ones the wallet's selection rule picks from the eligible notes; it
|
||||||
|
/// returns false without sending when they do not cover it or need more than 10 inputs.
|
||||||
|
fn send_with(&mut self, v: &Verifier, kind: SendKind, fixed: Option<u64>) -> bool {
|
||||||
|
assert!(kind != SendKind::Real || self.pending.is_none(), "{} send with a pending send", self.ctx());
|
||||||
self.op = match kind { SendKind::Probe(_) => "probe-send", SendKind::Real => "send" };
|
self.op = match kind { SendKind::Probe(_) => "probe-send", SendKind::Real => "send" };
|
||||||
let offset_cfg = match kind { SendKind::Probe(o) => o, SendKind::Real => self.cfg.anchor_offset };
|
let offset_cfg = match kind { SendKind::Probe(o) => o, SendKind::Real => self.cfg.anchor_offset };
|
||||||
let (tip, off) = self.anchor(offset_cfg);
|
let (tip, off) = self.anchor(offset_cfg);
|
||||||
@@ -1071,7 +1169,18 @@ impl Sim {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if covering.is_empty() {
|
let expected: Option<HashSet<[u8; 32]>> = match fixed {
|
||||||
|
None => None,
|
||||||
|
Some(a) => {
|
||||||
|
let cands: Vec<(u64, [u8; 32])> = elig.iter()
|
||||||
|
.map(|&i| (self.notes[i].value, self.notes[i].nf)).collect();
|
||||||
|
match expected_selection(&cands, a + fee) {
|
||||||
|
Some(s) if s.len() <= 10 => Some(s.into_iter().collect()),
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if covering.is_empty() && fixed.is_none() {
|
||||||
// Not enough: check the error path instead.
|
// Not enough: check the error path instead.
|
||||||
self.insufficient_check();
|
self.insufficient_check();
|
||||||
return false;
|
return false;
|
||||||
@@ -1081,19 +1190,19 @@ impl Sim {
|
|||||||
|
|
||||||
// Proving cost is per input, so keep the worst case over all selection orders bounded.
|
// Proving cost is per input, so keep the worst case over all selection orders bounded.
|
||||||
let values: Vec<u64> = elig.iter().map(|&i| self.notes[i].value).collect();
|
let values: Vec<u64> = elig.iter().map(|&i| self.notes[i].value).collect();
|
||||||
let max1 = *covering.last().unwrap();
|
let multi = if fixed.is_none() && covering.len() >= 2 && self.rng.gen_bool(0.5) {
|
||||||
let multi = if covering.len() >= 2 && self.rng.gen_bool(0.5) {
|
|
||||||
// More than the largest note: needs at least two inputs.
|
// More than the largest note: needs at least two inputs.
|
||||||
|
let max1 = *covering.last().unwrap();
|
||||||
let amount = self.rint(max1 - fee + 1, cmp::min(total, max1 + 100_000) - fee);
|
let amount = self.rint(max1 - fee + 1, cmp::min(total, max1 + 100_000) - fee);
|
||||||
if worst_case_inputs(&values, amount + fee) <= 10 { Some(amount) } else { None }
|
if worst_case_inputs(&values, amount + fee) <= 10 { Some(amount) } else { None }
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let amount = match multi {
|
let amount = match (fixed, multi) {
|
||||||
Some(a) => a,
|
(Some(a), _) | (None, Some(a)) => a,
|
||||||
None => self.rint(1, covering[0] - fee),
|
(None, None) => self.rint(1, covering[0] - fee),
|
||||||
};
|
};
|
||||||
let tos: Vec<(&str, u64, Option<String>)> = if amount >= 2 && self.rng.gen_bool(0.15) {
|
let tos: Vec<(&str, u64, Option<String>)> = if fixed.is_none() && amount >= 2 && self.rng.gen_bool(0.15) {
|
||||||
let a1 = self.rint(1, amount - 1);
|
let a1 = self.rint(1, amount - 1);
|
||||||
vec![(to.as_str(), a1, memo.clone()), (to.as_str(), amount - a1, None)]
|
vec![(to.as_str(), a1, memo.clone()), (to.as_str(), amount - a1, None)]
|
||||||
} else {
|
} else {
|
||||||
@@ -1132,6 +1241,9 @@ impl Sim {
|
|||||||
let tx_nfs: HashSet<[u8; 32]> = tx.shielded_spends.iter().map(|s| s.nullifier).collect();
|
let tx_nfs: HashSet<[u8; 32]> = tx.shielded_spends.iter().map(|s| s.nullifier).collect();
|
||||||
let selected: u64 = self.notes.iter().filter(|n| tx_nfs.contains(&n.nf)).map(|n| n.value).sum();
|
let selected: u64 = self.notes.iter().filter(|n| tx_nfs.contains(&n.nf)).map(|n| n.value).sum();
|
||||||
assert!(selected >= amount + fee, "{} selected {} < {} + fee", ctx, selected, amount);
|
assert!(selected >= amount + fee, "{} selected {} < {} + fee", ctx, selected, amount);
|
||||||
|
if let Some(e) = &expected {
|
||||||
|
assert!(tx_nfs == *e, "{} spent notes are not the ones the selection rule picks", ctx);
|
||||||
|
}
|
||||||
|
|
||||||
if !self.controls_done {
|
if !self.controls_done {
|
||||||
// The verifier is not vacuous: a wrong anchor or nullifier must be rejected.
|
// The verifier is not vacuous: a wrong anchor or nullifier must be rejected.
|
||||||
@@ -1185,7 +1297,8 @@ impl Sim {
|
|||||||
raw: Some(raw),
|
raw: Some(raw),
|
||||||
anchor_height: Some(tip - off),
|
anchor_height: Some(tip - off),
|
||||||
};
|
};
|
||||||
self.pending = Some(Pending { gtx, txid, sent_tip: tip });
|
self.pending = Some(Pending { gtx, txid, stale_from: tip });
|
||||||
|
self.last_send = Some((txid, tip, tip - off));
|
||||||
self.stats.sends += 1;
|
self.stats.sends += 1;
|
||||||
self.check(false);
|
self.check(false);
|
||||||
}
|
}
|
||||||
@@ -1365,14 +1478,65 @@ impl Sim {
|
|||||||
format!("seed {:#x}: {} blocks, {} outputs ({} ours, {} notes), {} compact spends, {} reorgs (max {}), \
|
format!("seed {:#x}: {} blocks, {} outputs ({} ours, {} notes), {} compact spends, {} reorgs (max {}), \
|
||||||
{} reloads, {} probes, {} sends ({} mined), {} insufficient, {} requeued/{} re-mined, \
|
{} reloads, {} probes, {} sends ({} mined), {} insufficient, {} requeued/{} re-mined, \
|
||||||
{} full checks, max {} cleared lists, {} keys; time gen {:.1}s scan {:.1}s ref {:.1}s \
|
{} full checks, max {} cleared lists, {} keys; time gen {:.1}s scan {:.1}s ref {:.1}s \
|
||||||
check {:.1}s send {:.1}s reorg {:.1}s reload {:.1}s periodic {:.1}s",
|
check {:.1}s send {:.1}s reorg {:.1}s reload {:.1}s periodic {:.1}s; {}",
|
||||||
self.seed, s.blocks, s.outputs, s.ours, self.notes.len(), s.spends, s.reorgs, s.max_depth,
|
self.seed, s.blocks, s.outputs, s.ours, self.notes.len(), s.spends, s.reorgs, s.max_depth,
|
||||||
s.reloads, s.probes, s.sends, s.mined_sends, s.insufficient, s.requeued, s.remined,
|
s.reloads, s.probes, s.sends, s.mined_sends, s.insufficient, s.requeued, s.remined,
|
||||||
s.full_checks, self.cleared_seen.get(), self.w.zkeys.read().unwrap().len(),
|
s.full_checks, self.cleared_seen.get(), self.w.zkeys.read().unwrap().len(),
|
||||||
s.t_gen, s.t_scan, s.t_ref, s.t_check, s.t_send, s.t_reorg, s.t_reload, s.t_full)
|
s.t_gen, s.t_scan, s.t_ref, s.t_check, s.t_send, s.t_reorg, s.t_reload, s.t_full,
|
||||||
|
s.pending_summary())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Stats {
|
||||||
|
fn pending_summary(&self) -> String {
|
||||||
|
format!("pending: reorgs above/across/popped {}/{}/{}, orphaned {}, anchor-root reuse {}",
|
||||||
|
self.pending_reorgs[0], self.pending_reorgs[1], self.pending_reorgs[2], self.orphaned,
|
||||||
|
self.anchor_root_reuse)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add the pending-state counters of one seed.
|
||||||
|
fn add_pending(&mut self, o: &Stats) {
|
||||||
|
for i in 0..3 {
|
||||||
|
self.pending_reorgs[i] += o.pending_reorgs[i];
|
||||||
|
}
|
||||||
|
self.orphaned += o.orphaned;
|
||||||
|
self.anchor_root_reuse += o.anchor_root_reuse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every pending-state class was hit (asserted by the full runs).
|
||||||
|
fn assert_pending_cover(&self) {
|
||||||
|
let all = [self.pending_reorgs[0], self.pending_reorgs[1], self.pending_reorgs[2], self.orphaned,
|
||||||
|
self.anchor_root_reuse];
|
||||||
|
assert!(all.iter().all(|&c| c > 0), "a pending-state class was never hit: {}", self.pending_summary());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The notes the wallet's selection rule spends for `needed` (amount + fee; no transparent funds
|
||||||
|
/// here), from (value, nullifier) of the eligible notes: the smallest non-zero note that covers
|
||||||
|
/// it, else the largest notes first (the shortest covering prefix) with the last one replaced by
|
||||||
|
/// the smallest remaining note that still covers. Ties go to the smaller nullifier. None if the
|
||||||
|
/// notes do not cover it.
|
||||||
|
fn expected_selection(cands: &[(u64, [u8; 32])], needed: u64) -> Option<Vec<[u8; 32]>> {
|
||||||
|
let mut c: Vec<(u64, [u8; 32])> = cands.iter().cloned().filter(|x| x.0 > 0).collect();
|
||||||
|
if c.iter().map(|x| x.0).sum::<u64>() < needed {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if let Some(best) = c.iter().filter(|x| x.0 >= needed).min() {
|
||||||
|
return Some(vec![best.1]);
|
||||||
|
}
|
||||||
|
c.sort_by(|x, y| y.0.cmp(&x.0).then(x.1.cmp(&y.1)));
|
||||||
|
let (mut sum, mut k) = (0u64, 0usize);
|
||||||
|
while sum < needed {
|
||||||
|
sum += c[k].0;
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
let remaining = needed - c[..k - 1].iter().map(|x| x.0).sum::<u64>();
|
||||||
|
let last = *c[k - 1..].iter().filter(|x| x.0 >= remaining).min().unwrap_or(&c[k - 1]);
|
||||||
|
let mut out: Vec<[u8; 32]> = c[..k - 1].iter().map(|x| x.1).collect();
|
||||||
|
out.push(last.1);
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
/// The most inputs any selection order can take for `target`: notes are taken while the running
|
/// The most inputs any selection order can take for `target`: notes are taken while the running
|
||||||
/// total is below the target, and ascending order reaches it last.
|
/// total is below the target, and ascending order reaches it last.
|
||||||
fn worst_case_inputs(values: &[u64], target: u64) -> usize {
|
fn worst_case_inputs(values: &[u64], target: u64) -> usize {
|
||||||
@@ -1437,7 +1601,7 @@ enum Proofs {
|
|||||||
One,
|
One,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_seed(seed: u64, n_blocks: usize, proofs: Proofs, v: &Verifier) {
|
fn run_seed(seed: u64, n_blocks: usize, proofs: Proofs, v: &Verifier) -> Stats {
|
||||||
assert!(n_blocks >= 20, "T6_BLOCKS must be >= 20");
|
assert!(n_blocks >= 20, "T6_BLOCKS must be >= 20");
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
let mut sim = Sim::new(seed);
|
let mut sim = Sim::new(seed);
|
||||||
@@ -1459,64 +1623,76 @@ fn run_seed(seed: u64, n_blocks: usize, proofs: Proofs, v: &Verifier) {
|
|||||||
_ => { probe0 = None; probe4 = None; }
|
_ => { probe0 = None; probe4 = None; }
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
// Rate of a reorg inside the pending window of the real send.
|
||||||
|
let p_pending_reorg = match proofs { Proofs::Three => 0.4, Proofs::One => 0.9 };
|
||||||
let mut insufficient_at = Some(sim.rng.gen_range(0, n));
|
let mut insufficient_at = Some(sim.rng.gen_range(0, n));
|
||||||
let mut deep_reorg_at = if n > 150 && sim.rng.gen_bool(0.4) { Some(sim.rng.gen_range(120, n)) } else { None };
|
let mut deep_reorg_at = if n > 150 && sim.rng.gen_bool(0.4) { Some(sim.rng.gen_range(120, n)) } else { None };
|
||||||
let mut mine_at: Option<usize> = None;
|
let mut mine_at: Option<usize> = None;
|
||||||
|
let mut pending_reorg_at: Option<usize> = None;
|
||||||
|
|
||||||
for step in 0..n {
|
for step in 0..n {
|
||||||
sim.step = step;
|
sim.step = step;
|
||||||
let calm = sim.pending.is_none() && !sim.frozen;
|
|
||||||
|
|
||||||
if calm {
|
let want = if pending_reorg_at == Some(step) {
|
||||||
let want = if deep_reorg_at.map_or(false, |s| step >= s) && sim.win.len() > 91 {
|
pending_reorg_at = None;
|
||||||
deep_reorg_at = None;
|
sim.pending_reorg_depth()
|
||||||
Some(cmp::min(sim.rint(90, 100) as usize, sim.win.len() - 1))
|
} else if deep_reorg_at.map_or(false, |s| step >= s) && sim.win.len() > 91 {
|
||||||
} else if sim.rng.gen_range(0, 1000) < 30 && sim.win.len() > 1 {
|
deep_reorg_at = None;
|
||||||
Some(sim.pick_depth())
|
Some(cmp::min(sim.rint(90, 100) as usize, sim.win.len() - 1))
|
||||||
} else {
|
} else if sim.rng.gen_range(0, 1000) < 30 && sim.win.len() > 1 {
|
||||||
None
|
Some(sim.pick_depth())
|
||||||
};
|
} else {
|
||||||
if let Some(k) = want.and_then(|k| sim.safe_depth(k)) {
|
None
|
||||||
let stepwise = sim.rng.gen_bool(0.3);
|
};
|
||||||
let t = Instant::now();
|
if let Some(k) = want.and_then(|k| sim.safe_depth(k)) {
|
||||||
sim.reorg(k, stepwise);
|
let stepwise = sim.rng.gen_bool(0.3);
|
||||||
sim.stats.t_reorg += t.elapsed().as_secs_f64();
|
let t = Instant::now();
|
||||||
}
|
sim.reorg(k, stepwise);
|
||||||
if sim.rng.gen_range(0, 1000) < 15 {
|
sim.stats.t_reorg += t.elapsed().as_secs_f64();
|
||||||
let t = Instant::now();
|
}
|
||||||
sim.reload();
|
// F5b: no reload or throwaway copy while a note is pending.
|
||||||
sim.stats.t_reload += t.elapsed().as_secs_f64();
|
if sim.rng.gen_range(0, 1000) < 15 && !sim.any_pending() {
|
||||||
}
|
let t = Instant::now();
|
||||||
if probe0.map_or(false, |s| step >= s) {
|
sim.reload();
|
||||||
probe0 = None;
|
sim.stats.t_reload += t.elapsed().as_secs_f64();
|
||||||
sim.send(v, SendKind::Probe(0));
|
}
|
||||||
}
|
if probe0.map_or(false, |s| step >= s) && !sim.any_pending() {
|
||||||
if probe4.map_or(false, |s| step >= s) {
|
probe0 = None;
|
||||||
probe4 = None;
|
sim.send(v, SendKind::Probe(0));
|
||||||
sim.send(v, SendKind::Probe(4));
|
}
|
||||||
}
|
if probe4.map_or(false, |s| step >= s) && !sim.any_pending() {
|
||||||
if send_at.map_or(false, |s| step >= s) {
|
probe4 = None;
|
||||||
send_at = None;
|
sim.send(v, SendKind::Probe(4));
|
||||||
if sim.send(v, SendKind::Real) {
|
}
|
||||||
if unmined {
|
if send_at.map_or(false, |s| step >= s) && sim.pending.is_none() {
|
||||||
sim.frozen = true;
|
send_at = None;
|
||||||
} else {
|
if sim.send(v, SendKind::Real) {
|
||||||
mine_at = Some(step + sim.rint(1, 12) as usize);
|
let end = step + sim.rint(3, 12) as usize;
|
||||||
}
|
if unmined {
|
||||||
|
sim.orphan_pending();
|
||||||
|
} else {
|
||||||
|
mine_at = Some(end);
|
||||||
|
}
|
||||||
|
if sim.rng.gen_bool(p_pending_reorg) {
|
||||||
|
pending_reorg_at = Some(step + sim.rint(1, (end - step) as u64) as usize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if insufficient_at.map_or(false, |s| step >= s) && sim.pending.is_none() {
|
if insufficient_at.map_or(false, |s| step >= s) {
|
||||||
insufficient_at = None;
|
insufficient_at = None;
|
||||||
sim.insufficient_check();
|
sim.insufficient_check();
|
||||||
}
|
}
|
||||||
|
|
||||||
if mine_at.map_or(false, |s| step >= s) {
|
if mine_at.map_or(false, |s| step >= s) {
|
||||||
mine_at = None;
|
mine_at = None;
|
||||||
let p = sim.pending.take().unwrap();
|
// A reorg may have orphaned it.
|
||||||
sim.mine(vec![p.gtx], true);
|
if let Some(p) = sim.pending.take() {
|
||||||
let mined_at = sim.tip();
|
sim.mine(vec![p.gtx], true);
|
||||||
sim.pending_windows.push((p.sent_tip, mined_at));
|
let mined_at = sim.tip();
|
||||||
|
sim.pending_windows.push((p.stale_from, mined_at));
|
||||||
|
} else {
|
||||||
|
sim.mine(vec![], true);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
sim.mine(vec![], true);
|
sim.mine(vec![], true);
|
||||||
}
|
}
|
||||||
@@ -1528,17 +1704,11 @@ fn run_seed(seed: u64, n_blocks: usize, proofs: Proofs, v: &Verifier) {
|
|||||||
sim.stats.t_full += t.elapsed().as_secs_f64();
|
sim.stats.t_full += t.elapsed().as_secs_f64();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(p) = &sim.pending {
|
|
||||||
// An unmined send: by now it has expired from the mempool.
|
|
||||||
if sim.tip() >= *sim.mempool.get(&p.txid).unwrap_or(&i32::MIN) + TX_EXPIRY_DELTA {
|
|
||||||
assert!(sim.w.mempool_txs.read().unwrap().is_empty(), "{} expired send still in the mempool",
|
|
||||||
sim.ctx());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sim.op = "end";
|
sim.op = "end";
|
||||||
sim.check(true);
|
sim.check(true);
|
||||||
sim.check_addresses(&sim.w);
|
sim.check_addresses(&sim.w);
|
||||||
println!("oracle {} ({:.1}s)", sim.summary(), t0.elapsed().as_secs_f64());
|
println!("oracle {} ({:.1}s)", sim.summary(), t0.elapsed().as_secs_f64());
|
||||||
|
std::mem::take(&mut sim.stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sim {
|
impl Sim {
|
||||||
@@ -1578,7 +1748,8 @@ fn env_u64(name: &str) -> Option<u64> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_seeds(first: u64, count: u64, n_blocks: usize, proofs: Proofs) {
|
/// Runs the seeds and returns their summed pending-state counters.
|
||||||
|
fn run_seeds(first: u64, count: u64, n_blocks: usize, proofs: Proofs) -> Stats {
|
||||||
let v = Verifier::new();
|
let v = Verifier::new();
|
||||||
let n_blocks = env_u64("T6_BLOCKS").map_or(n_blocks, |b| b as usize);
|
let n_blocks = env_u64("T6_BLOCKS").map_or(n_blocks, |b| b as usize);
|
||||||
let seeds: Vec<u64> = match env_u64("T6_SEED") {
|
let seeds: Vec<u64> = match env_u64("T6_SEED") {
|
||||||
@@ -1586,10 +1757,22 @@ fn run_seeds(first: u64, count: u64, n_blocks: usize, proofs: Proofs) {
|
|||||||
None => (first..first + count).collect(),
|
None => (first..first + count).collect(),
|
||||||
};
|
};
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
|
let mut sum = Stats::default();
|
||||||
for s in seeds.iter() {
|
for s in seeds.iter() {
|
||||||
run_seed(*s, n_blocks, proofs, &v);
|
sum.add_pending(&run_seed(*s, n_blocks, proofs, &v));
|
||||||
|
}
|
||||||
|
println!("oracle: {} seeds from {:#x} in {:.1}s; {}", seeds.len(), seeds[0], t0.elapsed().as_secs_f64(),
|
||||||
|
sum.pending_summary());
|
||||||
|
sum
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One eighth of the full run (125 seeds x 400 blocks, one proving send per seed). Every
|
||||||
|
/// pending-state class must be hit (not checked for a single T6_SEED).
|
||||||
|
fn run_full(first: u64) {
|
||||||
|
let sum = run_seeds(first, 125, 400, Proofs::One);
|
||||||
|
if env_u64("T6_SEED").is_none() {
|
||||||
|
sum.assert_pending_cover();
|
||||||
}
|
}
|
||||||
println!("oracle: {} seeds from {:#x} in {:.1}s", seeds.len(), seeds[0], t0.elapsed().as_secs_f64());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1615,49 +1798,49 @@ fn oracle_default_d() {
|
|||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn oracle_full_0() {
|
fn oracle_full_0() {
|
||||||
run_seeds(0x1_0000, 125, 400, Proofs::One);
|
run_full(0x1_0000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn oracle_full_1() {
|
fn oracle_full_1() {
|
||||||
run_seeds(0x1_1000, 125, 400, Proofs::One);
|
run_full(0x1_1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn oracle_full_2() {
|
fn oracle_full_2() {
|
||||||
run_seeds(0x1_2000, 125, 400, Proofs::One);
|
run_full(0x1_2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn oracle_full_3() {
|
fn oracle_full_3() {
|
||||||
run_seeds(0x1_3000, 125, 400, Proofs::One);
|
run_full(0x1_3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn oracle_full_4() {
|
fn oracle_full_4() {
|
||||||
run_seeds(0x1_4000, 125, 400, Proofs::One);
|
run_full(0x1_4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn oracle_full_5() {
|
fn oracle_full_5() {
|
||||||
run_seeds(0x1_5000, 125, 400, Proofs::One);
|
run_full(0x1_5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn oracle_full_6() {
|
fn oracle_full_6() {
|
||||||
run_seeds(0x1_6000, 125, 400, Proofs::One);
|
run_full(0x1_6000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn oracle_full_7() {
|
fn oracle_full_7() {
|
||||||
run_seeds(0x1_7000, 125, 400, Proofs::One);
|
run_full(0x1_7000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1680,11 +1863,10 @@ fn oracle_env_seed() {
|
|||||||
run_seeds(seed, 1, 300, Proofs::Three);
|
run_seeds(seed, 1, 300, Proofs::Three);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// F5a reproducer (pre-existing bug, out of Tier 6). A reorg while a send is pending panics in
|
/// F5a (regression test). A reorg while a send is pending panicked in invalidate_block:
|
||||||
/// invalidate_block: `nd.unconfirmed_spent.is_some() && txids_to_remove.contains(&nd.spent.unwrap())`
|
/// `nd.unconfirmed_spent.is_some() && txids_to_remove.contains(&nd.spent.unwrap())` unwrapped
|
||||||
/// unwraps `spent`, which is None for a pending note. Fails on BASE; passes once fixed.
|
/// `spent`, which is None for a pending note. The notes stay pending across the reorg.
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
|
||||||
fn oracle_f5a_reorg_while_send_pending() {
|
fn oracle_f5a_reorg_while_send_pending() {
|
||||||
let v = Verifier::new();
|
let v = Verifier::new();
|
||||||
let mut sim = Sim::new(0xF5A);
|
let mut sim = Sim::new(0xF5A);
|
||||||
@@ -1699,7 +1881,7 @@ fn oracle_f5a_reorg_while_send_pending() {
|
|||||||
let tip = sim.tip();
|
let tip = sim.tip();
|
||||||
let w = &sim.w;
|
let w = &sim.w;
|
||||||
let r = panic::catch_unwind(AssertUnwindSafe(|| w.invalidate_block(tip)));
|
let r = panic::catch_unwind(AssertUnwindSafe(|| w.invalidate_block(tip)));
|
||||||
assert!(r.is_ok(), "F5a (BASE bug): invalidate_block panicked while a send was pending \
|
assert!(r.is_ok(), "F5a (regression): invalidate_block panicked while a send was pending \
|
||||||
(it unwraps nd.spent of a note that only has unconfirmed_spent)");
|
(it unwraps nd.spent of a note that only has unconfirmed_spent)");
|
||||||
assert_eq!(r.unwrap(), 1);
|
assert_eq!(r.unwrap(), 1);
|
||||||
sim.ref_invalidate(tip, 1);
|
sim.ref_invalidate(tip, 1);
|
||||||
@@ -1711,6 +1893,71 @@ fn oracle_f5a_reorg_while_send_pending() {
|
|||||||
sim.check(true);
|
sim.check(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The first seed >= `from` whose Sim draws anchor offset `off` (the first draw of Sim::new).
|
||||||
|
fn seed_with_offset(from: u64, off: u32) -> u64 {
|
||||||
|
(from..).find(|&s| {
|
||||||
|
let mut r = StdRng::seed_from_u64(s);
|
||||||
|
(if r.gen_bool(0.5) { 0 } else { 4 }) == off
|
||||||
|
}).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// F5a: the mempool monitor (`scan_full_mempool_tx`) records our own pending send in `txs` at
|
||||||
|
/// the height the server reports, when it can decrypt one of its outputs (here the change). A
|
||||||
|
/// reorg that removes that entry must leave the notes pending: the send is still valid and is
|
||||||
|
/// mined on the new branch. Releasing them there would offer notes that are still in flight.
|
||||||
|
#[test]
|
||||||
|
fn oracle_f5a_monitor_entry_reorg_keeps_pending() {
|
||||||
|
let v = Verifier::new();
|
||||||
|
let mut sim = Sim::new(seed_with_offset(0xF5D0, 4));
|
||||||
|
assert_eq!(sim.cfg.anchor_offset, 4, "setup: an offset-4 seed");
|
||||||
|
sim.force_pay = 12;
|
||||||
|
for _ in 0..12 {
|
||||||
|
sim.mine(vec![], true);
|
||||||
|
}
|
||||||
|
// The smallest eligible note above fee + 1, sent less 1: one input and a change output of 1.
|
||||||
|
let (_, off) = sim.anchor(sim.cfg.anchor_offset);
|
||||||
|
let fee = u64::from(DEFAULT_FEE);
|
||||||
|
let vq = sim.notes.iter().filter(|n| n.live() && n.has_sk && n.len >= off as usize + 1 && n.value > fee + 1)
|
||||||
|
.map(|n| n.value).min().expect("setup: a note to spend");
|
||||||
|
assert!(sim.send_with(&v, SendKind::Real, Some(vq - fee - 1)), "setup: the send must be built");
|
||||||
|
let t = sim.tip();
|
||||||
|
let (txid, a) = {
|
||||||
|
let p = sim.pending.as_ref().unwrap();
|
||||||
|
(p.txid, p.gtx.anchor_height.unwrap())
|
||||||
|
};
|
||||||
|
let spent: HashSet<[u8; 32]> = sim.pending.as_ref().unwrap().gtx.nfs().into_iter().collect();
|
||||||
|
let tx = Transaction::read(&sim.pending.as_ref().unwrap().gtx.raw.as_ref().unwrap()[..]).unwrap();
|
||||||
|
sim.w.scan_full_mempool_tx(&tx, t, 0, true);
|
||||||
|
assert!(sim.w.txs.read().unwrap().get(&txid).map_or(false, |w| w.block == t),
|
||||||
|
"setup: the monitor recorded our send in txs at the tip");
|
||||||
|
sim.rel.insert(txid, (t, 0));
|
||||||
|
sim.mempool.remove(&txid); // the next block's cleanup drops it: txs has the txid
|
||||||
|
sim.mine(vec![], true);
|
||||||
|
|
||||||
|
// A 2-block reorg removes the monitor's entry (block t).
|
||||||
|
sim.force_orphan = Some(true);
|
||||||
|
sim.reorg(2, false);
|
||||||
|
{
|
||||||
|
let txs = sim.w.txs.read().unwrap();
|
||||||
|
assert!(txs.get(&txid).is_none(), "the reorg removed the monitor's entry");
|
||||||
|
let still: HashSet<[u8; 32]> = txs.values().flat_map(|w| w.notes.iter())
|
||||||
|
.filter(|nd| nd.unconfirmed_spent == Some(txid)).map(|nd| nd.nullifier).collect();
|
||||||
|
assert!(still == spent, "F5a (regression): a reorg that removed the monitor's entry for our pending \
|
||||||
|
send released its notes ({} of {} still pending)", still.len(), spent.len());
|
||||||
|
}
|
||||||
|
if a < t {
|
||||||
|
// The anchor block was kept: mine the send on the new branch.
|
||||||
|
let g = sim.pending.take().expect("anchor kept: still pending").gtx;
|
||||||
|
sim.mine(vec![g], true);
|
||||||
|
assert_eq!(sim.w.txs.read().unwrap().get(&txid).map(|w| w.block), Some(sim.tip()), "mined send's block");
|
||||||
|
assert!(sim.notes.iter().filter(|n| spent.contains(&n.nf)).all(|n| n.spent.map(|s| s.0) == Some(txid)));
|
||||||
|
sim.mine(vec![], true);
|
||||||
|
sim.check(true);
|
||||||
|
} else {
|
||||||
|
assert!(sim.pending.is_none(), "anchor popped: orphaned");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// F5b via a reorg (pre-existing bug, out of Tier 6; the F5b root cause). While a send is
|
/// F5b via a reorg (pre-existing bug, out of Tier 6; the F5b root cause). While a send is
|
||||||
/// pending, its notes' witness lists get a copy per block but are not advanced. Once the send is
|
/// pending, its notes' witness lists get a copy per block but are not advanced. Once the send is
|
||||||
/// mined the notes are spent and that does not matter, but a reorg that undoes the mining block
|
/// mined the notes are spent and that does not matter, but a reorg that undoes the mining block
|
||||||
|
|||||||
Reference in New Issue
Block a user