Skip to content

Commit 9ec6fbc

Browse files
committed
#129 Update SMT to use version 6a instead of 3o
1 parent b832bcd commit 9ec6fbc

7 files changed

Lines changed: 100 additions & 60 deletions

File tree

src/api/InclusionCertificate.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { HashAlgorithm } from '../crypto/hash/HashAlgorithm.js';
55
import { FinalizedBranch } from '../smt/radix/FinalizedBranch.js';
66
import { FinalizedLeafBranch } from '../smt/radix/FinalizedLeafBranch.js';
77
import { SparseMerkleTreeRootNode } from '../smt/radix/SparseMerkleTreeRootNode.js';
8+
import { pathToRegion } from '../smt/SparseMerkleTreePathUtils.js';
89
import { BitString } from '../util/BitString.js';
910
import { HexConverter } from '../util/HexConverter.js';
1011
import { dedent } from '../util/StringUtils.js';
@@ -145,6 +146,7 @@ export class InclusionCertificate {
145146

146147
hash = await new DataHasher(HashAlgorithm.SHA256)
147148
.update(new Uint8Array([0x01, depth]))
149+
.update(pathToRegion(keyPath, depth))
148150
.update(left)
149151
.update(right)
150152
.digest();

src/smt/SparseMerkleTreePathUtils.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
1-
type CommonPath = { length: bigint; path: bigint };
1+
type CommonPath = { length: number; path: bigint };
22

33
export function calculateCommonPath(path1: bigint, path2: bigint): CommonPath {
44
let path = 1n;
55
let mask = 1n;
6-
let length = 0n;
6+
let length = 0;
77

88
while ((path1 & mask) === (path2 & mask) && path < path1 && path < path2) {
99
mask <<= 1n;
10-
length += 1n;
10+
length += 1;
1111
path = mask | ((mask - 1n) & path1);
1212
}
1313

1414
return { length, path };
1515
}
1616

17+
/**
18+
* Region committed by an interior node (v6a): the node's `depth`-bit key prefix, packed into 32
19+
* bytes in the byte-order-preserving, LSB-in-byte convention. `path` is the absolute node/key path
20+
* (leading sentinel bit at `depth`); its low `depth` bits are the prefix. Bit `i` lives in byte
21+
* `⌊i/8⌋` least-significant-first, so the packing is little-endian.
22+
*/
23+
export function pathToRegion(path: bigint, depth: number): Uint8Array {
24+
const bits = path & ((1n << BigInt(depth)) - 1n);
25+
const region = new Uint8Array(32);
26+
for (let j = 0; j * 8 < depth; j++) {
27+
region[j] = Number((bits >> BigInt(8 * j)) & 0xffn);
28+
}
29+
return region;
30+
}
31+
1732
export function getBitAtDepth(data: Uint8Array, depth: number): number {
1833
depth = Number(depth);
1934
const byteIndex = Math.floor(depth / 8);

src/smt/radix/FinalizedNodeBranch.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DataHash } from '../../crypto/hash/DataHash.js';
44
import { IDataHasher } from '../../crypto/hash/IDataHasher.js';
55
import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js';
66
import { dedent } from '../../util/StringUtils.js';
7+
import { pathToRegion } from '../SparseMerkleTreePathUtils.js';
78

89
/**
910
* Finalized interior node in a radix sparse Merkle tree.
@@ -32,6 +33,7 @@ export class FinalizedNodeBranch {
3233
const hash = await factory
3334
.create()
3435
.update(new Uint8Array([0x01, node.depth]))
36+
.update(pathToRegion(node.path, node.depth))
3537
.update(left.hash.data)
3638
.update(right.hash.data)
3739
.digest();

src/smt/radix/SparseMerkleTree.ts

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class SparseMerkleTree {
4343
const isRight = Number(path & 1n);
4444
const branchPromise = isRight ? this.right : this.left;
4545
const newBranchPromise = branchPromise.then((branch) =>
46-
branch ? this.buildTree(branch, path, 0, key, data) : new PendingLeafBranch(path, key, data),
46+
branch ? this.buildTree(branch, path, key, data) : new PendingLeafBranch(path, key, data),
4747
);
4848

4949
if (isRight) {
@@ -74,18 +74,11 @@ export class SparseMerkleTree {
7474
return SparseMerkleTreeRootNode.create(left, right, this.factory);
7575
}
7676

77-
private buildTree(
78-
branch: PendingBranch,
79-
remainingPath: bigint,
80-
depth: number,
81-
key: Uint8Array,
82-
data: Uint8Array,
83-
): PendingBranch {
84-
const commonPath = calculateCommonPath(remainingPath, branch.path);
85-
const commonPathLength = Number(commonPath.length);
86-
const isRight = Number((remainingPath >> commonPath.length) & 1n);
77+
private buildTree(branch: PendingBranch, keyPath: bigint, key: Uint8Array, data: Uint8Array): PendingBranch {
78+
const commonPath = calculateCommonPath(keyPath, branch.path);
79+
const isRight = Number((keyPath >> BigInt(commonPath.length)) & 1n);
8780

88-
if (commonPath.path === remainingPath) {
81+
if (commonPath.path === keyPath) {
8982
throw new LeafInBranchError();
9083
}
9184

@@ -95,30 +88,23 @@ export class SparseMerkleTree {
9588
throw new LeafOutOfBoundsError();
9689
}
9790

98-
const oldBranch = new PendingLeafBranch(branch.path >> commonPath.length, branch.key, branch.data);
99-
const newBranch = new PendingLeafBranch(remainingPath >> commonPath.length, key, data);
91+
const newBranch = new PendingLeafBranch(keyPath, key, data);
10092
return new PendingNodeBranch(
10193
commonPath.path,
102-
depth + commonPathLength,
103-
isRight ? oldBranch : newBranch,
104-
isRight ? newBranch : oldBranch,
94+
commonPath.length,
95+
isRight ? branch : newBranch,
96+
isRight ? newBranch : branch,
10597
);
10698
}
10799

108100
// If node branch is split in the middle
109101
if (commonPath.path < branch.path) {
110-
const newBranch = new PendingLeafBranch(remainingPath >> commonPath.length, key, data);
111-
const oldBranch = new PendingNodeBranch(
112-
branch.path >> commonPath.length,
113-
branch.depth,
114-
branch.left,
115-
branch.right,
116-
);
102+
const newBranch = new PendingLeafBranch(keyPath, key, data);
117103
return new PendingNodeBranch(
118104
commonPath.path,
119-
depth + commonPathLength,
120-
isRight ? oldBranch : newBranch,
121-
isRight ? newBranch : oldBranch,
105+
commonPath.length,
106+
isRight ? branch : newBranch,
107+
isRight ? newBranch : branch,
122108
);
123109
}
124110

@@ -127,14 +113,14 @@ export class SparseMerkleTree {
127113
branch.path,
128114
branch.depth,
129115
branch.left,
130-
this.buildTree(branch.right, remainingPath >> commonPath.length, depth + commonPathLength, key, data),
116+
this.buildTree(branch.right, keyPath, key, data),
131117
);
132118
}
133119

134120
return new PendingNodeBranch(
135121
branch.path,
136122
branch.depth,
137-
this.buildTree(branch.left, remainingPath >> commonPath.length, depth + commonPathLength, key, data),
123+
this.buildTree(branch.left, keyPath, key, data),
138124
branch.right,
139125
);
140126
}

src/smt/radixsum/SparseMerkleSumTree.ts

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class SparseMerkleSumTree {
5656
const isRight = Number(path & 1n);
5757
const branchPromise = isRight ? this.right : this.left;
5858
const newBranchPromise = branchPromise.then((branch) =>
59-
branch ? this.buildTree(branch, path, 0, key, data, value) : new PendingLeafBranch(path, key, data, value),
59+
branch ? this.buildTree(branch, path, key, data, value) : new PendingLeafBranch(path, key, data, value),
6060
);
6161

6262
if (isRight) {
@@ -89,17 +89,15 @@ export class SparseMerkleSumTree {
8989

9090
private buildTree(
9191
branch: PendingBranch,
92-
remainingPath: bigint,
93-
depth: number,
92+
keyPath: bigint,
9493
key: Uint8Array,
9594
data: Uint8Array,
9695
value: bigint,
9796
): PendingBranch {
98-
const commonPath = calculateCommonPath(remainingPath, branch.path);
99-
const commonPathLength = Number(commonPath.length);
100-
const isRight = Number((remainingPath >> commonPath.length) & 1n);
97+
const commonPath = calculateCommonPath(keyPath, branch.path);
98+
const isRight = Number((keyPath >> BigInt(commonPath.length)) & 1n);
10199

102-
if (commonPath.path === remainingPath) {
100+
if (commonPath.path === keyPath) {
103101
throw new LeafInBranchError();
104102
}
105103

@@ -109,30 +107,23 @@ export class SparseMerkleSumTree {
109107
throw new LeafOutOfBoundsError();
110108
}
111109

112-
const oldBranch = new PendingLeafBranch(branch.path >> commonPath.length, branch.key, branch.data, branch.value);
113-
const newBranch = new PendingLeafBranch(remainingPath >> commonPath.length, key, data, value);
110+
const newBranch = new PendingLeafBranch(keyPath, key, data, value);
114111
return new PendingNodeBranch(
115112
commonPath.path,
116-
depth + commonPathLength,
117-
isRight ? oldBranch : newBranch,
118-
isRight ? newBranch : oldBranch,
113+
commonPath.length,
114+
isRight ? branch : newBranch,
115+
isRight ? newBranch : branch,
119116
);
120117
}
121118

122119
// If node branch is split in the middle
123120
if (commonPath.path < branch.path) {
124-
const newBranch = new PendingLeafBranch(remainingPath >> commonPath.length, key, data, value);
125-
const oldBranch = new PendingNodeBranch(
126-
branch.path >> commonPath.length,
127-
branch.depth,
128-
branch.left,
129-
branch.right,
130-
);
121+
const newBranch = new PendingLeafBranch(keyPath, key, data, value);
131122
return new PendingNodeBranch(
132123
commonPath.path,
133-
depth + commonPathLength,
134-
isRight ? oldBranch : newBranch,
135-
isRight ? newBranch : oldBranch,
124+
commonPath.length,
125+
isRight ? branch : newBranch,
126+
isRight ? newBranch : branch,
136127
);
137128
}
138129

@@ -141,14 +132,14 @@ export class SparseMerkleSumTree {
141132
branch.path,
142133
branch.depth,
143134
branch.left,
144-
this.buildTree(branch.right, remainingPath >> commonPath.length, depth + commonPathLength, key, data, value),
135+
this.buildTree(branch.right, keyPath, key, data, value),
145136
);
146137
}
147138

148139
return new PendingNodeBranch(
149140
branch.path,
150141
branch.depth,
151-
this.buildTree(branch.left, remainingPath >> commonPath.length, depth + commonPathLength, key, data, value),
142+
this.buildTree(branch.left, keyPath, key, data, value),
152143
branch.right,
153144
);
154145
}

tests/unit/smt/SparseMerkleTreeUtilsTest.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { calculateCommonPath } from '../../../src/smt/SparseMerkleTreePathUtils.
22

33
describe('Sparse Merkle Tree tests', function () {
44
it('calculate common path', () => {
5-
expect(calculateCommonPath(0b11n, 0b111101111n)).toStrictEqual({ length: 1n, path: 0b11n });
6-
expect(calculateCommonPath(0b111101111n, 0b11n)).toStrictEqual({ length: 1n, path: 0b11n });
5+
expect(calculateCommonPath(0b11n, 0b111101111n)).toStrictEqual({ length: 1, path: 0b11n });
6+
expect(calculateCommonPath(0b111101111n, 0b11n)).toStrictEqual({ length: 1, path: 0b11n });
77
expect(calculateCommonPath(0b110010000n, 0b100010000n)).toStrictEqual({
8-
length: 7n,
8+
length: 7,
99
path: 0b10010000n,
1010
});
1111
});

tests/unit/smt/radix/SparseMerkleTreeTest.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ describe('Sparse Merkle Tree tests', function () {
5959
const root = await smt.calculateRoot();
6060

6161
expect(root.hash.imprint).toStrictEqual(
62-
HexConverter.decode('00000fbe49c19df8bbf8612951ea26c8b0c52088424ff48adacf56234fc567950e89'),
62+
HexConverter.decode('0000cd23fc1265484a7173323cd862b85a61796b8e0af31149944a828e6c1734b846'),
6363
);
6464
});
6565

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

106+
it('empty and single-leaf roots', async () => {
107+
const hashFactory = new DataHasherFactory(HashAlgorithm.SHA256, NodeDataHasher);
108+
109+
// Empty tree → all-zero root hash.
110+
const emptyRoot = await new SparseMerkleTree(hashFactory).calculateRoot();
111+
expect(emptyRoot.hash.data).toStrictEqual(new Uint8Array(32));
112+
113+
// Single leaf → root hash equals the leaf hash (no interior node, so no region involved).
114+
const smt = new SparseMerkleTree(hashFactory);
115+
const key = key32(0b10110010);
116+
const value = new Uint8Array([9, 9, 9]);
117+
await smt.addLeaf(key, value);
118+
const root = await smt.calculateRoot();
119+
const leaf = await new PendingLeafBranch(0n, key, value).finalize(hashFactory);
120+
expect(root.hash.equals(leaf.hash)).toBe(true);
121+
});
122+
123+
it('deep split at depth 255 verifies with region', async () => {
124+
const hashFactory = new DataHasherFactory(HashAlgorithm.SHA256, NodeDataHasher);
125+
const smt = new SparseMerkleTree(hashFactory);
126+
127+
// Two keys identical except bit 255 (high bit of the last byte) → a single interior node at depth 255.
128+
const a = new Uint8Array(32);
129+
const b = new Uint8Array(32);
130+
b[31] = 0x80;
131+
const valueA = new Uint8Array(32);
132+
valueA[0] = 1;
133+
const valueB = new Uint8Array(32);
134+
valueB[0] = 2;
135+
await smt.addLeaf(a, valueA);
136+
await smt.addLeaf(b, valueB);
137+
const root = await smt.calculateRoot();
138+
139+
for (const [key, value] of [
140+
[a, valueA],
141+
[b, valueB],
142+
] as const) {
143+
const cert = InclusionCertificate.create(root, key);
144+
const stateId = StateId.fromCBOR(CborSerializer.encodeByteString(key));
145+
const leafValue = new DataHash(HashAlgorithm.SHA256, value);
146+
await expect(cert.verify(stateId, leafValue, root.hash)).resolves.toBe(true);
147+
}
148+
});
149+
106150
it('should handle concurrent addLeaf calls', async () => {
107151
const smt = new SparseMerkleTree(new DataHasherFactory(HashAlgorithm.SHA256, NodeDataHasher));
108152
const textEncoder = new TextEncoder();

0 commit comments

Comments
 (0)