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
};