@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 +8 -0
- package/DEVNET-SEAM-FINDING.md +102 -0
- package/index.js +50 -0
- package/instructions.md +784 -0
- package/package.json +28 -0
- package/readme.md +7 -0
- package/src/capabilities.js +110 -0
- package/src/devnet-rpc.js +114 -0
- package/src/devnet-rpc.test.mjs +91 -0
- package/src/devnet-run.mjs +46 -0
- package/src/devnet.js +180 -0
- package/src/devnet.test.mjs +117 -0
- package/src/logger.js +80 -0
- package/src/simulator.js +846 -0
- package/status.md +167 -0
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
|
+
|