This repository was archived by the owner on Nov 12, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
feat: init single merkle proof #85
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| const std = @import("std"); | ||
| const Allocator = std.mem.Allocator; | ||
|
|
||
| const GindexUint = @import("hashing").GindexUint; | ||
| const Node = @import("Node.zig"); | ||
| const Gindex = @import("gindex.zig").Gindex; | ||
|
|
||
| const root_gindex_value: GindexUint = 1; | ||
|
|
||
| pub const Error = error{ | ||
| /// Allocator or pool could not reserve enough memory. | ||
| OutOfMemory, | ||
| /// Provided generalized index is not part of the binary tree (must be >= 1). | ||
| InvalidGindex, | ||
| /// Witness list length does not match the gindex path length. | ||
| InvalidWitnessLength, | ||
| }; | ||
|
|
||
| pub const SingleProof = struct { | ||
| leaf: [32]u8, | ||
| witnesses: [][32]u8, | ||
|
|
||
| pub fn deinit(self: *SingleProof, allocator: Allocator) void { | ||
| allocator.free(self.witnesses); | ||
| self.* = undefined; | ||
| } | ||
| }; | ||
|
|
||
| /// Produces a single Merkle proof for the node at `gindex`. | ||
| pub fn createSingleProof( | ||
| allocator: Allocator, | ||
| pool: *Node.Pool, | ||
| root: Node.Id, | ||
| gindex: Gindex, | ||
| ) (Node.Error || Error)!SingleProof { | ||
| if (@intFromEnum(gindex) < root_gindex_value) { | ||
| return error.InvalidGindex; | ||
| } | ||
|
|
||
| const path_len = gindex.pathLen(); | ||
| var witnesses = try allocator.alloc([32]u8, path_len); | ||
| errdefer allocator.free(witnesses); | ||
|
|
||
| if (path_len == 0) { | ||
| return SingleProof{ | ||
| .leaf = root.getRoot(pool).*, | ||
| .witnesses = witnesses, | ||
| }; | ||
| } | ||
|
|
||
| var node_id = root; | ||
| var path = gindex.toPath(); | ||
|
|
||
| for (0..path_len) |depth_idx| { | ||
| const witness_index = path_len - 1 - depth_idx; | ||
|
|
||
| if (path.left()) { | ||
| const right_id = try node_id.getRight(pool); | ||
| witnesses[witness_index] = right_id.getRoot(pool).*; | ||
| node_id = try node_id.getLeft(pool); | ||
| } else { | ||
| const left_id = try node_id.getLeft(pool); | ||
| witnesses[witness_index] = left_id.getRoot(pool).*; | ||
| node_id = try node_id.getRight(pool); | ||
| } | ||
|
|
||
| path.next(); | ||
| } | ||
|
|
||
| return SingleProof{ | ||
| .leaf = node_id.getRoot(pool).*, | ||
| .witnesses = witnesses, | ||
| }; | ||
| } | ||
|
|
||
| /// Build a fresh node tree from a single Merkle proof. | ||
| pub fn createNodeFromSingleProof( | ||
| pool: *Node.Pool, | ||
| gindex: Gindex, | ||
| leaf: [32]u8, | ||
| witnesses: []const [32]u8, | ||
| ) (Node.Error || Error)!Node.Id { | ||
| if (@intFromEnum(gindex) < root_gindex_value) { | ||
| return error.InvalidGindex; | ||
| } | ||
|
|
||
| const path_len = gindex.pathLen(); | ||
| if (witnesses.len != path_len) { | ||
| return error.InvalidWitnessLength; | ||
| } | ||
|
|
||
| var node_id = try pool.createLeaf(&leaf, false); | ||
| errdefer pool.unref(node_id); | ||
| var index_value: GindexUint = @intFromEnum(gindex); | ||
|
|
||
| for (witnesses) |witness| { | ||
| const sibling_id = try pool.createLeaf(&witness, false); | ||
| errdefer pool.unref(sibling_id); | ||
|
|
||
| node_id = try if ((index_value & 1) == 0) | ||
| pool.createBranch(node_id, sibling_id, false) | ||
| else | ||
| pool.createBranch(sibling_id, node_id, false); | ||
|
|
||
| index_value >>= 1; | ||
| } | ||
|
|
||
| // Raise the reference count so callers own the result. | ||
| try pool.ref(node_id); | ||
| return node_id; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| const std = @import("std"); | ||
| const testing = std.testing; | ||
|
|
||
| const Node = @import("Node.zig"); | ||
| const Gindex = @import("gindex.zig").Gindex; | ||
| const proof = @import("proof.zig"); | ||
| const Depth = @import("hashing").Depth; | ||
|
|
||
| fn makeLeaf(value: u8) [32]u8 { | ||
| var out: [32]u8 = [_]u8{0} ** 32; | ||
| out[0] = value; | ||
| return out; | ||
| } | ||
|
|
||
| fn buildFullTree(pool: *Node.Pool, depth: usize, next_value: *u8) Node.Error!Node.Id { | ||
| if (depth == 0) { | ||
| const leaf_hash = makeLeaf(next_value.*); | ||
| next_value.* +%= 1; | ||
| return pool.createLeaf(&leaf_hash, false); | ||
| } | ||
|
|
||
| const left = try buildFullTree(pool, depth - 1, next_value); | ||
| const right = try buildFullTree(pool, depth - 1, next_value); | ||
| return pool.createBranch(left, right, false); | ||
| } | ||
|
|
||
| // Verifies a proof for gindex 6 (depth 2, index 2) reconstructs the original root. | ||
| test "single proof roundtrip" { | ||
| var pool = try Node.Pool.init(testing.allocator, 128); | ||
| defer pool.deinit(); | ||
|
|
||
| const leaf_hashes = [_][32]u8{ | ||
| makeLeaf(1), | ||
| makeLeaf(2), | ||
| makeLeaf(3), | ||
| makeLeaf(4), | ||
| }; | ||
|
|
||
| const leaf0 = try pool.createLeaf(&leaf_hashes[0], false); | ||
| const leaf1 = try pool.createLeaf(&leaf_hashes[1], false); | ||
| const leaf2 = try pool.createLeaf(&leaf_hashes[2], false); | ||
| const leaf3 = try pool.createLeaf(&leaf_hashes[3], false); | ||
|
|
||
| const left = try pool.createBranch(leaf0, leaf1, false); | ||
| const right = try pool.createBranch(leaf2, leaf3, false); | ||
| const root = try pool.createBranch(left, right, true); | ||
| defer pool.unref(root); | ||
|
|
||
| const gindex = Gindex.fromDepth(2, 2); | ||
|
|
||
| var single_proof = try proof.createSingleProof(testing.allocator, &pool, root, gindex); | ||
| defer single_proof.deinit(testing.allocator); | ||
|
|
||
| try testing.expectEqualSlices(u8, &leaf_hashes[2], &single_proof.leaf); | ||
| try testing.expectEqual(@as(usize, 2), single_proof.witnesses.len); | ||
|
|
||
| const root_hash = root.getRoot(&pool).*; | ||
|
|
||
| var pool2 = try Node.Pool.init(testing.allocator, 128); | ||
| defer pool2.deinit(); | ||
|
|
||
| const reconstructed = try proof.createNodeFromSingleProof(&pool2, gindex, single_proof.leaf, single_proof.witnesses); | ||
| defer pool2.unref(reconstructed); | ||
|
|
||
| const reconstructed_hash = reconstructed.getRoot(&pool2).*; | ||
| try testing.expectEqualSlices(u8, &root_hash, &reconstructed_hash); | ||
| } | ||
|
|
||
| // Checks every leaf in a depth-4 tree produces the same root after reconstruction. | ||
| test "single proof root matches across leaves" { | ||
| const build_depth: usize = 4; | ||
| const pool_capacity: u32 = @intCast((@as(usize, 1) << (build_depth + 1))); | ||
|
|
||
| var pool = try Node.Pool.init(testing.allocator, pool_capacity); | ||
| defer pool.deinit(); | ||
|
|
||
| var next_value: u8 = 1; | ||
| const raw_root = try buildFullTree(&pool, build_depth, &next_value); | ||
| try pool.ref(raw_root); | ||
| defer pool.unref(raw_root); | ||
|
|
||
| const expected_root = raw_root.getRoot(&pool).*; | ||
| const leaf_depth: Depth = @intCast(build_depth); | ||
| const leaf_count = @as(usize, 1) << build_depth; | ||
|
|
||
| for (0..leaf_count) |leaf_index| { | ||
| const gindex = Gindex.fromDepth(leaf_depth, leaf_index); | ||
| var single_proof = try proof.createSingleProof(testing.allocator, &pool, raw_root, gindex); | ||
| defer single_proof.deinit(testing.allocator); | ||
|
|
||
| var temp_pool = try Node.Pool.init(testing.allocator, 64); | ||
| defer temp_pool.deinit(); | ||
|
|
||
| const rebuilt = try proof.createNodeFromSingleProof(&temp_pool, gindex, single_proof.leaf, single_proof.witnesses); | ||
| defer temp_pool.unref(rebuilt); | ||
|
|
||
| const rebuilt_root = rebuilt.getRoot(&temp_pool).*; | ||
| try testing.expectEqualSlices(u8, &expected_root, &rebuilt_root); | ||
| } | ||
| } | ||
|
|
||
| // Attempting to prove beyond the tree height should bubble up Node.InvalidNode. | ||
| test "single proof invalid navigation" { | ||
| var pool = try Node.Pool.init(testing.allocator, 64); | ||
| defer pool.deinit(); | ||
|
|
||
| const leaf_hash = makeLeaf(42); | ||
| const root = try pool.createLeaf(&leaf_hash, true); | ||
| defer pool.unref(root); | ||
|
|
||
| const gindex = Gindex.fromDepth(3, 0); | ||
| try testing.expectError(Node.Error.InvalidNode, proof.createSingleProof(testing.allocator, &pool, root, gindex)); | ||
| } | ||
|
|
||
| // Zero gindex must be rejected by both proof creation and reconstruction entry points. | ||
| test "single proof invalid gindex" { | ||
| var pool = try Node.Pool.init(testing.allocator, 8); | ||
| defer pool.deinit(); | ||
|
|
||
| const leaf_hash = makeLeaf(9); | ||
| const root = try pool.createLeaf(&leaf_hash, true); | ||
| defer pool.unref(root); | ||
|
|
||
| const zero_gindex: Gindex = @enumFromInt(0); | ||
| try testing.expectError(proof.Error.InvalidGindex, proof.createSingleProof(testing.allocator, &pool, root, zero_gindex)); | ||
|
|
||
| const empty_witnesses: []const [32]u8 = &[_][32]u8{}; | ||
| try testing.expectError(proof.Error.InvalidGindex, proof.createNodeFromSingleProof(&pool, zero_gindex, leaf_hash, empty_witnesses)); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic within this
if/elseblock can be simplified to improve readability and reduce code duplication. Since both branches need to access the left and right children of the current node (one for the witness, one for the next step in the traversal), you can fetch both child IDs before the conditional statement.