Skip to content
This repository was archived by the owner on Nov 12, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions src/persistent_merkle_tree/proof.zig
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);
}
Comment on lines +57 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic within this if/else block 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.

            const left_id = try node_id.getLeft(pool);
            const right_id = try node_id.getRight(pool);

            if (path.left()) {
                witnesses[witness_index] = right_id.getRoot(pool).*;
                node_id = left_id;
            } else {
                witnesses[witness_index] = left_id.getRoot(pool).*;
                node_id = right_id;
            }


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;
}
129 changes: 129 additions & 0 deletions src/persistent_merkle_tree/proof_test.zig
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));
}
2 changes: 2 additions & 0 deletions src/persistent_merkle_tree/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ pub const max_depth = @import("hashing").max_depth;
pub const Gindex = @import("gindex.zig").Gindex;
pub const Node = @import("Node.zig");
pub const View = @import("View.zig");
pub const proof = @import("proof.zig");

test {
testing.refAllDeclsRecursive(@This());
testing.refAllDecls(@import("node_test.zig"));
testing.refAllDecls(@import("proof_test.zig"));
}