@xmbl/state-machine 0.1.11 → 0.1.13
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/CHANGELOG.md +35 -2
- package/package.json +1 -1
- package/src/applied-count.test.mjs +1 -1
- package/src/apply-path.test.mjs +1 -1
- package/src/rehydration.test.mjs +3 -3
- package/src/state-machine.js +3 -3
- package/src/verkle-independent-verify.test.mjs +1 -1
- package/src/verkle-integration.test.mjs +1 -1
- package/src/verkle-tree.js +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @xmbl/state-machine
|
|
2
2
|
|
|
3
|
+
## 0.1.13
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- **SECURITY — the AIR zero-knowledge path leaked the entire secret trace.** Reported against
|
|
8
|
+
0.1.12 and reproduced: the batching challenges `alpha` and `beta` are public and the composition
|
|
9
|
+
`C(x) = alpha*u + beta*v` is an `F_p^4` element over two base-field unknowns, so every FRI opening
|
|
10
|
+
was four equations in two unknowns. Solving two limbs for `v` gives `col'(x)` directly, and the
|
|
11
|
+
`2*nq` layer-0 openings interpolate the blinded trace column — row 0 IS the witness. It returned
|
|
12
|
+
the secret exactly, in about a second. The `(x^T - 1)` blind could not help: it is sized for the
|
|
13
|
+
`2*nc` direct openings and vanishes on the trace domain by construction.
|
|
14
|
+
|
|
15
|
+
The composition is now MASKED before FRI sees it: a uniformly random extension-valued polynomial
|
|
16
|
+
`M` of degree `< K` is committed first, `gamma` is drawn from a transcript that includes its root,
|
|
17
|
+
and FRI runs on `C + gamma*M`. Each opening is four equations in six unknowns. The verifier opens
|
|
18
|
+
`M` against its own root at the consistency points. A mask that is not of degree `< K` would pin
|
|
19
|
+
`gamma` to one value out of ~2^124, so masking cannot hide a false statement.
|
|
20
|
+
|
|
21
|
+
`setup` now doubles `K` until it exceeds everything the transcript reveals
|
|
22
|
+
(`nq*(log2 K + 1) + nc`, plus margin): `K = 2048`, `N = 16384`. `nc` is raised 16 -> 40, because
|
|
23
|
+
each consistency point is `-log2(K/N) = 3` bits and `nc = 16` was a ~48-bit check behind a
|
|
24
|
+
~100-bit FRI. Coset evaluation is now an NTT, so proving is faster than 0.1.12 despite the larger
|
|
25
|
+
domain. **PROOFS FROM 0.1.12 DO NOT VERIFY ON 0.1.13** — the proof carries new commitments
|
|
26
|
+
(`rootM`, `rootD`) and the derived parameters moved.
|
|
27
|
+
|
|
28
|
+
`xzk` (the cube-curve statement) was attacked the same way and survives: the committed curve is
|
|
29
|
+
fully recoverable from its FRI openings, but `Pt = P + Z_R*B` with `B` uniform of degree 18 over a
|
|
30
|
+
witness quotient of degree 1, so the same proof is carried by a 2-parameter family of witnesses.
|
|
31
|
+
|
|
32
|
+
The attack is now a permanent check in `air.test.mjs` and `xzk.test.mjs`.
|
|
33
|
+
|
|
34
|
+
## 0.1.12
|
|
35
|
+
|
|
3
36
|
## 0.1.11
|
|
4
37
|
|
|
5
38
|
### Patch Changes
|
|
@@ -18,7 +51,7 @@
|
|
|
18
51
|
never under the xid it claims (a datum fails `validateXid` precisely when that xid is somebody else's), and
|
|
19
52
|
the anchor dedup key claimed before validation is released on every failure. Both doors are closed on BOTH
|
|
20
53
|
entry points — `addTransaction` and `addSealedBatch`, the path a finalized transaction actually takes — and
|
|
21
|
-
one invalid entry in a batch no longer discards the
|
|
54
|
+
one invalid entry in a batch no longer discards the genuine transactions behind it.
|
|
22
55
|
- consensus: a content-addressed type-6 value tx is admitted on its content address (it carries no in-body
|
|
23
56
|
signature by design); a malformed anchor is refused at the door by the ledger's own `validateTransaction`;
|
|
24
57
|
`finalizeTransaction` preserves the signed `id`.
|
|
@@ -28,7 +61,7 @@
|
|
|
28
61
|
- networking: self-elected circuit-relay server; NO_FATAL transport tolerance; throttled bootstrap warnings.
|
|
29
62
|
- storage-compute: `CoordinateDelivery` (broken: `require` in ESM, keyed on the public key) is removed.
|
|
30
63
|
- core: ships the node daemon as the `xmbl-node` bin; the control socket implements the coordinator
|
|
31
|
-
contract (`xsc`, `submit_batch`, `ledger_capabilities`, `identity_status`, a signed `chain` claim,
|
|
64
|
+
contract (`xsc`, `submit_batch`, `ledger_capabilities`, `identity_status`, a signed `chain` claim, genuine
|
|
32
65
|
`submit_tx` rejections) and reports the RUNNING versions; boot waits for the stores.
|
|
33
66
|
- every package exports a load-time `VERSION`.
|
|
34
67
|
- PROTOCOL (operator, 2026-09-16): every transaction is typed by its xid (tokens.json type codes; `micromineTx`/
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// OUTCOME TEST: applied_tx_count after the convergence primitive. The metric
|
|
1
|
+
// OUTCOME TEST: applied_tx_count after the convergence primitive. The metric every node reads.
|
|
2
2
|
const { StateMachine } = await import("../index.js");
|
|
3
3
|
const sm = new StateMachine({ dbPath: null });
|
|
4
4
|
sm._dbOpen = false;
|
package/src/apply-path.test.mjs
CHANGED
|
@@ -50,7 +50,7 @@ const close = async ({ dir, xclt, xvsm }) => {
|
|
|
50
50
|
console.log('\n1. a SEALED face reaches the state tree (the missing block:added doorbell)');
|
|
51
51
|
{
|
|
52
52
|
const env = await fresh();
|
|
53
|
-
await check('the tree starts empty — the
|
|
53
|
+
await check('the tree starts empty — the true baseline, not an assumption', async () => {
|
|
54
54
|
eq(env.xvsm.getStateRoot(), ZERO, 'initial root');
|
|
55
55
|
});
|
|
56
56
|
await check('sealing 9 txs moves state_root off zero and applies 9 diffs', async () => {
|
package/src/rehydration.test.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// THE THREE DEFECTS THAT KEPT NODES OFF A SHARED ROOT, each asserted by the symptom the
|
|
1
|
+
// THE THREE DEFECTS THAT KEPT NODES OFF A SHARED ROOT, each asserted by the symptom the nodes showed,
|
|
2
2
|
// not by the mechanism. All three were measured live on 2026-09-15 before being fixed here.
|
|
3
3
|
import { test } from 'node:test';
|
|
4
4
|
import assert from 'node:assert';
|
|
@@ -25,7 +25,7 @@ test('a restarted node publishes a REAL root, not 64 zeros, from the state: keys
|
|
|
25
25
|
await sm.db.close();
|
|
26
26
|
|
|
27
27
|
// Reopen. The diff replay is NOT the thing under test — delete every diff row first, so the only
|
|
28
|
-
// surviving source for the trie is `state:`. 39 of 44 reporting
|
|
28
|
+
// surviving source for the trie is `state:`. 39 of 44 reporting nodes published ZERO here.
|
|
29
29
|
sm = await open(dir);
|
|
30
30
|
await sm.db.clear({ gte: 'diff:', lt: 'diff:\xFF' });
|
|
31
31
|
await sm.db.close();
|
|
@@ -73,7 +73,7 @@ test('legacy rows keyed by block id are re-keyed to content identity on the next
|
|
|
73
73
|
const dir = `/tmp/xvsm-rehydrate-${process.pid}-d`;
|
|
74
74
|
rmSync(dir, { recursive: true, force: true });
|
|
75
75
|
let sm = await open(dir);
|
|
76
|
-
// Write rows the way every node on the
|
|
76
|
+
// Write rows the way every node on the nodes already has them: one per submission, keyed by block id.
|
|
77
77
|
const set = anchors(40);
|
|
78
78
|
let written = 0;
|
|
79
79
|
for (let pass = 0; pass < 3; pass++) for (const a of set) {
|
package/src/state-machine.js
CHANGED
|
@@ -34,7 +34,7 @@ export class StateMachine extends EventEmitter {
|
|
|
34
34
|
|
|
35
35
|
// Initialize database. KEEP THE PROMISE: `_dbOpen` flips true the moment Level opens, long before the
|
|
36
36
|
// diff sweep and the tree rehydration have finished, so anything that polls `_dbOpen` and then reads a
|
|
37
|
-
// count is reading a store mid-rebuild. `ready()` is the only
|
|
37
|
+
// count is reading a store mid-rebuild. `ready()` is the only genuine join point.
|
|
38
38
|
this._ready = this._initDb().catch(() => {});
|
|
39
39
|
|
|
40
40
|
// Listen to ledger events if available
|
|
@@ -335,9 +335,9 @@ export class StateMachine extends EventEmitter {
|
|
|
335
335
|
// ⛔ RECORD A DIFF FOR EVERY ANCHOR APPLIED, or applied_tx_count IS ZERO BY CONSTRUCTION. This loop wrote
|
|
336
336
|
// straight into the tree and never touched `this.diffs`, which it had just emptied — and
|
|
337
337
|
// getStatistics().totalTransactions is transactionLog.length + diffs.length. So the moment a node runs the
|
|
338
|
-
// convergence primitive, the number
|
|
338
|
+
// convergence primitive, the number every node reads as "is this node applying anything" resets to 0
|
|
339
339
|
// and STAYS 0 no matter how many anchors it applied. MEASURED 2026-09-15: this node held a correct root
|
|
340
|
-
// over 3,941 applied anchors and published applied_tx_count 0; across the
|
|
340
|
+
// over 3,941 applied anchors and published applied_tx_count 0; across the nodes 42 of 46 reporting nodes
|
|
341
341
|
// read 0, and a design ruling was written on the premise that 41 of them had "applied nothing". They had.
|
|
342
342
|
// The diff is not bookkeeping — it is the same StateDiff the block path records, keyed by the anchor's own
|
|
343
343
|
// content, so a rebuild and a live apply of the same anchor upsert to ONE row rather than two.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// leaf hash = sha256(value canonicalised) (value node at depth 32)
|
|
10
10
|
// internal node hash = sha256( concat of 256 child slots, each 32 bytes; empty slot = 32 zeros )
|
|
11
11
|
// a key's nibble at depth d = sha256(key)[d]
|
|
12
|
-
// A proof carries, per level, the sibling hashes at that node; the
|
|
12
|
+
// A proof carries, per level, the sibling hashes at that node; the correct verifier fills the key's own
|
|
13
13
|
// slot with the hash carried up from below and every other slot from the siblings (or zeros), hashes,
|
|
14
14
|
// and repeats to the root. The proof is SOUND iff the reconstructed root equals the tree's real root —
|
|
15
15
|
// which the verifier learns independently (tree.getRoot()), never from the attacker-supplied proof.root.
|
|
@@ -10,7 +10,7 @@ import { join } from 'path';
|
|
|
10
10
|
let pass = 0, fail = 0;
|
|
11
11
|
const check = async (n, f) => { try { await f(); console.log(` ok ${n}`); pass++; } catch (e) { console.log(` FAIL ${n}\n ${e.message}`); fail++; } };
|
|
12
12
|
const EMPTY = '0'.repeat(64);
|
|
13
|
-
// Portable: this suite runs on the laptop AND on every Linux box
|
|
13
|
+
// Portable: this suite runs on the laptop AND on every Linux box among the nodes — so it JOINS on `ready()`
|
|
14
14
|
// rather than sleeping. A fixed timeout is a guess about someone else's scheduler: this suite waited
|
|
15
15
|
// 120/150/250/500ms for the store to open and the tree to rehydrate, which held on this laptop and LOST on
|
|
16
16
|
// ubuntu-latest, where the restart check read 64 zeros — the 150ms wait expired before `_initDb` had opened
|
package/src/verkle-tree.js
CHANGED
|
@@ -53,7 +53,7 @@ export class VerkleStateTree {
|
|
|
53
53
|
// `this.root` a fresh empty VerkleNode. So a node that had loaded EVERY key off disk answered getRoot()
|
|
54
54
|
// with 64 zeros: a tree that holds the whole state and commits to nothing. MEASURED 2026-09-15: after a
|
|
55
55
|
// canonical rebuild of 10 anchors, restart loaded all 10 keys and published root 0000…0000; across the
|
|
56
|
-
//
|
|
56
|
+
// nodes 39 of 44 reporting nodes published exactly that, including one with 11,691 blocks persisted. The
|
|
57
57
|
// only boxes showing a real root were showing it by accident, via XVSM's diff replay re-inserting keys
|
|
58
58
|
// through insert(). The state map is not the commitment — the hashed trie over it is, and it has to be
|
|
59
59
|
// reconstructed on open or every restart silently un-commits the node's entire state.
|
|
@@ -201,7 +201,7 @@ export class VerkleStateTree {
|
|
|
201
201
|
// transactions instead of the structure that actually commits them. This returns the real nodes and the
|
|
202
202
|
// real parent->child edges, bounded, with the counts needed to say what was left out.
|
|
203
203
|
//
|
|
204
|
-
// Bounded on BOTH axes and
|
|
204
|
+
// Bounded on BOTH axes and genuine about it: `maxDepth` limits how far down, `maxNodes` caps the total, and
|
|
205
205
|
// the result carries `truncated` plus each node's own `descendants` so a cut branch reports its real size
|
|
206
206
|
// rather than looking like a leaf. A viewer that silently stopped at 500 nodes would draw a tree that is
|
|
207
207
|
// simply the wrong shape.
|
|
@@ -217,7 +217,7 @@ export class VerkleStateTree {
|
|
|
217
217
|
}
|
|
218
218
|
if (!start.hash) this._updateHash(start);
|
|
219
219
|
|
|
220
|
-
// Subtree size is what makes a truncated branch
|
|
220
|
+
// Subtree size is what makes a truncated branch genuine, so it is computed for every node we emit.
|
|
221
221
|
const sizeOf = (node) => {
|
|
222
222
|
let n = 1;
|
|
223
223
|
for (const c of node.children.values()) n += sizeOf(c);
|