@ocash/sdk 0.1.4-rc.1 → 0.1.4-rc.2

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.
package/dist/node.cjs CHANGED
@@ -3291,8 +3291,8 @@ var MemoryStore = class {
3291
3291
  this.utxos = /* @__PURE__ */ new Map();
3292
3292
  this.operations = [];
3293
3293
  this.merkleLeavesByChain = /* @__PURE__ */ new Map();
3294
- this.merkleTreesByChain = /* @__PURE__ */ new Map();
3295
- this.merkleNodesByChain = /* @__PURE__ */ new Map();
3294
+ this.chairmanMerkleVersionsByChain = /* @__PURE__ */ new Map();
3295
+ this.chairmanMerkleNodesByChain = /* @__PURE__ */ new Map();
3296
3296
  this.entryMemosByChain = /* @__PURE__ */ new Map();
3297
3297
  this.entryNullifiersByChain = /* @__PURE__ */ new Map();
3298
3298
  const max = options?.maxOperations;
@@ -3308,8 +3308,8 @@ var MemoryStore = class {
3308
3308
  this.utxos.clear();
3309
3309
  this.operations = [];
3310
3310
  this.merkleLeavesByChain.clear();
3311
- this.merkleTreesByChain.clear();
3312
- this.merkleNodesByChain.clear();
3311
+ this.chairmanMerkleVersionsByChain.clear();
3312
+ this.chairmanMerkleNodesByChain.clear();
3313
3313
  this.entryMemosByChain.clear();
3314
3314
  this.entryNullifiersByChain.clear();
3315
3315
  }
@@ -3425,49 +3425,64 @@ var MemoryStore = class {
3425
3425
  return { chainId, cid: row.cid, commitment: row.commitment };
3426
3426
  }
3427
3427
  /**
3428
- * Get a merkle node by id.
3428
+ * Get a chairmanMerkle tree node by id.
3429
3429
  */
3430
- async getMerkleNode(chainId, id) {
3431
- return this.merkleNodesByChain.get(chainId)?.get(id);
3430
+ async getChairmanMerkleNode(chainId, id) {
3431
+ return this.chairmanMerkleNodesByChain.get(chainId)?.get(id);
3432
3432
  }
3433
3433
  /**
3434
- * Upsert merkle nodes for a chain.
3434
+ * Put chairmanMerkle tree nodes for a chain.
3435
3435
  */
3436
- async upsertMerkleNodes(chainId, nodes) {
3436
+ async putChairmanMerkleNodes(chainId, nodes) {
3437
3437
  if (!nodes.length) return;
3438
- let map = this.merkleNodesByChain.get(chainId);
3438
+ let map = this.chairmanMerkleNodesByChain.get(chainId);
3439
3439
  if (!map) {
3440
3440
  map = /* @__PURE__ */ new Map();
3441
- this.merkleNodesByChain.set(chainId, map);
3441
+ this.chairmanMerkleNodesByChain.set(chainId, map);
3442
3442
  }
3443
3443
  for (const node of nodes) {
3444
3444
  map.set(node.id, { ...node, chainId });
3445
3445
  }
3446
3446
  }
3447
3447
  /**
3448
- * Clear merkle nodes for a chain.
3448
+ * Get a chairmanMerkle version record by chain and version number.
3449
3449
  */
3450
- async clearMerkleNodes(chainId) {
3451
- this.merkleNodesByChain.delete(chainId);
3450
+ async getChairmanMerkleVersion(chainId, version) {
3451
+ const byVersion = this.chairmanMerkleVersionsByChain.get(chainId);
3452
+ const record = byVersion?.get(version);
3453
+ return record ? { ...record } : void 0;
3452
3454
  }
3453
3455
  /**
3454
- * Get persisted merkle tree state.
3456
+ * Get the latest chairmanMerkle version record (highest version number) for a chain.
3455
3457
  */
3456
- async getMerkleTree(chainId) {
3457
- const tree = this.merkleTreesByChain.get(chainId);
3458
- return tree ? { ...tree } : void 0;
3458
+ async getLatestChairmanMerkleVersion(chainId) {
3459
+ const byVersion = this.chairmanMerkleVersionsByChain.get(chainId);
3460
+ if (!byVersion || byVersion.size === 0) return void 0;
3461
+ let latest;
3462
+ for (const record of byVersion.values()) {
3463
+ if (!latest || record.version > latest.version) {
3464
+ latest = record;
3465
+ }
3466
+ }
3467
+ return latest ? { ...latest } : void 0;
3459
3468
  }
3460
3469
  /**
3461
- * Persist merkle tree state.
3470
+ * Persist a chairmanMerkle version record.
3462
3471
  */
3463
- async setMerkleTree(chainId, tree) {
3464
- this.merkleTreesByChain.set(chainId, { ...tree, chainId });
3472
+ async putChairmanMerkleVersion(chainId, record) {
3473
+ let byVersion = this.chairmanMerkleVersionsByChain.get(chainId);
3474
+ if (!byVersion) {
3475
+ byVersion = /* @__PURE__ */ new Map();
3476
+ this.chairmanMerkleVersionsByChain.set(chainId, byVersion);
3477
+ }
3478
+ byVersion.set(record.version, { ...record, chainId });
3465
3479
  }
3466
3480
  /**
3467
- * Clear merkle tree state.
3481
+ * Clear all chairmanMerkle tree state (both nodes and versions) for a chain.
3468
3482
  */
3469
- async clearMerkleTree(chainId) {
3470
- this.merkleTreesByChain.delete(chainId);
3483
+ async clearChairmanMerkleTree(chainId) {
3484
+ this.chairmanMerkleNodesByChain.delete(chainId);
3485
+ this.chairmanMerkleVersionsByChain.delete(chainId);
3471
3486
  }
3472
3487
  /**
3473
3488
  * Upsert entry memos (raw EntryService cache).
@@ -3991,13 +4006,14 @@ var KeyValueStore = class {
3991
4006
  this.utxoCache = /* @__PURE__ */ new Map();
3992
4007
  this.operationCache = /* @__PURE__ */ new Map();
3993
4008
  this.merkleLeafCids = {};
3994
- this.merkleTrees = {};
3995
- this.merkleNodeIds = {};
4009
+ this.chairmanMerkleLatestVersions = {};
4010
+ this.chairmanMerkleNodeIds = {};
4011
+ this.chairmanMerkleVersionNums = {};
3996
4012
  this.entryMemoCids = {};
3997
4013
  this.entryNullifierNids = {};
3998
4014
  this.loadedMerkleLeaves = /* @__PURE__ */ new Set();
3999
- this.loadedMerkleTrees = /* @__PURE__ */ new Set();
4000
- this.loadedMerkleNodes = /* @__PURE__ */ new Set();
4015
+ this.loadedChairmanMerkleVersions = /* @__PURE__ */ new Set();
4016
+ this.loadedChairmanMerkleNodes = /* @__PURE__ */ new Set();
4001
4017
  this.loadedEntryMemos = /* @__PURE__ */ new Set();
4002
4018
  this.loadedEntryNullifiers = /* @__PURE__ */ new Set();
4003
4019
  this.saveChain = Promise.resolve();
@@ -4031,9 +4047,6 @@ var KeyValueStore = class {
4031
4047
  walletOperationKey(id) {
4032
4048
  return `${this.walletBaseKey()}:operation:${id}`;
4033
4049
  }
4034
- sharedChainKey(part, chainId) {
4035
- return `${this.keyPrefix()}:shared:${part}:${chainId}`;
4036
- }
4037
4050
  sharedChainMetaKey(part, chainId) {
4038
4051
  return `${this.keyPrefix()}:shared:${part}:${chainId}:meta`;
4039
4052
  }
@@ -4086,13 +4099,14 @@ var KeyValueStore = class {
4086
4099
  this.operationCache.clear();
4087
4100
  this.walletMetaLoaded = false;
4088
4101
  this.merkleLeafCids = {};
4089
- this.merkleTrees = {};
4090
- this.merkleNodeIds = {};
4102
+ this.chairmanMerkleLatestVersions = {};
4103
+ this.chairmanMerkleNodeIds = {};
4104
+ this.chairmanMerkleVersionNums = {};
4091
4105
  this.entryMemoCids = {};
4092
4106
  this.entryNullifierNids = {};
4093
4107
  this.loadedMerkleLeaves.clear();
4094
- this.loadedMerkleTrees.clear();
4095
- this.loadedMerkleNodes.clear();
4108
+ this.loadedChairmanMerkleVersions.clear();
4109
+ this.loadedChairmanMerkleNodes.clear();
4096
4110
  this.loadedEntryMemos.clear();
4097
4111
  this.loadedEntryNullifiers.clear();
4098
4112
  }
@@ -4187,20 +4201,19 @@ var KeyValueStore = class {
4187
4201
  this.merkleLeafCids[key] = new Set(this.parseNumberIndex(cidsRaw));
4188
4202
  this.loadedMerkleLeaves.add(chainId);
4189
4203
  }
4190
- async ensureMerkleTreeLoaded(chainId) {
4191
- if (this.loadedMerkleTrees.has(chainId)) return;
4204
+ async ensureChairmanMerkleVersionsLoaded(chainId) {
4205
+ if (this.loadedChairmanMerkleVersions.has(chainId)) return;
4192
4206
  const key = String(chainId);
4193
- const raw = await this.options.client.get(this.sharedChainKey("merkleTrees", chainId));
4194
- const row = this.parseJson(raw, null);
4195
- if (row && typeof row === "object") this.merkleTrees[key] = row;
4196
- this.loadedMerkleTrees.add(chainId);
4207
+ const numsRaw = await this.options.client.get(this.sharedChainMetaKey("chairmanMerkleVersions", chainId));
4208
+ this.chairmanMerkleVersionNums[key] = new Set(this.parseNumberIndex(numsRaw));
4209
+ this.loadedChairmanMerkleVersions.add(chainId);
4197
4210
  }
4198
- async ensureMerkleNodesLoaded(chainId) {
4199
- if (this.loadedMerkleNodes.has(chainId)) return;
4211
+ async ensureChairmanMerkleNodesLoaded(chainId) {
4212
+ if (this.loadedChairmanMerkleNodes.has(chainId)) return;
4200
4213
  const key = String(chainId);
4201
- const idsRaw = await this.options.client.get(this.sharedChainMetaKey("merkleNodes", chainId));
4202
- this.merkleNodeIds[key] = new Set(this.parseStringIndex(idsRaw));
4203
- this.loadedMerkleNodes.add(chainId);
4214
+ const idsRaw = await this.options.client.get(this.sharedChainMetaKey("chairmanMerkleNodes", chainId));
4215
+ this.chairmanMerkleNodeIds[key] = new Set(this.parseStringIndex(idsRaw));
4216
+ this.loadedChairmanMerkleNodes.add(chainId);
4204
4217
  }
4205
4218
  async ensureEntryMemosLoaded(chainId) {
4206
4219
  if (this.loadedEntryMemos.has(chainId)) return;
@@ -4216,64 +4229,88 @@ var KeyValueStore = class {
4216
4229
  this.entryNullifierNids[key] = new Set(this.parseNumberIndex(nidsRaw));
4217
4230
  this.loadedEntryNullifiers.add(chainId);
4218
4231
  }
4219
- async getMerkleNode(chainId, id) {
4220
- await this.ensureMerkleNodesLoaded(chainId);
4221
- if (!this.merkleNodeIds[String(chainId)]?.has(id)) return void 0;
4222
- const raw = await this.options.client.get(this.sharedRecordKey("merkleNodes", chainId, id));
4232
+ async getChairmanMerkleNode(chainId, id) {
4233
+ await this.ensureChairmanMerkleNodesLoaded(chainId);
4234
+ if (!this.chairmanMerkleNodeIds[String(chainId)]?.has(id)) return void 0;
4235
+ const raw = await this.options.client.get(this.sharedRecordKey("chairmanMerkleNodes", chainId, id));
4223
4236
  const node = this.parseJson(raw, null);
4224
4237
  if (!node) return void 0;
4225
4238
  const hash = node.hash;
4226
4239
  if (typeof hash !== "string" || !hash.startsWith("0x")) return void 0;
4227
4240
  return { ...node, chainId };
4228
4241
  }
4229
- async upsertMerkleNodes(chainId, nodes) {
4242
+ async putChairmanMerkleNodes(chainId, nodes) {
4230
4243
  if (!nodes.length) return;
4231
- await this.ensureMerkleNodesLoaded(chainId);
4244
+ await this.ensureChairmanMerkleNodesLoaded(chainId);
4232
4245
  const key = String(chainId);
4233
- const ids = this.merkleNodeIds[key] ?? /* @__PURE__ */ new Set();
4246
+ const ids = this.chairmanMerkleNodeIds[key] ?? /* @__PURE__ */ new Set();
4234
4247
  const beforeSize = ids.size;
4235
4248
  for (const node of nodes) {
4236
4249
  ids.add(node.id);
4237
4250
  }
4238
- this.merkleNodeIds[key] = ids;
4251
+ this.chairmanMerkleNodeIds[key] = ids;
4239
4252
  await this.enqueueWrite(async () => {
4240
- await Promise.all(nodes.map((node) => this.writeJson(this.sharedRecordKey("merkleNodes", chainId, node.id), { ...node, chainId })));
4253
+ await Promise.all(nodes.map((node) => this.writeJson(this.sharedRecordKey("chairmanMerkleNodes", chainId, node.id), { ...node, chainId })));
4241
4254
  if (ids.size !== beforeSize) {
4242
- await this.writeJson(this.sharedChainMetaKey("merkleNodes", chainId), Array.from(ids));
4255
+ await this.writeJson(this.sharedChainMetaKey("chairmanMerkleNodes", chainId), Array.from(ids));
4243
4256
  }
4244
4257
  });
4245
4258
  }
4246
- async clearMerkleNodes(chainId) {
4247
- await this.ensureMerkleNodesLoaded(chainId);
4248
- const ids = Array.from(this.merkleNodeIds[String(chainId)] ?? []);
4249
- delete this.merkleNodeIds[String(chainId)];
4259
+ async getChairmanMerkleVersion(chainId, version) {
4260
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4261
+ if (!this.chairmanMerkleVersionNums[String(chainId)]?.has(version)) return void 0;
4262
+ const raw = await this.options.client.get(this.sharedRecordKey("chairmanMerkleVersions", chainId, version));
4263
+ const record = this.parseJson(raw, null);
4264
+ if (!record) return void 0;
4265
+ if (typeof record.rootHash !== "string" || !record.rootHash.startsWith("0x")) return void 0;
4266
+ if (typeof record.rootId !== "string") return void 0;
4267
+ const v = Number(record.version);
4268
+ if (!Number.isFinite(v) || v < 0) return void 0;
4269
+ return { chainId, version: Math.floor(v), rootId: record.rootId, rootHash: record.rootHash };
4270
+ }
4271
+ async getLatestChairmanMerkleVersion(chainId) {
4272
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4273
+ const nums = this.chairmanMerkleVersionNums[String(chainId)];
4274
+ if (!nums || nums.size === 0) return void 0;
4275
+ const maxVersion = Math.max(...nums);
4276
+ return this.getChairmanMerkleVersion(chainId, maxVersion);
4277
+ }
4278
+ async putChairmanMerkleVersion(chainId, record) {
4279
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4280
+ const key = String(chainId);
4281
+ const nums = this.chairmanMerkleVersionNums[key] ?? /* @__PURE__ */ new Set();
4282
+ const beforeSize = nums.size;
4283
+ nums.add(record.version);
4284
+ this.chairmanMerkleVersionNums[key] = nums;
4285
+ const current = this.chairmanMerkleLatestVersions[key];
4286
+ if (!current || record.version >= current.version) {
4287
+ this.chairmanMerkleLatestVersions[key] = { ...record, chainId };
4288
+ }
4289
+ const row = { ...record, chainId };
4250
4290
  await this.enqueueWrite(async () => {
4251
- await Promise.all(ids.map((id) => this.deleteOrReset(this.sharedRecordKey("merkleNodes", chainId, id), null)));
4252
- await this.deleteOrReset(this.sharedChainMetaKey("merkleNodes", chainId), []);
4291
+ await this.writeJson(this.sharedRecordKey("chairmanMerkleVersions", chainId, record.version), row);
4292
+ if (nums.size !== beforeSize) {
4293
+ await this.writeJson(this.sharedChainMetaKey("chairmanMerkleVersions", chainId), Array.from(nums).sort((a, b) => a - b));
4294
+ }
4253
4295
  });
4254
4296
  }
4255
- async getMerkleTree(chainId) {
4256
- await this.ensureMerkleTreeLoaded(chainId);
4257
- const row = this.merkleTrees[String(chainId)];
4258
- if (!row) return void 0;
4259
- const totalElements = Number(row.totalElements);
4260
- const lastUpdated = Number(row.lastUpdated);
4261
- const root = row.root;
4262
- if (typeof root !== "string" || !root.startsWith("0x")) return void 0;
4263
- if (!Number.isFinite(totalElements) || totalElements < 0) return void 0;
4264
- return { chainId, root, totalElements: Math.floor(totalElements), lastUpdated: Number.isFinite(lastUpdated) ? Math.floor(lastUpdated) : 0 };
4265
- }
4266
- async setMerkleTree(chainId, tree) {
4267
- await this.ensureMerkleTreeLoaded(chainId);
4268
- const row = { ...tree, chainId };
4269
- this.merkleTrees[String(chainId)] = row;
4270
- await this.enqueueWrite(() => this.writeJson(this.sharedChainKey("merkleTrees", chainId), row));
4271
- }
4272
- async clearMerkleTree(chainId) {
4273
- await this.ensureMerkleTreeLoaded(chainId);
4274
- delete this.merkleTrees[String(chainId)];
4297
+ async clearChairmanMerkleTree(chainId) {
4298
+ await this.ensureChairmanMerkleNodesLoaded(chainId);
4299
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4300
+ const nodeIds = Array.from(this.chairmanMerkleNodeIds[String(chainId)] ?? []);
4301
+ const versionNums = Array.from(this.chairmanMerkleVersionNums[String(chainId)] ?? []);
4302
+ delete this.chairmanMerkleNodeIds[String(chainId)];
4303
+ delete this.chairmanMerkleVersionNums[String(chainId)];
4304
+ delete this.chairmanMerkleLatestVersions[String(chainId)];
4275
4305
  await this.enqueueWrite(async () => {
4276
- await this.deleteOrReset(this.sharedChainKey("merkleTrees", chainId), null);
4306
+ await Promise.all([
4307
+ ...nodeIds.map((id) => this.deleteOrReset(this.sharedRecordKey("chairmanMerkleNodes", chainId, id), null)),
4308
+ ...versionNums.map((v) => this.deleteOrReset(this.sharedRecordKey("chairmanMerkleVersions", chainId, v), null))
4309
+ ]);
4310
+ await Promise.all([
4311
+ this.deleteOrReset(this.sharedChainMetaKey("chairmanMerkleNodes", chainId), []),
4312
+ this.deleteOrReset(this.sharedChainMetaKey("chairmanMerkleVersions", chainId), [])
4313
+ ]);
4277
4314
  });
4278
4315
  }
4279
4316
  async upsertEntryMemos(memos) {
@@ -7117,6 +7154,7 @@ var MerkleEngine = class _MerkleEngine {
7117
7154
  this.hydrateInFlight = /* @__PURE__ */ new Map();
7118
7155
  this.mode = options?.mode ?? "hybrid";
7119
7156
  this.treeDepth = Math.max(1, Math.floor(options?.treeDepth ?? TREE_DEPTH_DEFAULT));
7157
+ this.readContractRoot = options?.readContractRoot;
7120
7158
  }
7121
7159
  /**
7122
7160
  * Compute the current merkle root index from total elements.
@@ -7125,9 +7163,6 @@ var MerkleEngine = class _MerkleEngine {
7125
7163
  if (totalElements <= tempArraySize) return 0;
7126
7164
  return Math.floor((totalElements - 1) / tempArraySize);
7127
7165
  }
7128
- /**
7129
- * Get or initialize the pending leaf buffer for a chain.
7130
- */
7131
7166
  ensurePendingLeaves(chainId) {
7132
7167
  let pending = this.pendingLeavesByChain.get(chainId);
7133
7168
  if (!pending) {
@@ -7136,9 +7171,6 @@ var MerkleEngine = class _MerkleEngine {
7136
7171
  }
7137
7172
  return pending;
7138
7173
  }
7139
- /**
7140
- * Get or initialize chain-level merkle state.
7141
- */
7142
7174
  ensureChainState(chainId) {
7143
7175
  let state = this.chainStateByChain.get(chainId);
7144
7176
  if (!state) {
@@ -7147,15 +7179,32 @@ var MerkleEngine = class _MerkleEngine {
7147
7179
  }
7148
7180
  return state;
7149
7181
  }
7150
- /**
7151
- * Poseidon2 merkle hash for a left/right pair.
7152
- */
7182
+ // ── Hashing ──
7153
7183
  static hashPair(left, right) {
7154
7184
  return Poseidon2.hashToHex(BigInt(left), BigInt(right), Poseidon2Domain.Merkle);
7155
7185
  }
7186
+ static normalizeHex32(value, name) {
7187
+ try {
7188
+ const bi = BigInt(value);
7189
+ if (bi < 0n) throw new Error("negative");
7190
+ const hex = bi.toString(16).padStart(64, "0");
7191
+ if (hex.length > 64) throw new Error("too_large");
7192
+ return `0x${hex}`;
7193
+ } catch (error) {
7194
+ throw new SdkError("MERKLE", `Invalid ${name}`, { value }, error);
7195
+ }
7196
+ }
7197
+ // ── Static helpers ──
7198
+ static totalElementsInTree(totalElements, tempArraySize = TEMP_ARRAY_SIZE_DEFAULT) {
7199
+ if (tempArraySize <= 0) throw new SdkError("MERKLE", "tempArraySize must be greater than zero", { tempArraySize });
7200
+ if (totalElements <= 0n) return 0;
7201
+ const size = BigInt(tempArraySize);
7202
+ return Number((totalElements - 1n) / size * size);
7203
+ }
7204
+ // ── Subtree (levels 0-5, 32 leaves → 1 root) ──
7156
7205
  /**
7157
7206
  * Build a fixed-depth subtree from 32 contiguous leaves.
7158
- * Returns the subtree root and all intermediate nodes for storage.
7207
+ * Returns the subtree root hash and all intermediate nodes for storage.
7159
7208
  */
7160
7209
  static buildSubtree(leafCommitments, baseIndex) {
7161
7210
  if (leafCommitments.length !== SUBTREE_SIZE) {
@@ -7176,71 +7225,71 @@ var MerkleEngine = class _MerkleEngine {
7176
7225
  const position = basePos + i;
7177
7226
  nodesToStore.push({
7178
7227
  chainId: 0,
7179
- id: `${level}-${position}`,
7180
- level,
7181
- position,
7182
- hash: next[i]
7228
+ id: `st-${level}-${position}`,
7229
+ hash: next[i],
7230
+ leftId: null,
7231
+ rightId: null
7183
7232
  });
7184
7233
  }
7185
7234
  currentLevel = next;
7186
7235
  }
7187
7236
  return { subtreeRoot: currentLevel[0], nodesToStore };
7188
7237
  }
7238
+ // ── ChairmanMerkle tree (persistent segment tree, levels 5-32) ──
7189
7239
  /**
7190
- * Fetch a node hash from storage if available.
7191
- */
7192
- async getNodeHash(chainId, id) {
7193
- const node = await this.storage?.getMerkleNode?.(chainId, id);
7194
- return node?.hash;
7195
- }
7196
- /**
7197
- * Merge a completed subtree root into the main tree, updating frontier nodes.
7198
- */
7199
- async mergeSubtreeToMainTree(input) {
7200
- let currentValue = input.subtreeRoot;
7201
- let frontierUpdated = false;
7202
- const nodesToStore = [];
7203
- for (let level = SUBTREE_DEPTH; level < this.treeDepth; level++) {
7204
- const nodeIndex = input.newTotalElements - 1 >> level;
7205
- if ((nodeIndex & 1) === 0) {
7206
- if (!frontierUpdated) {
7207
- nodesToStore.push({
7208
- chainId: input.chainId,
7209
- id: `frontier-${level}`,
7210
- level,
7211
- position: nodeIndex,
7212
- hash: currentValue
7213
- });
7214
- frontierUpdated = true;
7240
+ * Insert a subtree root into the persistent main tree.
7241
+ *
7242
+ * Top-down recursive: descends from root (level treeDepth) to the target
7243
+ * leaf position (level SUBTREE_DEPTH). At each level only the node on the
7244
+ * update path is newly created; the sibling is shared from the previous
7245
+ * version's tree.
7246
+ *
7247
+ * @returns new root node ID/hash and all newly created nodes.
7248
+ */
7249
+ async insertSubtreeRoot(chainId, prevRootId, subtreeRootHash, batchIndex, version) {
7250
+ const MAIN_DEPTH = this.treeDepth - SUBTREE_DEPTH;
7251
+ const nodes = [];
7252
+ const descend = async (nodeId, depth) => {
7253
+ const originalLevel = this.treeDepth - depth;
7254
+ if (depth === MAIN_DEPTH) {
7255
+ const newId2 = `cm-${version}-${originalLevel}`;
7256
+ nodes.push({ chainId, id: newId2, hash: subtreeRootHash, leftId: null, rightId: null });
7257
+ return { id: newId2, hash: subtreeRootHash };
7258
+ }
7259
+ let prevLeftId = null;
7260
+ let prevRightId = null;
7261
+ if (nodeId) {
7262
+ const prevNode = await this.storage?.getChairmanMerkleNode?.(chainId, nodeId);
7263
+ if (prevNode) {
7264
+ prevLeftId = prevNode.leftId;
7265
+ prevRightId = prevNode.rightId;
7215
7266
  }
7216
- currentValue = _MerkleEngine.hashPair(currentValue, getZeroHash(level));
7217
- } else {
7218
- const leftHash = await this.getNodeHash(input.chainId, `frontier-${level}`) ?? getZeroHash(level);
7219
- currentValue = _MerkleEngine.hashPair(leftHash, currentValue);
7220
7267
  }
7221
- const nextLevel = level + 1;
7222
- nodesToStore.push({
7223
- chainId: input.chainId,
7224
- id: `${nextLevel}-${nodeIndex >> 1}`,
7225
- level: nextLevel,
7226
- position: nodeIndex >> 1,
7227
- hash: currentValue
7228
- });
7229
- }
7230
- return { finalRoot: currentValue, nodesToStore };
7231
- }
7232
- /**
7233
- * Convert on-chain totalElements to the count of fully merged elements.
7234
- */
7235
- static totalElementsInTree(totalElements, tempArraySize = TEMP_ARRAY_SIZE_DEFAULT) {
7236
- if (tempArraySize <= 0) throw new SdkError("MERKLE", "tempArraySize must be greater than zero", { tempArraySize });
7237
- if (totalElements <= 0n) return 0;
7238
- const size = BigInt(tempArraySize);
7239
- return Number((totalElements - 1n) / size * size);
7268
+ const childLevel = originalLevel - 1;
7269
+ const remainingDepth = MAIN_DEPTH - depth - 1;
7270
+ const goRight = (batchIndex >> remainingDepth & 1) === 1;
7271
+ let leftResult;
7272
+ let rightResult;
7273
+ if (goRight) {
7274
+ const leftHash = prevLeftId ? (await this.storage?.getChairmanMerkleNode?.(chainId, prevLeftId))?.hash ?? getZeroHash(childLevel) : getZeroHash(childLevel);
7275
+ leftResult = { id: prevLeftId, hash: leftHash };
7276
+ const right = await descend(prevRightId, depth + 1);
7277
+ rightResult = { id: right.id, hash: right.hash };
7278
+ } else {
7279
+ const left = await descend(prevLeftId, depth + 1);
7280
+ leftResult = { id: left.id, hash: left.hash };
7281
+ const rightHash = prevRightId ? (await this.storage?.getChairmanMerkleNode?.(chainId, prevRightId))?.hash ?? getZeroHash(childLevel) : getZeroHash(childLevel);
7282
+ rightResult = { id: prevRightId, hash: rightHash };
7283
+ }
7284
+ const hash = _MerkleEngine.hashPair(leftResult.hash, rightResult.hash);
7285
+ const newId = `cm-${version}-${originalLevel}`;
7286
+ nodes.push({ chainId, id: newId, hash, leftId: leftResult.id, rightId: rightResult.id });
7287
+ return { id: newId, hash };
7288
+ };
7289
+ const root = await descend(prevRootId, 0);
7290
+ return { rootId: root.id, rootHash: root.hash, nodes };
7240
7291
  }
7241
- /**
7242
- * Hydrate local merkle state from storage on first use.
7243
- */
7292
+ // ── Hydration ──
7244
7293
  async hydrateFromStorage(chainId) {
7245
7294
  if (this.mode === "remote") return;
7246
7295
  if (this.hydratedChains.has(chainId)) return;
@@ -7249,31 +7298,17 @@ var MerkleEngine = class _MerkleEngine {
7249
7298
  const task = (async () => {
7250
7299
  try {
7251
7300
  const state = this.ensureChainState(chainId);
7252
- const leaves = await this.storage?.getMerkleLeaves?.(chainId);
7253
- if (!leaves || leaves.length === 0) return;
7254
- const sorted = [...leaves].map((l) => ({
7255
- cid: l.cid,
7256
- commitment: _MerkleEngine.normalizeHex32(l.commitment, "memo.commitment")
7257
- })).sort((a, b) => a.cid - b.cid);
7258
- for (let i = 0; i < sorted.length; i++) {
7259
- if (sorted[i].cid !== i) throw new Error(`Non-contiguous persisted merkle leaves: expected cid=${i}, got cid=${sorted[i].cid}`);
7260
- }
7261
- const storedTree = await this.storage?.getMerkleTree?.(chainId);
7262
- const leafCount = sorted.length;
7263
- const mergedFromLeaves = _MerkleEngine.totalElementsInTree(BigInt(leafCount));
7264
- const mergedFromTree = typeof storedTree?.totalElements === "number" && storedTree.totalElements > 0 ? storedTree.totalElements : 0;
7265
- const mergedElements = Math.max(mergedFromLeaves, mergedFromTree);
7266
7301
  const pending = this.ensurePendingLeaves(chainId);
7267
- pending.length = 0;
7268
- state.mergedElements = mergedElements;
7269
- if (leafCount > mergedElements) {
7270
- pending.push(...sorted.slice(mergedElements).map((l) => l.commitment));
7302
+ const latest = await this.storage?.getLatestChairmanMerkleVersion?.(chainId);
7303
+ if (latest) {
7304
+ state.mergedElements = latest.version;
7305
+ state.root = _MerkleEngine.normalizeHex32(latest.rootHash, "chairmanMerkleVersion.rootHash");
7271
7306
  }
7272
- if (storedTree?.root) {
7273
- state.root = _MerkleEngine.normalizeHex32(storedTree.root, "merkleTree.root");
7274
- } else {
7275
- const rootNode = await this.storage?.getMerkleNode?.(chainId, `${this.treeDepth}-0`);
7276
- state.root = rootNode?.hash ?? getZeroHash(this.treeDepth);
7307
+ const leaves = await this.storage?.getMerkleLeaves?.(chainId);
7308
+ if (leaves && leaves.length > state.mergedElements) {
7309
+ const sorted = [...leaves].sort((a, b) => a.cid - b.cid).slice(state.mergedElements);
7310
+ pending.length = 0;
7311
+ pending.push(...sorted.map((l) => _MerkleEngine.normalizeHex32(l.commitment, "leaf.commitment")));
7277
7312
  }
7278
7313
  } catch (error) {
7279
7314
  if (this.mode === "hybrid") return;
@@ -7286,26 +7321,7 @@ var MerkleEngine = class _MerkleEngine {
7286
7321
  this.hydrateInFlight.set(chainId, task);
7287
7322
  return task;
7288
7323
  }
7289
- /**
7290
- * Normalize unknown values to a 32-byte hex string.
7291
- */
7292
- static normalizeHex32(value, name) {
7293
- try {
7294
- const bi = BigInt(value);
7295
- if (bi < 0n) throw new Error("negative");
7296
- const hex = bi.toString(16).padStart(64, "0");
7297
- if (hex.length > 64) throw new Error("too_large");
7298
- return `0x${hex}`;
7299
- } catch (error) {
7300
- throw new SdkError("MERKLE", `Invalid ${name}`, { value }, error);
7301
- }
7302
- }
7303
- /**
7304
- * Feed contiguous (cid-ordered) memo leaves into the local merkle tree.
7305
- *
7306
- * This mirrors the client/app behavior: only after we have a full consecutive batch of 32 leaves
7307
- * do we merge them into the main tree. Leaves that are still in the buffer do not get local proofs.
7308
- */
7324
+ // ── Ingestion ──
7309
7325
  async ingestEntryMemos(chainId, memos) {
7310
7326
  if (this.mode === "remote") return;
7311
7327
  await this.hydrateFromStorage(chainId);
@@ -7333,26 +7349,42 @@ var MerkleEngine = class _MerkleEngine {
7333
7349
  expected++;
7334
7350
  while (pending.length >= SUBTREE_SIZE) {
7335
7351
  const batch = pending.splice(0, SUBTREE_SIZE);
7336
- const baseIndex = state.mergedElements;
7337
- const subtree = _MerkleEngine.buildSubtree(batch, baseIndex);
7338
- const merged = await this.mergeSubtreeToMainTree({ chainId, subtreeRoot: subtree.subtreeRoot, newTotalElements: baseIndex + SUBTREE_SIZE });
7339
- state.mergedElements += SUBTREE_SIZE;
7340
- state.root = merged.finalRoot;
7341
- const checkpointNode = {
7342
- chainId,
7343
- id: `checkpoint-${state.mergedElements}`,
7344
- level: -1,
7345
- position: 0,
7346
- hash: state.root
7347
- };
7348
- const nodes = [...subtree.nodesToStore, ...merged.nodesToStore, checkpointNode].map((n) => ({ ...n, chainId }));
7349
- await this.storage?.upsertMerkleNodes?.(chainId, nodes);
7350
- await this.storage?.setMerkleTree?.(chainId, {
7352
+ const batchIndex = state.mergedElements / SUBTREE_SIZE;
7353
+ const subtree = _MerkleEngine.buildSubtree(batch, state.mergedElements);
7354
+ const subtreeNodes = subtree.nodesToStore.map((n) => ({ ...n, chainId }));
7355
+ const prevVersion = await this.storage?.getLatestChairmanMerkleVersion?.(chainId);
7356
+ const prevRootId = prevVersion?.rootId ?? null;
7357
+ const newVersion = state.mergedElements + SUBTREE_SIZE;
7358
+ const result = await this.insertSubtreeRoot(chainId, prevRootId, subtree.subtreeRoot, batchIndex, newVersion);
7359
+ if (this.readContractRoot) {
7360
+ const rootIndex = newVersion / SUBTREE_SIZE;
7361
+ const onChainRoot = await this.readContractRoot(chainId, rootIndex).catch(() => null);
7362
+ if (onChainRoot !== null) {
7363
+ const onChainNorm = _MerkleEngine.normalizeHex32(onChainRoot, "onChainRoot");
7364
+ const isZero = BigInt(onChainNorm) === 0n;
7365
+ if (!isZero && onChainNorm !== result.rootHash) {
7366
+ const target = state.mergedElements;
7367
+ await this.rollback(chainId, target);
7368
+ throw new SdkError("MERKLE", "Local merkle root mismatch with on-chain root \u2014 rolled back", {
7369
+ chainId,
7370
+ rootIndex,
7371
+ localRoot: result.rootHash,
7372
+ onChainRoot: onChainNorm,
7373
+ version: newVersion,
7374
+ rollbackTarget: target
7375
+ });
7376
+ }
7377
+ }
7378
+ }
7379
+ await this.storage?.putChairmanMerkleNodes?.(chainId, [...subtreeNodes, ...result.nodes]);
7380
+ await this.storage?.putChairmanMerkleVersion?.(chainId, {
7351
7381
  chainId,
7352
- root: state.root,
7353
- totalElements: state.mergedElements,
7354
- lastUpdated: Date.now()
7382
+ version: newVersion,
7383
+ rootId: result.rootId,
7384
+ rootHash: result.rootHash
7355
7385
  });
7386
+ state.mergedElements = newVersion;
7387
+ state.root = result.rootHash;
7356
7388
  }
7357
7389
  }
7358
7390
  this.hydratedChains.add(chainId);
@@ -7363,20 +7395,23 @@ var MerkleEngine = class _MerkleEngine {
7363
7395
  throw new SdkError("MERKLE", "Failed to ingest local merkle leaves", { chainId, leafCount: leaves.length }, error);
7364
7396
  }
7365
7397
  }
7398
+ // ── Rollback (tree O(1) + sync cursor reset) ──
7366
7399
  /**
7367
- * Roll back the local Merkle tree to a previous checkpoint.
7400
+ * Unified rollback: rewind tree to a previous batch boundary AND reset the
7401
+ * sync cursor so memo sync restarts from the same point.
7368
7402
  *
7369
- * Reconstructs correct frontier state by replaying merges from stored subtree
7370
- * roots (level-5 nodes). Cost: O(batches × depth) where batches = target / 32.
7403
+ * What gets rolled back:
7404
+ * - ChairmanMerkle tree version pointer (O(1) old nodes still in storage)
7405
+ * - Pending leaves buffer (cleared)
7406
+ * - Sync cursor: memo + merkle fields (nullifier left unchanged — independent)
7371
7407
  *
7372
- * @param targetMergedElements Must be a non-negative multiple of 32 (SUBTREE_SIZE).
7408
+ * @param targetMergedElements Must be a non-negative multiple of 32.
7373
7409
  * Pass 0 to reset to the empty tree.
7374
- * @returns true if rollback succeeded, false if required data (checkpoint or
7375
- * subtree roots) is missing in storage.
7410
+ * @returns true if rollback succeeded, false if the target version doesn't exist.
7376
7411
  */
7377
- async rollbackTree(chainId, targetMergedElements) {
7412
+ async rollback(chainId, targetMergedElements) {
7378
7413
  if (targetMergedElements < 0 || targetMergedElements % SUBTREE_SIZE !== 0) {
7379
- throw new SdkError("MERKLE", "rollbackTree target must be a non-negative multiple of 32", { targetMergedElements });
7414
+ throw new SdkError("MERKLE", "rollback target must be a non-negative multiple of 32", { targetMergedElements });
7380
7415
  }
7381
7416
  const state = this.ensureChainState(chainId);
7382
7417
  const pending = this.ensurePendingLeaves(chainId);
@@ -7384,78 +7419,35 @@ var MerkleEngine = class _MerkleEngine {
7384
7419
  state.mergedElements = 0;
7385
7420
  state.root = getZeroHash(this.treeDepth);
7386
7421
  pending.length = 0;
7387
- await this.storage?.setMerkleTree?.(chainId, {
7388
- chainId,
7389
- root: state.root,
7390
- totalElements: 0,
7391
- lastUpdated: Date.now()
7392
- });
7422
+ await this.resetSyncCursor(chainId, 0);
7393
7423
  return true;
7394
7424
  }
7395
- const checkpoint = await this.storage?.getMerkleNode?.(chainId, `checkpoint-${targetMergedElements}`);
7396
- if (!checkpoint) return false;
7397
- const numBatches = targetMergedElements / SUBTREE_SIZE;
7398
- const subtreeRoots = [];
7399
- for (let batch = 0; batch < numBatches; batch++) {
7400
- const node = await this.storage?.getMerkleNode?.(chainId, `${SUBTREE_DEPTH}-${batch}`);
7401
- if (!node) return false;
7402
- subtreeRoots.push(node.hash);
7403
- }
7404
- const resetNodes = [];
7405
- for (let level = SUBTREE_DEPTH; level < this.treeDepth; level++) {
7406
- resetNodes.push({
7407
- chainId,
7408
- id: `frontier-${level}`,
7409
- level,
7410
- position: 0,
7411
- hash: getZeroHash(level)
7412
- });
7413
- }
7414
- await this.storage?.upsertMerkleNodes?.(chainId, resetNodes);
7415
- let replayRoot = getZeroHash(this.treeDepth);
7416
- for (let batch = 0; batch < numBatches; batch++) {
7417
- const merged = await this.mergeSubtreeToMainTree({
7418
- chainId,
7419
- subtreeRoot: subtreeRoots[batch],
7420
- newTotalElements: (batch + 1) * SUBTREE_SIZE
7421
- });
7422
- replayRoot = merged.finalRoot;
7423
- await this.storage?.upsertMerkleNodes?.(chainId, merged.nodesToStore.map((n) => ({ ...n, chainId })));
7424
- }
7425
- const replayNorm = _MerkleEngine.normalizeHex32(replayRoot, "replay.root");
7426
- const checkpointNorm = _MerkleEngine.normalizeHex32(checkpoint.hash, "checkpoint.root");
7427
- if (replayNorm !== checkpointNorm) {
7428
- return false;
7429
- }
7425
+ const version = await this.storage?.getChairmanMerkleVersion?.(chainId, targetMergedElements);
7426
+ if (!version) return false;
7430
7427
  state.mergedElements = targetMergedElements;
7431
- state.root = checkpointNorm;
7428
+ state.root = _MerkleEngine.normalizeHex32(version.rootHash, "version.rootHash");
7432
7429
  pending.length = 0;
7433
7430
  this.hydratedChains.add(chainId);
7434
- await this.storage?.setMerkleTree?.(chainId, {
7435
- chainId,
7436
- root: state.root,
7437
- totalElements: targetMergedElements,
7438
- lastUpdated: Date.now()
7439
- });
7440
- const updatedCheckpoint = {
7441
- chainId,
7442
- id: `checkpoint-${targetMergedElements}`,
7443
- level: -1,
7444
- position: 0,
7445
- hash: checkpointNorm
7446
- };
7447
- await this.storage?.upsertMerkleNodes?.(chainId, [updatedCheckpoint]);
7431
+ await this.resetSyncCursor(chainId, targetMergedElements);
7448
7432
  return true;
7449
7433
  }
7450
7434
  /**
7451
- * Convenience wrapper to request a single proof.
7435
+ * Reset the sync cursor's memo field to `targetMemo` (and derive merkle cursor),
7436
+ * but only if the current cursor is ahead of the target.
7437
+ * Nullifier cursor is left unchanged — nullifiers are independent of tree state.
7452
7438
  */
7439
+ async resetSyncCursor(chainId, targetMemo) {
7440
+ if (!this.storage?.getSyncCursor || !this.storage?.setSyncCursor) return;
7441
+ const cursor = await this.storage.getSyncCursor(chainId);
7442
+ if (!cursor || cursor.memo <= targetMemo) return;
7443
+ cursor.memo = targetMemo;
7444
+ cursor.merkle = this.currentMerkleRootIndex(targetMemo);
7445
+ await this.storage.setSyncCursor(chainId, cursor);
7446
+ }
7447
+ // ── Proof generation ──
7453
7448
  async getProofByCid(input) {
7454
7449
  return this.getProofByCids({ chainId: input.chainId, cids: [input.cid], totalElements: input.totalElements });
7455
7450
  }
7456
- /**
7457
- * Get merkle proofs for a set of cids using local/hybrid/remote logic.
7458
- */
7459
7451
  async getProofByCids(input) {
7460
7452
  const cids = [...input.cids];
7461
7453
  if (cids.length === 0) throw new SdkError("MERKLE", "No cids provided", { chainId: input.chainId });
@@ -7470,15 +7462,16 @@ var MerkleEngine = class _MerkleEngine {
7470
7462
  await this.hydrateFromStorage(input.chainId);
7471
7463
  const canUseLocal = this.mode !== "remote";
7472
7464
  if (canUseLocal) {
7473
- const tree = await this.storage?.getMerkleTree?.(input.chainId);
7474
- const hasDb = typeof this.storage?.getMerkleLeaf === "function" && typeof this.storage?.getMerkleNode === "function" && typeof tree?.totalElements === "number" && typeof tree?.root === "string";
7475
- if (hasDb && tree) {
7476
- if (tree.totalElements < contractTreeElements) {
7465
+ const version = contractTreeElements > 0 ? await this.storage?.getChairmanMerkleVersion?.(input.chainId, contractTreeElements) : void 0;
7466
+ const hasDb = typeof this.storage?.getMerkleLeaf === "function" && typeof this.storage?.getChairmanMerkleNode === "function" && (contractTreeElements === 0 || !!version);
7467
+ if (hasDb) {
7468
+ const state = this.ensureChainState(input.chainId);
7469
+ if (contractTreeElements > 0 && state.mergedElements < contractTreeElements) {
7477
7470
  if (this.mode === "local") {
7478
7471
  throw new SdkError("MERKLE", "Local merkle db is behind contract", {
7479
7472
  chainId: input.chainId,
7480
7473
  cids,
7481
- localTotalElements: tree.totalElements,
7474
+ localMergedElements: state.mergedElements,
7482
7475
  contractTreeElements
7483
7476
  });
7484
7477
  }
@@ -7490,30 +7483,13 @@ var MerkleEngine = class _MerkleEngine {
7490
7483
  proof.push({ leaf_index: cid, path: new Array(this.treeDepth + 1).fill("0") });
7491
7484
  continue;
7492
7485
  }
7493
- const leaf = await this.storage.getMerkleLeaf(input.chainId, cid);
7494
- if (!leaf) throw new Error(`missing_leaf:${cid}`);
7495
- const path2 = [leaf.commitment];
7496
- for (let level = 1; level <= this.treeDepth; level++) {
7497
- const siblingIndex = cid >> level - 1 ^ 1;
7498
- if (level === 1) {
7499
- const siblingLeaf = await this.storage.getMerkleLeaf(input.chainId, siblingIndex);
7500
- path2.push(siblingLeaf?.commitment ?? getZeroHash(0));
7501
- continue;
7502
- }
7503
- const targetLevel = level - 1;
7504
- const siblingNode = await this.storage.getMerkleNode(input.chainId, `${targetLevel}-${siblingIndex}`);
7505
- path2.push(siblingNode?.hash ?? getZeroHash(targetLevel));
7506
- }
7486
+ const path2 = await this.buildLocalProofPath(input.chainId, cid, version);
7507
7487
  proof.push({ leaf_index: cid, path: path2 });
7508
7488
  }
7509
- let effectiveRoot = tree.root;
7510
- if (tree.totalElements > contractTreeElements && contractTreeElements > 0) {
7511
- const checkpoint = await this.storage.getMerkleNode(input.chainId, `checkpoint-${contractTreeElements}`);
7512
- if (checkpoint) effectiveRoot = checkpoint.hash;
7513
- }
7489
+ const effectiveRoot = contractTreeElements > 0 ? _MerkleEngine.normalizeHex32(version.rootHash, "version.rootHash") : getZeroHash(this.treeDepth);
7514
7490
  return {
7515
7491
  proof,
7516
- merkle_root: _MerkleEngine.normalizeHex32(effectiveRoot, "merkleTree.root"),
7492
+ merkle_root: effectiveRoot,
7517
7493
  latest_cid: totalElements > 0n ? Number(totalElements - 1n) : -1
7518
7494
  };
7519
7495
  } catch (error) {
@@ -7523,7 +7499,7 @@ var MerkleEngine = class _MerkleEngine {
7523
7499
  }
7524
7500
  }
7525
7501
  } else if (this.mode === "local" && needsTreeProof.length) {
7526
- throw new SdkError("MERKLE", "Local merkle db unavailable", { chainId: input.chainId, cids, reason: "missing_adapter_merkle_db" });
7502
+ throw new SdkError("MERKLE", "Local merkle db unavailable", { chainId: input.chainId, cids, reason: "missing_adapter_or_version" });
7527
7503
  }
7528
7504
  }
7529
7505
  if (needsTreeProof.length === 0) {
@@ -7548,15 +7524,64 @@ var MerkleEngine = class _MerkleEngine {
7548
7524
  };
7549
7525
  }
7550
7526
  /**
7551
- * Fetch a remote merkle root (used when no proofs are needed).
7527
+ * Build a local proof path by traversing the chairmanMerkle tree.
7528
+ *
7529
+ * Levels 0-4: sibling hashes from subtree internal nodes (st-{level}-{pos}).
7530
+ * Levels 5-31: sibling hashes from chairmanMerkle tree traversal (top-down from version root).
7552
7531
  */
7532
+ async buildLocalProofPath(chainId, cid, version) {
7533
+ const leaf = await this.storage.getMerkleLeaf(chainId, cid);
7534
+ if (!leaf) throw new Error(`missing_leaf:${cid}`);
7535
+ const path2 = [leaf.commitment];
7536
+ for (let level = 1; level <= SUBTREE_DEPTH; level++) {
7537
+ const siblingPos = cid >> level - 1 ^ 1;
7538
+ if (level === 1) {
7539
+ const siblingLeaf = await this.storage.getMerkleLeaf(chainId, siblingPos);
7540
+ path2.push(siblingLeaf?.commitment ?? getZeroHash(0));
7541
+ } else {
7542
+ const targetLevel = level - 1;
7543
+ const node = await this.storage.getChairmanMerkleNode(chainId, `st-${targetLevel}-${siblingPos}`);
7544
+ path2.push(node?.hash ?? getZeroHash(targetLevel));
7545
+ }
7546
+ }
7547
+ const batchIndex = cid >> SUBTREE_DEPTH;
7548
+ const MAIN_DEPTH = this.treeDepth - SUBTREE_DEPTH;
7549
+ const mainSiblings = [];
7550
+ let nodeId = version.rootId;
7551
+ for (let depth = 0; depth < MAIN_DEPTH; depth++) {
7552
+ const childLevel = this.treeDepth - depth - 1;
7553
+ if (!nodeId) {
7554
+ mainSiblings.push(getZeroHash(childLevel));
7555
+ continue;
7556
+ }
7557
+ const node = await this.storage.getChairmanMerkleNode(chainId, nodeId);
7558
+ if (!node) {
7559
+ mainSiblings.push(getZeroHash(childLevel));
7560
+ nodeId = null;
7561
+ continue;
7562
+ }
7563
+ const remainingDepth = MAIN_DEPTH - depth - 1;
7564
+ const goRight = (batchIndex >> remainingDepth & 1) === 1;
7565
+ if (goRight) {
7566
+ const leftNode = node.leftId ? await this.storage.getChairmanMerkleNode(chainId, node.leftId) : null;
7567
+ mainSiblings.push(leftNode?.hash ?? getZeroHash(childLevel));
7568
+ nodeId = node.rightId;
7569
+ } else {
7570
+ const rightNode = node.rightId ? await this.storage.getChairmanMerkleNode(chainId, node.rightId) : null;
7571
+ mainSiblings.push(rightNode?.hash ?? getZeroHash(childLevel));
7572
+ nodeId = node.leftId;
7573
+ }
7574
+ }
7575
+ for (let i = mainSiblings.length - 1; i >= 0; i--) {
7576
+ path2.push(mainSiblings[i]);
7577
+ }
7578
+ return path2;
7579
+ }
7580
+ // ── Remote helpers ──
7553
7581
  async fetchRemoteRootOnly(chainId) {
7554
7582
  const remote = await this.fetchRemoteProofFromService({ chainId, cids: [0] });
7555
7583
  return _MerkleEngine.normalizeHex32(remote.merkle_root, "remote.merkle_root");
7556
7584
  }
7557
- /**
7558
- * Fetch proofs from the remote merkle service.
7559
- */
7560
7585
  async fetchRemoteProofFromService(input) {
7561
7586
  const chain = this.getChain(input.chainId);
7562
7587
  if (!chain.merkleProofUrl) {
@@ -7565,9 +7590,7 @@ var MerkleEngine = class _MerkleEngine {
7565
7590
  const client = new MerkleClient(chain.merkleProofUrl);
7566
7591
  return client.getProofByCids(input.cids);
7567
7592
  }
7568
- /**
7569
- * Build membership witnesses for provided UTXOs from a remote proof response.
7570
- */
7593
+ // ── Witness builders (unchanged) ──
7571
7594
  buildAccMemberWitnesses(input) {
7572
7595
  return input.utxos.map((utxo, idx) => {
7573
7596
  const remoteProof = input.remote.proof[idx];
@@ -7581,9 +7604,6 @@ var MerkleEngine = class _MerkleEngine {
7581
7604
  };
7582
7605
  });
7583
7606
  }
7584
- /**
7585
- * Convert UTXOs into circuit input secrets, decrypting memos and padding if needed.
7586
- */
7587
7607
  async buildInputSecretsFromUtxos(input) {
7588
7608
  if (!Array.isArray(input.utxos) || input.utxos.length === 0) {
7589
7609
  throw new SdkError("MERKLE", "No utxos provided", { count: 0 });
@@ -8767,8 +8787,9 @@ var FileStore = class {
8767
8787
  this.cursors = /* @__PURE__ */ new Map();
8768
8788
  this.utxos = /* @__PURE__ */ new Map();
8769
8789
  this.operations = [];
8770
- this.merkleTrees = {};
8771
- this.merkleNodes = {};
8790
+ this.chairmanMerkleLatestVersions = {};
8791
+ this.chairmanMerkleVersions = {};
8792
+ this.chairmanMerkleNodes = {};
8772
8793
  this.entryMemos = {};
8773
8794
  this.entryNullifiers = {};
8774
8795
  this.saveChain = Promise.resolve();
@@ -8872,8 +8893,9 @@ var FileStore = class {
8872
8893
  this.utxos.clear();
8873
8894
  this.operations = [];
8874
8895
  this.merkleNextCid.clear();
8875
- this.merkleTrees = {};
8876
- this.merkleNodes = {};
8896
+ this.chairmanMerkleLatestVersions = {};
8897
+ this.chairmanMerkleVersions = {};
8898
+ this.chairmanMerkleNodes = {};
8877
8899
  this.entryMemos = {};
8878
8900
  this.entryNullifiers = {};
8879
8901
  try {
@@ -8889,13 +8911,24 @@ var FileStore = class {
8889
8911
  try {
8890
8912
  const raw = await (0, import_promises.readFile)(this.sharedFilePath(), "utf8");
8891
8913
  const parsed = JSON.parse(raw);
8892
- const merkleTreesRaw = parsed.merkleTrees;
8893
- if (merkleTreesRaw && typeof merkleTreesRaw === "object") {
8894
- this.merkleTrees = merkleTreesRaw;
8914
+ const chairmanMerkleVersionsRaw = parsed.chairmanMerkleVersions;
8915
+ if (chairmanMerkleVersionsRaw && typeof chairmanMerkleVersionsRaw === "object") {
8916
+ this.chairmanMerkleVersions = chairmanMerkleVersionsRaw;
8917
+ for (const [chainKey, versions] of Object.entries(chairmanMerkleVersionsRaw)) {
8918
+ let latest;
8919
+ for (const record of Object.values(versions)) {
8920
+ if (!latest || record.version > latest.version) {
8921
+ latest = record;
8922
+ }
8923
+ }
8924
+ if (latest) {
8925
+ this.chairmanMerkleLatestVersions[chainKey] = latest;
8926
+ }
8927
+ }
8895
8928
  }
8896
- const merkleNodesRaw = parsed.merkleNodes;
8897
- if (merkleNodesRaw && typeof merkleNodesRaw === "object") {
8898
- this.merkleNodes = merkleNodesRaw;
8929
+ const chairmanMerkleNodesRaw = parsed.chairmanMerkleNodes;
8930
+ if (chairmanMerkleNodesRaw && typeof chairmanMerkleNodesRaw === "object") {
8931
+ this.chairmanMerkleNodes = chairmanMerkleNodesRaw;
8899
8932
  }
8900
8933
  const entryMemosRaw = parsed.entryMemos;
8901
8934
  if (entryMemosRaw && typeof entryMemosRaw === "object") {
@@ -8935,8 +8968,8 @@ var FileStore = class {
8935
8968
  this.saveChain = this.saveChain.catch(() => void 0).then(async () => {
8936
8969
  await (0, import_promises.mkdir)(this.options.baseDir, { recursive: true });
8937
8970
  const sharedState = {
8938
- merkleTrees: this.merkleTrees,
8939
- merkleNodes: this.merkleNodes,
8971
+ chairmanMerkleVersions: this.chairmanMerkleVersions,
8972
+ chairmanMerkleNodes: this.chairmanMerkleNodes,
8940
8973
  entryMemos: this.entryMemos,
8941
8974
  entryNullifiers: this.entryNullifiers
8942
8975
  };
@@ -8948,56 +8981,70 @@ var FileStore = class {
8948
8981
  return this.saveChain;
8949
8982
  }
8950
8983
  /**
8951
- * Get persisted merkle tree metadata for a chain.
8984
+ * Get a chairmanMerkle tree node by id.
8952
8985
  */
8953
- async getMerkleTree(chainId) {
8954
- const row = this.merkleTrees[String(chainId)];
8955
- if (!row) return void 0;
8956
- const totalElements = Number(row.totalElements);
8957
- const lastUpdated = Number(row.lastUpdated);
8958
- const root = row.root;
8959
- if (typeof root !== "string" || !root.startsWith("0x")) return void 0;
8960
- if (!Number.isFinite(totalElements) || totalElements < 0) return void 0;
8961
- return { chainId, root, totalElements: Math.floor(totalElements), lastUpdated: Number.isFinite(lastUpdated) ? Math.floor(lastUpdated) : 0 };
8986
+ async getChairmanMerkleNode(chainId, id) {
8987
+ return this.chairmanMerkleNodes[String(chainId)]?.[id];
8962
8988
  }
8963
8989
  /**
8964
- * Persist merkle tree metadata for a chain.
8990
+ * Put chairmanMerkle tree nodes for a chain and persist.
8965
8991
  */
8966
- async setMerkleTree(chainId, tree) {
8967
- this.merkleTrees[String(chainId)] = { ...tree, chainId };
8992
+ async putChairmanMerkleNodes(chainId, nodes) {
8993
+ if (!nodes.length) return;
8994
+ const key = String(chainId);
8995
+ const existing = this.chairmanMerkleNodes[key] ?? {};
8996
+ for (const node of nodes) {
8997
+ existing[node.id] = { ...node, chainId };
8998
+ }
8999
+ this.chairmanMerkleNodes[key] = existing;
8968
9000
  await this.saveShared();
8969
9001
  }
8970
9002
  /**
8971
- * Clear merkle tree metadata for a chain.
9003
+ * Get a specific chairmanMerkle tree version for a chain.
8972
9004
  */
8973
- async clearMerkleTree(chainId) {
8974
- delete this.merkleTrees[String(chainId)];
8975
- await this.saveShared();
9005
+ async getChairmanMerkleVersion(chainId, version) {
9006
+ return this.chairmanMerkleVersions[String(chainId)]?.[version];
8976
9007
  }
8977
9008
  /**
8978
- * Get a merkle node by id.
9009
+ * Get the latest (highest version number) chairmanMerkle tree version for a chain.
8979
9010
  */
8980
- async getMerkleNode(chainId, id) {
8981
- return this.merkleNodes[String(chainId)]?.[id];
9011
+ async getLatestChairmanMerkleVersion(chainId) {
9012
+ const cached = this.chairmanMerkleLatestVersions[String(chainId)];
9013
+ if (cached) return { ...cached };
9014
+ const versions = this.chairmanMerkleVersions[String(chainId)];
9015
+ if (!versions) return void 0;
9016
+ let latest;
9017
+ for (const record of Object.values(versions)) {
9018
+ if (!latest || record.version > latest.version) {
9019
+ latest = record;
9020
+ }
9021
+ }
9022
+ if (latest) {
9023
+ this.chairmanMerkleLatestVersions[String(chainId)] = latest;
9024
+ }
9025
+ return latest ? { ...latest } : void 0;
8982
9026
  }
8983
9027
  /**
8984
- * Upsert merkle nodes for a chain and persist.
9028
+ * Persist a chairmanMerkle tree version for a chain and track latest.
8985
9029
  */
8986
- async upsertMerkleNodes(chainId, nodes) {
8987
- if (!nodes.length) return;
9030
+ async putChairmanMerkleVersion(chainId, record) {
8988
9031
  const key = String(chainId);
8989
- const existing = this.merkleNodes[key] ?? {};
8990
- for (const node of nodes) {
8991
- existing[node.id] = { ...node, chainId };
9032
+ const existing = this.chairmanMerkleVersions[key] ?? {};
9033
+ existing[record.version] = { ...record, chainId };
9034
+ this.chairmanMerkleVersions[key] = existing;
9035
+ const currentLatest = this.chairmanMerkleLatestVersions[key];
9036
+ if (!currentLatest || record.version >= currentLatest.version) {
9037
+ this.chairmanMerkleLatestVersions[key] = { ...record, chainId };
8992
9038
  }
8993
- this.merkleNodes[key] = existing;
8994
9039
  await this.saveShared();
8995
9040
  }
8996
9041
  /**
8997
- * Clear merkle nodes for a chain.
9042
+ * Clear chairmanMerkle tree nodes and versions for a chain.
8998
9043
  */
8999
- async clearMerkleNodes(chainId) {
9000
- delete this.merkleNodes[String(chainId)];
9044
+ async clearChairmanMerkleTree(chainId) {
9045
+ delete this.chairmanMerkleNodes[String(chainId)];
9046
+ delete this.chairmanMerkleVersions[String(chainId)];
9047
+ delete this.chairmanMerkleLatestVersions[String(chainId)];
9001
9048
  await this.saveShared();
9002
9049
  }
9003
9050
  /**
@@ -9277,10 +9324,6 @@ function toChanges(result) {
9277
9324
  if (typeof changes === "bigint") return Number(changes);
9278
9325
  return 0;
9279
9326
  }
9280
- function toHexOrNull(value) {
9281
- if (typeof value !== "string") return null;
9282
- return value.startsWith("0x") ? value : null;
9283
- }
9284
9327
  function sortTextValues(value) {
9285
9328
  if (value == null) return void 0;
9286
9329
  const list = Array.isArray(value) ? value : [value];
@@ -9420,21 +9463,21 @@ var SqliteStore = class {
9420
9463
  PRIMARY KEY (chain_id, cid)
9421
9464
  );
9422
9465
 
9423
- CREATE TABLE IF NOT EXISTS merkle_nodes (
9466
+ CREATE TABLE IF NOT EXISTS chairman_merkle_nodes (
9424
9467
  chain_id INTEGER NOT NULL,
9425
9468
  id TEXT NOT NULL,
9426
- level INTEGER NOT NULL,
9427
- position INTEGER NOT NULL,
9428
9469
  hash TEXT NOT NULL,
9470
+ left_id TEXT,
9471
+ right_id TEXT,
9429
9472
  PRIMARY KEY (chain_id, id)
9430
9473
  );
9431
- CREATE INDEX IF NOT EXISTS idx_merkle_nodes_chain_level_pos ON merkle_nodes(chain_id, level, position);
9432
9474
 
9433
- CREATE TABLE IF NOT EXISTS merkle_trees (
9434
- chain_id INTEGER PRIMARY KEY,
9435
- root TEXT NOT NULL,
9436
- total_elements INTEGER NOT NULL,
9437
- last_updated INTEGER NOT NULL
9475
+ CREATE TABLE IF NOT EXISTS chairman_merkle_versions (
9476
+ chain_id INTEGER NOT NULL,
9477
+ version INTEGER NOT NULL,
9478
+ root_id TEXT NOT NULL,
9479
+ root_hash TEXT NOT NULL,
9480
+ PRIMARY KEY (chain_id, version)
9438
9481
  );
9439
9482
 
9440
9483
  CREATE TABLE IF NOT EXISTS entry_memos (
@@ -9671,29 +9714,29 @@ var SqliteStore = class {
9671
9714
  async clearMerkleLeaves(chainId) {
9672
9715
  this.run(`DELETE FROM merkle_leaves WHERE chain_id = ?`, [chainId]);
9673
9716
  }
9674
- async getMerkleNode(chainId, id) {
9717
+ async getChairmanMerkleNode(chainId, id) {
9675
9718
  const row = this.row(
9676
- `SELECT id, level, position, hash FROM merkle_nodes WHERE chain_id = ? AND id = ?`,
9719
+ `SELECT id, hash, left_id, right_id FROM chairman_merkle_nodes WHERE chain_id = ? AND id = ?`,
9677
9720
  [chainId, id]
9678
9721
  );
9679
9722
  if (!row) return void 0;
9680
- return { chainId, id: row.id, level: row.level, position: row.position, hash: row.hash };
9723
+ return { chainId, id: row.id, hash: row.hash, leftId: row.left_id, rightId: row.right_id };
9681
9724
  }
9682
- async upsertMerkleNodes(chainId, nodes) {
9725
+ async putChairmanMerkleNodes(chainId, nodes) {
9683
9726
  if (!nodes.length) return;
9684
9727
  const db = this.ensureDb();
9685
9728
  const stmt = db.prepare(
9686
- `INSERT INTO merkle_nodes (chain_id, id, level, position, hash)
9729
+ `INSERT INTO chairman_merkle_nodes (chain_id, id, hash, left_id, right_id)
9687
9730
  VALUES (?, ?, ?, ?, ?)
9688
9731
  ON CONFLICT(chain_id, id) DO UPDATE SET
9689
- level = excluded.level,
9690
- position = excluded.position,
9691
- hash = excluded.hash`
9732
+ hash = excluded.hash,
9733
+ left_id = excluded.left_id,
9734
+ right_id = excluded.right_id`
9692
9735
  );
9693
9736
  db.exec("BEGIN IMMEDIATE");
9694
9737
  try {
9695
9738
  for (const node of nodes) {
9696
- stmt.run(chainId, node.id, node.level, node.position, node.hash);
9739
+ stmt.run(chainId, node.id, node.hash, node.leftId ?? null, node.rightId ?? null);
9697
9740
  }
9698
9741
  db.exec("COMMIT");
9699
9742
  } catch (error) {
@@ -9701,40 +9744,35 @@ var SqliteStore = class {
9701
9744
  throw error;
9702
9745
  }
9703
9746
  }
9704
- async clearMerkleNodes(chainId) {
9705
- this.run(`DELETE FROM merkle_nodes WHERE chain_id = ?`, [chainId]);
9747
+ async getChairmanMerkleVersion(chainId, version) {
9748
+ const row = this.row(
9749
+ `SELECT version, root_id, root_hash FROM chairman_merkle_versions WHERE chain_id = ? AND version = ?`,
9750
+ [chainId, version]
9751
+ );
9752
+ if (!row) return void 0;
9753
+ return { chainId, version: row.version, rootId: row.root_id, rootHash: row.root_hash };
9706
9754
  }
9707
- async getMerkleTree(chainId) {
9755
+ async getLatestChairmanMerkleVersion(chainId) {
9708
9756
  const row = this.row(
9709
- `SELECT root, total_elements, last_updated FROM merkle_trees WHERE chain_id = ?`,
9757
+ `SELECT version, root_id, root_hash FROM chairman_merkle_versions WHERE chain_id = ? ORDER BY version DESC LIMIT 1`,
9710
9758
  [chainId]
9711
9759
  );
9712
9760
  if (!row) return void 0;
9713
- const root = toHexOrNull(row.root);
9714
- if (!root) return void 0;
9715
- const totalElements = Number(row.total_elements);
9716
- if (!Number.isFinite(totalElements) || totalElements < 0) return void 0;
9717
- const lastUpdated = Number(row.last_updated);
9718
- return {
9719
- chainId,
9720
- root,
9721
- totalElements: Math.floor(totalElements),
9722
- lastUpdated: Number.isFinite(lastUpdated) ? Math.floor(lastUpdated) : 0
9723
- };
9761
+ return { chainId, version: row.version, rootId: row.root_id, rootHash: row.root_hash };
9724
9762
  }
9725
- async setMerkleTree(chainId, tree) {
9763
+ async putChairmanMerkleVersion(chainId, record) {
9726
9764
  this.run(
9727
- `INSERT INTO merkle_trees (chain_id, root, total_elements, last_updated)
9765
+ `INSERT INTO chairman_merkle_versions (chain_id, version, root_id, root_hash)
9728
9766
  VALUES (?, ?, ?, ?)
9729
- ON CONFLICT(chain_id) DO UPDATE SET
9730
- root = excluded.root,
9731
- total_elements = excluded.total_elements,
9732
- last_updated = excluded.last_updated`,
9733
- [chainId, tree.root, tree.totalElements, tree.lastUpdated]
9767
+ ON CONFLICT(chain_id, version) DO UPDATE SET
9768
+ root_id = excluded.root_id,
9769
+ root_hash = excluded.root_hash`,
9770
+ [chainId, record.version, record.rootId, record.rootHash]
9734
9771
  );
9735
9772
  }
9736
- async clearMerkleTree(chainId) {
9737
- this.run(`DELETE FROM merkle_trees WHERE chain_id = ?`, [chainId]);
9773
+ async clearChairmanMerkleTree(chainId) {
9774
+ this.run(`DELETE FROM chairman_merkle_nodes WHERE chain_id = ?`, [chainId]);
9775
+ this.run(`DELETE FROM chairman_merkle_versions WHERE chain_id = ?`, [chainId]);
9738
9776
  }
9739
9777
  async upsertEntryMemos(memos) {
9740
9778
  if (!memos.length) return;