Skip to content

Commit 9c3d5d0

Browse files
committed
feat(rpc): expose the Ironwood note commitment tree and subtrees
Add the Ironwood read path that mirrors Orchard, so wallets and indexers can obtain Ironwood frontiers and subtree roots after NU6.3: - state: ReadRequest/ReadResponse IronwoodTree and IronwoodSubtrees variants, read::ironwood_tree / ironwood_subtrees, and the ZebraDb::ironwood_tree_by_hash_or_height accessor. - rpc: an ironwood treestate in z_gettreestate (present from NU6.3), the ironwood pool in z_getsubtreesbyindex, and the ironwood tree size in verbose getblock. All are skipped/omitted when empty, so pre-NU6.3 responses and fixtures are unchanged.
1 parent d6996d9 commit 9c3d5d0

10 files changed

Lines changed: 231 additions & 6 deletions

File tree

zebra-rpc/src/methods.rs

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,6 +1304,8 @@ where
13041304
transactions_request,
13051305
// Orchard trees
13061306
zebra_state::ReadRequest::OrchardTree(hash_or_height),
1307+
// Ironwood trees
1308+
zebra_state::ReadRequest::IronwoodTree(hash_or_height),
13071309
// Block info
13081310
zebra_state::ReadRequest::BlockInfo(previous_block_hash.into()),
13091311
zebra_state::ReadRequest::BlockInfo(hash_or_height),
@@ -1382,7 +1384,25 @@ where
13821384
size: orchard_tree_size,
13831385
};
13841386

1385-
let trees = GetBlockTrees { sapling, orchard };
1387+
let ironwood_tree_response = futs.next().await.expect("`futs` should not be empty");
1388+
let zebra_state::ReadResponse::IronwoodTree(ironwood_tree) =
1389+
ironwood_tree_response.map_misc_error()?
1390+
else {
1391+
unreachable!("unmatched response to an IronwoodTree request");
1392+
};
1393+
1394+
// This could be `None` if there's a chain reorg between state queries. Before NU6.3 the
1395+
// Ironwood tree is empty (size 0).
1396+
let ironwood_tree = ironwood_tree.ok_or_misc_error("missing Ironwood tree")?;
1397+
let ironwood = IronwoodTrees {
1398+
size: ironwood_tree.count(),
1399+
};
1400+
1401+
let trees = GetBlockTrees {
1402+
sapling,
1403+
orchard,
1404+
ironwood,
1405+
};
13861406

13871407
let block_info_response = futs.next().await.expect("`futs` should not be empty");
13881408
let zebra_state::ReadResponse::BlockInfo(prev_block_info) =
@@ -1959,6 +1979,27 @@ where
19591979
let (orchard_tree, orchard_root) =
19601980
orchard.map_or((None, None), |(tree, root)| (Some(tree), Some(root)));
19611981

1982+
let ironwood = if network.is_nu_active(consensus::NetworkUpgrade::Nu6_3, height.into()) {
1983+
match read_state
1984+
.ready()
1985+
.and_then(|service| {
1986+
service.call(zebra_state::ReadRequest::IronwoodTree(hash.into()))
1987+
})
1988+
.await
1989+
.map_misc_error()?
1990+
{
1991+
zebra_state::ReadResponse::IronwoodTree(tree) => {
1992+
tree.map(|t| (t.to_rpc_bytes(), t.root().bytes_in_display_order().to_vec()))
1993+
}
1994+
_ => unreachable!("unmatched response to an Ironwood tree request"),
1995+
}
1996+
} else {
1997+
None
1998+
};
1999+
// Only present from NU6.3, so pre-NU6.3 responses keep the sprout/sapling/orchard shape.
2000+
let ironwood = ironwood
2001+
.map(|(tree, root)| Treestate::new(trees::Commitments::new(Some(root), Some(tree))));
2002+
19622003
Ok(GetTreestateResponse::new(
19632004
hash,
19642005
height,
@@ -1968,6 +2009,7 @@ where
19682009
None,
19692010
Treestate::new(trees::Commitments::new(sapling_root, sapling_tree)),
19702011
Treestate::new(trees::Commitments::new(orchard_root, orchard_tree)),
2012+
ironwood,
19712013
))
19722014
}
19732015

@@ -1979,7 +2021,7 @@ where
19792021
) -> Result<GetSubtreesByIndexResponse> {
19802022
let mut read_state = self.read_state.clone();
19812023

1982-
const POOL_LIST: &[&str] = &["sapling", "orchard"];
2024+
const POOL_LIST: &[&str] = &["sapling", "orchard", "ironwood"];
19832025

19842026
if pool == "sapling" {
19852027
let request = zebra_state::ReadRequest::SaplingSubtrees { start_index, limit };
@@ -2020,6 +2062,33 @@ where
20202062
_ => unreachable!("unmatched response to a subtrees request"),
20212063
};
20222064

2065+
let subtrees = subtrees
2066+
.values()
2067+
.map(|subtree| SubtreeRpcData {
2068+
root: subtree.root.encode_hex(),
2069+
end_height: subtree.end_height,
2070+
})
2071+
.collect();
2072+
2073+
Ok(GetSubtreesByIndexResponse {
2074+
pool,
2075+
start_index,
2076+
subtrees,
2077+
})
2078+
} else if pool == "ironwood" {
2079+
let request = zebra_state::ReadRequest::IronwoodSubtrees { start_index, limit };
2080+
let response = read_state
2081+
.ready()
2082+
.and_then(|service| service.call(request))
2083+
.await
2084+
.map_misc_error()?;
2085+
2086+
let subtrees = match response {
2087+
zebra_state::ReadResponse::IronwoodSubtrees(subtrees) => subtrees,
2088+
_ => unreachable!("unmatched response to a subtrees request"),
2089+
};
2090+
2091+
// Ironwood reuses the Orchard note type, so the subtree root is encoded like Orchard's.
20232092
let subtrees = subtrees
20242093
.values()
20252094
.map(|subtree| SubtreeRpcData {
@@ -4372,23 +4441,29 @@ pub struct GetBlockTrees {
43724441
sapling: SaplingTrees,
43734442
#[serde(skip_serializing_if = "OrchardTrees::is_empty")]
43744443
orchard: OrchardTrees,
4444+
// `default` so responses and fixtures from before Ironwood (which have no `ironwood` field)
4445+
// still deserialize, as the empty tree.
4446+
#[serde(default, skip_serializing_if = "IronwoodTrees::is_empty")]
4447+
ironwood: IronwoodTrees,
43754448
}
43764449

43774450
impl Default for GetBlockTrees {
43784451
fn default() -> Self {
43794452
GetBlockTrees {
43804453
sapling: SaplingTrees { size: 0 },
43814454
orchard: OrchardTrees { size: 0 },
4455+
ironwood: IronwoodTrees { size: 0 },
43824456
}
43834457
}
43844458
}
43854459

43864460
impl GetBlockTrees {
43874461
/// Constructs a new instance of ['GetBlockTrees'].
4388-
pub fn new(sapling: u64, orchard: u64) -> Self {
4462+
pub fn new(sapling: u64, orchard: u64, ironwood: u64) -> Self {
43894463
GetBlockTrees {
43904464
sapling: SaplingTrees { size: sapling },
43914465
orchard: OrchardTrees { size: orchard },
4466+
ironwood: IronwoodTrees { size: ironwood },
43924467
}
43934468
}
43944469

@@ -4401,6 +4476,11 @@ impl GetBlockTrees {
44014476
pub fn orchard(self) -> u64 {
44024477
self.orchard.size
44034478
}
4479+
4480+
/// Returns ironwood data held by ['GetBlockTrees'].
4481+
pub fn ironwood(self) -> u64 {
4482+
self.ironwood.size
4483+
}
44044484
}
44054485

44064486
/// Sapling note commitment tree information.
@@ -4427,6 +4507,18 @@ impl OrchardTrees {
44274507
}
44284508
}
44294509

4510+
/// Ironwood note commitment tree information. Ironwood reuses the Orchard tree type.
4511+
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
4512+
pub struct IronwoodTrees {
4513+
size: u64,
4514+
}
4515+
4516+
impl IronwoodTrees {
4517+
fn is_empty(&self) -> bool {
4518+
self.size == 0
4519+
}
4520+
}
4521+
44304522
/// Build a valid height range from the given optional start and end numbers.
44314523
///
44324524
/// # Parameters

zebra-rpc/src/methods/tests/vectors.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,12 @@ async fn rpc_getblock() {
281281
// Create empty note commitment tree information.
282282
let sapling = SaplingTrees { size: 0 };
283283
let orchard = OrchardTrees { size: 0 };
284-
let trees = GetBlockTrees { sapling, orchard };
284+
let ironwood = IronwoodTrees { size: 0 };
285+
let trees = GetBlockTrees {
286+
sapling,
287+
orchard,
288+
ironwood,
289+
};
285290

286291
// Make height calls with verbosity=1 and check response
287292
let mut prev_block_info: Option<BlockInfo> = None;

zebra-rpc/src/methods/trees.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ pub struct GetTreestateResponse {
8888

8989
/// A treestate containing an Orchard note commitment tree, hex-encoded.
9090
orchard: Treestate,
91+
92+
/// A treestate containing an Ironwood note commitment tree, hex-encoded. Only present from
93+
/// NU6.3, so that pre-NU6.3 responses are unchanged.
94+
#[serde(skip_serializing_if = "Option::is_none")]
95+
ironwood: Option<Treestate>,
9196
}
9297

9398
impl GetTreestateResponse {
@@ -120,6 +125,7 @@ impl GetTreestateResponse {
120125
sprout: None,
121126
sapling,
122127
orchard,
128+
ironwood: None,
123129
}
124130
}
125131

@@ -145,6 +151,7 @@ impl Default for GetTreestateResponse {
145151
sprout: Default::default(),
146152
sapling: Default::default(),
147153
orchard: Default::default(),
154+
ironwood: None,
148155
}
149156
}
150157
}

zebra-rpc/tests/serialization_tests.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ fn test_get_block_1() -> Result<(), Box<dyn std::error::Error>> {
240240
let trees = block.trees();
241241
let trees_sapling = trees.sapling();
242242
let trees_orchard = trees.orchard();
243+
let trees_ironwood = trees.ironwood();
243244
// We already tested that GetBlockHash is readable with `hash`, so we don't
244245
// bother unpacking it here
245246
let previous_block_hash = block.previous_block_hash();
@@ -269,7 +270,7 @@ fn test_get_block_1() -> Result<(), Box<dyn std::error::Error>> {
269270
difficulty,
270271
chain_supply,
271272
value_pools,
272-
GetBlockTrees::new(trees_sapling, trees_orchard),
273+
GetBlockTrees::new(trees_sapling, trees_orchard, trees_ironwood),
273274
previous_block_hash,
274275
next_block_hash,
275276
)));
@@ -600,6 +601,7 @@ fn test_z_get_treestate() -> Result<(), Box<dyn std::error::Error>> {
600601
))),
601602
Treestate::new(Commitments::new(sapling_final_root, sapling_final_state)),
602603
Treestate::new(Commitments::new(orchard_final_root, orchard_final_state)),
604+
obj.ironwood().clone(),
603605
);
604606

605607
assert_eq!(obj, new_obj);

zebra-state/src/request.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,6 +1315,15 @@ pub enum ReadRequest {
13151315
/// * [`ReadResponse::OrchardTree(None)`](crate::ReadResponse::OrchardTree) otherwise.
13161316
OrchardTree(HashOrHeight),
13171317

1318+
/// Looks up an Ironwood note commitment tree either by a hash or height.
1319+
///
1320+
/// Returns
1321+
///
1322+
/// * [`ReadResponse::IronwoodTree(Some(Arc<NoteCommitmentTree>))`](crate::ReadResponse::IronwoodTree)
1323+
/// if the corresponding block contains an Ironwood note commitment tree.
1324+
/// * [`ReadResponse::IronwoodTree(None)`](crate::ReadResponse::IronwoodTree) otherwise.
1325+
IronwoodTree(HashOrHeight),
1326+
13181327
/// Returns a list of Sapling note commitment subtrees by their indexes, starting at
13191328
/// `start_index`, and returning up to `limit` subtrees.
13201329
///
@@ -1343,6 +1352,20 @@ pub enum ReadRequest {
13431352
limit: Option<NoteCommitmentSubtreeIndex>,
13441353
},
13451354

1355+
/// Returns a list of Ironwood note commitment subtrees by their indexes, starting at
1356+
/// `start_index`, and returning up to `limit` subtrees.
1357+
///
1358+
/// Returns
1359+
///
1360+
/// * [`ReadResponse::IronwoodSubtree(BTreeMap<_, NoteCommitmentSubtreeData<_>>))`](crate::ReadResponse::IronwoodSubtrees)
1361+
/// * An empty list if there is no subtree at `start_index`.
1362+
IronwoodSubtrees {
1363+
/// The index of the first 2^16-leaf subtree to return.
1364+
start_index: NoteCommitmentSubtreeIndex,
1365+
/// The maximum number of subtree values to return.
1366+
limit: Option<NoteCommitmentSubtreeIndex>,
1367+
},
1368+
13461369
/// Looks up the balance of a set of transparent addresses.
13471370
///
13481371
/// Returns an [`Amount`](zebra_chain::amount::Amount) with the total
@@ -1471,8 +1494,10 @@ impl ReadRequest {
14711494
ReadRequest::FindForkPoint { .. } => "find_fork_point",
14721495
ReadRequest::SaplingTree { .. } => "sapling_tree",
14731496
ReadRequest::OrchardTree { .. } => "orchard_tree",
1497+
ReadRequest::IronwoodTree { .. } => "ironwood_tree",
14741498
ReadRequest::SaplingSubtrees { .. } => "sapling_subtrees",
14751499
ReadRequest::OrchardSubtrees { .. } => "orchard_subtrees",
1500+
ReadRequest::IronwoodSubtrees { .. } => "ironwood_subtrees",
14761501
ReadRequest::AddressBalance { .. } => "address_balance",
14771502
ReadRequest::TransactionIdsByAddresses { .. } => "transaction_ids_by_addresses",
14781503
ReadRequest::UtxosByAddresses(_) => "utxos_by_addresses",

zebra-state/src/response.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,9 @@ pub enum ReadResponse {
448448
/// Response to [`ReadRequest::OrchardTree`] with the specified Orchard note commitment tree.
449449
OrchardTree(Option<Arc<orchard::tree::NoteCommitmentTree>>),
450450

451+
/// Response to [`ReadRequest::IronwoodTree`] with the specified Ironwood note commitment tree.
452+
IronwoodTree(Option<Arc<orchard::tree::NoteCommitmentTree>>),
453+
451454
/// Response to [`ReadRequest::SaplingSubtrees`] with the specified Sapling note commitment
452455
/// subtrees.
453456
SaplingSubtrees(
@@ -460,6 +463,12 @@ pub enum ReadResponse {
460463
BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<orchard::tree::Node>>,
461464
),
462465

466+
/// Response to [`ReadRequest::IronwoodSubtrees`] with the specified Ironwood note commitment
467+
/// subtrees. Ironwood reuses the Orchard note type.
468+
IronwoodSubtrees(
469+
BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<orchard::tree::Node>>,
470+
),
471+
463472
/// Response to [`ReadRequest::AddressBalance`] with the total balance of the addresses,
464473
/// and the total received funds, including change.
465474
AddressBalance {
@@ -594,8 +603,10 @@ impl TryFrom<ReadResponse> for Response {
594603
| ReadResponse::AnyChainTransactionIdsForBlock(_)
595604
| ReadResponse::SaplingTree(_)
596605
| ReadResponse::OrchardTree(_)
606+
| ReadResponse::IronwoodTree(_)
597607
| ReadResponse::SaplingSubtrees(_)
598608
| ReadResponse::OrchardSubtrees(_)
609+
| ReadResponse::IronwoodSubtrees(_)
599610
| ReadResponse::AddressBalance { .. }
600611
| ReadResponse::AddressesTransactionIds(_)
601612
| ReadResponse::AddressUtxos(_)

zebra-state/src/service.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1586,6 +1586,10 @@ impl Service<ReadRequest> for ReadStateService {
15861586
read::orchard_tree(state.latest_best_chain(), &state.db, hash_or_height),
15871587
)),
15881588

1589+
ReadRequest::IronwoodTree(hash_or_height) => Ok(ReadResponse::IronwoodTree(
1590+
read::ironwood_tree(state.latest_best_chain(), &state.db, hash_or_height),
1591+
)),
1592+
15891593
ReadRequest::SaplingSubtrees { start_index, limit } => {
15901594
let end_index = limit
15911595
.and_then(|limit| start_index.0.checked_add(limit.0))
@@ -1624,6 +1628,25 @@ impl Service<ReadRequest> for ReadStateService {
16241628
Ok(ReadResponse::OrchardSubtrees(orchard_subtrees))
16251629
}
16261630

1631+
ReadRequest::IronwoodSubtrees { start_index, limit } => {
1632+
let end_index = limit
1633+
.and_then(|limit| start_index.0.checked_add(limit.0))
1634+
.map(NoteCommitmentSubtreeIndex);
1635+
1636+
let best_chain = state.latest_best_chain();
1637+
let ironwood_subtrees = if let Some(end_index) = end_index {
1638+
read::ironwood_subtrees(best_chain, &state.db, start_index..end_index)
1639+
} else {
1640+
// If there is no end bound, just return all the trees.
1641+
// If the end bound would overflow, just returns all the trees, because that's what
1642+
// `zcashd` does. (It never calculates an end bound, so it just keeps iterating until
1643+
// the trees run out.)
1644+
read::ironwood_subtrees(best_chain, &state.db, start_index..)
1645+
};
1646+
1647+
Ok(ReadResponse::IronwoodSubtrees(ironwood_subtrees))
1648+
}
1649+
16271650
// For the get_address_balance RPC.
16281651
ReadRequest::AddressBalance(addresses) => {
16291652
let (balance, received) =

zebra-state/src/service/finalized_state/zebra_db/block.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,18 @@ impl ZebraDb {
254254
self.orchard_tree_by_height(&height)
255255
}
256256

257+
/// Returns the Ironwood [`note commitment tree`](orchard::tree::NoteCommitmentTree) specified by
258+
/// a hash or height, if it exists in the finalized state.
259+
#[allow(clippy::unwrap_in_result)]
260+
pub fn ironwood_tree_by_hash_or_height(
261+
&self,
262+
hash_or_height: HashOrHeight,
263+
) -> Option<Arc<orchard::tree::NoteCommitmentTree>> {
264+
let height = hash_or_height.height_or_else(|hash| self.height(hash))?;
265+
266+
self.ironwood_tree_by_height(&height)
267+
}
268+
257269
// Read tip block methods
258270

259271
/// Returns the hash of the current finalized tip block.

zebra-state/src/service/read.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ pub use find::{
4242
find_chain_headers, find_fork_point, hash_by_height, height_by_hash, next_median_time_past,
4343
non_finalized_state_contains_block_hash, tip, tip_height, tip_with_value_balance,
4444
};
45-
pub use tree::{orchard_subtrees, orchard_tree, sapling_subtrees, sapling_tree};
45+
pub use tree::{
46+
ironwood_subtrees, ironwood_tree, orchard_subtrees, orchard_tree, sapling_subtrees,
47+
sapling_tree,
48+
};
4649

4750
#[cfg(any(test, feature = "proptest-impl"))]
4851
#[allow(unused_imports)]

0 commit comments

Comments
 (0)