Skip to content

Commit 4905aac

Browse files
committed
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
1 parent b01608b commit 4905aac

7 files changed

Lines changed: 172 additions & 58 deletions

File tree

packages/wasm-privacy-coin/README.md

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -157,15 +157,14 @@ Implements `AutoCloseable`. Always use in try-with-resources.
157157

158158
#### Instance methods
159159

160-
| Method | Returns | Description |
161-
| ------------------------------------------------------------------------------------------------------ | ---------------- | --------------------------------------------------------------- |
162-
| `ping()` | `void` | Verifies the WASM module is alive |
163-
| `appendCommitments(long blockHeight, List<ShieldedCommitment> commitments)` | `ShieldedRoot` | Append cmx values, checkpoint the tree, return the new root |
164-
| `appendCommitments(long blockHeight, List<ShieldedCommitment> commitments, ShieldedRoot expectedRoot)` | `ShieldedRoot` | Same, with optional root verification |
165-
| `truncateToCheckpoint(long blockHeight)` | `ShieldedRoot` | Roll back to a prior checkpoint, return the root at that height |
166-
| `save()` | `TreeState` | Serialize tree state for persistence |
167-
| `getInfo()` | `MerkleTreeInfo` | Return tip height, leaf count, checkpoint count |
168-
| `close()` | `void` | Drop the in-WASM tree and release the Chicory instance |
160+
| Method | Returns | Description |
161+
| --------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
162+
| `ping()` | `void` | Verifies the WASM module is alive |
163+
| `appendCommitments(long blockHeight, List<ShieldedCommitment> commitments, List<Boolean> owned, ShieldedRoot expectedRoot)` | `ShieldedRoot` | Append cmx values, checkpoint the tree; `owned` marks which commitments belong to this wallet (pass `List.of()` if unused); `expectedRoot` is optional root verification (pass `null` to skip) |
164+
| `truncateToCheckpoint(long blockHeight)` | `ShieldedRoot` | Roll back to a prior checkpoint, return the root at that height |
165+
| `save()` | `TreeState` | Serialize tree state for persistence |
166+
| `getInfo()` | `MerkleTreeInfo` | Return tip height, leaf count, checkpoint count |
167+
| `close()` | `void` | Drop the in-WASM tree and release the Chicory instance |
169168

170169
**`blockHeight`** must be in the range `[0, 4_294_967_295]` (Rust `u32`). Passing a
171170
negative value or a value above `0xFFFFFFFFL` throws `IllegalArgumentException`
@@ -275,9 +274,9 @@ ShieldedCommitment cmx = ShieldedCommitment.of(HexFormat.of().parseHex(
275274
"0100000000000000000000000000000000000000000000000000000000000000"));
276275

277276
try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(savedState)) {
278-
ShieldedRoot root = tree.appendCommitments(2_500_001L, List.of(cmx));
277+
ShieldedRoot root = tree.appendCommitments(2_500_001L, List.of(cmx), List.of(), null);
279278
// Empty block — still creates a checkpoint
280-
tree.appendCommitments(2_500_002L, Collections.emptyList());
279+
tree.appendCommitments(2_500_002L, Collections.emptyList(), List.of(), null);
281280
}
282281
```
283282

@@ -287,7 +286,7 @@ try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(savedState)) {
287286
ShieldedRoot expected = ShieldedRoot.of(expectedRootBytes);
288287

289288
try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(savedState)) {
290-
ShieldedRoot root = tree.appendCommitments(2_500_001L, cmxList, expected);
289+
ShieldedRoot root = tree.appendCommitments(2_500_001L, cmxList, List.of(), expected);
291290
// root.equals(expected) is guaranteed
292291
}
293292
```
@@ -299,7 +298,7 @@ try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(savedState)) {
299298
```java
300299
byte[] snapshot;
301300
try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(emptyState)) {
302-
tree.appendCommitments(100L, List.of(cmx));
301+
tree.appendCommitments(100L, List.of(cmx), List.of(), null);
303302
snapshot = tree.save().bytes();
304303
}
305304

@@ -313,8 +312,8 @@ try (ShieldedMerkleTree restored = ShieldedMerkleTree.fromState(TreeState.of(sna
313312

314313
```java
315314
try (ShieldedMerkleTree tree = ShieldedMerkleTree.fromState(savedState)) {
316-
ShieldedRoot root100 = tree.appendCommitments(100L, List.of(cmx));
317-
tree.appendCommitments(101L, List.of(cmx));
315+
ShieldedRoot root100 = tree.appendCommitments(100L, List.of(cmx), List.of(), null);
316+
tree.appendCommitments(101L, List.of(cmx), List.of(), null);
318317

319318
ShieldedRoot restoredRoot = tree.truncateToCheckpoint(100L);
320319
// restoredRoot.equals(root100)

packages/wasm-privacy-coin/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@
4141
<version>5.10.3</version>
4242
<scope>test</scope>
4343
</dependency>
44+
<dependency>
45+
<groupId>com.fasterxml.jackson.core</groupId>
46+
<artifactId>jackson-databind</artifactId>
47+
<version>2.17.0</version>
48+
<scope>test</scope>
49+
</dependency>
4450
</dependencies>
4551

4652
<distributionManagement>

packages/wasm-privacy-coin/proto/privacy_coin.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ message AppendCommitmentsRequest {
1515
uint32 block_height = 1;
1616
repeated bytes commitments = 2; // each exactly 32 bytes (cmx)
1717
optional bytes expected_root = 3; // 32 bytes; absent = skip verification
18+
repeated bool owned = 4; // per-commitment ownership flag; absent/empty = all not-owned
1819
}
1920

2021
message TruncateRequest {

packages/wasm-privacy-coin/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,9 @@ pub unsafe extern "C" fn append_commitments(ptr: *const u8, len: u32) -> i32 {
186186
Some(tree) => {
187187
let commitments: Vec<Vec<u8>> =
188188
req.commitments.iter().map(|b| b.to_vec()).collect();
189+
let owned = req.owned;
189190
let exp = expected_root.as_deref();
190-
match tree.append_commitments(req.block_height, commitments, exp) {
191+
match tree.append_commitments(req.block_height, commitments, owned, exp) {
191192
Ok(root) => write_ok_bytes(root),
192193
Err(e) => {
193194
let (code, msg) = split_error(&e);

packages/wasm-privacy-coin/src/main/java/com/bitgo/wasm/privacycoin/zcash/ShieldedMerkleTree.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,14 @@ public void ping() {
100100
*
101101
* @param blockHeight block height (u32 range)
102102
* @param commitments shielded note commitment values (cmx) for this block
103+
* @param owned per-commitment ownership flags; {@code null} or shorter list → remaining are false
103104
* @param expectedRoot root to verify against; {@code null} to skip
104105
* @return computed root after appending
105106
* @throws WasmException with code {@code ROOT_MISMATCH} if verification fails
106107
*/
107108
public ShieldedRoot appendCommitments(
108-
long blockHeight, List<ShieldedCommitment> commitments, ShieldedRoot expectedRoot) {
109+
long blockHeight, List<ShieldedCommitment> commitments, List<Boolean> owned,
110+
ShieldedRoot expectedRoot) {
109111
requireU32(blockHeight, "blockHeight");
110112
Objects.requireNonNull(commitments, "commitments must not be null");
111113

@@ -115,6 +117,9 @@ public ShieldedRoot appendCommitments(
115117
.addAllCommitments(commitments.stream()
116118
.map(c -> ByteString.copyFrom(c.bytes()))
117119
.toList());
120+
if (owned != null && !owned.isEmpty()) {
121+
req.addAllOwned(owned);
122+
}
118123
if (expectedRoot != null) {
119124
req.setExpectedRoot(ByteString.copyFrom(expectedRoot.bytes()));
120125
}
@@ -123,11 +128,6 @@ public ShieldedRoot appendCommitments(
123128
return ShieldedRoot.of(unwrap(r, resp -> resp.getBytesValue().toByteArray()));
124129
}
125130

126-
/** Convenience overload — appends without root verification. */
127-
public ShieldedRoot appendCommitments(long blockHeight, List<ShieldedCommitment> commitments) {
128-
return appendCommitments(blockHeight, commitments, null);
129-
}
130-
131131
/**
132132
* Rolls the tree back to the checkpoint at the given block height.
133133
*

0 commit comments

Comments
 (0)