@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/src/simulator.js
ADDED
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { faker } from '@faker-js/faker';
|
|
3
|
+
import Chance from 'chance';
|
|
4
|
+
import { StructuredLogger } from './logger.js';
|
|
5
|
+
|
|
6
|
+
// Initialize faker with seed for deterministic mode if needed
|
|
7
|
+
if (process.env.FAKER_SEED) {
|
|
8
|
+
faker.seed(parseInt(process.env.FAKER_SEED));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const chance = new Chance();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Comprehensive XMBL System Simulator
|
|
15
|
+
* Demonstrates all core functionality: identities, consensus, ledger, state machine, storage, compute
|
|
16
|
+
*/
|
|
17
|
+
export class SystemSimulator extends EventEmitter {
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
super();
|
|
20
|
+
|
|
21
|
+
this.options = {
|
|
22
|
+
initialIdentities: options.initialIdentities || 10,
|
|
23
|
+
transactionRate: options.transactionRate || 2, // transactions per second
|
|
24
|
+
stateDiffRate: options.stateDiffRate || 1, // state diffs per second
|
|
25
|
+
storageOpRate: options.storageOpRate || 0.5, // storage ops per second
|
|
26
|
+
computeOpRate: options.computeOpRate || 0.5, // compute ops per second
|
|
27
|
+
useRealModules: options.useRealModules !== false, // Try to use real modules if available
|
|
28
|
+
...options
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
this.logger = new StructuredLogger({ logToConsole: true });
|
|
32
|
+
this.identities = [];
|
|
33
|
+
this.transactions = [];
|
|
34
|
+
this.stateDiffs = [];
|
|
35
|
+
this.blocks = [];
|
|
36
|
+
this.cubes = [];
|
|
37
|
+
this.superCubes = [];
|
|
38
|
+
|
|
39
|
+
// Module instances (will be initialized if available)
|
|
40
|
+
this.modules = {
|
|
41
|
+
xid: null,
|
|
42
|
+
xn: null,
|
|
43
|
+
xclt: null,
|
|
44
|
+
xvsm: null,
|
|
45
|
+
xpc: null,
|
|
46
|
+
xsc: null
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
this.metrics = {
|
|
50
|
+
identitiesCreated: 0,
|
|
51
|
+
transactionsCreated: 0,
|
|
52
|
+
transactionsValidated: 0,
|
|
53
|
+
blocksAdded: 0,
|
|
54
|
+
facesCompleted: 0,
|
|
55
|
+
cubesCompleted: 0,
|
|
56
|
+
superCubesCompleted: 0,
|
|
57
|
+
stateDiffsCreated: 0,
|
|
58
|
+
stateAssemblies: 0,
|
|
59
|
+
storageOperations: 0,
|
|
60
|
+
computeOperations: 0,
|
|
61
|
+
startTime: null,
|
|
62
|
+
uptime: 0
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
this.running = false;
|
|
66
|
+
this.intervals = [];
|
|
67
|
+
this.leaders = []; // Consensus leaders
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Initialize and connect to real modules if available
|
|
72
|
+
*/
|
|
73
|
+
async initializeModules() {
|
|
74
|
+
this.logger.system('initializing', { useRealModules: this.options.useRealModules });
|
|
75
|
+
|
|
76
|
+
if (!this.options.useRealModules) {
|
|
77
|
+
this.logger.system('modules_disabled', { reason: 'useRealModules is false' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Try to import and initialize XID
|
|
82
|
+
try {
|
|
83
|
+
const { Identity } = await import('../../identity/index.js');
|
|
84
|
+
this.modules.xid = Identity;
|
|
85
|
+
this.logger.system('module_connected', { module: '@xmbl/identity', status: 'connected' });
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.logger.system('module_unavailable', { module: '@xmbl/identity', reason: error.message });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Try to import and initialize XN
|
|
91
|
+
try {
|
|
92
|
+
const { XNNode, ConnectionManager } = await import('../../networking/index.js');
|
|
93
|
+
this.modules.xn = { XNNode, ConnectionManager };
|
|
94
|
+
this.logger.system('module_connected', { module: '@xmbl/networking', status: 'connected' });
|
|
95
|
+
} catch (error) {
|
|
96
|
+
this.logger.system('module_unavailable', { module: '@xmbl/networking', reason: error.message });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Try to import and initialize XCLT (Ledger)
|
|
100
|
+
try {
|
|
101
|
+
const { Ledger } = await import('../../cubic-ledger/index.js');
|
|
102
|
+
this.modules.xclt = new Ledger({
|
|
103
|
+
dbPath: './data/xsim/ledger',
|
|
104
|
+
xid: this.modules.xid,
|
|
105
|
+
xn: this.modules.xn,
|
|
106
|
+
// Pass identity lookup function so ledger can verify signatures
|
|
107
|
+
getPublicKeyByAddress: (address) => {
|
|
108
|
+
const identity = this.identities.find(id => id.address === address);
|
|
109
|
+
return identity ? identity.publicKey : null;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Listen to ledger events
|
|
114
|
+
this.modules.xclt.on('block:added', (block) => {
|
|
115
|
+
this.metrics.blocksAdded++;
|
|
116
|
+
this.blocks.push(block);
|
|
117
|
+
this.logger.ledger('block_added', {
|
|
118
|
+
blockId: block.id,
|
|
119
|
+
txId: block.txId,
|
|
120
|
+
timestamp: block.timestamp
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
this.modules.xclt.on('face:complete', (data) => {
|
|
125
|
+
this.metrics.facesCompleted++;
|
|
126
|
+
const face = data.face || data;
|
|
127
|
+
this.logger.ledger('face_complete', {
|
|
128
|
+
faceIndex: face.index || face.faceIndex || data.faceIndex,
|
|
129
|
+
blockCount: face.blocks?.size || face.blocks?.length || 0,
|
|
130
|
+
timestamp: face.timestamp || data.timestamp
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
this.modules.xclt.on('cube:complete', (data) => {
|
|
135
|
+
this.metrics.cubesCompleted++;
|
|
136
|
+
const cube = data.cube || data;
|
|
137
|
+
this.cubes.push(cube);
|
|
138
|
+
this.logger.ledger('cube_complete', {
|
|
139
|
+
cubeId: cube.id || data.cubeId,
|
|
140
|
+
level: cube.level || data.level || 1,
|
|
141
|
+
faceCount: cube.faces?.size || cube.faces?.length || 0,
|
|
142
|
+
timestamp: cube.timestamp || data.timestamp
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
this.modules.xclt.on('supercube:complete', (data) => {
|
|
147
|
+
this.metrics.superCubesCompleted++;
|
|
148
|
+
const superCube = data.superCube || data;
|
|
149
|
+
this.superCubes.push(superCube);
|
|
150
|
+
this.logger.ledger('supercube_complete', {
|
|
151
|
+
superCubeId: superCube.id || data.superCubeId,
|
|
152
|
+
level: superCube.level || data.level,
|
|
153
|
+
cubeCount: superCube.cubes?.size || superCube.cubes?.length || 0,
|
|
154
|
+
timestamp: superCube.timestamp || data.timestamp
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
this.logger.system('module_connected', { module: '@xmbl/cubic-ledger', status: 'connected' });
|
|
159
|
+
} catch (error) {
|
|
160
|
+
this.logger.system('module_unavailable', { module: '@xmbl/cubic-ledger', reason: error.message });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Try to import and initialize XVSM (State Machine)
|
|
164
|
+
try {
|
|
165
|
+
const { StateMachine } = await import('../../state-machine/index.js');
|
|
166
|
+
this.modules.xvsm = new StateMachine({
|
|
167
|
+
dbPath: './data/xsim/xvsm',
|
|
168
|
+
xclt: this.modules.xclt
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// StateMachine doesn't extend EventEmitter, so we'll track state through ledger events
|
|
172
|
+
// State diffs are processed automatically when blocks with state_diff type are added
|
|
173
|
+
this.logger.system('module_connected', { module: '@xmbl/state-machine', status: 'connected' });
|
|
174
|
+
|
|
175
|
+
// Periodically check state machine statistics
|
|
176
|
+
setInterval(() => {
|
|
177
|
+
if (this.modules.xvsm) {
|
|
178
|
+
try {
|
|
179
|
+
const stats = this.modules.xvsm.getStatistics();
|
|
180
|
+
this.logger.stateMachine('statistics', {
|
|
181
|
+
totalTransactions: stats.totalTransactions,
|
|
182
|
+
totalDiffs: stats.totalDiffs,
|
|
183
|
+
stateRoot: stats.stateRoot,
|
|
184
|
+
shardCount: stats.shards.length
|
|
185
|
+
});
|
|
186
|
+
} catch (error) {
|
|
187
|
+
// Ignore errors
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}, 10000); // Every 10 seconds
|
|
191
|
+
} catch (error) {
|
|
192
|
+
this.logger.system('module_unavailable', { module: '@xmbl/state-machine', reason: error.message });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Try to import and initialize XPC (Consensus)
|
|
196
|
+
try {
|
|
197
|
+
const { ConsensusWorkflow } = await import('../../consensus/index.js');
|
|
198
|
+
this.modules.xpc = new ConsensusWorkflow({
|
|
199
|
+
dbPath: './data/xsim/xpc',
|
|
200
|
+
xid: this.modules.xid,
|
|
201
|
+
xclt: this.modules.xclt,
|
|
202
|
+
xn: this.modules.xn,
|
|
203
|
+
// Pass identity lookup function so xpc can verify signatures
|
|
204
|
+
getPublicKeyByAddress: (address) => {
|
|
205
|
+
const identity = this.identities.find(id => id.address === address);
|
|
206
|
+
return identity ? identity.publicKey : null;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Listen to consensus events
|
|
211
|
+
this.modules.xpc.on('raw_tx:added', (data) => {
|
|
212
|
+
this.logger.consensus('raw_tx_added', {
|
|
213
|
+
leaderId: data.leaderId,
|
|
214
|
+
rawTxId: data.rawTxId
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
this.modules.xpc.on('validation_tasks:created', (data) => {
|
|
219
|
+
this.logger.consensus('validation_tasks_created', {
|
|
220
|
+
rawTxId: data.rawTxId,
|
|
221
|
+
taskCount: data.tasks.length
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
this.modules.xpc.on('validation:complete', (data) => {
|
|
226
|
+
this.logger.consensus('validation_complete', {
|
|
227
|
+
rawTxId: data.rawTxId,
|
|
228
|
+
taskId: data.taskId,
|
|
229
|
+
validatorId: data.validatorId
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
this.modules.xpc.on('tx:finalized', (data) => {
|
|
234
|
+
this.metrics.transactionsValidated++;
|
|
235
|
+
this.logger.consensus('tx_finalized', {
|
|
236
|
+
rawTxId: data.rawTxId,
|
|
237
|
+
txId: data.txData.id
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Simulate validation completion for transactions
|
|
242
|
+
this.modules.xpc.on('validation_tasks:created', async (data) => {
|
|
243
|
+
console.log('[XSIM] ===== VALIDATION TASKS CREATED - AUTO-COMPLETING =====');
|
|
244
|
+
console.log('[XSIM] rawTxId:', data.rawTxId);
|
|
245
|
+
console.log('[XSIM] tasks count:', data.tasks ? data.tasks.length : 0);
|
|
246
|
+
|
|
247
|
+
if (data.tasks && Array.isArray(data.tasks) && data.tasks.length > 0) {
|
|
248
|
+
for (const task of data.tasks) {
|
|
249
|
+
setTimeout(async () => {
|
|
250
|
+
if (this.modules.xpc) {
|
|
251
|
+
try {
|
|
252
|
+
// Find the validator identity by leaderId (which is an identity address)
|
|
253
|
+
const validatorIdentity = this.identities.find(id => id.address === task.leaderId);
|
|
254
|
+
if (!validatorIdentity) {
|
|
255
|
+
console.error(`[XSIM] Validator identity not found for leaderId: ${task.leaderId}`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.log(`[XSIM] Completing validation for rawTxId: ${data.rawTxId}, taskId: ${task.task}, validator: ${task.leaderId}`);
|
|
260
|
+
|
|
261
|
+
// Sign validation if we have the validator identity
|
|
262
|
+
let validationSignature = null;
|
|
263
|
+
if (validatorIdentity && validatorIdentity.privateKey) {
|
|
264
|
+
try {
|
|
265
|
+
// Create Identity instance if needed
|
|
266
|
+
let identity = validatorIdentity;
|
|
267
|
+
if (!validatorIdentity.signTransaction || typeof validatorIdentity.signTransaction !== 'function') {
|
|
268
|
+
const { Identity } = await import('../../identity/index.js');
|
|
269
|
+
identity = new Identity(validatorIdentity.publicKey, validatorIdentity.privateKey);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Sign validation message (rawTxId + taskId)
|
|
273
|
+
const validationMessage = { rawTxId: data.rawTxId, taskId: task.task };
|
|
274
|
+
const signedValidation = await identity.signTransaction(validationMessage);
|
|
275
|
+
validationSignature = signedValidation.sig;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error('[XSIM] Failed to sign validation:', error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
await this.modules.xpc.completeValidation(
|
|
282
|
+
data.rawTxId,
|
|
283
|
+
task.task,
|
|
284
|
+
process.hrtime.bigint(), // timestamp
|
|
285
|
+
validationSignature, // signature from validator identity
|
|
286
|
+
task.leaderId // validatorId (identity address)
|
|
287
|
+
);
|
|
288
|
+
console.log(`[XSIM] Validation completed successfully for task: ${task.task}`);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error('[XSIM] Failed to complete validation:', error);
|
|
291
|
+
console.error('[XSIM] Error stack:', error.stack);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}, 50 + Math.random() * 100); // Random delay 50-150ms per task
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Auto-finalize transactions after they move to processing
|
|
300
|
+
this.modules.xpc.on('tx:processing', async (data) => {
|
|
301
|
+
setTimeout(async () => {
|
|
302
|
+
if (this.modules.xpc) {
|
|
303
|
+
try {
|
|
304
|
+
// Use validatedHash (txId) to finalize
|
|
305
|
+
const result = await this.modules.xpc.finalizeTransaction(data.txId);
|
|
306
|
+
if (result) {
|
|
307
|
+
console.log(`[XSIM] Auto-finalized transaction: ${data.txId}`);
|
|
308
|
+
} else {
|
|
309
|
+
console.warn(`[XSIM] Failed to finalize transaction: ${data.txId}`);
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error('[XSIM] Finalization error:', error);
|
|
313
|
+
this.logger.error('consensus', 'finalization_failed', error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}, 100); // Small delay before finalization
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
this.logger.system('module_connected', { module: '@xmbl/consensus', status: 'connected' });
|
|
320
|
+
} catch (error) {
|
|
321
|
+
this.logger.system('module_unavailable', { module: '@xmbl/consensus', reason: error.message });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Try to import and initialize XSC (Storage and Compute)
|
|
325
|
+
try {
|
|
326
|
+
const { StorageNode, ComputeRuntime } = await import('../../storage-compute/index.js');
|
|
327
|
+
this.modules.xsc = {
|
|
328
|
+
StorageNode: StorageNode,
|
|
329
|
+
ComputeRuntime: ComputeRuntime
|
|
330
|
+
};
|
|
331
|
+
this.logger.system('module_connected', { module: '@xmbl/storage-compute', status: 'connected' });
|
|
332
|
+
} catch (error) {
|
|
333
|
+
this.logger.system('module_unavailable', { module: '@xmbl/storage-compute', reason: error.message });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Create an identity using real module if available, otherwise simulate
|
|
339
|
+
*/
|
|
340
|
+
async createIdentity() {
|
|
341
|
+
let identity;
|
|
342
|
+
|
|
343
|
+
if (this.modules.xid) {
|
|
344
|
+
try {
|
|
345
|
+
identity = await this.modules.xid.create();
|
|
346
|
+
this.logger.identity('identity_created', {
|
|
347
|
+
address: identity.address,
|
|
348
|
+
publicKey: identity.publicKey?.substring(0, 20) + '...',
|
|
349
|
+
source: 'real_module'
|
|
350
|
+
});
|
|
351
|
+
} catch (error) {
|
|
352
|
+
this.logger.error('identity', 'creation_failed', error);
|
|
353
|
+
// Fall back to simulated identity
|
|
354
|
+
identity = this._createSimulatedIdentity();
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
identity = this._createSimulatedIdentity();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
this.identities.push(identity);
|
|
361
|
+
this.metrics.identitiesCreated++;
|
|
362
|
+
|
|
363
|
+
// If we have network module, add as node
|
|
364
|
+
if (this.modules.xn && this.modules.xn.XNNode) {
|
|
365
|
+
try {
|
|
366
|
+
// In real implementation, would create XNNode here
|
|
367
|
+
this.logger.network('node_added', {
|
|
368
|
+
identityAddress: identity.address,
|
|
369
|
+
source: 'identity_creation'
|
|
370
|
+
});
|
|
371
|
+
} catch (error) {
|
|
372
|
+
// Ignore network errors
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this.emit('identity:created', identity);
|
|
377
|
+
return identity;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
_createSimulatedIdentity() {
|
|
381
|
+
const identity = {
|
|
382
|
+
address: faker.string.alphanumeric({ length: 40 }),
|
|
383
|
+
publicKey: faker.string.alphanumeric({ length: 64 }),
|
|
384
|
+
privateKey: faker.string.alphanumeric({ length: 64 }),
|
|
385
|
+
createdAt: Date.now()
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
this.logger.identity('identity_created', {
|
|
389
|
+
address: identity.address,
|
|
390
|
+
publicKey: identity.publicKey.substring(0, 20) + '...',
|
|
391
|
+
source: 'simulated'
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
return identity;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Create and submit a transaction
|
|
399
|
+
*/
|
|
400
|
+
async createTransaction() {
|
|
401
|
+
if (this.identities.length < 2) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const from = chance.pickone(this.identities);
|
|
406
|
+
const to = chance.pickone(this.identities.filter(i => i.address !== from.address));
|
|
407
|
+
|
|
408
|
+
const txType = chance.pickone(['utxo', 'token_creation', 'contract', 'state_diff']);
|
|
409
|
+
const tx = {
|
|
410
|
+
id: `tx_${Date.now()}_${chance.string({ length: 8 })}`,
|
|
411
|
+
type: txType,
|
|
412
|
+
from: from.address,
|
|
413
|
+
to: to.address,
|
|
414
|
+
amount: parseFloat(chance.floating({ min: 0.1, max: 1000, fixed: 2 })),
|
|
415
|
+
fee: parseFloat(chance.floating({ min: 0.01, max: 10, fixed: 2 })),
|
|
416
|
+
stake: parseFloat(chance.floating({ min: 0.1, max: 100, fixed: 2 })),
|
|
417
|
+
timestamp: Date.now(),
|
|
418
|
+
data: faker.lorem.sentence()
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// Add required fields based on transaction type
|
|
422
|
+
if (txType === 'token_creation') {
|
|
423
|
+
tx.creator = from.address;
|
|
424
|
+
tx.tokenId = faker.string.alphanumeric({ length: 32 });
|
|
425
|
+
} else if (txType === 'contract') {
|
|
426
|
+
tx.contractHash = faker.string.alphanumeric({ length: 64 });
|
|
427
|
+
tx.abi = JSON.stringify([{ type: 'function', name: 'test' }]);
|
|
428
|
+
} else if (txType === 'state_diff') {
|
|
429
|
+
tx.function = faker.hacker.verb() + '_' + faker.hacker.noun();
|
|
430
|
+
tx.args = { key: faker.string.alphanumeric({ length: 20 }) };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Sign transaction if we have identity module
|
|
434
|
+
if (this.modules.xid && from.privateKey) {
|
|
435
|
+
try {
|
|
436
|
+
// If from is already an Identity instance, use it directly
|
|
437
|
+
let identity = from;
|
|
438
|
+
if (!from.signTransaction || typeof from.signTransaction !== 'function') {
|
|
439
|
+
// Create Identity instance from privateKey/publicKey
|
|
440
|
+
const { Identity } = await import('../../identity/index.js');
|
|
441
|
+
identity = new Identity(from.publicKey, from.privateKey);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Ensure from address is set (derived from public key)
|
|
445
|
+
tx.from = identity.address;
|
|
446
|
+
|
|
447
|
+
// Sign transaction (this adds sig field)
|
|
448
|
+
const signedTx = await identity.signTransaction(tx);
|
|
449
|
+
tx.sig = signedTx.sig;
|
|
450
|
+
// DO NOT include publicKey - only address in from field
|
|
451
|
+
} catch (error) {
|
|
452
|
+
console.error('[XSIM] Transaction signing failed:', error);
|
|
453
|
+
// If signing fails, continue without signature
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
this.transactions.push(tx);
|
|
458
|
+
this.metrics.transactionsCreated++;
|
|
459
|
+
|
|
460
|
+
this.logger.transaction('transaction_created', {
|
|
461
|
+
txId: tx.id,
|
|
462
|
+
type: tx.type,
|
|
463
|
+
from: tx.from.substring(0, 10) + '...',
|
|
464
|
+
to: tx.to.substring(0, 10) + '...',
|
|
465
|
+
amount: tx.amount
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Submit to consensus if available
|
|
469
|
+
if (this.modules.xpc && this.leaders.length > 0) {
|
|
470
|
+
try {
|
|
471
|
+
const leaderId = chance.pickone(this.leaders);
|
|
472
|
+
await this.modules.xpc.submitTransaction(leaderId, tx);
|
|
473
|
+
this.logger.consensus('transaction_submitted', {
|
|
474
|
+
txId: tx.id,
|
|
475
|
+
leaderId
|
|
476
|
+
});
|
|
477
|
+
} catch (error) {
|
|
478
|
+
this.logger.error('consensus', 'transaction_submission_failed', error);
|
|
479
|
+
}
|
|
480
|
+
} else if (this.modules.xclt) {
|
|
481
|
+
// If no consensus, add directly to ledger
|
|
482
|
+
try {
|
|
483
|
+
await this.modules.xclt.addTransaction(tx);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
this.logger.error('ledger', 'transaction_addition_failed', error);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
this.emit('transaction:created', tx);
|
|
490
|
+
return tx;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Create a state diff transaction
|
|
495
|
+
*/
|
|
496
|
+
async createStateDiff() {
|
|
497
|
+
if (this.identities.length === 0) return;
|
|
498
|
+
|
|
499
|
+
const from = chance.pickone(this.identities);
|
|
500
|
+
const changes = {
|
|
501
|
+
[faker.string.alphanumeric({ length: 20 })]: faker.string.alphanumeric({ length: 50 }),
|
|
502
|
+
[faker.string.alphanumeric({ length: 20 })]: faker.string.alphanumeric({ length: 50 })
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// Create a state_diff type transaction
|
|
506
|
+
const tx = {
|
|
507
|
+
id: `tx_state_${Date.now()}_${chance.string({ length: 8 })}`,
|
|
508
|
+
type: 'state_diff',
|
|
509
|
+
from: from.address,
|
|
510
|
+
function: faker.hacker.verb() + '_' + faker.hacker.noun(),
|
|
511
|
+
args: changes, // State diff changes go in args (required field)
|
|
512
|
+
timestamp: Date.now()
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// Sign transaction if we have identity module
|
|
516
|
+
if (this.modules.xid && from.privateKey) {
|
|
517
|
+
try {
|
|
518
|
+
// If from is already an Identity instance, use it directly
|
|
519
|
+
let identity = from;
|
|
520
|
+
if (!from.signTransaction || typeof from.signTransaction !== 'function') {
|
|
521
|
+
// Create Identity instance from privateKey/publicKey
|
|
522
|
+
const { Identity } = await import('../../identity/index.js');
|
|
523
|
+
identity = new Identity(from.publicKey, from.privateKey);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Ensure from address is set (derived from public key)
|
|
527
|
+
tx.from = identity.address;
|
|
528
|
+
|
|
529
|
+
// Sign transaction (this adds sig field)
|
|
530
|
+
const signedTx = await identity.signTransaction(tx);
|
|
531
|
+
tx.sig = signedTx.sig;
|
|
532
|
+
// DO NOT include publicKey - only address in from field
|
|
533
|
+
} catch (error) {
|
|
534
|
+
console.error('[XSIM] State diff signing failed:', error);
|
|
535
|
+
// If signing fails, continue without signature
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
this.metrics.stateDiffsCreated++;
|
|
540
|
+
this.logger.stateMachine('state_diff_created', {
|
|
541
|
+
txId: tx.id,
|
|
542
|
+
changeCount: Object.keys(changes).length,
|
|
543
|
+
source: 'transaction'
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Submit to consensus or ledger
|
|
547
|
+
if (this.modules.xpc && this.leaders.length > 0) {
|
|
548
|
+
try {
|
|
549
|
+
const leaderId = chance.pickone(this.leaders);
|
|
550
|
+
await this.modules.xpc.submitTransaction(leaderId, tx);
|
|
551
|
+
this.logger.consensus('state_diff_submitted', {
|
|
552
|
+
txId: tx.id,
|
|
553
|
+
leaderId
|
|
554
|
+
});
|
|
555
|
+
} catch (error) {
|
|
556
|
+
this.logger.error('consensus', 'state_diff_submission_failed', error);
|
|
557
|
+
}
|
|
558
|
+
} else if (this.modules.xclt) {
|
|
559
|
+
// If no consensus, add directly to ledger
|
|
560
|
+
try {
|
|
561
|
+
await this.modules.xclt.addTransaction(tx);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
this.logger.error('ledger', 'state_diff_addition_failed', error);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
this.emit('state:diff:created', { txId: tx.id, changes });
|
|
568
|
+
return tx;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Simulate storage operation
|
|
573
|
+
*/
|
|
574
|
+
async simulateStorageOperation() {
|
|
575
|
+
const operation = {
|
|
576
|
+
type: chance.pickone(['store', 'retrieve', 'delete']),
|
|
577
|
+
key: faker.string.alphanumeric({ length: 32 }),
|
|
578
|
+
size: chance.integer({ min: 100, max: 10000 }),
|
|
579
|
+
timestamp: Date.now()
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
this.metrics.storageOperations++;
|
|
583
|
+
this.logger.storage('operation', {
|
|
584
|
+
type: operation.type,
|
|
585
|
+
key: operation.key.substring(0, 10) + '...',
|
|
586
|
+
size: operation.size
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
this.emit('storage:operation', operation);
|
|
590
|
+
return operation;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Simulate compute operation
|
|
595
|
+
*/
|
|
596
|
+
async simulateComputeOperation() {
|
|
597
|
+
const operation = {
|
|
598
|
+
functionName: `${faker.hacker.verb()}_${faker.hacker.noun()}`,
|
|
599
|
+
duration: chance.normal({ mean: 100, dev: 20 }),
|
|
600
|
+
memory: chance.integer({ min: 10, max: 100 }),
|
|
601
|
+
timestamp: Date.now()
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
this.metrics.computeOperations++;
|
|
605
|
+
this.logger.compute('operation', {
|
|
606
|
+
functionName: operation.functionName,
|
|
607
|
+
duration: Math.max(0, operation.duration),
|
|
608
|
+
memory: operation.memory
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
this.emit('compute:operation', operation);
|
|
612
|
+
|
|
613
|
+
// Compute operations can trigger state changes. This used to call a fake
|
|
614
|
+
// xvsm.executeTransaction with a comment string as "WASM"; that path is gone (real
|
|
615
|
+
// contract execution is @xmbl/contracts' ContractHost over @xmbl/storage-compute). The
|
|
616
|
+
// simulator's honest job here is just to drive a state change, so it uses the real
|
|
617
|
+
// state-diff path directly.
|
|
618
|
+
if (this.modules.xvsm && chance.bool({ likelihood: 70 })) {
|
|
619
|
+
setTimeout(() => {
|
|
620
|
+
this.createStateDiff().catch(e => {
|
|
621
|
+
this.logger.error('stateMachine', 'compute_triggered_state_error', e);
|
|
622
|
+
});
|
|
623
|
+
}, Math.max(50, operation.duration));
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return operation;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Start the simulator
|
|
631
|
+
*/
|
|
632
|
+
async start() {
|
|
633
|
+
if (this.running) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
this.logger.system('starting', { options: this.options });
|
|
638
|
+
this.running = true;
|
|
639
|
+
this.metrics.startTime = Date.now();
|
|
640
|
+
|
|
641
|
+
// Initialize modules
|
|
642
|
+
await this.initializeModules();
|
|
643
|
+
|
|
644
|
+
// Create initial identities
|
|
645
|
+
this.logger.system('creating_initial_identities', { count: this.options.initialIdentities });
|
|
646
|
+
for (let i = 0; i < this.options.initialIdentities; i++) {
|
|
647
|
+
await this.createIdentity();
|
|
648
|
+
// Small delay between identity creation
|
|
649
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Set up leaders for consensus (use first few identities)
|
|
653
|
+
this.leaders = this.identities.slice(0, Math.min(5, this.identities.length)).map(id => id.address);
|
|
654
|
+
this.logger.consensus('leaders_established', {
|
|
655
|
+
leaderCount: this.leaders.length,
|
|
656
|
+
leaders: this.leaders.map(l => l.substring(0, 10) + '...')
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// Update xpc with actual validation leaders (identity addresses)
|
|
660
|
+
if (this.modules.xpc && this.leaders.length >= 3) {
|
|
661
|
+
this.modules.xpc.validationLeaders = this.leaders.slice(0, 3);
|
|
662
|
+
console.log('[XSIM] Set validation leaders in xpc:', this.modules.xpc.validationLeaders);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Start transaction generation
|
|
666
|
+
const txInterval = setInterval(() => {
|
|
667
|
+
this.createTransaction().catch(err => {
|
|
668
|
+
this.logger.error('transaction', 'creation_error', err);
|
|
669
|
+
});
|
|
670
|
+
}, 1000 / this.options.transactionRate);
|
|
671
|
+
this.intervals.push(txInterval);
|
|
672
|
+
|
|
673
|
+
// Start state diff generation
|
|
674
|
+
const stateDiffInterval = setInterval(() => {
|
|
675
|
+
this.createStateDiff().catch(err => {
|
|
676
|
+
this.logger.error('stateMachine', 'state_diff_error', err);
|
|
677
|
+
});
|
|
678
|
+
}, 1000 / this.options.stateDiffRate);
|
|
679
|
+
this.intervals.push(stateDiffInterval);
|
|
680
|
+
|
|
681
|
+
// Start storage operations (varied rate with randomness)
|
|
682
|
+
const storageInterval = setInterval(() => {
|
|
683
|
+
const delay = (1000 / this.options.storageOpRate) * (0.7 + Math.random() * 0.6); // ±30% variation
|
|
684
|
+
setTimeout(() => {
|
|
685
|
+
this.simulateStorageOperation().catch(err => {
|
|
686
|
+
this.logger.error('storage', 'operation_error', err);
|
|
687
|
+
});
|
|
688
|
+
}, delay);
|
|
689
|
+
}, 1000 / this.options.storageOpRate);
|
|
690
|
+
this.intervals.push(storageInterval);
|
|
691
|
+
|
|
692
|
+
// Start compute operations (varied rate with randomness, slightly different base rate)
|
|
693
|
+
const computeBaseRate = this.options.computeOpRate * (0.8 + Math.random() * 0.4); // Vary base rate
|
|
694
|
+
const computeInterval = setInterval(() => {
|
|
695
|
+
const delay = (1000 / computeBaseRate) * (0.7 + Math.random() * 0.6); // ±30% variation
|
|
696
|
+
setTimeout(() => {
|
|
697
|
+
this.simulateComputeOperation().catch(err => {
|
|
698
|
+
this.logger.error('compute', 'operation_error', err);
|
|
699
|
+
});
|
|
700
|
+
}, delay);
|
|
701
|
+
}, 1000 / computeBaseRate);
|
|
702
|
+
this.intervals.push(computeInterval);
|
|
703
|
+
|
|
704
|
+
// Periodic state assembly (simulate app-centric state assembly)
|
|
705
|
+
const stateAssemblyInterval = setInterval(() => {
|
|
706
|
+
if (this.modules.xvsm && this.stateDiffs.length > 0) {
|
|
707
|
+
try {
|
|
708
|
+
// Assemble state from diffs
|
|
709
|
+
const state = this.modules.xvsm.getState();
|
|
710
|
+
this.metrics.stateAssemblies++;
|
|
711
|
+
this.logger.stateMachine('state_assembled', {
|
|
712
|
+
keyCount: Object.keys(state).length,
|
|
713
|
+
diffCount: this.modules.xvsm.diffs.length,
|
|
714
|
+
stateRoot: this.modules.xvsm.getStateRoot()
|
|
715
|
+
});
|
|
716
|
+
} catch (error) {
|
|
717
|
+
// Ignore errors
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}, 15000); // Every 15 seconds
|
|
721
|
+
this.intervals.push(stateAssemblyInterval);
|
|
722
|
+
|
|
723
|
+
// Periodic metrics update
|
|
724
|
+
const metricsInterval = setInterval(() => {
|
|
725
|
+
this.metrics.uptime = Date.now() - this.metrics.startTime;
|
|
726
|
+
this.logger.system('metrics_update', { ...this.metrics });
|
|
727
|
+
this.emit('metrics:update', { ...this.metrics });
|
|
728
|
+
}, 5000);
|
|
729
|
+
this.intervals.push(metricsInterval);
|
|
730
|
+
|
|
731
|
+
this.logger.system('started', {
|
|
732
|
+
identities: this.identities.length,
|
|
733
|
+
transactionRate: this.options.transactionRate,
|
|
734
|
+
stateDiffRate: this.options.stateDiffRate
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
this.emit('started');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Pause the simulator (keeps state, can be resumed)
|
|
742
|
+
*/
|
|
743
|
+
pause() {
|
|
744
|
+
if (!this.running) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
this.logger.system('pausing', {});
|
|
749
|
+
this.running = false;
|
|
750
|
+
|
|
751
|
+
// Clear intervals but keep all state
|
|
752
|
+
this.intervals.forEach(interval => clearInterval(interval));
|
|
753
|
+
this.intervals = [];
|
|
754
|
+
|
|
755
|
+
this.logger.system('paused', {
|
|
756
|
+
metrics: { ...this.metrics }
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
this.emit('paused');
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Resume the simulator from paused state
|
|
764
|
+
*/
|
|
765
|
+
resume() {
|
|
766
|
+
if (this.running) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
this.logger.system('resuming', {});
|
|
771
|
+
this.running = true;
|
|
772
|
+
|
|
773
|
+
// Restart transaction generation
|
|
774
|
+
const txInterval = setInterval(() => {
|
|
775
|
+
this.createTransaction().catch(err => {
|
|
776
|
+
this.logger.error('transaction', 'creation_error', err);
|
|
777
|
+
});
|
|
778
|
+
}, 1000 / this.options.transactionRate);
|
|
779
|
+
this.intervals.push(txInterval);
|
|
780
|
+
|
|
781
|
+
// Restart state diff generation
|
|
782
|
+
const stateDiffInterval = setInterval(() => {
|
|
783
|
+
this.createStateDiff().catch(err => {
|
|
784
|
+
this.logger.error('stateMachine', 'state_diff_error', err);
|
|
785
|
+
});
|
|
786
|
+
}, 1000 / this.options.stateDiffRate);
|
|
787
|
+
this.intervals.push(stateDiffInterval);
|
|
788
|
+
|
|
789
|
+
// Restart storage operations
|
|
790
|
+
const storageInterval = setInterval(() => {
|
|
791
|
+
this.simulateStorageOperation().catch(err => {
|
|
792
|
+
this.logger.error('storage', 'operation_error', err);
|
|
793
|
+
});
|
|
794
|
+
}, 1000 / this.options.storageOpRate);
|
|
795
|
+
this.intervals.push(storageInterval);
|
|
796
|
+
|
|
797
|
+
// Restart compute operations
|
|
798
|
+
const computeInterval = setInterval(() => {
|
|
799
|
+
this.simulateComputeOperation().catch(err => {
|
|
800
|
+
this.logger.error('compute', 'operation_error', err);
|
|
801
|
+
});
|
|
802
|
+
}, 1000 / this.options.computeOpRate);
|
|
803
|
+
this.intervals.push(computeInterval);
|
|
804
|
+
|
|
805
|
+
this.logger.system('resumed', {});
|
|
806
|
+
this.emit('resumed');
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Stop the simulator (completely stops, different from pause)
|
|
811
|
+
*/
|
|
812
|
+
stop() {
|
|
813
|
+
if (!this.running) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
this.logger.system('stopping', {});
|
|
818
|
+
this.running = false;
|
|
819
|
+
|
|
820
|
+
this.intervals.forEach(interval => clearInterval(interval));
|
|
821
|
+
this.intervals = [];
|
|
822
|
+
|
|
823
|
+
this.logger.system('stopped', {
|
|
824
|
+
finalMetrics: { ...this.metrics }
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
this.emit('stopped');
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Get current metrics
|
|
832
|
+
*/
|
|
833
|
+
getMetrics() {
|
|
834
|
+
return {
|
|
835
|
+
...this.metrics,
|
|
836
|
+
uptime: this.running ? Date.now() - this.metrics.startTime : this.metrics.uptime
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Check if simulator is running
|
|
842
|
+
*/
|
|
843
|
+
isRunning() {
|
|
844
|
+
return this.running;
|
|
845
|
+
}
|
|
846
|
+
}
|