@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.
@@ -0,0 +1,117 @@
1
+ // Gate test for the XMBL LocalDevnet (the "hardhat for XMBL").
2
+ //
3
+ // Self-contained: asserts and exits non-zero on failure (the protocol hard-gate contract).
4
+ // It drives the REAL modules — MAYO identities, the cubic ledger with signature verification
5
+ // ON — through a network pipeline and asserts the OUTCOME by count: N signed transfers
6
+ // submitted → N landed → blocks sealed into a face, with a negative control proving the
7
+ // verification is live (a tampered tx is rejected). It then proves the two consensus→ledger
8
+ // seams documented in ../DEVNET-SEAM-FINDING.md are now FIXED, each with a live negative
9
+ // control so a regression fails loudly.
10
+ import assert from 'node:assert/strict';
11
+ import { LocalDevnet } from './devnet.js';
12
+ import { Identity } from '../../identity/index.js';
13
+ import { ConsensusWorkflow } from '../../consensus/index.js';
14
+
15
+ let pass = 0;
16
+ const ok = (name, cond, detail = '') => {
17
+ assert.ok(cond, `${name}${detail ? ' — ' + detail : ''}`);
18
+ console.log(' ok ' + name);
19
+ pass++;
20
+ };
21
+
22
+ // ── 1) REAL network pipeline: signed transfers land, a face seals, verification is live ──
23
+ {
24
+ const net = await new LocalDevnet({ identities: 4 }).start();
25
+ ok('devnet mints the requested real identities', net.metrics.identities === 4, `got ${net.metrics.identities}`);
26
+ ok('every identity has a distinct MAYO address', new Set(net.identities.map((i) => i.address)).size === 4);
27
+
28
+ // 12 distinct signed transfers through the direct ledger path (verification ON).
29
+ const N = 12;
30
+ for (let k = 0; k < N; k++) {
31
+ const r = await net.submitTransfer(0, 1, 1 + k); // id0 → id1, varying amount ⇒ distinct blocks
32
+ assert.ok(r.ok, `transfer ${k} rejected: ${r.error}`);
33
+ }
34
+ ok('all signed transfers were submitted', net.metrics.submitted === N, `submitted=${net.metrics.submitted}`);
35
+ ok('all signed transfers LANDED in the ledger', net.metrics.landed === N, `landed=${net.metrics.landed}`);
36
+ ok('none were rejected', net.metrics.rejected === 0, `rejected=${net.metrics.rejected}`);
37
+ ok('≥1 face sealed from the 12 blocks (9 per face)', net.metrics.facesCompleted >= 1, `faces=${net.metrics.facesCompleted}`);
38
+
39
+ const m = await net.getMetrics();
40
+ ok('ledger exposes a state root after activity', m.root !== null && m.root !== undefined, `root=${m.root}`);
41
+
42
+ // Balance is the net of APPLIED deltas — real state, not fabricated.
43
+ const sent = Array.from({ length: N }, (_, k) => 1 + k).reduce((a, b) => a + b, 0); // 1..12 = 78
44
+ ok('recipient balance equals the net of landed transfers', net.balanceOf(net.addressOf(1)) === sent, `bal=${net.balanceOf(net.addressOf(1))} expected=${sent}`);
45
+ ok('sender balance is the negation', net.balanceOf(net.addressOf(0)) === -sent, `bal=${net.balanceOf(net.addressOf(0))}`);
46
+ ok('an unknown address has zero balance', net.balanceOf('xmbdoesnotexist') === 0);
47
+
48
+ // NEGATIVE CONTROL — verification is actually ON: a tx tampered after signing is rejected.
49
+ const from = net.identities[0];
50
+ const signed = await from.signTransaction({ id: 'tamper_1', type: 'utxo', from: from.address, to: net.addressOf(1), amount: 5, timestamp: Date.now() });
51
+ const tampered = { ...signed, amount: 9999 }; // mutate a signed field
52
+ let threw = '';
53
+ try { await net.ledger.addTransaction(tampered); } catch (e) { threw = e.message; }
54
+ ok('ledger REJECTS a tampered signed tx (verification is live)', /Invalid transaction signature|address mismatch/.test(threw), `threw="${threw}"`);
55
+
56
+ await net.stop();
57
+ }
58
+
59
+ // ── 2) SEAM FIXES — the two consensus→ledger defects are corrected (each with a neg-control) ──
60
+ {
61
+ const net = await new LocalDevnet({ identities: 2 }).start();
62
+ const from = net.identities[0];
63
+ const signed = await from.signTransaction({ id: 'seam_client_id', type: 'utxo', from: from.address, to: net.addressOf(1), amount: 7, timestamp: Date.now() });
64
+
65
+ // FIX(a): consensus finalizeTransaction now PRESERVES the originator's signed `id`. It used to
66
+ // overwrite it with validatedHash (workflow.js), corrupting the signed message so the ledger's
67
+ // re-verification could never match. Driven at the real code site: a signed tx placed in the
68
+ // processing mempool is finalized, and the emitted txData must keep its signed id AND still
69
+ // verify against the signer's key. The consensus hash rides separately as the event's txId.
70
+ {
71
+ const w = new ConsensusWorkflow({});
72
+ const validatedHash = 'validatedHash_' + 'ab'.repeat(8);
73
+ w.mempool.processingTx.set(validatedHash, { txData: { ...signed }, validationTimestamp: null });
74
+ let emitted = null;
75
+ w.on('tx:finalized', (d) => { emitted = d; });
76
+ await w.finalizeTransaction(validatedHash);
77
+ ok('FIX(a): finalize PRESERVES the signed id (not validatedHash)', !!emitted && emitted.txData.id === 'seam_client_id', `id=${emitted?.txData?.id}`);
78
+ ok('FIX(a): the finalized tx STILL verifies against the signer key', (await Identity.verifyTransaction(emitted.txData, from.publicKey)) === true);
79
+ ok('FIX(a): the consensus hash is carried separately as the event txId', emitted.txId === validatedHash);
80
+ // OPEN defect (c), PINNED here (not only in DEVNET-SEAM-FINDING.md prose): moveToProcessing
81
+ // injects a `validationTimestamp` INTO the signed tx body, which is outside the signed domain,
82
+ // so a real finalized tx does NOT re-verify against the signer key. This is why ledger-side
83
+ // re-verification stays OFF in production. If someone wires getPublicKeyByAddress into the
84
+ // Ledger without first resolving (c) (block-id-from-signed-body OR carrying validationTimestamp
85
+ // as a sibling), THIS assertion flips and the gate fails — which is the intended tripwire.
86
+ const withVt = { ...signed, validationTimestamp: '123' };
87
+ ok('FIX(a) scope: a tx carrying consensus-injected validationTimestamp does NOT re-verify (open defect (c))',
88
+ (await Identity.verifyTransaction(withVt, from.publicKey)) === false);
89
+ try { await w.mempool?.db?.close?.(); } catch { /* in-memory / already closed */ }
90
+ }
91
+
92
+ // FIX(a) negative control: mutating `id` AFTER signing (what the old overwrite effectively did)
93
+ // is REJECTED by the ledger — `id` is inside the signed domain, which is exactly why consensus
94
+ // must not overwrite it.
95
+ const idMutated = { ...signed, id: 'attacker_reid' };
96
+ let aThrew = '';
97
+ try { await net.ledger.addTransaction(idMutated); } catch (e) { aThrew = e.message; }
98
+ ok('FIX(a) neg-control: an id-mutated signed tx is REJECTED (id is signed)', /Invalid transaction signature|address mismatch/.test(aThrew), `threw="${aThrew}"`);
99
+
100
+ // FIX(b): addSealedBatch now verifies via the static Identity.verifyTransaction. It used to call
101
+ // this.xid.verify(...) — a method that does not exist on an Identity instance → TypeError, so its
102
+ // signature check had never verified anything. Identity instances still expose no `.verify`
103
+ // (only the static), so the corrected call site is the reason this now works.
104
+ ok('Identity instances have no .verify (only static verifyTransaction)', typeof from.verify === 'undefined' && typeof Identity.verifyTransaction === 'function');
105
+ const sealedResult = await net.ledger.addSealedBatch([{ ...signed }]);
106
+ ok('FIX(b): addSealedBatch VERIFIES a valid sig and lands (no TypeError)', !!sealedResult && typeof sealedResult === 'object');
107
+
108
+ // FIX(b) negative control: a tx tampered after signing is REJECTED on the sealed-batch path too.
109
+ let bThrew = '';
110
+ try { await net.ledger.addSealedBatch([{ ...signed, amount: 4242 }]); } catch (e) { bThrew = e.message; }
111
+ ok('FIX(b) neg-control: addSealedBatch REJECTS a tampered tx (verification is live)', /Invalid signature|address mismatch/.test(bThrew), `threw="${bThrew}"`);
112
+
113
+ await net.stop();
114
+ }
115
+
116
+ console.log(`\n✅ devnet: ${pass} checks passed`);
117
+ process.exit(0);
package/src/logger.js ADDED
@@ -0,0 +1,80 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ /**
4
+ * Structured logger for XSIM
5
+ * Emits well-organized, explicit logs for consumption by other tools
6
+ */
7
+ export class StructuredLogger extends EventEmitter {
8
+ constructor(options = {}) {
9
+ super();
10
+ this.enabled = options.enabled !== false;
11
+ this.logLevel = options.logLevel || 'info';
12
+ this.logToConsole = options.logToConsole !== false;
13
+ }
14
+
15
+ _log(level, category, event, data = {}) {
16
+ if (!this.enabled) return;
17
+
18
+ const logEntry = {
19
+ timestamp: new Date().toISOString(),
20
+ level,
21
+ category,
22
+ event,
23
+ data
24
+ };
25
+
26
+ if (this.logToConsole) {
27
+ const prefix = `[${logEntry.timestamp}] [${level.toUpperCase()}] [${category}]`;
28
+ console.log(`${prefix} ${event}`, data);
29
+ }
30
+
31
+ this.emit('log', logEntry);
32
+ this.emit(`log:${category}`, logEntry);
33
+ }
34
+
35
+ identity(event, data) {
36
+ this._log('info', 'identity', event, data);
37
+ }
38
+
39
+ transaction(event, data) {
40
+ this._log('info', 'transaction', event, data);
41
+ }
42
+
43
+ consensus(event, data) {
44
+ this._log('info', 'consensus', event, data);
45
+ }
46
+
47
+ ledger(event, data) {
48
+ this._log('info', 'ledger', event, data);
49
+ }
50
+
51
+ stateMachine(event, data) {
52
+ this._log('info', 'stateMachine', event, data);
53
+ }
54
+
55
+ storage(event, data) {
56
+ this._log('info', 'storage', event, data);
57
+ }
58
+
59
+ compute(event, data) {
60
+ this._log('info', 'compute', event, data);
61
+ }
62
+
63
+ network(event, data) {
64
+ this._log('info', 'network', event, data);
65
+ }
66
+
67
+ system(event, data) {
68
+ this._log('info', 'system', event, data);
69
+ }
70
+
71
+ error(category, event, error) {
72
+ this._log('error', category, event, {
73
+ error: error.message,
74
+ stack: error.stack
75
+ });
76
+ }
77
+ }
78
+
79
+
80
+