Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions src/api/InclusionCertificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { HashAlgorithm } from '../crypto/hash/HashAlgorithm.js';
import { FinalizedBranch } from '../smt/radix/FinalizedBranch.js';
import { FinalizedLeafBranch } from '../smt/radix/FinalizedLeafBranch.js';
import { SparseMerkleTreeRootNode } from '../smt/radix/SparseMerkleTreeRootNode.js';
import { pathToRegion } from '../smt/SparseMerkleTreePathUtils.js';
import { BitString } from '../util/BitString.js';
import { HexConverter } from '../util/HexConverter.js';
import { dedent } from '../util/StringUtils.js';
Expand Down Expand Up @@ -145,6 +146,7 @@ export class InclusionCertificate {

hash = await new DataHasher(HashAlgorithm.SHA256)
.update(new Uint8Array([0x01, depth]))
.update(pathToRegion(keyPath, depth))
.update(left)
.update(right)
.digest();
Expand Down
27 changes: 24 additions & 3 deletions src/smt/SparseMerkleTreePathUtils.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
type CommonPath = { length: bigint; path: bigint };
type CommonPath = { length: number; path: bigint };

export function calculateCommonPath(path1: bigint, path2: bigint): CommonPath {
let path = 1n;
let mask = 1n;
let length = 0n;
let length = 0;

while ((path1 & mask) === (path2 & mask) && path < path1 && path < path2) {
mask <<= 1n;
length += 1n;
length += 1;
path = mask | ((mask - 1n) & path1);
}

return { length, path };
}

/**
* Region committed by an interior node: the `depth`-bit common prefix of all leaves in the node's
* sub-tree. The `i`th lowest bit of path will be the `i mod 8`th lowest bit in the `i div 8`th
* byte of the returned 32-byte array (so the packing is little-endian); the remaining bits of the array
* are set to zero.
*/
export function pathToRegion(path: bigint, depth: number): Uint8Array {
const region = new Uint8Array(32);
const fullBytes = Math.floor(depth / 8);
const remainderBits = depth % 8;

let bits = path;
for (let j = 0; j < fullBytes; j++, bits >>= 8n) {
region[j] = Number(bits & 0xffn);
}
if (remainderBits > 0) {
region[fullBytes] = Number(bits & 0xffn) & ((1 << remainderBits) - 1);
}
return region;
}
Comment thread
martti007 marked this conversation as resolved.

export function getBitAtDepth(data: Uint8Array, depth: number): number {
depth = Number(depth);
const byteIndex = Math.floor(depth / 8);
Expand Down
2 changes: 2 additions & 0 deletions src/smt/radix/FinalizedNodeBranch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DataHash } from '../../crypto/hash/DataHash.js';
import { IDataHasher } from '../../crypto/hash/IDataHasher.js';
import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js';
import { dedent } from '../../util/StringUtils.js';
import { pathToRegion } from '../SparseMerkleTreePathUtils.js';

/**
* Finalized interior node in a radix sparse Merkle tree.
Expand Down Expand Up @@ -32,6 +33,7 @@ export class FinalizedNodeBranch {
const hash = await factory
.create()
.update(new Uint8Array([0x01, node.depth]))
.update(pathToRegion(node.path, node.depth))
.update(left.hash.data)
.update(right.hash.data)
.digest();
Expand Down
44 changes: 15 additions & 29 deletions src/smt/radix/SparseMerkleTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class SparseMerkleTree {
const isRight = Number(path & 1n);
const branchPromise = isRight ? this.right : this.left;
const newBranchPromise = branchPromise.then((branch) =>
branch ? this.buildTree(branch, path, 0, key, data) : new PendingLeafBranch(path, key, data),
branch ? this.buildTree(branch, path, key, data) : new PendingLeafBranch(path, key, data),
);

if (isRight) {
Expand Down Expand Up @@ -74,18 +74,11 @@ export class SparseMerkleTree {
return SparseMerkleTreeRootNode.create(left, right, this.factory);
}

private buildTree(
branch: PendingBranch,
remainingPath: bigint,
depth: number,
key: Uint8Array,
data: Uint8Array,
): PendingBranch {
const commonPath = calculateCommonPath(remainingPath, branch.path);
const commonPathLength = Number(commonPath.length);
const isRight = Number((remainingPath >> commonPath.length) & 1n);
private buildTree(branch: PendingBranch, keyPath: bigint, key: Uint8Array, data: Uint8Array): PendingBranch {
const commonPath = calculateCommonPath(keyPath, branch.path);
const isRight = Number((keyPath >> BigInt(commonPath.length)) & 1n);

if (commonPath.path === remainingPath) {
if (commonPath.path === keyPath) {
throw new LeafInBranchError();
}

Expand All @@ -95,30 +88,23 @@ export class SparseMerkleTree {
throw new LeafOutOfBoundsError();
}

const oldBranch = new PendingLeafBranch(branch.path >> commonPath.length, branch.key, branch.data);
const newBranch = new PendingLeafBranch(remainingPath >> commonPath.length, key, data);
const newBranch = new PendingLeafBranch(keyPath, key, data);
return new PendingNodeBranch(
commonPath.path,
depth + commonPathLength,
isRight ? oldBranch : newBranch,
isRight ? newBranch : oldBranch,
commonPath.length,
isRight ? branch : newBranch,
isRight ? newBranch : branch,
);
}

// If node branch is split in the middle
if (commonPath.path < branch.path) {
const newBranch = new PendingLeafBranch(remainingPath >> commonPath.length, key, data);
const oldBranch = new PendingNodeBranch(
branch.path >> commonPath.length,
branch.depth,
branch.left,
branch.right,
);
const newBranch = new PendingLeafBranch(keyPath, key, data);
return new PendingNodeBranch(
commonPath.path,
depth + commonPathLength,
isRight ? oldBranch : newBranch,
isRight ? newBranch : oldBranch,
commonPath.length,
isRight ? branch : newBranch,
isRight ? newBranch : branch,
);
}

Expand All @@ -127,14 +113,14 @@ export class SparseMerkleTree {
branch.path,
branch.depth,
branch.left,
this.buildTree(branch.right, remainingPath >> commonPath.length, depth + commonPathLength, key, data),
this.buildTree(branch.right, keyPath, key, data),
);
}

return new PendingNodeBranch(
branch.path,
branch.depth,
this.buildTree(branch.left, remainingPath >> commonPath.length, depth + commonPathLength, key, data),
this.buildTree(branch.left, keyPath, key, data),
branch.right,
);
}
Expand Down
39 changes: 15 additions & 24 deletions src/smt/radixsum/SparseMerkleSumTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class SparseMerkleSumTree {
const isRight = Number(path & 1n);
const branchPromise = isRight ? this.right : this.left;
const newBranchPromise = branchPromise.then((branch) =>
branch ? this.buildTree(branch, path, 0, key, data, value) : new PendingLeafBranch(path, key, data, value),
branch ? this.buildTree(branch, path, key, data, value) : new PendingLeafBranch(path, key, data, value),
);

if (isRight) {
Expand Down Expand Up @@ -89,17 +89,15 @@ export class SparseMerkleSumTree {

private buildTree(
branch: PendingBranch,
remainingPath: bigint,
depth: number,
keyPath: bigint,
key: Uint8Array,
data: Uint8Array,
value: bigint,
): PendingBranch {
const commonPath = calculateCommonPath(remainingPath, branch.path);
const commonPathLength = Number(commonPath.length);
const isRight = Number((remainingPath >> commonPath.length) & 1n);
const commonPath = calculateCommonPath(keyPath, branch.path);
const isRight = Number((keyPath >> BigInt(commonPath.length)) & 1n);

if (commonPath.path === remainingPath) {
if (commonPath.path === keyPath) {
throw new LeafInBranchError();
}

Expand All @@ -109,30 +107,23 @@ export class SparseMerkleSumTree {
throw new LeafOutOfBoundsError();
}

const oldBranch = new PendingLeafBranch(branch.path >> commonPath.length, branch.key, branch.data, branch.value);
const newBranch = new PendingLeafBranch(remainingPath >> commonPath.length, key, data, value);
const newBranch = new PendingLeafBranch(keyPath, key, data, value);
return new PendingNodeBranch(
commonPath.path,
depth + commonPathLength,
isRight ? oldBranch : newBranch,
isRight ? newBranch : oldBranch,
commonPath.length,
isRight ? branch : newBranch,
isRight ? newBranch : branch,
);
}

// If node branch is split in the middle
if (commonPath.path < branch.path) {
const newBranch = new PendingLeafBranch(remainingPath >> commonPath.length, key, data, value);
const oldBranch = new PendingNodeBranch(
branch.path >> commonPath.length,
branch.depth,
branch.left,
branch.right,
);
const newBranch = new PendingLeafBranch(keyPath, key, data, value);
return new PendingNodeBranch(
commonPath.path,
depth + commonPathLength,
isRight ? oldBranch : newBranch,
isRight ? newBranch : oldBranch,
commonPath.length,
isRight ? branch : newBranch,
isRight ? newBranch : branch,
);
}

Expand All @@ -141,14 +132,14 @@ export class SparseMerkleSumTree {
branch.path,
branch.depth,
branch.left,
this.buildTree(branch.right, remainingPath >> commonPath.length, depth + commonPathLength, key, data, value),
this.buildTree(branch.right, keyPath, key, data, value),
);
}

return new PendingNodeBranch(
branch.path,
branch.depth,
this.buildTree(branch.left, remainingPath >> commonPath.length, depth + commonPathLength, key, data, value),
this.buildTree(branch.left, keyPath, key, data, value),
branch.right,
);
}
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/smt/SparseMerkleTreeUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { calculateCommonPath } from '../../../src/smt/SparseMerkleTreePathUtils.

describe('Sparse Merkle Tree tests', function () {
it('calculate common path', () => {
expect(calculateCommonPath(0b11n, 0b111101111n)).toStrictEqual({ length: 1n, path: 0b11n });
expect(calculateCommonPath(0b111101111n, 0b11n)).toStrictEqual({ length: 1n, path: 0b11n });
expect(calculateCommonPath(0b11n, 0b111101111n)).toStrictEqual({ length: 1, path: 0b11n });
expect(calculateCommonPath(0b111101111n, 0b11n)).toStrictEqual({ length: 1, path: 0b11n });
expect(calculateCommonPath(0b110010000n, 0b100010000n)).toStrictEqual({
length: 7n,
length: 7,
path: 0b10010000n,
});
});
Expand Down
46 changes: 45 additions & 1 deletion tests/unit/smt/radix/SparseMerkleTreeTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe('Sparse Merkle Tree tests', function () {
const root = await smt.calculateRoot();

expect(root.hash.imprint).toStrictEqual(
HexConverter.decode('00000fbe49c19df8bbf8612951ea26c8b0c52088424ff48adacf56234fc567950e89'),
HexConverter.decode('0000cd23fc1265484a7173323cd862b85a61796b8e0af31149944a828e6c1734b846'),
);
});

Expand Down Expand Up @@ -103,6 +103,50 @@ describe('Sparse Merkle Tree tests', function () {
expect(() => InclusionCertificate.create(emptyRoot, keys[0])).toThrow();
});

it('empty and single-leaf roots', async () => {
const hashFactory = new DataHasherFactory(HashAlgorithm.SHA256, NodeDataHasher);

// Empty tree → all-zero root hash.
const emptyRoot = await new SparseMerkleTree(hashFactory).calculateRoot();
expect(emptyRoot.hash.data).toStrictEqual(new Uint8Array(32));

// Single leaf → root hash equals the leaf hash (no interior node, so no region involved).
const smt = new SparseMerkleTree(hashFactory);
const key = key32(0b10110010);
const value = new Uint8Array([9, 9, 9]);
await smt.addLeaf(key, value);
const root = await smt.calculateRoot();
const leaf = await new PendingLeafBranch(0n, key, value).finalize(hashFactory);
expect(root.hash.equals(leaf.hash)).toBe(true);
});

it('deep split at depth 255 verifies with region', async () => {
const hashFactory = new DataHasherFactory(HashAlgorithm.SHA256, NodeDataHasher);
const smt = new SparseMerkleTree(hashFactory);

// Two keys identical except bit 255 (high bit of the last byte) → a single interior node at depth 255.
const a = new Uint8Array(32);
const b = new Uint8Array(32);
b[31] = 0x80;
const valueA = new Uint8Array(32);
valueA[0] = 1;
const valueB = new Uint8Array(32);
valueB[0] = 2;
await smt.addLeaf(a, valueA);
await smt.addLeaf(b, valueB);
const root = await smt.calculateRoot();

for (const [key, value] of [
[a, valueA],
[b, valueB],
] as const) {
const cert = InclusionCertificate.create(root, key);
const stateId = StateId.fromCBOR(CborSerializer.encodeByteString(key));
const leafValue = new DataHash(HashAlgorithm.SHA256, value);
await expect(cert.verify(stateId, leafValue, root.hash)).resolves.toBe(true);
}
});

it('should handle concurrent addLeaf calls', async () => {
const smt = new SparseMerkleTree(new DataHasherFactory(HashAlgorithm.SHA256, NodeDataHasher));
const textEncoder = new TextEncoder();
Expand Down
Loading