From 31b7b1bd02796e93193acd670bab6b33aac8a7ab Mon Sep 17 00:00:00 2001 From: Ranjna Ganesh Ram Date: Thu, 16 Jul 2026 17:52:06 +0530 Subject: [PATCH] feat(wasm-privacy-coin): thread owned flag into shardtree retention Per-commitment ownership booleans are now threaded from the proto wire format into the Rust shardtree append path. Owned leaves are assigned Retention::Marked so the shardtree preserves their witness paths; non-owned leaves remain Retention::Ephemeral (or Checkpoint for the last commitment in a block). Ticket: CSHLD-1208 --- packages/wasm-privacy-coin/pom.xml | 6 + .../proto/privacy_coin.proto | 1 + packages/wasm-privacy-coin/src/lib.rs | 3 +- .../privacycoin/zcash/ShieldedMerkleTree.java | 12 +- .../zcash/ShieldedMerkleTreeTest.java | 179 ++++++++++++++---- packages/wasm-privacy-coin/src/zcash/tree.rs | 10 +- 6 files changed, 169 insertions(+), 42 deletions(-) diff --git a/packages/wasm-privacy-coin/pom.xml b/packages/wasm-privacy-coin/pom.xml index 0d4f7cd3550..4e8c71fbaa2 100644 --- a/packages/wasm-privacy-coin/pom.xml +++ b/packages/wasm-privacy-coin/pom.xml @@ -41,6 +41,12 @@ 5.10.3 test + + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + test + diff --git a/packages/wasm-privacy-coin/proto/privacy_coin.proto b/packages/wasm-privacy-coin/proto/privacy_coin.proto index 8268bfd1ab9..691fb10cb02 100644 --- a/packages/wasm-privacy-coin/proto/privacy_coin.proto +++ b/packages/wasm-privacy-coin/proto/privacy_coin.proto @@ -15,6 +15,7 @@ message AppendCommitmentsRequest { uint32 block_height = 1; repeated bytes commitments = 2; // each exactly 32 bytes (cmx) optional bytes expected_root = 3; // 32 bytes; absent = skip verification + repeated bool owned = 4; // per-commitment ownership flag; absent/empty = all not-owned } message TruncateRequest { diff --git a/packages/wasm-privacy-coin/src/lib.rs b/packages/wasm-privacy-coin/src/lib.rs index 56467db1377..f0a08be359f 100644 --- a/packages/wasm-privacy-coin/src/lib.rs +++ b/packages/wasm-privacy-coin/src/lib.rs @@ -186,8 +186,9 @@ pub unsafe extern "C" fn append_commitments(ptr: *const u8, len: u32) -> i32 { Some(tree) => { let commitments: Vec> = req.commitments.iter().map(|b| b.to_vec()).collect(); + let owned = req.owned; let exp = expected_root.as_deref(); - match tree.append_commitments(req.block_height, commitments, exp) { + match tree.append_commitments(req.block_height, commitments, owned, exp) { Ok(root) => write_ok_bytes(root), Err(e) => { let (code, msg) = split_error(&e); diff --git a/packages/wasm-privacy-coin/src/main/java/com/bitgo/wasm/privacycoin/zcash/ShieldedMerkleTree.java b/packages/wasm-privacy-coin/src/main/java/com/bitgo/wasm/privacycoin/zcash/ShieldedMerkleTree.java index 8017c574195..596784b9347 100644 --- a/packages/wasm-privacy-coin/src/main/java/com/bitgo/wasm/privacycoin/zcash/ShieldedMerkleTree.java +++ b/packages/wasm-privacy-coin/src/main/java/com/bitgo/wasm/privacycoin/zcash/ShieldedMerkleTree.java @@ -100,12 +100,14 @@ public void ping() { * * @param blockHeight block height (u32 range) * @param commitments shielded note commitment values (cmx) for this block + * @param owned per-commitment ownership flags; {@code null} or shorter list → remaining are false * @param expectedRoot root to verify against; {@code null} to skip * @return computed root after appending * @throws WasmException with code {@code ROOT_MISMATCH} if verification fails */ public ShieldedRoot appendCommitments( - long blockHeight, List commitments, ShieldedRoot expectedRoot) { + long blockHeight, List commitments, List owned, + ShieldedRoot expectedRoot) { requireU32(blockHeight, "blockHeight"); Objects.requireNonNull(commitments, "commitments must not be null"); @@ -115,6 +117,9 @@ public ShieldedRoot appendCommitments( .addAllCommitments(commitments.stream() .map(c -> ByteString.copyFrom(c.bytes())) .toList()); + if (owned != null && !owned.isEmpty()) { + req.addAllOwned(owned); + } if (expectedRoot != null) { req.setExpectedRoot(ByteString.copyFrom(expectedRoot.bytes())); } @@ -123,11 +128,6 @@ public ShieldedRoot appendCommitments( return ShieldedRoot.of(unwrap(r, resp -> resp.getBytesValue().toByteArray())); } - /** Convenience overload — appends without root verification. */ - public ShieldedRoot appendCommitments(long blockHeight, List commitments) { - return appendCommitments(blockHeight, commitments, null); - } - /** * Rolls the tree back to the checkpoint at the given block height. * diff --git a/packages/wasm-privacy-coin/src/test/java/com/bitgo/wasm/privacycoin/zcash/ShieldedMerkleTreeTest.java b/packages/wasm-privacy-coin/src/test/java/com/bitgo/wasm/privacycoin/zcash/ShieldedMerkleTreeTest.java index 6257925f15a..cf509da9900 100644 --- a/packages/wasm-privacy-coin/src/test/java/com/bitgo/wasm/privacycoin/zcash/ShieldedMerkleTreeTest.java +++ b/packages/wasm-privacy-coin/src/test/java/com/bitgo/wasm/privacycoin/zcash/ShieldedMerkleTreeTest.java @@ -2,6 +2,8 @@ import com.bitgo.wasm.privacycoin.MerkleTreeInfo; import com.bitgo.wasm.privacycoin.WasmException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import java.util.Collections; @@ -35,10 +37,22 @@ class ShieldedMerkleTreeTest { "0101000000000000000000000000000000000000000000000000000000000000000000"); /** - * A valid 32-byte Orchard commitment (Pallas base field element = 1, LE-encoded). + * Valid 32-byte Orchard commitments (Pallas base field elements 1, 2, 3 LE-encoded). + * All three are below the Pallas base field prime and therefore valid field elements. */ private static final ShieldedCommitment CMX = ShieldedCommitment.of(HexFormat.of().parseHex( "0100000000000000000000000000000000000000000000000000000000000000")); + private static final ShieldedCommitment CMX_2 = ShieldedCommitment.of(HexFormat.of().parseHex( + "0200000000000000000000000000000000000000000000000000000000000000")); + private static final ShieldedCommitment CMX_3 = ShieldedCommitment.of(HexFormat.of().parseHex( + "0300000000000000000000000000000000000000000000000000000000000000")); + + private static final ObjectMapper JSON = new ObjectMapper(); + + // f-flag bit values from shardtree RetentionFlags + private static final int F_CHECKPOINT = 1; // not owned, last in block + private static final int F_MARKED = 2; // owned, not last + private static final int F_CHECKPOINT_MARKED = 3; // owned, last in block // ------------------------------------------------------------------------- // ping @@ -130,14 +144,14 @@ void fromState_invalidJson_throwsWasmException() { void appendCommitments_nullCommitmentsList_throwsNullPointerException() { try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { assertThrows(NullPointerException.class, () -> - tree.appendCommitments(1L, null)); + tree.appendCommitments(1L, null, List.of(), null)); } } @Test void appendCommitments_emptyBlock_returnsRoot() { try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - ShieldedRoot root = tree.appendCommitments(1L, Collections.emptyList()); + ShieldedRoot root = tree.appendCommitments(1L, Collections.emptyList(), List.of(), null); assertNotNull(root); assertEquals(ShieldedRoot.SIZE, root.bytes().length); } @@ -146,7 +160,7 @@ void appendCommitments_emptyBlock_returnsRoot() { @Test void appendCommitments_emptyBlock_doesNotChangeLeafCount() { try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - tree.appendCommitments(1L, Collections.emptyList()); + tree.appendCommitments(1L, Collections.emptyList(), List.of(), null); MerkleTreeInfo info = tree.getInfo(); assertEquals(0L, info.leafCount); assertEquals(Long.valueOf(1L), info.tipHeight); @@ -156,9 +170,9 @@ void appendCommitments_emptyBlock_doesNotChangeLeafCount() { @Test void appendCommitments_multipleEmptyBlocks_incrementsCheckpointCount() { try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - tree.appendCommitments(1L, Collections.emptyList()); - tree.appendCommitments(2L, Collections.emptyList()); - tree.appendCommitments(3L, Collections.emptyList()); + tree.appendCommitments(1L, Collections.emptyList(), List.of(), null); + tree.appendCommitments(2L, Collections.emptyList(), List.of(), null); + tree.appendCommitments(3L, Collections.emptyList(), List.of(), null); MerkleTreeInfo info = tree.getInfo(); assertEquals(Long.valueOf(3L), info.tipHeight); assertEquals(3, info.checkpointCount); @@ -173,7 +187,7 @@ void appendCommitments_multipleEmptyBlocks_incrementsCheckpointCount() { @Test void appendCommitments_withCommitment_increasesLeafCount() { try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - tree.appendCommitments(1L, List.of(CMX)); + tree.appendCommitments(1L, List.of(CMX), List.of(), null); MerkleTreeInfo info = tree.getInfo(); assertEquals(1L, info.leafCount); assertEquals(Long.valueOf(1L), info.tipHeight); @@ -183,8 +197,8 @@ void appendCommitments_withCommitment_increasesLeafCount() { @Test void appendCommitments_twoBlocks_leafCountAccumulates() { try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - tree.appendCommitments(1L, List.of(CMX)); - tree.appendCommitments(2L, List.of(CMX)); + tree.appendCommitments(1L, List.of(CMX), List.of(), null); + tree.appendCommitments(2L, List.of(CMX), List.of(), null); MerkleTreeInfo info = tree.getInfo(); assertEquals(2L, info.leafCount); assertEquals(Long.valueOf(2L), info.tipHeight); @@ -195,8 +209,8 @@ void appendCommitments_twoBlocks_leafCountAccumulates() { @Test void appendCommitments_twoBlocks_rootChanges() { try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - ShieldedRoot root1 = tree.appendCommitments(1L, List.of(CMX)); - ShieldedRoot root2 = tree.appendCommitments(2L, List.of(CMX)); + ShieldedRoot root1 = tree.appendCommitments(1L, List.of(CMX), List.of(), null); + ShieldedRoot root2 = tree.appendCommitments(2L, List.of(CMX), List.of(), null); assertNotEquals(root1, root2, "Root must change when new leaves are appended"); } } @@ -206,10 +220,10 @@ void appendCommitments_returnsRoot_isDeterministic() { // Two fresh trees given the same commitment produce the same root. ShieldedRoot root1, root2; try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - root1 = tree.appendCommitments(1L, List.of(CMX)); + root1 = tree.appendCommitments(1L, List.of(CMX), List.of(), null); } try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - root2 = tree.appendCommitments(1L, List.of(CMX)); + root2 = tree.appendCommitments(1L, List.of(CMX), List.of(), null); } assertEquals(root1, root2); } @@ -219,10 +233,10 @@ void appendCommitments_correctExpectedRoot_succeeds() { // Capture actual root first, then confirm passing it as expectedRoot doesn't throw. ShieldedRoot actualRoot; try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - actualRoot = tree.appendCommitments(1L, List.of(CMX)); + actualRoot = tree.appendCommitments(1L, List.of(CMX), List.of(), null); } try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - ShieldedRoot returned = tree.appendCommitments(1L, List.of(CMX), actualRoot); + ShieldedRoot returned = tree.appendCommitments(1L, List.of(CMX), List.of(), actualRoot); assertEquals(actualRoot, returned); } } @@ -232,7 +246,7 @@ void appendCommitments_wrongExpectedRoot_throwsRootMismatch() { ShieldedRoot wrongRoot = ShieldedRoot.of(new byte[ShieldedRoot.SIZE]); WasmException ex = assertThrows(WasmException.class, () -> { try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - tree.appendCommitments(1L, List.of(CMX), wrongRoot); + tree.appendCommitments(1L, List.of(CMX), List.of(), wrongRoot); } }); assertEquals("ROOT_MISMATCH", ex.getErrorCode()); @@ -241,7 +255,7 @@ void appendCommitments_wrongExpectedRoot_throwsRootMismatch() { @Test void appendCommitments_multipleCommitmentsInOneBlock_allLeavesCounted() { try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - tree.appendCommitments(1L, List.of(CMX, CMX, CMX)); + tree.appendCommitments(1L, List.of(CMX, CMX, CMX), List.of(), null); MerkleTreeInfo info = tree.getInfo(); assertEquals(3L, info.leafCount); assertEquals(1, info.checkpointCount); @@ -255,8 +269,8 @@ void appendCommitments_multipleCommitmentsInOneBlock_allLeavesCounted() { @Test void truncateToCheckpoint_rollsBackTipHeightAndLeafCount() { try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - tree.appendCommitments(1L, List.of(CMX)); - tree.appendCommitments(2L, List.of(CMX)); + tree.appendCommitments(1L, List.of(CMX), List.of(), null); + tree.appendCommitments(2L, List.of(CMX), List.of(), null); tree.truncateToCheckpoint(1L); @@ -270,8 +284,8 @@ void truncateToCheckpoint_rollsBackTipHeightAndLeafCount() { void truncateToCheckpoint_returnsRootMatchingOriginalAppend() { ShieldedRoot rootAt1; try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - rootAt1 = tree.appendCommitments(1L, List.of(CMX)); - tree.appendCommitments(2L, List.of(CMX)); + rootAt1 = tree.appendCommitments(1L, List.of(CMX), List.of(), null); + tree.appendCommitments(2L, List.of(CMX), List.of(), null); ShieldedRoot restoredRoot = tree.truncateToCheckpoint(1L); assertEquals(rootAt1, restoredRoot); @@ -282,7 +296,7 @@ void truncateToCheckpoint_returnsRootMatchingOriginalAppend() { void truncateToCheckpoint_unknownHeight_throwsCheckpointNotFound() { WasmException ex = assertThrows(WasmException.class, () -> { try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - tree.appendCommitments(100L, Collections.emptyList()); + tree.appendCommitments(100L, Collections.emptyList(), List.of(), null); tree.truncateToCheckpoint(999L); } }); @@ -297,7 +311,7 @@ void truncateToCheckpoint_unknownHeight_throwsCheckpointNotFound() { void saveAndLoad_roundTrip_infoFieldsSurvive() { TreeState savedState; try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - tree.appendCommitments(100L, Collections.emptyList()); + tree.appendCommitments(100L, Collections.emptyList(), List.of(), null); savedState = tree.save(); } try (ShieldedMerkleTree restored = ShieldedMerkleTree.fromState(savedState)) { @@ -312,7 +326,7 @@ void saveAndLoad_roundTrip_infoFieldsSurvive() { void saveAndLoad_withLeaves_preservesLeafCount() { TreeState savedState; try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - tree.appendCommitments(1L, List.of(CMX, CMX)); + tree.appendCommitments(1L, List.of(CMX, CMX), List.of(), null); savedState = tree.save(); } try (ShieldedMerkleTree restored = ShieldedMerkleTree.fromState(savedState)) { @@ -326,11 +340,11 @@ void saveAndLoad_withLeaves_preservesLeafCount() { void saveAndLoad_restoredTreeCanContinueAppending() { TreeState savedState; try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - tree.appendCommitments(1L, List.of(CMX)); + tree.appendCommitments(1L, List.of(CMX), List.of(), null); savedState = tree.save(); } try (ShieldedMerkleTree restored = ShieldedMerkleTree.fromState(savedState)) { - assertDoesNotThrow(() -> restored.appendCommitments(2L, List.of(CMX))); + assertDoesNotThrow(() -> restored.appendCommitments(2L, List.of(CMX), List.of(), null)); MerkleTreeInfo info = restored.getInfo(); assertEquals(2L, info.leafCount); assertEquals(Long.valueOf(2L), info.tipHeight); @@ -342,7 +356,7 @@ void saveAndLoad_stateBytesRoundTrip() { // Verify TreeState.bytes() / TreeState.of() round-trips without data loss. TreeState saved; try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - tree.appendCommitments(5L, List.of(CMX)); + tree.appendCommitments(5L, List.of(CMX), List.of(), null); saved = tree.save(); } TreeState restored = TreeState.of(saved.bytes()); @@ -362,11 +376,11 @@ void appendCommitments_wrongSizeCommitment_throwsIllegalArgumentException() { @Test void truncateToCheckpoint_treeIsUsableAfterRollback() { try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - tree.appendCommitments(1L, List.of(CMX)); - tree.appendCommitments(2L, List.of(CMX)); + tree.appendCommitments(1L, List.of(CMX), List.of(), null); + tree.appendCommitments(2L, List.of(CMX), List.of(), null); tree.truncateToCheckpoint(1L); - ShieldedRoot root = tree.appendCommitments(3L, List.of(CMX)); + ShieldedRoot root = tree.appendCommitments(3L, List.of(CMX), List.of(), null); assertEquals(ShieldedRoot.SIZE, root.bytes().length); MerkleTreeInfo info = tree.getInfo(); assertEquals(Long.valueOf(3L), info.tipHeight); @@ -382,7 +396,7 @@ void truncateToCheckpoint_treeIsUsableAfterRollback() { void appendCommitments_negativeBlockHeight_throwsIllegalArgumentException() { try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { assertThrows(IllegalArgumentException.class, () -> - tree.appendCommitments(-1L, Collections.emptyList())); + tree.appendCommitments(-1L, Collections.emptyList(), List.of(), null)); } } @@ -390,7 +404,7 @@ void appendCommitments_negativeBlockHeight_throwsIllegalArgumentException() { void appendCommitments_blockHeightAboveU32Max_throwsIllegalArgumentException() { try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { assertThrows(IllegalArgumentException.class, () -> - tree.appendCommitments(0x1_0000_0000L, Collections.emptyList())); + tree.appendCommitments(0x1_0000_0000L, Collections.emptyList(), List.of(), null)); } } @@ -411,11 +425,108 @@ void twoInstances_shareNoState() { try (ShieldedMerkleTree tree1 = ShieldedMerkleTree.fromState(EMPTY_STATE); ShieldedMerkleTree tree2 = ShieldedMerkleTree.fromState(EMPTY_STATE)) { - tree1.appendCommitments(1L, List.of(CMX)); + tree1.appendCommitments(1L, List.of(CMX), List.of(), null); MerkleTreeInfo info2 = tree2.getInfo(); assertNull(info2.tipHeight); assertEquals(0L, info2.leafCount); } } + + // ------------------------------------------------------------------------- + // Retention f-flag assertions + // ------------------------------------------------------------------------- + + /** + * Verifies that the per-leaf f-flag in the serialized shard tree JSON reflects ownership: + * + *
    + *
  • CMX (i=0, owned, not last) → f=2 MARKED + *
  • CMX_2 (i=1, not owned, not last) → f=0 EPHEMERAL + *
  • CMX_3 (i=2, owned, last) → f=3 CHECKPOINT|MARKED + *
+ * + * The hex value stored in the "h" field round-trips through MerkleHashOrchard unchanged, + * so individual leaves that have not yet been paired with a sibling are findable by their + * commitment hex. Completed 2-leaf subtrees are collapsed into a single node whose "h" is + * the combined Merkle hash (not the original commitment bytes) — those are matched by f-flag. + */ + @Test + void appendCommitments_ownedFlag_setsCorrectRetentionFlagInTreeState() throws Exception { + // Block: [CMX(owned, not-last), CMX_2(not-owned, not-last), CMX_3(owned, last)] + // + // MARKED leaves are never pruned by shardtree (they must be retained for witness generation), + // so each owned leaf is individually findable by its commitment hex. + // CMX_2 (EPHEMERAL) at position 1 may be pruned; it is not asserted. + String cmx1Hex = "0100000000000000000000000000000000000000000000000000000000000000"; + String cmx3Hex = "0300000000000000000000000000000000000000000000000000000000000000"; + + try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { + tree.appendCommitments( + 1L, + List.of(CMX, CMX_2, CMX_3), + List.of(true, false, true), + null); + + JsonNode state = JSON.readTree(tree.save().bytes()); + + assertEquals(F_MARKED, findLeafF(state, cmx1Hex), + "CMX: owned, not last → MARKED"); + assertEquals(F_CHECKPOINT_MARKED, findLeafF(state, cmx3Hex), + "CMX_3: owned, last → CHECKPOINT|MARKED"); + } + } + + @Test + void appendCommitments_notOwned_lastLeafIsCheckpointOnly() throws Exception { + // Single commitment — position 0 has no sibling, so it is stored as an individual leaf + String cmxHex = "0100000000000000000000000000000000000000000000000000000000000000"; + + try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(EMPTY_STATE)) { + tree.appendCommitments( + 1L, + List.of(CMX), + List.of(false), + null); + + JsonNode state = JSON.readTree(tree.save().bytes()); + + assertEquals(F_CHECKPOINT, findLeafF(state, cmxHex), + "CMX: not owned, last (only commitment) → CHECKPOINT"); + } + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /** + * Searches the full tree state JSON (shards + cap) for a Leaf node whose "h" field + * equals {@code cmxHex} and returns its "f" value. + * + * @throws AssertionError if the leaf is not found + */ + private static int findLeafF(JsonNode state, String cmxHex) { + for (JsonNode shard : state.path("shards")) { + Integer f = findLeafFInNode(shard.path("tree"), cmxHex); + if (f != null) return f; + } + Integer f = findLeafFInNode(state.path("cap"), cmxHex); + if (f != null) return f; + throw new AssertionError("Leaf with h=" + cmxHex + " not found in tree state"); + } + + private static Integer findLeafFInNode(JsonNode node, String cmxHex) { + if (node == null || node.isMissingNode()) return null; + String type = node.path("type").asText(""); + if ("Leaf".equals(type)) { + return cmxHex.equals(node.path("h").asText()) ? node.path("f").intValue() : null; + } + if ("Parent".equals(type)) { + Integer l = findLeafFInNode(node.path("l"), cmxHex); + return l != null ? l : findLeafFInNode(node.path("r"), cmxHex); + } + return null; + } + } diff --git a/packages/wasm-privacy-coin/src/zcash/tree.rs b/packages/wasm-privacy-coin/src/zcash/tree.rs index 2e06f66c366..b306c2ecc9e 100644 --- a/packages/wasm-privacy-coin/src/zcash/tree.rs +++ b/packages/wasm-privacy-coin/src/zcash/tree.rs @@ -425,6 +425,7 @@ impl OwnedTree { &mut self, block_height: u32, commitments: Vec>, + owned: Vec, expected_root: Option<&[u8]>, ) -> Result, String> { if commitments.is_empty() { @@ -449,11 +450,18 @@ impl OwnedTree { let last_idx = hashes.len() - 1; for (i, hash) in hashes.into_iter().enumerate() { + let is_owned = owned.get(i).copied().unwrap_or(false); let retention = if i == last_idx { Retention::Checkpoint { id: block_height, - marking: Marking::None, + marking: if is_owned { + Marking::Marked + } else { + Marking::None + }, } + } else if is_owned { + Retention::Marked } else { Retention::Ephemeral };