@xmbl/state-machine 0.1.1 → 0.1.3
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/package.json +1 -1
- package/src/applied-count.test.mjs +12 -0
- package/src/state-machine.js +35 -4
package/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// OUTCOME TEST: applied_tx_count after the convergence primitive. The metric the whole fleet reads.
|
|
2
|
+
const { StateMachine } = await import("../index.js");
|
|
3
|
+
const sm = new StateMachine({ dbPath: null });
|
|
4
|
+
sm._dbOpen = false;
|
|
5
|
+
const anchors = Array.from({ length: 3941 }, (_, i) => ({ event: 'task.created', hash: 'h' + i, ts: 1000 + i }));
|
|
6
|
+
const r = await sm.rebuildFromCanonical(anchors);
|
|
7
|
+
const s = sm.getStatistics();
|
|
8
|
+
console.log(`ROOT ${r.state_root.slice(0,10)} applied(returned) ${r.applied} applied_tx_count(published) ${s.totalTransactions}`);
|
|
9
|
+
// idempotence: the same set twice must not double the count
|
|
10
|
+
const r2 = await sm.rebuildFromCanonical(anchors);
|
|
11
|
+
console.log(`SECOND PASS same set: root ${r2.state_root.slice(0,10)} applied_tx_count ${sm.getStatistics().totalTransactions}`);
|
|
12
|
+
process.exit(0);
|
package/src/state-machine.js
CHANGED
|
@@ -21,6 +21,7 @@ export class StateMachine extends EventEmitter {
|
|
|
21
21
|
this.shards = [];
|
|
22
22
|
this.totalShards = options.totalShards || 4;
|
|
23
23
|
this.diffs = [];
|
|
24
|
+
this._diffIndex = new Map();
|
|
24
25
|
this.transactionLog = [];
|
|
25
26
|
|
|
26
27
|
// Initialize shards
|
|
@@ -76,7 +77,7 @@ export class StateMachine extends EventEmitter {
|
|
|
76
77
|
const diffData = JSON.parse(value.toString());
|
|
77
78
|
const diff = new StateDiff(diffData.txId, diffData.changes);
|
|
78
79
|
diff.timestamp = diffData.timestamp;
|
|
79
|
-
this.
|
|
80
|
+
this._recordDiff(diff);
|
|
80
81
|
loaded.push(diff);
|
|
81
82
|
}
|
|
82
83
|
loaded.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0) || String(a.txId).localeCompare(String(b.txId)));
|
|
@@ -142,6 +143,23 @@ export class StateMachine extends EventEmitter {
|
|
|
142
143
|
// Every transaction type IS a state change, so each maps to its natural key space. Keys are namespaced by
|
|
143
144
|
// type so two kinds can never collide, and values carry only consensus-derived fields — nothing node-local,
|
|
144
145
|
// because the state root is a cross-node commitment.
|
|
146
|
+
// applied_tx_count COUNTED APPLY CALLS, NOT TRANSACTIONS. Every apply site appended to `this.diffs`
|
|
147
|
+
// unconditionally while persisting to `diff:<txId>`, a key that OVERWRITES. So a second apply_backfill over
|
|
148
|
+
// an unchanged block set left the verkle root identical (the root is a function of the final key SET) and
|
|
149
|
+
// still added one array entry per block: MEASURED on node xmb0844bbed..., 82910 -> 104446 over the same
|
|
150
|
+
// 21536 blocks, then 88486 after a restart reloaded the deduped rows from disk. A monitoring number that
|
|
151
|
+
// moves on a repeated no-op cannot tell work from a replay, and /api/v1/xmbl/status publishes this one.
|
|
152
|
+
// Upsert by txId so the in-memory set matches the durable one it was always meant to mirror.
|
|
153
|
+
//
|
|
154
|
+
// REPLACE, don't skip: re-applying a txId with different changes is a legitimate later state for that key
|
|
155
|
+
// and the disk already resolves it that way. Skipping would leave the array disagreeing with the tree.
|
|
156
|
+
_recordDiff(diff) {
|
|
157
|
+
const at = this._diffIndex.get(diff.txId);
|
|
158
|
+
if (at === undefined) { this._diffIndex.set(diff.txId, this.diffs.length); this.diffs.push(diff); return true; }
|
|
159
|
+
this.diffs[at] = diff;
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
145
163
|
_stateChangesFor(block) {
|
|
146
164
|
const tx = block?.tx;
|
|
147
165
|
if (!tx || typeof tx !== 'object') return null;
|
|
@@ -182,7 +200,7 @@ export class StateMachine extends EventEmitter {
|
|
|
182
200
|
if (!changes || !Object.keys(changes).length) return;
|
|
183
201
|
try {
|
|
184
202
|
const diff = new StateDiff(block.id, changes);
|
|
185
|
-
this.
|
|
203
|
+
this._recordDiff(diff);
|
|
186
204
|
for (const [key, value] of Object.entries(changes)) {
|
|
187
205
|
await this.stateTree.insert(key, value);
|
|
188
206
|
}
|
|
@@ -231,7 +249,7 @@ export class StateMachine extends EventEmitter {
|
|
|
231
249
|
if (!changes || !Object.keys(changes).length) { out.skipped++; continue; }
|
|
232
250
|
try {
|
|
233
251
|
const diff = new StateDiff(block.id, changes);
|
|
234
|
-
this.
|
|
252
|
+
this._recordDiff(diff);
|
|
235
253
|
for (const [key, val] of Object.entries(changes)) await this.stateTree.insert(key, val);
|
|
236
254
|
if (this._dbOpen !== false) {
|
|
237
255
|
try { await this.db.put(`diff:${block.id}`, diff.serialize()); } catch { /* in-memory fallback */ }
|
|
@@ -254,10 +272,23 @@ export class StateMachine extends EventEmitter {
|
|
|
254
272
|
const out = { requested: list.length, applied: 0, skipped: 0, state_root: null, started: true };
|
|
255
273
|
await this.stateTree.clear();
|
|
256
274
|
this.diffs = [];
|
|
275
|
+
this._diffIndex.clear();
|
|
276
|
+
// ⛔ RECORD A DIFF FOR EVERY ANCHOR APPLIED, or applied_tx_count IS ZERO BY CONSTRUCTION. This loop wrote
|
|
277
|
+
// straight into the tree and never touched `this.diffs`, which it had just emptied — and
|
|
278
|
+
// getStatistics().totalTransactions is transactionLog.length + diffs.length. So the moment a node runs the
|
|
279
|
+
// convergence primitive, the number the whole fleet reads as "is this node applying anything" resets to 0
|
|
280
|
+
// and STAYS 0 no matter how many anchors it applied. MEASURED 2026-09-15: this node held a correct root
|
|
281
|
+
// over 3,941 applied anchors and published applied_tx_count 0; across the fleet 42 of 46 reporting nodes
|
|
282
|
+
// read 0, and a design ruling was written on the premise that 41 of them had "applied nothing". They had.
|
|
283
|
+
// The diff is not bookkeeping — it is the same StateDiff the block path records, keyed by the anchor's own
|
|
284
|
+
// content, so a rebuild and a live apply of the same anchor upsert to ONE row rather than two.
|
|
257
285
|
for (const a of list) {
|
|
258
286
|
if (!a || !a.event || !a.hash) { out.skipped++; continue; }
|
|
259
287
|
try {
|
|
260
|
-
|
|
288
|
+
const key = `anchor:${a.event}:${a.hash}`;
|
|
289
|
+
const value = { ts: a.ts ?? null };
|
|
290
|
+
await this.stateTree.insert(key, value);
|
|
291
|
+
this._recordDiff(new StateDiff(key, { [key]: value }));
|
|
261
292
|
out.applied++;
|
|
262
293
|
} catch { out.skipped++; }
|
|
263
294
|
}
|