Skip to content

Commit 138a830

Browse files
authored
fix!: address Ironwood review findings across mempool, consensus, rpc, and state (#10884)
* fix(consensus): enforce coinbase output-decryptability for the Ironwood pool NU6.3+ routes shielded coinbase value into the Ironwood pool, but `decrypts_successfully` only iterated the Sapling and Orchard bundles, so the all-zero-OVK coinbase decryptability rule (ZIP-213) was never applied to Ironwood coinbase outputs. Add the Ironwood arm using `IronwoodDomain`. * fix(mempool): track Ironwood nullifiers for spend-conflict detection `VerifiedSet` tracked sprout/sapling/orchard nullifiers but not Ironwood, so two mempool txs revealing the same Ironwood nullifier were both admitted, and `reject_and_remove_same_effects` never evicted a mempool tx double-spending a just-mined Ironwood nullifier. Either lets `getblocktemplate` build a block template that every node then rejects. Mirror the Orchard handling for the Ironwood pool. * fix(consensus): fail closed instead of panicking on unexpected halo2 batch error The `RestrictionUnsupportedByKey` arm called `unreachable!` inside the shared, long-lived halo2 verifier service, so a future routing change or new `#[non_exhaustive]` `BatchError` variant would crash the node instead of rejecting one proof. Collapse it into the existing graceful per-item arm; the routing invariant is kept as a comment. * fix(state): include the Ironwood pool in supply metrics and spend indexing `value_pool_metrics`/`chain_supply.total` omitted the Ironwood pool, so total supply under-reported it every block post-NU6.3; and the `indexer` back-fill's spend-sampling chain skipped Ironwood-only v6 txs. Add the Ironwood gauge, the supply sum term, and Ironwood to the sampled spends. * feat(rpc): expose Ironwood bundles in verbose transaction output `getrawtransaction <txid> 1` / `getblock <hash> 2` serialized an `orchard` section but no `ironwood` one, so v6 Ironwood bundles (including Zebra's own Ironwood coinbase) were invisible to explorers and wallets. Add an `ironwood` field reusing the Orchard-shaped object, and factor the shared construction into `orchard_shaped_object` (also dropping an incidental O(n^2) action scan). * refactor: deduplicate Ironwood/Orchard tree-update and bundle-verify paths Ironwood reuses the Orchard tree and bundle types, so `update_ironwood_note_commitment_tree` now delegates to the Orchard version (re-tagging only the error variant), and `verify_orchard_v6_bundle` / `verify_orchard_bundle` share a `queue_orchard_bundle` helper. No behavior change. * chore: remove the dead ZIP-235 cfg and error variant The ZIP-235 burn validation and its tests were removed, but the `zip235` `cfg` value and `SubsidyError::InvalidZip233Amount` (gated on it) remained, so a `--cfg zcash_unstable="zip235"` build compiled yet enforced nothing. Drop both; they can return with the NU7 NSM implementation. * fix(state): fall back to the empty Ironwood tree before the genesis backfill `ironwood_tree_by_height` panicked when the Ironwood column family was still empty, which happens in the brief window after a v27->v28 upgrade before the background `add_ironwood_tree` migration backfills genesis. That window is always pre-NU6.3, when the Ironwood tree at every height is in fact empty, so return the empty tree instead of panicking. * refactor(state): share the Orchard/Ironwood tree-and-anchor add/remove logic `add_/remove_{orchard,ironwood}_tree_and_anchor` were verbatim copies over identically-typed field triples, including the reorg `RevertPosition::Root` invariant-restoration. Extract shared `add_/remove_note_commitment_tree_and_anchor` helpers parameterized on the target fields and a pool label. No behavior change (non-finalized-state prop + reorg tests pass). * refactor(chain): pass block commitment tree roots in a named struct The history-tree functions took `sapling_root`, `orchard_root`, and `ironwood_root` as adjacent positional arguments; the Orchard and Ironwood roots reuse the same `orchard::tree::Root` type, so a swapped pair compiled cleanly and would silently corrupt the ZIP-221 chain-history commitment. Group them in a named `BlockCommitmentTreeRoots` struct so a swap can't happen. * refactor(state): share the Orchard/Ironwood note-commitment DB accessors The Ironwood tree-range, subtree-list, subtree-for-tip, tree-create, and subtree-insert accessors were verbatim copies of their Orchard counterparts, differing only in the column-family name (both pools reuse the `orchard::tree` types). Route them through CF-parameterized private helpers; the public signatures are unchanged.
1 parent 6c274bb commit 138a830

27 files changed

Lines changed: 563 additions & 530 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ debug = false
330330
# The linter should ignore these expected config flags/values
331331
unexpected_cfgs = { level = "warn", check-cfg = [
332332
'cfg(tokio_unstable)', # Used by tokio-console
333-
'cfg(zcash_unstable, values("zfuture", "nu6.1", "nu7", "zip235"))' # Used in Zebra and librustzcash
333+
'cfg(zcash_unstable, values("zfuture", "nu6.1", "nu7"))' # Used in Zebra and librustzcash
334334
] }
335335

336336
# High-risk code

zebra-chain/CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3636
- `parallel::tree::NoteCommitmentTreeError::Ironwood`
3737
- `primitives::zcash_history::V3` (the ZIP-221 Ironwood history node).
3838
- `impl Version for zcash_history::version::V3`
39+
- `primitives::zcash_history::BlockCommitmentTreeRoots`, grouping a block's Sapling,
40+
Orchard, and Ironwood note commitment tree roots.
3941

4042
### Changed
4143

42-
- The following history-tree functions now take an additional
43-
`ironwood_root: &orchard::tree::Root` parameter:
44+
- The following history-tree functions now take a
45+
`primitives::zcash_history::BlockCommitmentTreeRoots` struct grouping the Sapling,
46+
Orchard, and Ironwood roots by name, instead of separate positional root parameters:
4447
- `history_tree::HistoryTree::{from_block, push}`
4548
- `history_tree::NonEmptyHistoryTree::{from_block, push, try_extend}`
4649
- `primitives::zcash_history::Tree::{append_leaf, new_from_block}`

zebra-chain/src/block/arbitrary.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::{
88
fmt::{HexDebug, SummaryDebug},
99
history_tree::HistoryTree,
1010
parameters::{NetworkUpgrade::*, GENESIS_PREVIOUS_BLOCK_HASH},
11+
primitives::zcash_history::BlockCommitmentTreeRoots,
1112
serialization::{self, BytesInDisplayOrder},
1213
transaction::arbitrary::MAX_ARBITRARY_ITEMS,
1314
transparent::{
@@ -537,19 +538,23 @@ impl Block {
537538
.push(
538539
&current.network,
539540
Arc::new(block.clone()),
540-
&sapling_tree.root(),
541-
&orchard_tree.root(),
542-
&ironwood_tree.root(),
541+
BlockCommitmentTreeRoots {
542+
sapling: &sapling_tree.root(),
543+
orchard: &orchard_tree.root(),
544+
ironwood: &ironwood_tree.root(),
545+
},
543546
)
544547
.unwrap();
545548
} else {
546549
history_tree = Some(
547550
HistoryTree::from_block(
548551
&current.network,
549552
Arc::new(block.clone()),
550-
&sapling_tree.root(),
551-
&orchard_tree.root(),
552-
&ironwood_tree.root(),
553+
BlockCommitmentTreeRoots {
554+
sapling: &sapling_tree.root(),
555+
orchard: &orchard_tree.root(),
556+
ironwood: &ironwood_tree.root(),
557+
},
553558
)
554559
.unwrap(),
555560
);

zebra-chain/src/history_tree.rs

Lines changed: 24 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ use thiserror::Error;
1515
use crate::{
1616
block::{Block, ChainHistoryMmrRootHash, Height},
1717
fmt::SummaryDebug,
18-
orchard,
1918
parameters::{Network, NetworkUpgrade},
2019
primitives::zcash_history::{
21-
Entry, Tree, V1 as PreOrchard, V2 as OrchardOnward, V3 as IronwoodOnward,
20+
BlockCommitmentTreeRoots, Entry, Tree, V1 as PreOrchard, V2 as OrchardOnward,
21+
V3 as IronwoodOnward,
2222
},
23-
sapling,
2423
};
2524

2625
/// An error describing why a history tree operation failed.
@@ -154,16 +153,13 @@ impl NonEmptyHistoryTree {
154153

155154
/// Create a new history tree with a single block.
156155
///
157-
/// `sapling_root` is the root of the Sapling note commitment tree of the block.
158-
/// `orchard_root` is the root of the Orchard note commitment tree of the block;
159-
/// (ignored for pre-Orchard blocks).
156+
/// The Orchard root in `roots` is ignored for pre-Orchard blocks, and the Ironwood root for
157+
/// pre-NU6.3 blocks; each inner tree version only reads the roots it commits to.
160158
#[allow(clippy::unwrap_in_result)]
161159
pub fn from_block(
162160
network: &Network,
163161
block: Arc<Block>,
164-
sapling_root: &sapling::tree::Root,
165-
orchard_root: &orchard::tree::Root,
166-
ironwood_root: &orchard::tree::Root,
162+
roots: BlockCommitmentTreeRoots,
167163
) -> Result<Self, HistoryTreeError> {
168164
let height = block
169165
.coinbase_height()
@@ -178,48 +174,24 @@ impl NonEmptyHistoryTree {
178174
panic!("HistoryTree does not exist for pre-Heartwood upgrades")
179175
}
180176
NetworkUpgrade::Heartwood | NetworkUpgrade::Canopy => {
181-
let (tree, entry) = Tree::<PreOrchard>::new_from_block(
182-
network,
183-
block,
184-
sapling_root,
185-
&Default::default(),
186-
&Default::default(),
187-
)?;
177+
let (tree, entry) = Tree::<PreOrchard>::new_from_block(network, block, roots)?;
188178
(InnerHistoryTree::PreOrchard(tree), entry)
189179
}
190180
NetworkUpgrade::Nu5
191181
| NetworkUpgrade::Nu6
192182
| NetworkUpgrade::Nu6_1
193183
| NetworkUpgrade::Nu6_2 => {
194-
let (tree, entry) = Tree::<OrchardOnward>::new_from_block(
195-
network,
196-
block,
197-
sapling_root,
198-
orchard_root,
199-
&Default::default(),
200-
)?;
184+
let (tree, entry) = Tree::<OrchardOnward>::new_from_block(network, block, roots)?;
201185
(InnerHistoryTree::OrchardOnward(tree), entry)
202186
}
203187
NetworkUpgrade::Nu6_3 | NetworkUpgrade::Nu7 => {
204-
let (tree, entry) = Tree::<IronwoodOnward>::new_from_block(
205-
network,
206-
block,
207-
sapling_root,
208-
orchard_root,
209-
ironwood_root,
210-
)?;
188+
let (tree, entry) = Tree::<IronwoodOnward>::new_from_block(network, block, roots)?;
211189
(InnerHistoryTree::IronwoodOnward(tree), entry)
212190
}
213191

214192
#[cfg(zcash_unstable = "zfuture")]
215193
NetworkUpgrade::ZFuture => {
216-
let (tree, entry) = Tree::<IronwoodOnward>::new_from_block(
217-
network,
218-
block,
219-
sapling_root,
220-
orchard_root,
221-
ironwood_root,
222-
)?;
194+
let (tree, entry) = Tree::<IronwoodOnward>::new_from_block(network, block, roots)?;
223195
(InnerHistoryTree::IronwoodOnward(tree), entry)
224196
}
225197
};
@@ -237,9 +209,8 @@ impl NonEmptyHistoryTree {
237209

238210
/// Add block data to the tree.
239211
///
240-
/// `sapling_root` is the root of the Sapling note commitment tree of the block.
241-
/// `orchard_root` is the root of the Orchard note commitment tree of the block;
242-
/// (ignored for pre-Orchard blocks).
212+
/// The Orchard root in `roots` is ignored for pre-Orchard blocks, and the Ironwood root for
213+
/// pre-NU6.3 blocks.
243214
///
244215
/// # Panics
245216
///
@@ -248,9 +219,7 @@ impl NonEmptyHistoryTree {
248219
pub fn push(
249220
&mut self,
250221
block: Arc<Block>,
251-
sapling_root: &sapling::tree::Root,
252-
orchard_root: &orchard::tree::Root,
253-
ironwood_root: &orchard::tree::Root,
222+
roots: BlockCommitmentTreeRoots,
254223
) -> Result<(), HistoryTreeError> {
255224
// Check if the block has the expected height.
256225
// librustzcash assumes the heights are correct and corrupts the tree if they are wrong,
@@ -270,13 +239,7 @@ impl NonEmptyHistoryTree {
270239
if network_upgrade != self.network_upgrade {
271240
// This is the activation block of a network upgrade.
272241
// Create a new tree.
273-
let new_tree = Self::from_block(
274-
&self.network,
275-
block,
276-
sapling_root,
277-
orchard_root,
278-
ironwood_root,
279-
)?;
242+
let new_tree = Self::from_block(&self.network, block, roots)?;
280243
// Replaces self with the new tree
281244
*self = new_tree;
282245
assert_eq!(self.network_upgrade, network_upgrade);
@@ -285,13 +248,13 @@ impl NonEmptyHistoryTree {
285248

286249
let new_entries = match &mut self.inner {
287250
InnerHistoryTree::PreOrchard(tree) => tree
288-
.append_leaf(block, sapling_root, orchard_root, ironwood_root)
251+
.append_leaf(block, roots)
289252
.map_err(|e| HistoryTreeError::InnerError { inner: e })?,
290253
InnerHistoryTree::OrchardOnward(tree) => tree
291-
.append_leaf(block, sapling_root, orchard_root, ironwood_root)
254+
.append_leaf(block, roots)
292255
.map_err(|e| HistoryTreeError::InnerError { inner: e })?,
293256
InnerHistoryTree::IronwoodOnward(tree) => tree
294-
.append_leaf(block, sapling_root, orchard_root, ironwood_root)
257+
.append_leaf(block, roots)
295258
.map_err(|e| HistoryTreeError::InnerError { inner: e })?,
296259
};
297260
for entry in new_entries {
@@ -305,22 +268,12 @@ impl NonEmptyHistoryTree {
305268
}
306269

307270
/// Extend the history tree with the given blocks.
308-
pub fn try_extend<
309-
'a,
310-
T: IntoIterator<
311-
Item = (
312-
Arc<Block>,
313-
&'a sapling::tree::Root,
314-
&'a orchard::tree::Root,
315-
&'a orchard::tree::Root,
316-
),
317-
>,
318-
>(
271+
pub fn try_extend<'a, T: IntoIterator<Item = (Arc<Block>, BlockCommitmentTreeRoots<'a>)>>(
319272
&mut self,
320273
iter: T,
321274
) -> Result<(), HistoryTreeError> {
322-
for (block, sapling_root, orchard_root, ironwood_root) in iter {
323-
self.push(block, sapling_root, orchard_root, ironwood_root)?;
275+
for (block, roots) in iter {
276+
self.push(block, roots)?;
324277
}
325278
Ok(())
326279
}
@@ -519,9 +472,7 @@ impl HistoryTree {
519472
pub fn from_block(
520473
network: &Network,
521474
block: Arc<Block>,
522-
sapling_root: &sapling::tree::Root,
523-
orchard_root: &orchard::tree::Root,
524-
ironwood_root: &orchard::tree::Root,
475+
roots: BlockCommitmentTreeRoots,
525476
) -> Result<Self, HistoryTreeError> {
526477
let Some(heartwood_height) = NetworkUpgrade::Heartwood.activation_height(network) else {
527478
// Return early if there is no Heartwood activation height.
@@ -534,14 +485,7 @@ impl HistoryTree {
534485
.cmp(&heartwood_height)
535486
{
536487
std::cmp::Ordering::Less => Ok(HistoryTree(None)),
537-
_ => Ok(NonEmptyHistoryTree::from_block(
538-
network,
539-
block,
540-
sapling_root,
541-
orchard_root,
542-
ironwood_root,
543-
)?
544-
.into()),
488+
_ => Ok(NonEmptyHistoryTree::from_block(network, block, roots)?.into()),
545489
}
546490
}
547491

@@ -554,9 +498,7 @@ impl HistoryTree {
554498
&mut self,
555499
network: &Network,
556500
block: Arc<Block>,
557-
sapling_root: &sapling::tree::Root,
558-
orchard_root: &orchard::tree::Root,
559-
ironwood_root: &orchard::tree::Root,
501+
roots: BlockCommitmentTreeRoots,
560502
) -> Result<(), HistoryTreeError> {
561503
let Some(heartwood_height) = NetworkUpgrade::Heartwood.activation_height(network) else {
562504
assert!(
@@ -579,21 +521,15 @@ impl HistoryTree {
579521
);
580522
}
581523
std::cmp::Ordering::Equal => {
582-
let tree = Some(NonEmptyHistoryTree::from_block(
583-
network,
584-
block,
585-
sapling_root,
586-
orchard_root,
587-
ironwood_root,
588-
)?);
524+
let tree = Some(NonEmptyHistoryTree::from_block(network, block, roots)?);
589525
// Replace the current object with the new tree
590526
*self = HistoryTree(tree);
591527
}
592528
std::cmp::Ordering::Greater => {
593529
self.0
594530
.as_mut()
595531
.expect("history tree must exist Heartwood-onward")
596-
.push(block.clone(), sapling_root, orchard_root, ironwood_root)?;
532+
.push(block.clone(), roots)?;
597533
}
598534
};
599535
Ok(())

zebra-chain/src/history_tree/tests/vectors.rs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{
77
},
88
history_tree::NonEmptyHistoryTree,
99
parameters::{Network, NetworkUpgrade},
10+
primitives::zcash_history::BlockCommitmentTreeRoots,
1011
sapling,
1112
serialization::ZcashDeserializeInto,
1213
};
@@ -61,9 +62,11 @@ fn push_and_prune_for_network_upgrade(
6162
let mut tree = NonEmptyHistoryTree::from_block(
6263
&network,
6364
first_block,
64-
&first_sapling_root,
65-
&Default::default(),
66-
&Default::default(),
65+
BlockCommitmentTreeRoots {
66+
sapling: &first_sapling_root,
67+
orchard: &Default::default(),
68+
ironwood: &Default::default(),
69+
},
6770
)?;
6871

6972
assert_eq!(tree.size(), 1);
@@ -94,9 +97,11 @@ fn push_and_prune_for_network_upgrade(
9497
)?;
9598
tree.push(
9699
second_block,
97-
&second_sapling_root,
98-
&Default::default(),
99-
&Default::default(),
100+
BlockCommitmentTreeRoots {
101+
sapling: &second_sapling_root,
102+
orchard: &Default::default(),
103+
ironwood: &Default::default(),
104+
},
100105
)
101106
.unwrap();
102107

@@ -143,9 +148,11 @@ fn upgrade_for_network_upgrade(network: Network, network_upgrade: NetworkUpgrade
143148
let mut tree = NonEmptyHistoryTree::from_block(
144149
&network,
145150
block_prev,
146-
&sapling_root_prev,
147-
&Default::default(),
148-
&Default::default(),
151+
BlockCommitmentTreeRoots {
152+
sapling: &sapling_root_prev,
153+
orchard: &Default::default(),
154+
ironwood: &Default::default(),
155+
},
149156
)?;
150157

151158
assert_eq!(tree.size(), 1);
@@ -170,9 +177,11 @@ fn upgrade_for_network_upgrade(network: Network, network_upgrade: NetworkUpgrade
170177
)?;
171178
tree.push(
172179
activation_block,
173-
&activation_sapling_root,
174-
&Default::default(),
175-
&Default::default(),
180+
BlockCommitmentTreeRoots {
181+
sapling: &activation_sapling_root,
182+
orchard: &Default::default(),
183+
ironwood: &Default::default(),
184+
},
176185
)
177186
.unwrap();
178187

0 commit comments

Comments
 (0)