@xmbl/simulator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `xsim` module will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
@@ -0,0 +1,102 @@
1
+ # Devnet finding: the consensus→ledger signature-re-verification seams (two FIXED, one open)
2
+
3
+ Surfaced while reviving the simulator into `LocalDevnet` (the "hardhat for XMBL"). The devnet
4
+ opts into ledger-side signature verification — it wires **both** `xid` and
5
+ `getPublicKeyByAddress` into the `Ledger` — which is precisely the caller configuration that
6
+ exercises defects no production code path currently reaches. Two of the three defects below are
7
+ now **FIXED**; the third is a genuine signature-domain decision left for the audit and documented
8
+ honestly rather than papered over. All are covered by `src/devnet.test.mjs`.
9
+
10
+ ## Where ledger-side verification runs
11
+
12
+ `cubic-ledger/src/ledger.js` re-verifies a tx's signature on entry in two methods:
13
+
14
+ - `addTransaction`: `if (this.xid && tx.sig && tx.from)` → look up the pubkey via
15
+ `this.getPublicKeyByAddress(tx.from)`; if found, `Identity.verifyTransaction(tx, publicKey)`.
16
+ - `addSealedBatch`: same guard.
17
+
18
+ The pubkey lookup is the gate. **`packages/core/index.js` constructs the `Ledger` WITHOUT
19
+ `getPublicKeyByAddress`** (it passes only `dbPath`, `xn`, `xid`, `consensusV2`), so in the
20
+ production daemon the lookup is `undefined`, `publicKey` resolves `null`, and **neither
21
+ verification block is entered**. Ledger-side re-verification is therefore **OFF in production
22
+ today** — a deliberate posture (consensus verifies at validation time via
23
+ `workflow.js completeValidation`, wired at `core/index.js:304`); the ledger block is a
24
+ defense-in-depth layer that is not yet enabled. Enabling it is gated on defect (c) below.
25
+
26
+ ## Defect (a) — FIXED: `finalizeTransaction` no longer overwrites the signed `id`
27
+
28
+ `consensus/src/workflow.js finalizeTransaction` used to do
29
+ `const txDataWithId = { ...processingTx.txData, id: validatedHash }` unconditionally.
30
+ `identity`'s `signingMessage` covers every field except `sig`/`publicKey`, so `id` is inside the
31
+ signed message; overwriting it made re-verification stringify a different tx and a valid
32
+ signature could never match ("Invalid transaction signature or address mismatch" — the exact
33
+ error observed in earlier simulator runs).
34
+
35
+ **Fix:** preserve the originator's signed `id`; only fall back to `validatedHash` when the tx
36
+ carried none:
37
+
38
+ ```js
39
+ const txDataWithId = processingTx.txData.id != null
40
+ ? processingTx.txData
41
+ : { ...processingTx.txData, id: validatedHash };
42
+ ```
43
+
44
+ The consensus hash is already carried to the ledger as the finalized event's `txId`, and the
45
+ ledger derives its own content-addressed block id via `Block.fromTransaction` — it never needs
46
+ `txData.id` to equal `validatedHash`. `src/devnet.test.mjs` drives `finalizeTransaction` at the
47
+ real code site and asserts the emitted `txData.id` is preserved AND the tx still verifies against
48
+ the signer key, with a negative control that an `id`-mutated signed tx is rejected (id is inside
49
+ the signed domain — the reason it must not be overwritten).
50
+
51
+ ## Defect (b) — FIXED: `addSealedBatch` called a method that does not exist
52
+
53
+ `cubic-ledger/src/ledger.js addSealedBatch` called **`this.xid.verify(tx, tx.sig, publicKey)`**.
54
+ `ledger.xid` is only ever an **`Identity` instance**, which exposes `signTransaction` and the
55
+ static `Identity.verifyTransaction` — it has **no `verify` method**. When reached this threw
56
+ `TypeError: this.xid.verify is not a function`, so `addSealedBatch`'s signature check had **never
57
+ actually verified a signature**, and it used a **different method name** than `addTransaction`.
58
+
59
+ **Fix:** call the same static `Identity.verifyTransaction(tx, publicKey)` `addTransaction` uses
60
+ (importing `Identity` the same way), which also enforces `derivedAddress===from` sig-ownership.
61
+ The real lead-role seal path routes through here (`core/lead-worker.js` →
62
+ `xclt.addSealedBatch([txData])`), so the two entry points are now consistent. `src/devnet.test.mjs`
63
+ asserts a validly signed tx VERIFIES and lands via `addSealedBatch`, with a negative control that
64
+ a tampered tx is rejected.
65
+
66
+ ## Defect (c) — OPEN (audit-level): consensus injects `validationTimestamp` into the signed body
67
+
68
+ `consensus/src/workflow.js moveToProcessing` adds `validationTimestamp` (the quorum-averaged
69
+ validator timestamp) **inside** `txData` ("Include in txData for xclt to use"). That field is not
70
+ in the originator's signed message, so — exactly like the old `id` overwrite — a tx that has
71
+ passed through `moveToProcessing` will **not** re-verify at the ledger against the originator's
72
+ signature. Fixes (a)/(b) make the code correct for the **direct** path (an originator-signed tx
73
+ handed straight to the ledger, as the devnet does); they do **not** by themselves make the full
74
+ `submit → validate → moveToProcessing → finalize → ledger` path re-verifiable, because of this
75
+ injection.
76
+
77
+ This cannot be fixed by simply excluding `validationTimestamp` (or `id`) from the signature,
78
+ because **`Block.fromTransaction` derives the block's content-address `id` from the WHOLE tx**
79
+ (`sha256(JSON.stringify(tx)).slice(0,16)`). Any field that (i) affects `block.id` but (ii) is not
80
+ signed becomes an inflation/double-apply vector: one valid finalized tx re-broadcast with N
81
+ different values of that field yields N distinct `block.id`s and applies the same value N times.
82
+ So a correct enablement of ledger-side re-verification on the consensus path requires one of two
83
+ architectural choices, **which is an audit-level signature-domain / block-identity decision**:
84
+
85
+ 1. **Derive `block.id` from the signed body only** (exclude consensus-assigned fields such as
86
+ `validationTimestamp`), so those fields cannot mint distinct blocks — then they may safely be
87
+ excluded from the signature; or
88
+ 2. **Carry `validationTimestamp` as a sibling of `txData`, never inside it** (the ledger reads it
89
+ as a second input and dedups on the signed-body hash), so the signed body the ledger verifies
90
+ is byte-identical to what the originator signed.
91
+
92
+ Both touch `block.js`, ledger dedup, and cross-node determinism, so neither rides in on a tooling
93
+ commit. Until one is made and audited, **ledger-side re-verification stays OFF in production**
94
+ (the `getPublicKeyByAddress` lookup is deliberately not wired into the `Ledger`), and consensus
95
+ remains the single verification point. This is the honest, current posture — not a silent gap.
96
+
97
+ ## Why the devnet drives the direct path
98
+
99
+ `LocalDevnet` submits signed txs straight to `ledger.addTransaction` (not through consensus
100
+ finalization), because that path verifies the tx as-signed and works. This keeps the devnet a
101
+ real, verifiable local network today while defect (c) — the only remaining consensus→ledger
102
+ verification seam — is an explicit, documented, audit-scoped decision.
package/index.js ADDED
@@ -0,0 +1,50 @@
1
+ import { SystemSimulator } from './src/simulator.js';
2
+ import { StructuredLogger } from './src/logger.js';
3
+ import { LocalDevnet } from './src/devnet.js';
4
+ import { DevnetRpc } from './src/devnet-rpc.js';
5
+
6
+ // NOTE: the legacy SystemSimulator serves NO network port (it is an in-process soak). For a
7
+ // runnable local network WITH an HTTP surface, use the LocalDevnet runner: `npm run devnet`
8
+ // (src/devnet-run.mjs), which actually binds a loopback RPC and prints its URL.
9
+ console.log('XSIM (XMBL Simulator) — legacy in-process soak; for a local network with an RPC run `npm run devnet`.');
10
+
11
+ // If run directly, start the simulator
12
+ const isMainModule = process.argv[1] && (
13
+ process.argv[1].endsWith('index.js') ||
14
+ process.argv[1].includes('xsim/index.js') ||
15
+ process.argv[1].includes('xsim\\index.js')
16
+ );
17
+
18
+ if (isMainModule || process.argv.includes('--run')) {
19
+ const sim = new SystemSimulator({
20
+ initialIdentities: 10,
21
+ transactionRate: 2,
22
+ stateDiffRate: 1,
23
+ storageOpRate: 0.5,
24
+ computeOpRate: 0.5,
25
+ useRealModules: true
26
+ });
27
+
28
+ sim.start().catch(err => {
29
+ console.error('Failed to start simulator:', err);
30
+ process.exit(1);
31
+ });
32
+
33
+ // Graceful shutdown
34
+ process.on('SIGINT', () => {
35
+ console.log('\nShutting down simulator...');
36
+ sim.stop();
37
+ process.exit(0);
38
+ });
39
+
40
+ process.on('SIGTERM', () => {
41
+ console.log('\nShutting down simulator...');
42
+ sim.stop();
43
+ process.exit(0);
44
+ });
45
+ }
46
+
47
+ export { SystemSimulator, StructuredLogger, LocalDevnet, DevnetRpc };
48
+
49
+
50
+