@feelyourprotocol/binarytree 8141.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +46 -0
  3. package/dist/cjs/binaryTree.d.ts +167 -0
  4. package/dist/cjs/binaryTree.d.ts.map +1 -0
  5. package/dist/cjs/binaryTree.js +606 -0
  6. package/dist/cjs/binaryTree.js.map +1 -0
  7. package/dist/cjs/constructors.d.ts +4 -0
  8. package/dist/cjs/constructors.d.ts.map +1 -0
  9. package/dist/cjs/constructors.js +44 -0
  10. package/dist/cjs/constructors.js.map +1 -0
  11. package/dist/cjs/db/checkpoint.d.ts +87 -0
  12. package/dist/cjs/db/checkpoint.d.ts.map +1 -0
  13. package/dist/cjs/db/checkpoint.js +257 -0
  14. package/dist/cjs/db/checkpoint.js.map +1 -0
  15. package/dist/cjs/db/index.d.ts +2 -0
  16. package/dist/cjs/db/index.d.ts.map +1 -0
  17. package/dist/cjs/db/index.js +18 -0
  18. package/dist/cjs/db/index.js.map +1 -0
  19. package/dist/cjs/index.d.ts +7 -0
  20. package/dist/cjs/index.d.ts.map +1 -0
  21. package/dist/cjs/index.js +23 -0
  22. package/dist/cjs/index.js.map +1 -0
  23. package/dist/cjs/node/index.d.ts +4 -0
  24. package/dist/cjs/node/index.d.ts.map +1 -0
  25. package/dist/cjs/node/index.js +20 -0
  26. package/dist/cjs/node/index.js.map +1 -0
  27. package/dist/cjs/node/internalNode.d.ts +37 -0
  28. package/dist/cjs/node/internalNode.d.ts.map +1 -0
  29. package/dist/cjs/node/internalNode.js +92 -0
  30. package/dist/cjs/node/internalNode.js.map +1 -0
  31. package/dist/cjs/node/stemNode.d.ts +34 -0
  32. package/dist/cjs/node/stemNode.d.ts.map +1 -0
  33. package/dist/cjs/node/stemNode.js +75 -0
  34. package/dist/cjs/node/stemNode.js.map +1 -0
  35. package/dist/cjs/node/types.d.ts +35 -0
  36. package/dist/cjs/node/types.d.ts.map +1 -0
  37. package/dist/cjs/node/types.js +9 -0
  38. package/dist/cjs/node/types.js.map +1 -0
  39. package/dist/cjs/node/util.d.ts +9 -0
  40. package/dist/cjs/node/util.d.ts.map +1 -0
  41. package/dist/cjs/node/util.js +40 -0
  42. package/dist/cjs/node/util.js.map +1 -0
  43. package/dist/cjs/package.json +3 -0
  44. package/dist/cjs/proof.d.ts +16 -0
  45. package/dist/cjs/proof.d.ts.map +1 -0
  46. package/dist/cjs/proof.js +49 -0
  47. package/dist/cjs/proof.js.map +1 -0
  48. package/dist/cjs/types.d.ts +45 -0
  49. package/dist/cjs/types.d.ts.map +1 -0
  50. package/dist/cjs/types.js +6 -0
  51. package/dist/cjs/types.js.map +1 -0
  52. package/dist/cjs/util.d.ts +17 -0
  53. package/dist/cjs/util.d.ts.map +1 -0
  54. package/dist/cjs/util.js +71 -0
  55. package/dist/cjs/util.js.map +1 -0
  56. package/dist/esm/binaryTree.d.ts +167 -0
  57. package/dist/esm/binaryTree.d.ts.map +1 -0
  58. package/dist/esm/binaryTree.js +602 -0
  59. package/dist/esm/binaryTree.js.map +1 -0
  60. package/dist/esm/constructors.d.ts +4 -0
  61. package/dist/esm/constructors.d.ts.map +1 -0
  62. package/dist/esm/constructors.js +41 -0
  63. package/dist/esm/constructors.js.map +1 -0
  64. package/dist/esm/db/checkpoint.d.ts +87 -0
  65. package/dist/esm/db/checkpoint.d.ts.map +1 -0
  66. package/dist/esm/db/checkpoint.js +253 -0
  67. package/dist/esm/db/checkpoint.js.map +1 -0
  68. package/dist/esm/db/index.d.ts +2 -0
  69. package/dist/esm/db/index.d.ts.map +1 -0
  70. package/dist/esm/db/index.js +2 -0
  71. package/dist/esm/db/index.js.map +1 -0
  72. package/dist/esm/index.d.ts +7 -0
  73. package/dist/esm/index.d.ts.map +1 -0
  74. package/dist/esm/index.js +7 -0
  75. package/dist/esm/index.js.map +1 -0
  76. package/dist/esm/node/index.d.ts +4 -0
  77. package/dist/esm/node/index.d.ts.map +1 -0
  78. package/dist/esm/node/index.js +4 -0
  79. package/dist/esm/node/index.js.map +1 -0
  80. package/dist/esm/node/internalNode.d.ts +37 -0
  81. package/dist/esm/node/internalNode.d.ts.map +1 -0
  82. package/dist/esm/node/internalNode.js +88 -0
  83. package/dist/esm/node/internalNode.js.map +1 -0
  84. package/dist/esm/node/stemNode.d.ts +34 -0
  85. package/dist/esm/node/stemNode.d.ts.map +1 -0
  86. package/dist/esm/node/stemNode.js +71 -0
  87. package/dist/esm/node/stemNode.js.map +1 -0
  88. package/dist/esm/node/types.d.ts +35 -0
  89. package/dist/esm/node/types.d.ts.map +1 -0
  90. package/dist/esm/node/types.js +6 -0
  91. package/dist/esm/node/types.js.map +1 -0
  92. package/dist/esm/node/util.d.ts +9 -0
  93. package/dist/esm/node/util.d.ts.map +1 -0
  94. package/dist/esm/node/util.js +33 -0
  95. package/dist/esm/node/util.js.map +1 -0
  96. package/dist/esm/package.json +3 -0
  97. package/dist/esm/proof.d.ts +16 -0
  98. package/dist/esm/proof.d.ts.map +1 -0
  99. package/dist/esm/proof.js +45 -0
  100. package/dist/esm/proof.js.map +1 -0
  101. package/dist/esm/types.d.ts +45 -0
  102. package/dist/esm/types.d.ts.map +1 -0
  103. package/dist/esm/types.js +3 -0
  104. package/dist/esm/types.js.map +1 -0
  105. package/dist/esm/util.d.ts +17 -0
  106. package/dist/esm/util.d.ts.map +1 -0
  107. package/dist/esm/util.js +66 -0
  108. package/dist/esm/util.js.map +1 -0
  109. package/dist/tsconfig.prod.cjs.tsbuildinfo +1 -0
  110. package/dist/tsconfig.prod.esm.tsbuildinfo +1 -0
  111. package/package.json +77 -0
  112. package/src/binaryTree.ts +742 -0
  113. package/src/constructors.ts +50 -0
  114. package/src/db/checkpoint.ts +297 -0
  115. package/src/db/index.ts +1 -0
  116. package/src/index.ts +6 -0
  117. package/src/node/index.ts +3 -0
  118. package/src/node/internalNode.ts +112 -0
  119. package/src/node/stemNode.ts +87 -0
  120. package/src/node/types.ts +41 -0
  121. package/src/node/util.ts +38 -0
  122. package/src/proof.ts +54 -0
  123. package/src/types.ts +58 -0
  124. package/src/util.ts +80 -0
@@ -0,0 +1,50 @@
1
+ import {
2
+ KeyEncoding,
3
+ MapDB,
4
+ ValueEncoding,
5
+ bytesToHex,
6
+ unprefixedHexToBytes,
7
+ } from '@feelyourprotocol/util'
8
+ import { blake3 } from '@noble/hashes/blake3.js'
9
+
10
+ import { BinaryTree } from './binaryTree.ts'
11
+ import { ROOT_DB_KEY } from './types.ts'
12
+
13
+ import type { BinaryTreeOpts } from './types.ts'
14
+
15
+ export async function createBinaryTree(opts?: Partial<BinaryTreeOpts>) {
16
+ const key = bytesToHex(ROOT_DB_KEY)
17
+
18
+ // Provide sensible default options
19
+ const parsedOptions = {
20
+ ...opts,
21
+ db: opts?.db ?? new MapDB<string, Uint8Array>(),
22
+ useRootPersistence: opts?.useRootPersistence ?? false,
23
+ cacheSize: opts?.cacheSize ?? 0,
24
+ hashFunction: opts?.hashFunction ?? blake3,
25
+ }
26
+
27
+ if (parsedOptions.useRootPersistence === true) {
28
+ if (parsedOptions.root === undefined) {
29
+ const root = await parsedOptions.db.get(key, {
30
+ keyEncoding: KeyEncoding.Bytes,
31
+ valueEncoding: ValueEncoding.Bytes,
32
+ })
33
+ if (typeof root === 'string') {
34
+ parsedOptions.root = unprefixedHexToBytes(root)
35
+ } else {
36
+ parsedOptions.root = root
37
+ }
38
+ } else {
39
+ await parsedOptions.db.put(key, parsedOptions.root, {
40
+ keyEncoding: KeyEncoding.Bytes,
41
+ valueEncoding: ValueEncoding.Bytes,
42
+ })
43
+ }
44
+ }
45
+
46
+ const tree = new BinaryTree(parsedOptions)
47
+ // If the root node does not exist, initialize the empty root node
48
+ if (parsedOptions.root === undefined) await tree.createRootNode()
49
+ return tree
50
+ }
@@ -0,0 +1,297 @@
1
+ import {
2
+ KeyEncoding,
3
+ ValueEncoding,
4
+ bytesToUnprefixedHex,
5
+ unprefixedHexToBytes,
6
+ } from '@feelyourprotocol/util'
7
+ import { LRUCache } from 'lru-cache'
8
+
9
+ import type { BatchDBOp, DB, EncodingOpts } from '@feelyourprotocol/util'
10
+ import type { Checkpoint, CheckpointDBOpts } from '../types.ts'
11
+
12
+ /**
13
+ * DB is a thin wrapper around the underlying levelup db,
14
+ * which validates inputs and sets encoding type.
15
+ */
16
+ export class CheckpointDB implements DB {
17
+ public checkpoints: Checkpoint[]
18
+ public db: DB<string, string | Uint8Array>
19
+ public readonly cacheSize: number
20
+ private readonly valueEncoding: ValueEncoding
21
+
22
+ // Starting with lru-cache v8 undefined and null are not allowed any more
23
+ // as cache values. At the same time our design works well, since undefined
24
+ // indicates for us that we know that the value is not present in the
25
+ // underlying trie database as well (so it carries real value).
26
+ //
27
+ // Solution here seems therefore adequate, other solutions would rather
28
+ // be some not so clean workaround.
29
+ //
30
+ // (note that @ts-ignore doesn't work since stripped on declaration (.d.ts) files)
31
+ protected _cache?: LRUCache<string, Uint8Array>
32
+ // protected _cache?: LRUCache<string, Uint8Array | undefined>
33
+
34
+ _stats = {
35
+ cache: {
36
+ reads: 0,
37
+ hits: 0,
38
+ writes: 0,
39
+ },
40
+ db: {
41
+ reads: 0,
42
+ hits: 0,
43
+ writes: 0,
44
+ },
45
+ }
46
+
47
+ /**
48
+ * Initialize a DB instance.
49
+ */
50
+ constructor(opts: CheckpointDBOpts) {
51
+ this.db = opts.db
52
+ this.cacheSize = opts.cacheSize ?? 0
53
+ this.valueEncoding = opts.valueEncoding ?? ValueEncoding.String
54
+ // Roots of trie at the moment of checkpoint
55
+ this.checkpoints = []
56
+
57
+ if (this.cacheSize > 0) {
58
+ this._cache = new LRUCache({
59
+ max: this.cacheSize,
60
+ updateAgeOnGet: true,
61
+ })
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Flush the checkpoints and use the given checkpoints instead.
67
+ * @param {Checkpoint[]} checkpoints
68
+ */
69
+ setCheckpoints(checkpoints: Checkpoint[]) {
70
+ this.checkpoints = []
71
+
72
+ for (let i = 0; i < checkpoints.length; i++) {
73
+ this.checkpoints.push({
74
+ root: checkpoints[i].root,
75
+ keyValueMap: new Map(checkpoints[i].keyValueMap),
76
+ })
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Is the DB during a checkpoint phase?
82
+ */
83
+ hasCheckpoints() {
84
+ return this.checkpoints.length > 0
85
+ }
86
+
87
+ /**
88
+ * Adds a new checkpoint to the stack
89
+ * @param root
90
+ */
91
+ checkpoint(root: Uint8Array) {
92
+ this.checkpoints.push({ keyValueMap: new Map<string, Uint8Array>(), root })
93
+ }
94
+
95
+ /**
96
+ * Commits the latest checkpoint
97
+ */
98
+ async commit() {
99
+ const { keyValueMap } = this.checkpoints.pop()!
100
+ if (!this.hasCheckpoints()) {
101
+ // This was the final checkpoint, we should now commit and flush everything to disk
102
+ const batchOp: BatchDBOp[] = []
103
+ for (const [key, value] of keyValueMap.entries()) {
104
+ if (value === undefined) {
105
+ batchOp.push({
106
+ type: 'del',
107
+ key: unprefixedHexToBytes(key),
108
+ })
109
+ } else {
110
+ batchOp.push({
111
+ type: 'put',
112
+ key: unprefixedHexToBytes(key),
113
+ value,
114
+ })
115
+ }
116
+ }
117
+ await this.batch(batchOp)
118
+ } else {
119
+ // dump everything into the current (higher level) diff cache
120
+ const currentKeyValueMap = this.checkpoints[this.checkpoints.length - 1].keyValueMap
121
+ for (const [key, value] of keyValueMap.entries()) {
122
+ currentKeyValueMap.set(key, value)
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Reverts the latest checkpoint
129
+ */
130
+ async revert() {
131
+ const { root } = this.checkpoints.pop()!
132
+ return root
133
+ }
134
+
135
+ /**
136
+ * @inheritDoc
137
+ */
138
+ async get(key: Uint8Array): Promise<Uint8Array | undefined> {
139
+ // Using deprecated bytesToUnprefixedHex for performance: used as cache/database keys (string encoding).
140
+ const keyHex = bytesToUnprefixedHex(key)
141
+ if (this._cache !== undefined) {
142
+ const value = this._cache.get(keyHex)
143
+ this._stats.cache.reads += 1
144
+ if (value !== undefined) {
145
+ this._stats.cache.hits += 1
146
+ return value
147
+ }
148
+ }
149
+
150
+ // Lookup the value in our diff cache. We return the latest checkpointed value (which should be the value on disk)
151
+ for (let index = this.checkpoints.length - 1; index >= 0; index--) {
152
+ if (this.checkpoints[index].keyValueMap.has(keyHex)) {
153
+ return this.checkpoints[index].keyValueMap.get(keyHex)
154
+ }
155
+ }
156
+ // Nothing has been found in diff cache, look up from disk
157
+ const value = await this.db.get(keyHex, {
158
+ keyEncoding: KeyEncoding.String,
159
+ valueEncoding: this.valueEncoding,
160
+ })
161
+ this._stats.db.reads += 1
162
+ if (value !== undefined) {
163
+ this._stats.db.hits += 1
164
+ }
165
+ const returnValue =
166
+ value !== undefined
167
+ ? value instanceof Uint8Array
168
+ ? value
169
+ : unprefixedHexToBytes(value as string)
170
+ : undefined
171
+ this._cache?.set(keyHex, returnValue)
172
+ if (this.hasCheckpoints()) {
173
+ // Since we are a checkpoint, put this value in diff cache,
174
+ // so future `get` calls will not look the key up again from disk.
175
+ this.checkpoints[this.checkpoints.length - 1].keyValueMap.set(keyHex, returnValue)
176
+ }
177
+
178
+ return returnValue
179
+ }
180
+
181
+ /**
182
+ * @inheritDoc
183
+ */
184
+ async put(key: Uint8Array, value: Uint8Array): Promise<void> {
185
+ // Using deprecated bytesToUnprefixedHex for performance: used as cache/database keys and values (string encoding).
186
+ const keyHex = bytesToUnprefixedHex(key)
187
+ if (this.hasCheckpoints()) {
188
+ // put value in diff cache
189
+ this.checkpoints[this.checkpoints.length - 1].keyValueMap.set(keyHex, value)
190
+ } else {
191
+ const valuePut =
192
+ this.valueEncoding === ValueEncoding.Bytes ? value : bytesToUnprefixedHex(value)
193
+ await this.db.put(keyHex, valuePut, {
194
+ keyEncoding: KeyEncoding.String,
195
+ valueEncoding: this.valueEncoding,
196
+ })
197
+ this._stats.db.writes += 1
198
+
199
+ if (this._cache !== undefined) {
200
+ this._cache.set(keyHex, value)
201
+ this._stats.cache.writes += 1
202
+ }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * @inheritDoc
208
+ */
209
+ async del(key: Uint8Array): Promise<void> {
210
+ // Using deprecated bytesToUnprefixedHex for performance: used as cache/database keys (string encoding).
211
+ const keyHex = bytesToUnprefixedHex(key)
212
+ if (this.hasCheckpoints()) {
213
+ // delete the value in the current diff cache
214
+ this.checkpoints[this.checkpoints.length - 1].keyValueMap.set(keyHex, undefined)
215
+ } else {
216
+ // delete the value on disk
217
+ await this.db.del(keyHex, {
218
+ keyEncoding: KeyEncoding.String,
219
+ })
220
+ this._stats.db.writes += 1
221
+
222
+ if (this._cache !== undefined) {
223
+ this._cache.set(keyHex, undefined)
224
+ this._stats.cache.writes += 1
225
+ }
226
+ }
227
+ }
228
+
229
+ /**
230
+ * @inheritDoc
231
+ */
232
+ async batch(opStack: BatchDBOp[]): Promise<void> {
233
+ if (this.hasCheckpoints()) {
234
+ for (const op of opStack) {
235
+ if (op.type === 'put') {
236
+ await this.put(op.key, op.value)
237
+ } else if (op.type === 'del') {
238
+ await this.del(op.key)
239
+ }
240
+ }
241
+ } else {
242
+ const convertedOps = opStack.map((op) => {
243
+ const convertedOp: {
244
+ key: string
245
+ value: Uint8Array | string | undefined
246
+ type: 'put' | 'del'
247
+ opts?: EncodingOpts
248
+ } = {
249
+ key: bytesToUnprefixedHex(op.key),
250
+ value: op.type === 'put' ? op.value : undefined,
251
+ type: op.type,
252
+ opts: { ...op.opts, ...{ valueEncoding: this.valueEncoding } },
253
+ }
254
+ this._stats.db.writes += 1
255
+ if (op.type === 'put' && this.valueEncoding === ValueEncoding.String) {
256
+ convertedOp.value = bytesToUnprefixedHex(convertedOp.value as Uint8Array)
257
+ }
258
+ return convertedOp
259
+ })
260
+ await this.db.batch(convertedOps as any)
261
+ }
262
+ }
263
+
264
+ stats(reset = true) {
265
+ const stats = { ...this._stats, size: this._cache?.size ?? 0 }
266
+ if (reset) {
267
+ this._stats = {
268
+ cache: {
269
+ reads: 0,
270
+ hits: 0,
271
+ writes: 0,
272
+ },
273
+ db: {
274
+ reads: 0,
275
+ hits: 0,
276
+ writes: 0,
277
+ },
278
+ }
279
+ }
280
+ return stats
281
+ }
282
+
283
+ /**
284
+ * @inheritDoc
285
+ */
286
+ shallowCopy(): CheckpointDB {
287
+ return new CheckpointDB({
288
+ db: this.db,
289
+ cacheSize: this.cacheSize,
290
+ valueEncoding: this.valueEncoding,
291
+ })
292
+ }
293
+
294
+ open() {
295
+ return Promise.resolve()
296
+ }
297
+ }
@@ -0,0 +1 @@
1
+ export * from './checkpoint.ts'
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './binaryTree.ts'
2
+ export * from './constructors.ts'
3
+ export * from './db/index.ts'
4
+ export * from './node/index.ts'
5
+ export * from './proof.ts'
6
+ export * from './types.ts'
@@ -0,0 +1,3 @@
1
+ export * from './internalNode.ts'
2
+ export * from './types.ts'
3
+ export * from './util.ts'
@@ -0,0 +1,112 @@
1
+ import { RLP } from '@feelyourprotocol/rlp'
2
+ import { EthereumJSErrorWithoutCode, bitsToBytes, bytesToBits } from '@feelyourprotocol/util'
3
+
4
+ import { BinaryNodeType } from './types.ts'
5
+
6
+ import type { BinaryNodeOptions, ChildBinaryNode } from './types.ts'
7
+
8
+ export class InternalBinaryNode {
9
+ public children: Array<ChildBinaryNode | null>
10
+
11
+ public type = BinaryNodeType.Internal
12
+
13
+ constructor(options: BinaryNodeOptions[typeof BinaryNodeType.Internal]) {
14
+ this.children = options.children ?? Array(2).fill(null)
15
+ }
16
+
17
+ static fromRawNode(rawNode: Uint8Array[]): InternalBinaryNode {
18
+ const nodeType = rawNode[0][0]
19
+ if (nodeType !== BinaryNodeType.Internal) {
20
+ throw EthereumJSErrorWithoutCode('Invalid node type')
21
+ }
22
+
23
+ // The length of the rawNode should be the # of children * 2 (for hash and path) + 1 for the node type
24
+
25
+ if (rawNode.length !== 2 * 2 + 1) {
26
+ throw EthereumJSErrorWithoutCode('Invalid node length')
27
+ }
28
+ const [, leftChildHash, rightChildHash, leftChildRawPath, rightChildRawPath] = rawNode
29
+
30
+ const decodeChild = (hash: Uint8Array, rawPath: Uint8Array): ChildBinaryNode | null => {
31
+ if (hash.length === 0) return null
32
+ const decoded = RLP.decode(rawPath)
33
+
34
+ if (!Array.isArray(decoded) || decoded.length !== 2) {
35
+ throw EthereumJSErrorWithoutCode('Invalid RLP encoding for child path')
36
+ }
37
+
38
+ const [encodedLength, encodedPath] = decoded as Uint8Array[]
39
+
40
+ if (encodedLength.length !== 1) {
41
+ throw EthereumJSErrorWithoutCode('Invalid path length encoding')
42
+ }
43
+
44
+ const pathLength = encodedLength[0]
45
+ const path = bytesToBits(encodedPath, pathLength)
46
+
47
+ return { hash, path }
48
+ }
49
+
50
+ const children = [
51
+ decodeChild(leftChildHash, leftChildRawPath),
52
+ decodeChild(rightChildHash, rightChildRawPath),
53
+ ]
54
+
55
+ return new InternalBinaryNode({ children })
56
+ }
57
+
58
+ /**
59
+ * Generates a new Internal node
60
+ * @param children the children nodes
61
+ * @returns a new Internal node
62
+ */
63
+ static create(children?: (ChildBinaryNode | null)[]): InternalBinaryNode {
64
+ if (children !== undefined && children.length !== 2) {
65
+ throw EthereumJSErrorWithoutCode('Internal node must have 2 children')
66
+ }
67
+ return new InternalBinaryNode({ children })
68
+ }
69
+
70
+ getChild(index: number): ChildBinaryNode | null {
71
+ return this.children[index]
72
+ }
73
+
74
+ setChild(index: number, child: ChildBinaryNode | null): void {
75
+ this.children[index] = child
76
+ }
77
+
78
+ /**
79
+ * @returns the RLP serialized node
80
+ */
81
+ serialize(): Uint8Array {
82
+ return RLP.encode(this.raw())
83
+ }
84
+
85
+ /**
86
+ * Returns the raw serialized representation of this internal node as an array of Uint8Arrays.
87
+ *
88
+ * The returned array contains:
89
+ * 1. A single-byte Uint8Array indicating the node type (BinaryNodeType.Internal).
90
+ * 2. For each child (left then right):
91
+ * - The child’s hash, or an empty Uint8Array if the child is null.
92
+ * 3. For each child (left then right):
93
+ * - An RLP-encoded tuple [pathLength, packedPathBytes] where:
94
+ * - `pathLength` is a one-byte Uint8Array representing the number of meaningful bits in the child’s path.
95
+ * - `packedPathBytes` is the packed byte representation of the child's bit path (as produced by `bitsToBytes`).
96
+ *
97
+ * @returns {Uint8Array[]} An array of Uint8Arrays representing the node's serialized internal data.
98
+ * @dev When decoding, the stored child path (an RLP-encoded tuple) must be converted back into the original bit array.
99
+ */
100
+
101
+ raw(): Uint8Array[] {
102
+ return [
103
+ new Uint8Array([BinaryNodeType.Internal]),
104
+ ...this.children.map((child) => (child !== null ? child.hash : new Uint8Array())),
105
+ ...this.children.map((child) =>
106
+ child !== null
107
+ ? RLP.encode([new Uint8Array([child.path.length]), bitsToBytes(child.path)])
108
+ : new Uint8Array(),
109
+ ),
110
+ ]
111
+ }
112
+ }
@@ -0,0 +1,87 @@
1
+ import { RLP } from '@feelyourprotocol/rlp'
2
+ import { EthereumJSErrorWithoutCode } from '@feelyourprotocol/util'
3
+
4
+ import { BinaryNodeType, NODE_WIDTH } from './types.ts'
5
+
6
+ import type { BinaryNodeOptions } from './types.ts'
7
+
8
+ export class StemBinaryNode {
9
+ public stem: Uint8Array
10
+ public values: (Uint8Array | null)[] // Array of 256 possible values represented as 32 byte Uint8Arrays
11
+
12
+ public type = BinaryNodeType.Stem
13
+
14
+ constructor(options: BinaryNodeOptions[typeof BinaryNodeType.Stem]) {
15
+ this.stem = options.stem
16
+ this.values = options.values ?? new Array(256).fill(null)
17
+ }
18
+
19
+ static fromRawNode(rawNode: Uint8Array[]): StemBinaryNode {
20
+ const nodeType = rawNode[0][0]
21
+ if (nodeType !== BinaryNodeType.Stem) {
22
+ throw EthereumJSErrorWithoutCode('Invalid node type')
23
+ }
24
+
25
+ // The length of the rawNode should be the # of values (node width) + 2 for the node type and the stem
26
+ if (rawNode.length !== NODE_WIDTH + 2) {
27
+ throw EthereumJSErrorWithoutCode('Invalid node length')
28
+ }
29
+
30
+ const stem = rawNode[1]
31
+ const rawValues = rawNode.slice(2, rawNode.length)
32
+ const values = rawValues.map((el) => (el.length === 0 ? null : el))
33
+
34
+ return new StemBinaryNode({ stem, values })
35
+ }
36
+
37
+ /**
38
+ * Generates a new Stem node
39
+ * @param stem the 31 byte stem corresponding to the where the stem node is located in the tree
40
+ * @returns a new Stem node
41
+ */
42
+ static create(stem: Uint8Array): StemBinaryNode {
43
+ return new StemBinaryNode({ stem })
44
+ }
45
+
46
+ // Retrieve the value at the provided index from the values array
47
+ getValue(index: number): Uint8Array | null {
48
+ return this.values[index]
49
+ }
50
+
51
+ setValue(index: number, value: Uint8Array | null): void {
52
+ this.values[index] = value
53
+ }
54
+
55
+ /**
56
+ * @returns the RLP serialized node
57
+ */
58
+ serialize(): Uint8Array {
59
+ return RLP.encode(this.raw())
60
+ }
61
+
62
+ /**
63
+ * Returns the raw serialized representation of the node as an array of Uint8Arrays.
64
+ * The returned array is constructed as follows:
65
+ * - The first element is a Uint8Array containing a single byte that represents the node type,
66
+ * - The second element is the node's `stem` property.
67
+ * - The remaining elements are derived from the node's `values` array:
68
+ * - For each value, if it is `null`, it is converted to an empty Uint8Array.
69
+ * - Otherwise, the value is included as-is.
70
+ *
71
+ * @returns {Uint8Array[]} An array of Uint8Arrays representing the node's raw data.
72
+ */
73
+ raw(): Uint8Array[] {
74
+ return [
75
+ new Uint8Array([BinaryNodeType.Stem]),
76
+ this.stem,
77
+ ...this.values.map((val) => {
78
+ switch (val) {
79
+ case null:
80
+ return new Uint8Array()
81
+ default:
82
+ return val
83
+ }
84
+ }),
85
+ ]
86
+ }
87
+ }
@@ -0,0 +1,41 @@
1
+ import type { InternalBinaryNode } from './internalNode.ts'
2
+ import type { StemBinaryNode } from './stemNode.ts'
3
+
4
+ export type BinaryNodeType = (typeof BinaryNodeType)[keyof typeof BinaryNodeType]
5
+ export const BinaryNodeType = {
6
+ Internal: 0,
7
+ Stem: 1,
8
+ } as const
9
+
10
+ export interface TypedBinaryNode {
11
+ [BinaryNodeType.Internal]: InternalBinaryNode
12
+ [BinaryNodeType.Stem]: StemBinaryNode
13
+ }
14
+
15
+ export type BinaryNode = TypedBinaryNode[BinaryNodeType]
16
+
17
+ /**
18
+ * @dev A child node in a binary tree internal node.
19
+ * @param hash The hash of the child node.
20
+ * @param path The path to the child node, in bits.
21
+ * */
22
+ export type ChildBinaryNode = {
23
+ hash: Uint8Array
24
+ path: number[]
25
+ }
26
+
27
+ interface InternalBinaryNodeOptions {
28
+ children?: (ChildBinaryNode | null)[]
29
+ }
30
+
31
+ interface StemBinaryNodeOptions {
32
+ stem: Uint8Array
33
+ values?: (Uint8Array | null)[]
34
+ }
35
+
36
+ export interface BinaryNodeOptions {
37
+ [BinaryNodeType.Internal]: InternalBinaryNodeOptions
38
+ [BinaryNodeType.Stem]: StemBinaryNodeOptions
39
+ }
40
+
41
+ export const NODE_WIDTH = 256
@@ -0,0 +1,38 @@
1
+ import { RLP } from '@feelyourprotocol/rlp'
2
+ import { EthereumJSErrorWithoutCode } from '@feelyourprotocol/util'
3
+
4
+ import { InternalBinaryNode } from './internalNode.ts'
5
+ import { StemBinaryNode } from './stemNode.ts'
6
+ import { type BinaryNode, BinaryNodeType } from './types.ts'
7
+
8
+ export function decodeRawBinaryNode(raw: Uint8Array[]): BinaryNode {
9
+ const nodeType = raw[0][0]
10
+ switch (nodeType) {
11
+ case BinaryNodeType.Internal:
12
+ return InternalBinaryNode.fromRawNode(raw)
13
+ case BinaryNodeType.Stem:
14
+ return StemBinaryNode.fromRawNode(raw)
15
+ default:
16
+ throw EthereumJSErrorWithoutCode('Invalid node type')
17
+ }
18
+ }
19
+
20
+ export function decodeBinaryNode(raw: Uint8Array) {
21
+ const decoded = RLP.decode(Uint8Array.from(raw)) as Uint8Array[]
22
+ if (!Array.isArray(decoded)) {
23
+ throw EthereumJSErrorWithoutCode('Invalid node')
24
+ }
25
+ return decodeRawBinaryNode(decoded)
26
+ }
27
+
28
+ export function isRawBinaryNode(node: Uint8Array | Uint8Array[]): node is Uint8Array[] {
29
+ return Array.isArray(node) && !(node instanceof Uint8Array)
30
+ }
31
+
32
+ export function isInternalBinaryNode(node: BinaryNode): node is InternalBinaryNode {
33
+ return node.type === BinaryNodeType.Internal
34
+ }
35
+
36
+ export function isStemBinaryNode(node: BinaryNode): node is StemBinaryNode {
37
+ return node.type === BinaryNodeType.Stem
38
+ }
package/src/proof.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { EthereumJSErrorWithoutCode, equalsBytes } from '@feelyourprotocol/util'
2
+
3
+ import { createBinaryTree } from './constructors.ts'
4
+ import { decodeBinaryNode } from './node/index.ts'
5
+
6
+ import type { BinaryTree } from './binaryTree.ts'
7
+ import type { BinaryNode } from './node/index.ts'
8
+ import type { StemBinaryNode } from './node/stemNode.ts'
9
+
10
+ /**
11
+ * Saves the nodes from a proof into the tree.
12
+ * @param proof
13
+ */
14
+ export async function binaryTreeFromProof(proof: Uint8Array[]): Promise<BinaryTree> {
15
+ const proofTrie = await createBinaryTree()
16
+ const putStack: [Uint8Array, BinaryNode][] = proof.map((bytes) => {
17
+ const node = decodeBinaryNode(bytes)
18
+ return [proofTrie['merkelize'](node), node]
19
+ })
20
+ await proofTrie.saveStack(putStack)
21
+ const root = putStack[0][0]
22
+ proofTrie.root(root)
23
+ return proofTrie
24
+ }
25
+
26
+ /**
27
+ * Verifies a proof.
28
+ * @param rootHash
29
+ * @param key
30
+ * @param proof
31
+ * @throws If proof is found to be invalid.
32
+ * @returns The value from the key, or null if valid proof of non-existence.
33
+ */
34
+ export async function verifyBinaryProof(
35
+ rootHash: Uint8Array,
36
+ key: Uint8Array,
37
+ proof: Uint8Array[],
38
+ ): Promise<Uint8Array | null> {
39
+ const proofTrie = await binaryTreeFromProof(proof)
40
+ if (!equalsBytes(proofTrie.root(), rootHash)) {
41
+ throw EthereumJSErrorWithoutCode('rootHash does not match proof root')
42
+ }
43
+ const [value] = await proofTrie.get(key.slice(0, 31), [key[31]])
44
+ const valueNode = decodeBinaryNode(proof[proof.length - 1]) as StemBinaryNode
45
+ const expectedValue = valueNode.values[key[31]]
46
+ if (!expectedValue) {
47
+ if (value) {
48
+ throw EthereumJSErrorWithoutCode('Proof is invalid')
49
+ }
50
+ } else if (value && !equalsBytes(value, expectedValue)) {
51
+ throw EthereumJSErrorWithoutCode('Proof is invalid')
52
+ }
53
+ return value
54
+ }