@waku/rln 0.0.14 → 0.1.0-b7cb3f9

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.
@@ -0,0 +1,72 @@
1
+ class RootPerBlock {
2
+ constructor(root, blockNumber) {
3
+ this.root = root;
4
+ this.blockNumber = blockNumber;
5
+ }
6
+ }
7
+ const maxBufferSize = 20;
8
+ export class MerkleRootTracker {
9
+ constructor(acceptableRootWindowSize, initialRoot) {
10
+ this.acceptableRootWindowSize = acceptableRootWindowSize;
11
+ this.validMerkleRoots = new Array();
12
+ this.merkleRootBuffer = new Array();
13
+ this.pushRoot(0, initialRoot);
14
+ }
15
+ backFill(fromBlockNumber) {
16
+ if (this.validMerkleRoots.length == 0)
17
+ return;
18
+ let numBlocks = 0;
19
+ for (let i = this.validMerkleRoots.length - 1; i >= 0; i--) {
20
+ if (this.validMerkleRoots[i].blockNumber >= fromBlockNumber) {
21
+ numBlocks++;
22
+ }
23
+ }
24
+ if (numBlocks == 0)
25
+ return;
26
+ const olderBlock = fromBlockNumber < this.validMerkleRoots[0].blockNumber;
27
+ // Remove last roots
28
+ let rootsToPop = numBlocks;
29
+ if (this.validMerkleRoots.length < rootsToPop) {
30
+ rootsToPop = this.validMerkleRoots.length;
31
+ }
32
+ this.validMerkleRoots = this.validMerkleRoots.slice(0, this.validMerkleRoots.length - rootsToPop);
33
+ if (this.merkleRootBuffer.length == 0)
34
+ return;
35
+ if (olderBlock) {
36
+ const idx = this.merkleRootBuffer.findIndex((x) => x.blockNumber == fromBlockNumber);
37
+ if (idx > -1) {
38
+ this.merkleRootBuffer = this.merkleRootBuffer.slice(0, idx);
39
+ }
40
+ }
41
+ // Backfill the tree's acceptable roots
42
+ let rootsToRestore = this.acceptableRootWindowSize - this.validMerkleRoots.length;
43
+ if (this.merkleRootBuffer.length < rootsToRestore) {
44
+ rootsToRestore = this.merkleRootBuffer.length;
45
+ }
46
+ for (let i = 0; i < rootsToRestore; i++) {
47
+ const x = this.merkleRootBuffer.pop();
48
+ if (x)
49
+ this.validMerkleRoots.unshift(x);
50
+ }
51
+ }
52
+ pushRoot(blockNumber, root) {
53
+ this.validMerkleRoots.push(new RootPerBlock(root, blockNumber));
54
+ // Maintain valid merkle root window
55
+ if (this.validMerkleRoots.length > this.acceptableRootWindowSize) {
56
+ const x = this.validMerkleRoots.shift();
57
+ if (x)
58
+ this.merkleRootBuffer.push(x);
59
+ }
60
+ // Maintain merkle root buffer
61
+ if (this.merkleRootBuffer.length > maxBufferSize) {
62
+ this.merkleRootBuffer.shift();
63
+ }
64
+ }
65
+ roots() {
66
+ return this.validMerkleRoots.map((x) => x.root);
67
+ }
68
+ buffer() {
69
+ return this.merkleRootBuffer.map((x) => x.root);
70
+ }
71
+ }
72
+ //# sourceMappingURL=root_tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root_tracker.js","sourceRoot":"","sources":["../src/root_tracker.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waku/rln",
3
- "version": "0.0.14",
3
+ "version": "0.1.0-b7cb3f9",
4
4
  "description": "Rate Limit Nullifier for js-waku",
5
5
  "types": "./dist/index.d.ts",
6
6
  "module": "./dist/index.js",
@@ -83,9 +83,9 @@
83
83
  "husky": "^7.0.4",
84
84
  "ignore-loader": "^0.1.2",
85
85
  "isomorphic-fetch": "^3.0.0",
86
- "@waku/interfaces": "^0.0.11",
87
- "@waku/message-encryption": "^0.0.14",
88
- "@waku/core": "^0.0.16",
86
+ "@waku/interfaces": "^0.0.12",
87
+ "@waku/message-encryption": "^0.0.15",
88
+ "@waku/core": "^0.0.17",
89
89
  "jsdom": "^19.0.0",
90
90
  "jsdom-global": "^3.0.2",
91
91
  "karma": "^6.3.12",
@@ -93,7 +93,7 @@
93
93
  "karma-mocha": "^2.0.1",
94
94
  "karma-webpack": "^5.0.0",
95
95
  "lint-staged": "^13.0.3",
96
- "mocha": "^9.1.3",
96
+ "mocha": "10.1.0",
97
97
  "npm-run-all": "^4.1.5",
98
98
  "p-timeout": "^4.1.0",
99
99
  "prettier": "^2.1.1",
@@ -129,8 +129,8 @@
129
129
  ]
130
130
  },
131
131
  "dependencies": {
132
- "@waku/utils": "^0.0.4",
133
- "@waku/zerokit-rln-wasm": "^0.0.5",
132
+ "@waku/utils": "^0.0.5",
133
+ "@waku/zerokit-rln-wasm": "^0.0.10",
134
134
  "ethers": "^5.7.2"
135
135
  }
136
136
  }
package/src/codec.ts CHANGED
@@ -9,21 +9,21 @@ import type {
9
9
  import debug from "debug";
10
10
 
11
11
  import { RlnMessage, toRLNSignal } from "./message.js";
12
- import { MembershipKey, RLNInstance } from "./rln.js";
12
+ import { IdentityCredential, RLNInstance } from "./rln.js";
13
13
 
14
14
  const log = debug("waku:rln:encoder");
15
15
 
16
16
  export class RLNEncoder implements IEncoder {
17
- private readonly idKey: Uint8Array;
17
+ private readonly idSecretHash: Uint8Array;
18
18
 
19
19
  constructor(
20
20
  private encoder: IEncoder,
21
21
  private rlnInstance: RLNInstance,
22
22
  private index: number,
23
- membershipKey: MembershipKey
23
+ identityCredential: IdentityCredential
24
24
  ) {
25
25
  if (index < 0) throw "invalid membership index";
26
- this.idKey = membershipKey.IDKey;
26
+ this.idSecretHash = identityCredential.IDSecretHash;
27
27
  }
28
28
 
29
29
  async toWire(message: IMessage): Promise<Uint8Array | undefined> {
@@ -50,7 +50,7 @@ export class RLNEncoder implements IEncoder {
50
50
  signal,
51
51
  this.index,
52
52
  message.timestamp,
53
- this.idKey
53
+ this.idSecretHash
54
54
  );
55
55
  console.timeEnd("proof_gen_timer");
56
56
  return proof;
@@ -69,7 +69,7 @@ type RLNEncoderOptions = {
69
69
  encoder: IEncoder;
70
70
  rlnInstance: RLNInstance;
71
71
  index: number;
72
- membershipKey: MembershipKey;
72
+ credential: IdentityCredential;
73
73
  };
74
74
 
75
75
  export const createRLNEncoder = (options: RLNEncoderOptions): RLNEncoder => {
@@ -77,7 +77,7 @@ export const createRLNEncoder = (options: RLNEncoderOptions): RLNEncoder => {
77
77
  options.encoder,
78
78
  options.rlnInstance,
79
79
  options.index,
80
- options.membershipKey
80
+ options.credential
81
81
  );
82
82
  };
83
83
 
package/src/index.ts CHANGED
@@ -1,8 +1,13 @@
1
1
  import { RLNDecoder, RLNEncoder } from "./codec.js";
2
2
  import { GOERLI_CONTRACT, RLN_ABI } from "./constants.js";
3
- import { Proof, RLNInstance } from "./rln.js";
4
- import { MembershipKey } from "./rln.js";
3
+ import {
4
+ IdentityCredential,
5
+ Proof,
6
+ ProofMetadata,
7
+ RLNInstance,
8
+ } from "./rln.js";
5
9
  import { RLNContract } from "./rln_contract.js";
10
+ import { MerkleRootTracker } from "./root_tracker.js";
6
11
 
7
12
  // reexport the create function, dynamically imported from rln.ts
8
13
  export async function create(): Promise<RLNInstance> {
@@ -15,10 +20,12 @@ export async function create(): Promise<RLNInstance> {
15
20
 
16
21
  export {
17
22
  RLNInstance,
18
- MembershipKey,
23
+ IdentityCredential,
19
24
  Proof,
25
+ ProofMetadata,
20
26
  RLNEncoder,
21
27
  RLNDecoder,
28
+ MerkleRootTracker,
22
29
  RLNContract,
23
30
  RLN_ABI,
24
31
  GOERLI_CONTRACT,
package/src/rln.ts CHANGED
@@ -66,18 +66,29 @@ export async function create(): Promise<RLNInstance> {
66
66
  return new RLNInstance(zkRLN, witnessCalculator);
67
67
  }
68
68
 
69
- export class MembershipKey {
69
+ export class IdentityCredential {
70
70
  constructor(
71
- public readonly IDKey: Uint8Array,
71
+ public readonly IDTrapdoor: Uint8Array,
72
+ public readonly IDNullifier: Uint8Array,
73
+ public readonly IDSecretHash: Uint8Array,
72
74
  public readonly IDCommitment: Uint8Array,
73
75
  public readonly IDCommitmentBigInt: bigint
74
76
  ) {}
75
77
 
76
- static fromBytes(memKeys: Uint8Array): MembershipKey {
77
- const idKey = memKeys.subarray(0, 32);
78
- const idCommitment = memKeys.subarray(32);
78
+ static fromBytes(memKeys: Uint8Array): IdentityCredential {
79
+ const idTrapdoor = memKeys.subarray(0, 32);
80
+ const idNullifier = memKeys.subarray(32, 64);
81
+ const idSecretHash = memKeys.subarray(64, 96);
82
+ const idCommitment = memKeys.subarray(96);
79
83
  const idCommitmentBigInt = buildBigIntFromUint8Array(idCommitment);
80
- return new MembershipKey(idKey, idCommitment, idCommitmentBigInt);
84
+
85
+ return new IdentityCredential(
86
+ idTrapdoor,
87
+ idNullifier,
88
+ idSecretHash,
89
+ idCommitment,
90
+ idCommitmentBigInt
91
+ );
81
92
  }
82
93
  }
83
94
 
@@ -89,6 +100,14 @@ const shareYOffset = shareXOffset + 32;
89
100
  const nullifierOffset = shareYOffset + 32;
90
101
  const rlnIdentifierOffset = nullifierOffset + 32;
91
102
 
103
+ export class ProofMetadata {
104
+ constructor(
105
+ public readonly nullifier: Uint8Array,
106
+ public readonly shareX: Uint8Array,
107
+ public readonly shareY: Uint8Array,
108
+ public readonly externalNullifier: Uint8Array
109
+ ) {}
110
+ }
92
111
  export class Proof implements IRateLimitProof {
93
112
  readonly proof: Uint8Array;
94
113
  readonly merkleRoot: Uint8Array;
@@ -112,6 +131,16 @@ export class Proof implements IRateLimitProof {
112
131
  rlnIdentifierOffset
113
132
  );
114
133
  }
134
+
135
+ extractMetadata(): ProofMetadata {
136
+ const externalNullifier = poseidonHash(this.epoch, this.rlnIdentifier);
137
+ return new ProofMetadata(
138
+ this.nullifier,
139
+ this.shareX,
140
+ this.shareY,
141
+ externalNullifier
142
+ );
143
+ }
115
144
  }
116
145
 
117
146
  export function proofToBytes(p: IRateLimitProof): Uint8Array {
@@ -126,30 +155,60 @@ export function proofToBytes(p: IRateLimitProof): Uint8Array {
126
155
  );
127
156
  }
128
157
 
158
+ export function poseidonHash(...input: Array<Uint8Array>): Uint8Array {
159
+ const inputLen = writeUIntLE(new Uint8Array(8), input.length, 0, 8);
160
+ const lenPrefixedData = concatenate(inputLen, ...input);
161
+ return zerokitRLN.poseidonHash(lenPrefixedData);
162
+ }
163
+
164
+ export function sha256(input: Uint8Array): Uint8Array {
165
+ const inputLen = writeUIntLE(new Uint8Array(8), input.length, 0, 8);
166
+ const lenPrefixedData = concatenate(inputLen, input);
167
+ return zerokitRLN.hash(lenPrefixedData);
168
+ }
169
+
129
170
  export class RLNInstance {
130
171
  constructor(
131
172
  private zkRLN: number,
132
173
  private witnessCalculator: WitnessCalculator
133
174
  ) {}
134
175
 
135
- generateMembershipKey(): MembershipKey {
136
- const memKeys = zerokitRLN.generateMembershipKey(this.zkRLN);
137
- return MembershipKey.fromBytes(memKeys);
176
+ generateIdentityCredentials(): IdentityCredential {
177
+ const memKeys = zerokitRLN.generateExtendedMembershipKey(this.zkRLN); // TODO: rename this function in zerokit rln-wasm
178
+ return IdentityCredential.fromBytes(memKeys);
138
179
  }
139
180
 
140
- generateSeededMembershipKey(seed: string): MembershipKey {
181
+ generateSeededIdentityCredential(seed: string): IdentityCredential {
141
182
  const seedBytes = stringEncoder.encode(seed);
142
- const memKeys = zerokitRLN.generateSeededMembershipKey(
183
+ // TODO: rename this function in zerokit rln-wasm
184
+ const memKeys = zerokitRLN.generateSeededExtendedMembershipKey(
143
185
  this.zkRLN,
144
186
  seedBytes
145
187
  );
146
- return MembershipKey.fromBytes(memKeys);
188
+ return IdentityCredential.fromBytes(memKeys);
147
189
  }
148
190
 
149
191
  insertMember(idCommitment: Uint8Array): void {
150
192
  zerokitRLN.insertMember(this.zkRLN, idCommitment);
151
193
  }
152
194
 
195
+ insertMembers(index: number, ...idCommitments: Array<Uint8Array>): void {
196
+ // serializes a seq of IDCommitments to a byte seq
197
+ // the order of serialization is |id_commitment_len<8>|id_commitment<var>|
198
+ const idCommitmentLen = writeUIntLE(
199
+ new Uint8Array(8),
200
+ idCommitments.length,
201
+ 0,
202
+ 8
203
+ );
204
+ const idCommitmentBytes = concatenate(idCommitmentLen, ...idCommitments);
205
+ zerokitRLN.setLeavesFrom(this.zkRLN, index, idCommitmentBytes);
206
+ }
207
+
208
+ deleteMember(index: number): void {
209
+ zerokitRLN.deleteLeaf(this.zkRLN, index);
210
+ }
211
+
153
212
  getMerkleRoot(): Uint8Array {
154
213
  return zerokitRLN.getRoot(this.zkRLN);
155
214
  }
@@ -174,7 +233,7 @@ export class RLNInstance {
174
233
  msg: Uint8Array,
175
234
  index: number,
176
235
  epoch: Uint8Array | Date | undefined,
177
- idKey: Uint8Array
236
+ idSecretHash: Uint8Array
178
237
  ): Promise<IRateLimitProof> {
179
238
  if (epoch == undefined) {
180
239
  epoch = epochIntToBytes(dateToEpoch(new Date()));
@@ -183,10 +242,15 @@ export class RLNInstance {
183
242
  }
184
243
 
185
244
  if (epoch.length != 32) throw "invalid epoch";
186
- if (idKey.length != 32) throw "invalid id key";
245
+ if (idSecretHash.length != 32) throw "invalid id secret hash";
187
246
  if (index < 0) throw "index must be >= 0";
188
247
 
189
- const serialized_msg = this.serializeMessage(msg, index, epoch, idKey);
248
+ const serialized_msg = this.serializeMessage(
249
+ msg,
250
+ index,
251
+ epoch,
252
+ idSecretHash
253
+ );
190
254
  const rlnWitness = zerokitRLN.getSerializedRLNWitness(
191
255
  this.zkRLN,
192
256
  serialized_msg
@@ -228,7 +292,8 @@ export class RLNInstance {
228
292
 
229
293
  verifyWithRoots(
230
294
  proof: IRateLimitProof | Uint8Array,
231
- msg: Uint8Array
295
+ msg: Uint8Array,
296
+ ...roots: Array<Uint8Array>
232
297
  ): boolean {
233
298
  let pBytes: Uint8Array;
234
299
  if (proof instanceof Uint8Array) {
@@ -236,17 +301,15 @@ export class RLNInstance {
236
301
  } else {
237
302
  pBytes = proofToBytes(proof);
238
303
  }
239
-
240
304
  // calculate message length
241
305
  const msgLen = writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
242
306
 
243
- // obtain root
244
- const root = zerokitRLN.getRoot(this.zkRLN);
307
+ const rootsBytes = concatenate(...roots);
245
308
 
246
309
  return zerokitRLN.verifyWithRoots(
247
310
  this.zkRLN,
248
311
  concatenate(pBytes, msgLen, msg),
249
- root
312
+ rootsBytes
250
313
  );
251
314
  }
252
315
 
@@ -1,7 +1,7 @@
1
1
  import { ethers } from "ethers";
2
2
 
3
3
  import { RLN_ABI } from "./constants.js";
4
- import { RLNInstance } from "./rln.js";
4
+ import { IdentityCredential, RLNInstance } from "./rln.js";
5
5
 
6
6
  type Member = {
7
7
  pubkey: string;
@@ -13,6 +13,12 @@ type ContractOptions = {
13
13
  provider: ethers.Signer | ethers.providers.Provider;
14
14
  };
15
15
 
16
+ type FetchMembersOptions = {
17
+ fromBlock?: number;
18
+ fetchRange?: number;
19
+ fetchChunks?: number;
20
+ };
21
+
16
22
  export class RLNContract {
17
23
  private _contract: ethers.Contract;
18
24
  private membersFilter: ethers.EventFilter;
@@ -46,12 +52,12 @@ export class RLNContract {
46
52
 
47
53
  public async fetchMembers(
48
54
  rlnInstance: RLNInstance,
49
- fromBlock?: number
55
+ options: FetchMembersOptions = {}
50
56
  ): Promise<void> {
51
- const registeredMemberEvents = await this.contract.queryFilter(
52
- this.membersFilter,
53
- fromBlock
54
- );
57
+ const registeredMemberEvents = await queryFilter(this.contract, {
58
+ ...options,
59
+ membersFilter: this.membersFilter,
60
+ });
55
61
 
56
62
  for (const event of registeredMemberEvents) {
57
63
  this.addMemberFromEvent(rlnInstance, event);
@@ -84,17 +90,23 @@ export class RLNContract {
84
90
  rlnInstance.insertMember(idCommitment);
85
91
  }
86
92
 
87
- public async registerMember(
93
+ public async registerWithSignature(
88
94
  rlnInstance: RLNInstance,
89
95
  signature: string
90
96
  ): Promise<ethers.Event | undefined> {
91
- const membershipKey = await rlnInstance.generateSeededMembershipKey(
92
- signature
93
- );
97
+ const identityCredential =
98
+ await rlnInstance.generateSeededIdentityCredential(signature);
99
+
100
+ return this.registerWithKey(identityCredential);
101
+ }
102
+
103
+ public async registerWithKey(
104
+ credential: IdentityCredential
105
+ ): Promise<ethers.Event | undefined> {
94
106
  const depositValue = await this.contract.MEMBERSHIP_DEPOSIT();
95
107
 
96
108
  const txRegisterResponse: ethers.ContractTransaction =
97
- await this.contract.register(membershipKey.IDCommitmentBigInt, {
109
+ await this.contract.register(credential.IDCommitmentBigInt, {
98
110
  value: depositValue,
99
111
  });
100
112
  const txRegisterReceipt = await txRegisterResponse.wait();
@@ -102,3 +114,90 @@ export class RLNContract {
102
114
  return txRegisterReceipt?.events?.[0];
103
115
  }
104
116
  }
117
+
118
+ type CustomQueryOptions = FetchMembersOptions & {
119
+ membersFilter: ethers.EventFilter;
120
+ };
121
+
122
+ // these value should be tested on other networks
123
+ const FETCH_CHUNK = 5;
124
+ const BLOCK_RANGE = 3000;
125
+
126
+ async function queryFilter(
127
+ contract: ethers.Contract,
128
+ options: CustomQueryOptions
129
+ ): Promise<ethers.Event[]> {
130
+ const {
131
+ fromBlock,
132
+ membersFilter,
133
+ fetchRange = BLOCK_RANGE,
134
+ fetchChunks = FETCH_CHUNK,
135
+ } = options;
136
+
137
+ if (!fromBlock) {
138
+ return contract.queryFilter(membersFilter);
139
+ }
140
+
141
+ if (!contract.signer.provider) {
142
+ throw Error("No provider found on the contract's signer.");
143
+ }
144
+
145
+ const toBlock = await contract.signer.provider.getBlockNumber();
146
+
147
+ if (toBlock - fromBlock < fetchRange) {
148
+ return contract.queryFilter(membersFilter);
149
+ }
150
+
151
+ const events: ethers.Event[][] = [];
152
+ const chunks = splitToChunks(fromBlock, toBlock, fetchRange);
153
+
154
+ for (const portion of takeN<[number, number]>(chunks, fetchChunks)) {
155
+ const promises = portion.map(([left, right]) =>
156
+ ignoreErrors(contract.queryFilter(membersFilter, left, right), [])
157
+ );
158
+ const fetchedEvents = await Promise.all(promises);
159
+ events.push(fetchedEvents.flatMap((v) => v));
160
+ }
161
+
162
+ return events.flatMap((v) => v);
163
+ }
164
+
165
+ function splitToChunks(
166
+ from: number,
167
+ to: number,
168
+ step: number
169
+ ): Array<[number, number]> {
170
+ const chunks = [];
171
+
172
+ let left = from;
173
+ while (left < to) {
174
+ const right = left + step < to ? left + step : to;
175
+
176
+ chunks.push([left, right] as [number, number]);
177
+
178
+ left = right;
179
+ }
180
+
181
+ return chunks;
182
+ }
183
+
184
+ function* takeN<T>(array: T[], size: number): Iterable<T[]> {
185
+ let start = 0;
186
+ let skip = size;
187
+
188
+ while (skip < array.length) {
189
+ const portion = array.slice(start, skip);
190
+
191
+ yield portion;
192
+
193
+ start = skip;
194
+ skip += size;
195
+ }
196
+ }
197
+
198
+ function ignoreErrors<T>(promise: Promise<T>, defaultValue: T): Promise<T> {
199
+ return promise.catch((err) => {
200
+ console.error(`Ignoring an error during query: ${err?.message}`);
201
+ return defaultValue;
202
+ });
203
+ }
@@ -0,0 +1,88 @@
1
+ class RootPerBlock {
2
+ constructor(public root: Uint8Array, public blockNumber: number) {}
3
+ }
4
+
5
+ const maxBufferSize = 20;
6
+
7
+ export class MerkleRootTracker {
8
+ private validMerkleRoots: Array<RootPerBlock> = new Array<RootPerBlock>();
9
+ private merkleRootBuffer: Array<RootPerBlock> = new Array<RootPerBlock>();
10
+ constructor(
11
+ private acceptableRootWindowSize: number,
12
+ initialRoot: Uint8Array
13
+ ) {
14
+ this.pushRoot(0, initialRoot);
15
+ }
16
+
17
+ backFill(fromBlockNumber: number): void {
18
+ if (this.validMerkleRoots.length == 0) return;
19
+
20
+ let numBlocks = 0;
21
+ for (let i = this.validMerkleRoots.length - 1; i >= 0; i--) {
22
+ if (this.validMerkleRoots[i].blockNumber >= fromBlockNumber) {
23
+ numBlocks++;
24
+ }
25
+ }
26
+
27
+ if (numBlocks == 0) return;
28
+
29
+ const olderBlock = fromBlockNumber < this.validMerkleRoots[0].blockNumber;
30
+
31
+ // Remove last roots
32
+ let rootsToPop = numBlocks;
33
+ if (this.validMerkleRoots.length < rootsToPop) {
34
+ rootsToPop = this.validMerkleRoots.length;
35
+ }
36
+
37
+ this.validMerkleRoots = this.validMerkleRoots.slice(
38
+ 0,
39
+ this.validMerkleRoots.length - rootsToPop
40
+ );
41
+
42
+ if (this.merkleRootBuffer.length == 0) return;
43
+
44
+ if (olderBlock) {
45
+ const idx = this.merkleRootBuffer.findIndex(
46
+ (x) => x.blockNumber == fromBlockNumber
47
+ );
48
+ if (idx > -1) {
49
+ this.merkleRootBuffer = this.merkleRootBuffer.slice(0, idx);
50
+ }
51
+ }
52
+
53
+ // Backfill the tree's acceptable roots
54
+ let rootsToRestore =
55
+ this.acceptableRootWindowSize - this.validMerkleRoots.length;
56
+ if (this.merkleRootBuffer.length < rootsToRestore) {
57
+ rootsToRestore = this.merkleRootBuffer.length;
58
+ }
59
+
60
+ for (let i = 0; i < rootsToRestore; i++) {
61
+ const x = this.merkleRootBuffer.pop();
62
+ if (x) this.validMerkleRoots.unshift(x);
63
+ }
64
+ }
65
+
66
+ pushRoot(blockNumber: number, root: Uint8Array): void {
67
+ this.validMerkleRoots.push(new RootPerBlock(root, blockNumber));
68
+
69
+ // Maintain valid merkle root window
70
+ if (this.validMerkleRoots.length > this.acceptableRootWindowSize) {
71
+ const x = this.validMerkleRoots.shift();
72
+ if (x) this.merkleRootBuffer.push(x);
73
+ }
74
+
75
+ // Maintain merkle root buffer
76
+ if (this.merkleRootBuffer.length > maxBufferSize) {
77
+ this.merkleRootBuffer.shift();
78
+ }
79
+ }
80
+
81
+ roots(): Array<Uint8Array> {
82
+ return this.validMerkleRoots.map((x) => x.root);
83
+ }
84
+
85
+ buffer(): Array<Uint8Array> {
86
+ return this.merkleRootBuffer.map((x) => x.root);
87
+ }
88
+ }