@leofcoin/chain 1.8.30 → 1.9.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/exports/browser/chain.js +726 -188
- package/exports/browser/{constants-BKKQytjd.js → constants-BTdMMS4w.js} +172 -132
- package/exports/browser/node-browser.js +1 -1
- package/exports/browser/workers/block-worker.js +1 -1
- package/exports/browser/workers/machine-worker.js +2 -2
- package/exports/browser/workers/{worker-iOnLaHA--iOnLaHA-.js → worker-BrtyXRJ7-BrtyXRJ7.js} +287 -208
- package/exports/chain.js +608 -109
- package/exports/constants.d.ts +1 -0
- package/exports/version-control.d.ts +1 -0
- package/exports/workers/block-worker.js +1 -1
- package/exports/workers/machine-worker.js +2 -2
- package/exports/workers/{worker-iOnLaHA--iOnLaHA-.js → worker-BrtyXRJ7-BrtyXRJ7.js} +287 -208
- package/package.json +5 -5
package/exports/browser/chain.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as toBase58, T as TransactionMessage, C as ContractMessage, R as RawTransactionMessage, B as BlockMessage, u as utils, L as LastBlockMessage, P as PROTOCOL_VERSION, a as REACHED_ONE_ZERO_ZERO, b as BWMessage, c as BWRequestMessage } from './constants-
|
|
1
|
+
import { t as toBase58, T as TransactionMessage, C as ContractMessage, R as RawTransactionMessage, B as BlockMessage, u as utils, L as LastBlockMessage, P as PROTOCOL_VERSION, a as REACHED_ONE_ZERO_ZERO, b as BWMessage, c as BWRequestMessage } from './constants-BTdMMS4w.js';
|
|
2
2
|
import { log } from 'console';
|
|
3
3
|
import { log as log$1 } from 'node:console';
|
|
4
4
|
|
|
@@ -964,7 +964,7 @@ function parseUnits(value, unit) {
|
|
|
964
964
|
}
|
|
965
965
|
|
|
966
966
|
const jsonStringifyBigInt = (key, value) => (typeof value === 'bigint' ? { $bigint: value.toString() } : value);
|
|
967
|
-
const jsonParseBigInt = (key, value) => typeof value === 'object' && value
|
|
967
|
+
const jsonParseBigInt = (key, value) => typeof value === 'object' && value?.$bigint ? BigInt(value.$bigint) : value;
|
|
968
968
|
|
|
969
969
|
const byteFormats = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
970
970
|
const formatBytes = (bytes, decimals = 2) => {
|
|
@@ -3921,8 +3921,6 @@ class Transaction extends Protocol {
|
|
|
3921
3921
|
transactions = await this.promiseTransactions(transactions);
|
|
3922
3922
|
transactions = transactions.filter((tx) => tx.decoded.from === address);
|
|
3923
3923
|
for (const transaction of transactions) {
|
|
3924
|
-
if (transaction.decoded.nonce > nonce)
|
|
3925
|
-
throw new Error(`a transaction with a higher nonce already exists`);
|
|
3926
3924
|
if (transaction.decoded.nonce === nonce)
|
|
3927
3925
|
throw new Error(`a transaction with the same nonce already exists`);
|
|
3928
3926
|
}
|
|
@@ -4356,82 +4354,121 @@ class Contract extends Transaction {
|
|
|
4356
4354
|
|
|
4357
4355
|
const randombytes = (strength) => crypto.getRandomValues(new Uint8Array(strength));
|
|
4358
4356
|
|
|
4359
|
-
class EasyWorker {
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
this
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4357
|
+
class EasyWorker {
|
|
4358
|
+
#messageEvent = 'message';
|
|
4359
|
+
#errorEvent = 'error';
|
|
4360
|
+
#isBrowser = false;
|
|
4361
|
+
#isWorker = false;
|
|
4362
|
+
#messageHandler = null;
|
|
4363
|
+
#errorHandler = null;
|
|
4364
|
+
worker;
|
|
4365
|
+
get isWorker() {
|
|
4366
|
+
return this.#isWorker;
|
|
4367
|
+
}
|
|
4368
|
+
constructor(url, options) {
|
|
4369
|
+
return this.#init(url, options);
|
|
4370
|
+
}
|
|
4371
|
+
#init(url, options = {}) {
|
|
4372
|
+
if (url) {
|
|
4373
|
+
if (globalThis.Worker) {
|
|
4374
|
+
this.#isBrowser = true;
|
|
4375
|
+
this.worker = new Worker(url, { ...options });
|
|
4376
|
+
}
|
|
4377
|
+
else {
|
|
4378
|
+
return new Promise((resolve, reject) => {
|
|
4379
|
+
import('child_process').then(({ fork }) => {
|
|
4380
|
+
const child = fork(url, ['easy-worker-child'], options);
|
|
4381
|
+
child.once('error', reject);
|
|
4382
|
+
this.worker = child;
|
|
4383
|
+
resolve(this);
|
|
4384
|
+
}).catch(reject);
|
|
4385
|
+
});
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
else {
|
|
4389
|
+
this.#isWorker = true;
|
|
4390
|
+
if (globalThis.process?.argv[2] === 'easy-worker-child') {
|
|
4391
|
+
this.worker = process;
|
|
4392
|
+
}
|
|
4393
|
+
else {
|
|
4394
|
+
this.#isBrowser = true;
|
|
4395
|
+
this.worker = globalThis;
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
return this;
|
|
4399
|
+
}
|
|
4400
|
+
onmessage(fn) {
|
|
4401
|
+
if (this.#isBrowser) {
|
|
4402
|
+
this.worker.onmessage = ({ data }) => fn(data);
|
|
4403
|
+
}
|
|
4404
|
+
else {
|
|
4405
|
+
if (this.#messageHandler)
|
|
4406
|
+
this.worker.off(this.#messageEvent, this.#messageHandler);
|
|
4407
|
+
this.#messageHandler = fn;
|
|
4408
|
+
this.worker.on(this.#messageEvent, fn);
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
postMessage(message) {
|
|
4412
|
+
if (this.#isBrowser)
|
|
4413
|
+
this.worker.postMessage(message);
|
|
4414
|
+
else
|
|
4415
|
+
this.worker.send(message);
|
|
4416
|
+
}
|
|
4417
|
+
terminate() {
|
|
4418
|
+
if (this.#isBrowser)
|
|
4419
|
+
this.worker.terminate();
|
|
4420
|
+
else
|
|
4421
|
+
this.worker.kill();
|
|
4422
|
+
}
|
|
4423
|
+
onerror(fn) {
|
|
4424
|
+
if (this.#isBrowser) {
|
|
4425
|
+
this.worker.onerror = fn;
|
|
4426
|
+
}
|
|
4427
|
+
else {
|
|
4428
|
+
if (this.#errorHandler)
|
|
4429
|
+
this.worker.off(this.#errorEvent, this.#errorHandler);
|
|
4430
|
+
this.#errorHandler = fn;
|
|
4431
|
+
this.worker.on(this.#errorEvent, fn);
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
/**
|
|
4435
|
+
*
|
|
4436
|
+
* @param {*} data
|
|
4437
|
+
* @returns {Promise} resolves result onmessage & rejects on error
|
|
4438
|
+
*/
|
|
4439
|
+
once(data) {
|
|
4440
|
+
return new Promise((resolve, reject) => {
|
|
4441
|
+
if (this.#isBrowser) {
|
|
4442
|
+
const handleMessage = ({ data: result }) => {
|
|
4443
|
+
this.worker.onerror = null;
|
|
4444
|
+
resolve(result);
|
|
4445
|
+
this.terminate();
|
|
4446
|
+
};
|
|
4447
|
+
const handleError = (error) => {
|
|
4448
|
+
this.worker.onmessage = null;
|
|
4449
|
+
reject(error);
|
|
4450
|
+
this.terminate();
|
|
4451
|
+
};
|
|
4452
|
+
this.worker.onmessage = handleMessage;
|
|
4453
|
+
this.worker.onerror = handleError;
|
|
4454
|
+
}
|
|
4455
|
+
else {
|
|
4456
|
+
const handleMessage = (result) => {
|
|
4457
|
+
this.worker.off(this.#errorEvent, handleError);
|
|
4458
|
+
resolve(result);
|
|
4459
|
+
this.terminate();
|
|
4460
|
+
};
|
|
4461
|
+
const handleError = (error) => {
|
|
4462
|
+
this.worker.off(this.#messageEvent, handleMessage);
|
|
4463
|
+
reject(error);
|
|
4464
|
+
this.terminate();
|
|
4465
|
+
};
|
|
4466
|
+
this.worker.once(this.#messageEvent, handleMessage);
|
|
4467
|
+
this.worker.once(this.#errorEvent, handleError);
|
|
4468
|
+
}
|
|
4469
|
+
this.postMessage(data);
|
|
4470
|
+
});
|
|
4471
|
+
}
|
|
4435
4472
|
}
|
|
4436
4473
|
|
|
4437
4474
|
class LeofcoinError extends Error {
|
|
@@ -4630,20 +4667,30 @@ class Machine {
|
|
|
4630
4667
|
}
|
|
4631
4668
|
await Promise.all(promises);
|
|
4632
4669
|
}
|
|
4670
|
+
// Helper to sort object keys for deterministic serialization
|
|
4671
|
+
const sortedStringify = (obj, replacer) => {
|
|
4672
|
+
const sorted = {};
|
|
4673
|
+
const keys = Object.keys(obj).sort();
|
|
4674
|
+
for (const key of keys) {
|
|
4675
|
+
sorted[key] = obj[key];
|
|
4676
|
+
}
|
|
4677
|
+
return JSON.stringify(sorted, replacer);
|
|
4678
|
+
};
|
|
4633
4679
|
const tasks = [
|
|
4634
|
-
stateStore.put('lastBlock',
|
|
4635
|
-
stateStore.put('states',
|
|
4636
|
-
stateStore.put('accounts',
|
|
4637
|
-
stateStore.put('info',
|
|
4680
|
+
stateStore.put('lastBlock', sortedStringify(await this.lastBlock, jsonStringifyBigInt)),
|
|
4681
|
+
stateStore.put('states', sortedStringify(state, jsonStringifyBigInt)),
|
|
4682
|
+
stateStore.put('accounts', sortedStringify(accounts, jsonStringifyBigInt)),
|
|
4683
|
+
stateStore.put('info', sortedStringify({
|
|
4684
|
+
nativeBurnAmount: await this.totalBurnAmount,
|
|
4685
|
+
nativeBurns: await this.nativeBurns,
|
|
4638
4686
|
nativeCalls: await this.nativeCalls,
|
|
4639
4687
|
nativeMints: await this.nativeMints,
|
|
4640
|
-
nativeBurns: await this.nativeBurns,
|
|
4641
4688
|
nativeTransfers: await this.nativeTransfers,
|
|
4642
|
-
|
|
4689
|
+
totalBlocks: await blockStore.length,
|
|
4643
4690
|
totalBurnAmount: await this.totalBurnAmount,
|
|
4644
4691
|
totalMintAmount: await this.totalMintAmount,
|
|
4645
|
-
|
|
4646
|
-
|
|
4692
|
+
totalTransactions: await this.totalTransactions,
|
|
4693
|
+
totalTransferAmount: await this.totalTransferAmount
|
|
4647
4694
|
}, jsonStringifyBigInt))
|
|
4648
4695
|
// accountsStore.clear()
|
|
4649
4696
|
];
|
|
@@ -5499,6 +5546,14 @@ class State extends Contract {
|
|
|
5499
5546
|
debug$1(`Local index ${localIndex} is ahead of remote ${remoteIndex}, skipping sync`);
|
|
5500
5547
|
return;
|
|
5501
5548
|
}
|
|
5549
|
+
// CRITICAL: Prevent DoS from excessive reorgs
|
|
5550
|
+
const MAX_REORG_DEPTH = 6;
|
|
5551
|
+
const reorgDepth = localIndex - remoteIndex;
|
|
5552
|
+
if (reorgDepth > 0 && reorgDepth > MAX_REORG_DEPTH) {
|
|
5553
|
+
console.warn(`[consensus-safety] Peer proposing reorg depth of ${reorgDepth} blocks ` +
|
|
5554
|
+
`(limit is ${MAX_REORG_DEPTH}). Rejecting to prevent DoS.`);
|
|
5555
|
+
throw new Error(`Excessive reorg depth: ${reorgDepth} blocks (max ${MAX_REORG_DEPTH})`);
|
|
5556
|
+
}
|
|
5502
5557
|
// Use state hash comparison: only resolve if remote hash differs from local state hash
|
|
5503
5558
|
if (localStateHash !== remoteBlockHash) {
|
|
5504
5559
|
if (this.wantList.length > 0) {
|
|
@@ -5559,7 +5614,15 @@ class State extends Contract {
|
|
|
5559
5614
|
for (const id in globalThis.peernet.connections) {
|
|
5560
5615
|
// @ts-ignore
|
|
5561
5616
|
const peer = globalThis.peernet.connections[id];
|
|
5562
|
-
|
|
5617
|
+
// CRITICAL FIX: Use semver comparison (major.minor) not exact match
|
|
5618
|
+
const isVersionCompatible = () => {
|
|
5619
|
+
if (!peer.version || !this.version)
|
|
5620
|
+
return false;
|
|
5621
|
+
const [peerMajor, peerMinor] = peer.version.split('.');
|
|
5622
|
+
const [localMajor, localMinor] = this.version.split('.');
|
|
5623
|
+
return peerMajor === localMajor && peerMinor === localMinor;
|
|
5624
|
+
};
|
|
5625
|
+
if (peer.connected && isVersionCompatible()) {
|
|
5563
5626
|
const task = async () => {
|
|
5564
5627
|
try {
|
|
5565
5628
|
const result = await peer.request(node.encoded);
|
|
@@ -5593,7 +5656,15 @@ class State extends Contract {
|
|
|
5593
5656
|
throw new Error('invalid block @getLatestBlock');
|
|
5594
5657
|
latest = { ...message.decoded, hash };
|
|
5595
5658
|
const peer = promises[0].peer;
|
|
5596
|
-
|
|
5659
|
+
// CRITICAL FIX: Check version compatibility using semver
|
|
5660
|
+
const isVersionCompatible = () => {
|
|
5661
|
+
if (!peer.version || !this.version)
|
|
5662
|
+
return false;
|
|
5663
|
+
const [peerMajor, peerMinor] = peer.version.split('.');
|
|
5664
|
+
const [localMajor, localMinor] = this.version.split('.');
|
|
5665
|
+
return peerMajor === localMajor && peerMinor === localMinor;
|
|
5666
|
+
};
|
|
5667
|
+
if (peer.connected && isVersionCompatible()) {
|
|
5597
5668
|
let data = await new globalThis.peernet.protos['peernet-request']({
|
|
5598
5669
|
request: 'knownBlocks'
|
|
5599
5670
|
});
|
|
@@ -5601,7 +5672,11 @@ class State extends Contract {
|
|
|
5601
5672
|
try {
|
|
5602
5673
|
let message = await peer.request(node.encode());
|
|
5603
5674
|
message = await new globalThis.peernet.protos['peernet-response'](message);
|
|
5604
|
-
|
|
5675
|
+
const MAX_WANTLIST_SIZE = 1000;
|
|
5676
|
+
const incoming = message.decoded.response.blocks.filter((block) => !this.knownBlocks.includes(block));
|
|
5677
|
+
const remaining = MAX_WANTLIST_SIZE - this.wantList.length;
|
|
5678
|
+
if (remaining > 0)
|
|
5679
|
+
this.wantList.push(...incoming.slice(0, remaining));
|
|
5605
5680
|
}
|
|
5606
5681
|
catch (error) {
|
|
5607
5682
|
const peerId = peer?.peerId || peer?.id || peer?.address || 'unknown';
|
|
@@ -5832,6 +5907,13 @@ class VersionControl extends State {
|
|
|
5832
5907
|
return this.#setCurrentVersion();
|
|
5833
5908
|
}
|
|
5834
5909
|
}
|
|
5910
|
+
isVersionCompatible(peerVersion) {
|
|
5911
|
+
if (!peerVersion || !this.version)
|
|
5912
|
+
return false;
|
|
5913
|
+
const [peerMajor, peerMinor] = peerVersion.split('.');
|
|
5914
|
+
const [localMajor, localMinor] = this.version.split('.');
|
|
5915
|
+
return peerMajor === localMajor && peerMinor === localMinor;
|
|
5916
|
+
}
|
|
5835
5917
|
}
|
|
5836
5918
|
|
|
5837
5919
|
/**
|
|
@@ -5953,14 +6035,12 @@ class ConnectionMonitor {
|
|
|
5953
6035
|
}
|
|
5954
6036
|
const connectedPeers = this.connectedPeers;
|
|
5955
6037
|
const compatiblePeers = this.compatiblePeers;
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
// If we have no connections or none are compatible, try to reconnect
|
|
5963
|
-
if (connectedPeers.length === 0) {
|
|
6038
|
+
const disconnectedPeers = this.disconnectedPeers;
|
|
6039
|
+
console.log(`🔍 Health check: ${connectedPeers.length} connected, ${compatiblePeers.length} compatible, ${disconnectedPeers.length} negotiating`);
|
|
6040
|
+
// If we have no connections or none are compatible, try to reconnect.
|
|
6041
|
+
// Don't trigger reconnection if peers are still in WebRTC ICE negotiation (disconnectedPeers > 0) —
|
|
6042
|
+
// reinit() would tear down the in-flight handshake.
|
|
6043
|
+
if (connectedPeers.length === 0 && disconnectedPeers.length === 0) {
|
|
5964
6044
|
console.warn('⚠️ No peer connections detected — attempting reconnection');
|
|
5965
6045
|
await this.#attemptReconnection();
|
|
5966
6046
|
}
|
|
@@ -6062,25 +6142,12 @@ class ConnectionMonitor {
|
|
|
6062
6142
|
console.warn(' ⚠️ peernet.start() failed:', e?.message || e);
|
|
6063
6143
|
}
|
|
6064
6144
|
}
|
|
6065
|
-
// Approach 3:
|
|
6066
|
-
if (globalThis.peernet?.client &&
|
|
6067
|
-
'connect' in globalThis.peernet.client &&
|
|
6068
|
-
typeof globalThis.peernet.client.connect === 'function') {
|
|
6069
|
-
console.log(' → Trying client.connect()');
|
|
6070
|
-
try {
|
|
6071
|
-
await globalThis.peernet.client.connect();
|
|
6072
|
-
console.log(' ✅ client.connect() succeeded');
|
|
6073
|
-
}
|
|
6074
|
-
catch (e) {
|
|
6075
|
-
console.warn(' ⚠️ client.connect() failed:', e?.message || e);
|
|
6076
|
-
}
|
|
6077
|
-
}
|
|
6078
|
-
// Approach 4: Explicitly dial star servers if available
|
|
6145
|
+
// Approach 3: Explicitly dial star servers if available (only if client.reinit() didn't succeed)
|
|
6079
6146
|
try {
|
|
6080
6147
|
const networkName = globalThis.peernet?.network;
|
|
6081
6148
|
if (networkName && typeof networkName === 'string') {
|
|
6082
6149
|
// Try to import network config
|
|
6083
|
-
const { default: networks } = await import('./constants-
|
|
6150
|
+
const { default: networks } = await import('./constants-BTdMMS4w.js').then(function (n) { return n.n; });
|
|
6084
6151
|
const [mainKey, subKey] = networkName.split(':');
|
|
6085
6152
|
const networkConfig = networks?.[mainKey]?.[subKey];
|
|
6086
6153
|
if (networkConfig?.stars && Array.isArray(networkConfig.stars)) {
|
|
@@ -6091,11 +6158,6 @@ class ConnectionMonitor {
|
|
|
6091
6158
|
await globalThis.peernet.client.dial(star);
|
|
6092
6159
|
console.log(` ✅ Connected to star server: ${star}`);
|
|
6093
6160
|
}
|
|
6094
|
-
else if (globalThis.peernet?.client && 'connect' in globalThis.peernet.client) {
|
|
6095
|
-
// Try connect with the star URL
|
|
6096
|
-
await globalThis.peernet.client.connect(star);
|
|
6097
|
-
console.log(` ✅ Connected to star server: ${star}`);
|
|
6098
|
-
}
|
|
6099
6161
|
}
|
|
6100
6162
|
catch (e) {
|
|
6101
6163
|
console.warn(` ⚠️ Failed to dial ${star}:`, e?.message || e);
|
|
@@ -6137,14 +6199,13 @@ class ConnectionMonitor {
|
|
|
6137
6199
|
});
|
|
6138
6200
|
}
|
|
6139
6201
|
async #attemptReconnection() {
|
|
6140
|
-
if (this.#reconnecting) {
|
|
6141
|
-
console.log('⏭️ Reconnection already in progress');
|
|
6142
|
-
return;
|
|
6143
|
-
}
|
|
6144
6202
|
try {
|
|
6145
6203
|
await this.#restoreNetwork();
|
|
6146
|
-
// Check if reconnection was successful
|
|
6147
|
-
|
|
6204
|
+
// Check if reconnection was successful.
|
|
6205
|
+
// Treat peers that are still negotiating (in connections but not yet connected) as success —
|
|
6206
|
+
// they were discovered via the star and WebRTC ICE is in progress. Retrying now would call
|
|
6207
|
+
// reinit() again and tear down the in-flight handshake.
|
|
6208
|
+
const hasConnections = this.connectedPeers.length > 0 || this.disconnectedPeers.length > 0;
|
|
6148
6209
|
if (hasConnections) {
|
|
6149
6210
|
console.log('✅ Reconnection successful, resetting backoff delay');
|
|
6150
6211
|
this.#reconnectDelay = 5000;
|
|
@@ -6194,35 +6255,252 @@ class Chain extends VersionControl {
|
|
|
6194
6255
|
#state;
|
|
6195
6256
|
#slotTime;
|
|
6196
6257
|
#blockTime; // 6 second target block time
|
|
6258
|
+
#epochLength; // Blocks per epoch (enables block-based epoch boundaries)
|
|
6197
6259
|
/** {Address[]} */
|
|
6198
6260
|
#validators;
|
|
6199
6261
|
/** {Boolean} */
|
|
6200
6262
|
#runningEpoch;
|
|
6263
|
+
/** Block height at which current epoch started (for block-based epoch timing) */
|
|
6264
|
+
#currentEpochStartHeight;
|
|
6265
|
+
/** {Object} Block cache by index for conflict detection: {index: {hash, ...block}} */
|
|
6266
|
+
#blocks;
|
|
6201
6267
|
#participants;
|
|
6202
6268
|
#participating;
|
|
6203
6269
|
#jail;
|
|
6270
|
+
#jailReleaseTimers;
|
|
6204
6271
|
#peerConnectionRetries;
|
|
6205
6272
|
#maxPeerRetries;
|
|
6206
6273
|
#peerRetryDelay;
|
|
6274
|
+
/** {Map} Peer reputation tracking: {peerId: {score, failures}} */
|
|
6275
|
+
#peerReputations;
|
|
6276
|
+
#minPeerScore;
|
|
6277
|
+
#maxPeerFailures;
|
|
6278
|
+
// ── Tendermint consensus state ──────────────────────────────────────────────
|
|
6279
|
+
/** Current consensus round (increments when proposer is unresponsive) */
|
|
6280
|
+
#consensusRound;
|
|
6281
|
+
/** Timer that advances #consensusRound when the proposer doesn't propose in time */
|
|
6282
|
+
#roundTimer;
|
|
6283
|
+
/** prevotes collected per `height:round:blockHash` key */
|
|
6284
|
+
#prevotes;
|
|
6285
|
+
/** precommits collected per `height:round:blockHash` key */
|
|
6286
|
+
#precommits;
|
|
6287
|
+
/** Index of the last block that reached 2f+1 precommits */
|
|
6288
|
+
#committedHeight;
|
|
6289
|
+
/** Prevents casting duplicate prevote/precommit per height:round */
|
|
6290
|
+
#castedVotes;
|
|
6291
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
6207
6292
|
#connectionMonitor;
|
|
6208
6293
|
constructor(config) {
|
|
6209
6294
|
super(config);
|
|
6210
6295
|
this.#slotTime = 10000;
|
|
6211
6296
|
this.#blockTime = 6000; // 6 second target block time
|
|
6297
|
+
this.#epochLength = 10; // Blocks per epoch (enables block-based epoch boundaries)
|
|
6212
6298
|
this.utils = {};
|
|
6213
6299
|
/** {Address[]} */
|
|
6214
6300
|
this.#validators = [];
|
|
6215
6301
|
/** {Boolean} */
|
|
6216
6302
|
this.#runningEpoch = false;
|
|
6303
|
+
/** Block height at which current epoch started (for block-based epoch timing) */
|
|
6304
|
+
this.#currentEpochStartHeight = 0;
|
|
6305
|
+
/** {Object} Block cache by index for conflict detection: {index: {hash, ...block}} */
|
|
6306
|
+
this.#blocks = {};
|
|
6217
6307
|
this.#participants = [];
|
|
6218
6308
|
this.#participating = false;
|
|
6219
|
-
this.#jail =
|
|
6309
|
+
this.#jail = new Set();
|
|
6310
|
+
this.#jailReleaseTimers = new Map();
|
|
6220
6311
|
this.#peerConnectionRetries = new Map();
|
|
6221
6312
|
this.#maxPeerRetries = 5;
|
|
6222
6313
|
this.#peerRetryDelay = 5000;
|
|
6314
|
+
/** {Map} Peer reputation tracking: {peerId: {score, failures}} */
|
|
6315
|
+
this.#peerReputations = new Map();
|
|
6316
|
+
this.#minPeerScore = -10;
|
|
6317
|
+
this.#maxPeerFailures = 100;
|
|
6318
|
+
// ── Tendermint consensus state ──────────────────────────────────────────────
|
|
6319
|
+
/** Current consensus round (increments when proposer is unresponsive) */
|
|
6320
|
+
this.#consensusRound = 0;
|
|
6321
|
+
/** Timer that advances #consensusRound when the proposer doesn't propose in time */
|
|
6322
|
+
this.#roundTimer = null;
|
|
6323
|
+
/** prevotes collected per `height:round:blockHash` key */
|
|
6324
|
+
this.#prevotes = new Map();
|
|
6325
|
+
/** precommits collected per `height:round:blockHash` key */
|
|
6326
|
+
this.#precommits = new Map();
|
|
6327
|
+
/** Index of the last block that reached 2f+1 precommits */
|
|
6328
|
+
this.#committedHeight = -1;
|
|
6329
|
+
/** Prevents casting duplicate prevote/precommit per height:round */
|
|
6330
|
+
this.#castedVotes = new Set();
|
|
6223
6331
|
this.ready = new Promise((resolve) => {
|
|
6224
6332
|
this.readyResolve = resolve;
|
|
6225
6333
|
});
|
|
6334
|
+
// ── Tendermint consensus handlers ────────────────────────────────────────
|
|
6335
|
+
/**
|
|
6336
|
+
* Publish a prevote or precommit. Idempotent — will not cast the same
|
|
6337
|
+
* vote twice for the same height:round.
|
|
6338
|
+
*/
|
|
6339
|
+
this.#castVote = async (type, blockHash, index, round) => {
|
|
6340
|
+
const voteKey = `${type}:${index}:${round}`;
|
|
6341
|
+
if (this.#castedVotes.has(voteKey))
|
|
6342
|
+
return;
|
|
6343
|
+
this.#castedVotes.add(voteKey);
|
|
6344
|
+
const from = peernet.selectedAccount;
|
|
6345
|
+
const payload = new TextEncoder().encode(JSON.stringify({ blockHash, index, round, from }));
|
|
6346
|
+
try {
|
|
6347
|
+
globalThis.peernet.publish(`consensus:${type}`, payload);
|
|
6348
|
+
}
|
|
6349
|
+
catch (e) {
|
|
6350
|
+
debug(`peernet publish failed: consensus:${type}`, e?.message ?? e);
|
|
6351
|
+
}
|
|
6352
|
+
};
|
|
6353
|
+
/**
|
|
6354
|
+
* Phase 2 — receive a block proposal from the designated proposer.
|
|
6355
|
+
* Validates the proposer is correct for height/round, fetches + validates
|
|
6356
|
+
* the block from peernet, then casts a prevote.
|
|
6357
|
+
*/
|
|
6358
|
+
this.#handleProposal = async (payload) => {
|
|
6359
|
+
try {
|
|
6360
|
+
const msg = JSON.parse(new TextDecoder().decode(payload));
|
|
6361
|
+
const { blockHash, index, round, from } = msg;
|
|
6362
|
+
const validators = await this.#getConsensusValidators(index);
|
|
6363
|
+
const expectedProposerIdx = (index + round) % validators.length;
|
|
6364
|
+
if (!validators[expectedProposerIdx] || validators[expectedProposerIdx] !== from) {
|
|
6365
|
+
debug(`[consensus] Proposal from wrong proposer at height ${index} round ${round}`);
|
|
6366
|
+
return;
|
|
6367
|
+
}
|
|
6368
|
+
const localBlock = await this.lastBlock;
|
|
6369
|
+
const localIndex = localBlock?.index !== undefined ? Number(localBlock.index) : -1;
|
|
6370
|
+
if (index <= localIndex) {
|
|
6371
|
+
debug(`[consensus] Ignoring stale proposal at height ${index} (local: ${localIndex})`);
|
|
6372
|
+
return;
|
|
6373
|
+
}
|
|
6374
|
+
// Fetch block from peernet and verify its hash
|
|
6375
|
+
try {
|
|
6376
|
+
const blockData = await globalThis.peernet.get(blockHash, 'block');
|
|
6377
|
+
const blockMessage = await new BlockMessage(blockData);
|
|
6378
|
+
const actualHash = await blockMessage.hash();
|
|
6379
|
+
if (actualHash !== blockHash) {
|
|
6380
|
+
debug(`[consensus] Block hash mismatch in proposal: expected ${blockHash}, got ${actualHash}`);
|
|
6381
|
+
return;
|
|
6382
|
+
}
|
|
6383
|
+
}
|
|
6384
|
+
catch (e) {
|
|
6385
|
+
debug(`[consensus] Cannot fetch proposed block ${blockHash}:`, e?.message);
|
|
6386
|
+
return;
|
|
6387
|
+
}
|
|
6388
|
+
this.#consensusRound = round;
|
|
6389
|
+
if (this.#roundTimer) {
|
|
6390
|
+
clearTimeout(this.#roundTimer);
|
|
6391
|
+
this.#roundTimer = null;
|
|
6392
|
+
}
|
|
6393
|
+
if (validators.includes(peernet.selectedAccount) && !this.#isJailed(peernet.selectedAccount)) {
|
|
6394
|
+
await this.#castVote('prevote', blockHash, index, round);
|
|
6395
|
+
}
|
|
6396
|
+
}
|
|
6397
|
+
catch (e) {
|
|
6398
|
+
debug('[consensus] Error handling proposal:', e?.message);
|
|
6399
|
+
}
|
|
6400
|
+
};
|
|
6401
|
+
/**
|
|
6402
|
+
* Phase 2 — collect prevotes. Once 2f+1 prevotes are seen for a block,
|
|
6403
|
+
* cast a precommit.
|
|
6404
|
+
*/
|
|
6405
|
+
this.#handlePrevote = async (payload) => {
|
|
6406
|
+
try {
|
|
6407
|
+
const msg = JSON.parse(new TextDecoder().decode(payload));
|
|
6408
|
+
const { blockHash, index, round, from } = msg;
|
|
6409
|
+
const validators = await this.#getConsensusValidators(index);
|
|
6410
|
+
if (!validators.includes(from))
|
|
6411
|
+
return;
|
|
6412
|
+
const localBlock = await this.lastBlock;
|
|
6413
|
+
const localIndex = localBlock?.index !== undefined ? Number(localBlock.index) : -1;
|
|
6414
|
+
if (index <= localIndex)
|
|
6415
|
+
return;
|
|
6416
|
+
const voteKey = `${index}:${round}:${blockHash}`;
|
|
6417
|
+
if (!this.#prevotes.has(voteKey))
|
|
6418
|
+
this.#prevotes.set(voteKey, new Set());
|
|
6419
|
+
this.#prevotes.get(voteKey).add(from);
|
|
6420
|
+
const threshold = Math.ceil((2 * validators.length) / 3);
|
|
6421
|
+
const voteCount = this.#prevotes.get(voteKey).size;
|
|
6422
|
+
debug(`[consensus] Prevotes ${voteKey}: ${voteCount}/${validators.length} (need ${threshold})`);
|
|
6423
|
+
if (voteCount >= threshold &&
|
|
6424
|
+
validators.includes(peernet.selectedAccount) &&
|
|
6425
|
+
!this.#isJailed(peernet.selectedAccount)) {
|
|
6426
|
+
await this.#castVote('precommit', blockHash, index, round);
|
|
6427
|
+
}
|
|
6428
|
+
}
|
|
6429
|
+
catch (e) {
|
|
6430
|
+
debug('[consensus] Error handling prevote:', e?.message);
|
|
6431
|
+
}
|
|
6432
|
+
};
|
|
6433
|
+
/**
|
|
6434
|
+
* Phase 3 — collect precommits. Once 2f+1 precommits are seen for a block,
|
|
6435
|
+
* commit it: non-proposers call #addBlock, then broadcast on add-block for
|
|
6436
|
+
* syncing nodes.
|
|
6437
|
+
*/
|
|
6438
|
+
this.#handlePrecommit = async (payload) => {
|
|
6439
|
+
try {
|
|
6440
|
+
const msg = JSON.parse(new TextDecoder().decode(payload));
|
|
6441
|
+
const { blockHash, index, round, from } = msg;
|
|
6442
|
+
const validators = await this.#getConsensusValidators(index);
|
|
6443
|
+
if (!validators.includes(from))
|
|
6444
|
+
return;
|
|
6445
|
+
if (index <= this.#committedHeight)
|
|
6446
|
+
return;
|
|
6447
|
+
const voteKey = `${index}:${round}:${blockHash}`;
|
|
6448
|
+
if (!this.#precommits.has(voteKey))
|
|
6449
|
+
this.#precommits.set(voteKey, new Set());
|
|
6450
|
+
this.#precommits.get(voteKey).add(from);
|
|
6451
|
+
const threshold = Math.ceil((2 * validators.length) / 3);
|
|
6452
|
+
const voteCount = this.#precommits.get(voteKey).size;
|
|
6453
|
+
debug(`[consensus] Precommits ${voteKey}: ${voteCount}/${validators.length} (need ${threshold})`);
|
|
6454
|
+
if (voteCount >= threshold && index > this.#committedHeight) {
|
|
6455
|
+
this.#committedHeight = index;
|
|
6456
|
+
this.#consensusRound = 0;
|
|
6457
|
+
// Prune vote state for committed and older heights
|
|
6458
|
+
for (const key of [...this.#prevotes.keys()]) {
|
|
6459
|
+
if (Number(key.split(':')[0]) <= index)
|
|
6460
|
+
this.#prevotes.delete(key);
|
|
6461
|
+
}
|
|
6462
|
+
for (const key of [...this.#precommits.keys()]) {
|
|
6463
|
+
if (Number(key.split(':')[0]) <= index)
|
|
6464
|
+
this.#precommits.delete(key);
|
|
6465
|
+
}
|
|
6466
|
+
for (const key of [...this.#castedVotes]) {
|
|
6467
|
+
if (Number(key.split(':')[1]) <= index)
|
|
6468
|
+
this.#castedVotes.delete(key);
|
|
6469
|
+
}
|
|
6470
|
+
// Non-proposers add the block to local state now.
|
|
6471
|
+
// Proposers already committed state in #createBlock() and their
|
|
6472
|
+
// lastBlock.index === index, so the guard below skips them.
|
|
6473
|
+
const currentBlock = await this.lastBlock;
|
|
6474
|
+
const currentIndex = currentBlock?.index !== undefined ? Number(currentBlock.index) : -1;
|
|
6475
|
+
if (index > currentIndex) {
|
|
6476
|
+
debug(`[consensus] ✅ Committing block ${blockHash} at height ${index}`);
|
|
6477
|
+
try {
|
|
6478
|
+
const blockData = await globalThis.peernet.get(blockHash, 'block');
|
|
6479
|
+
await this.#addBlock(blockData);
|
|
6480
|
+
}
|
|
6481
|
+
catch (e) {
|
|
6482
|
+
debug(`[consensus] Failed to commit block ${blockHash}:`, e?.message);
|
|
6483
|
+
}
|
|
6484
|
+
}
|
|
6485
|
+
else {
|
|
6486
|
+
debug(`[consensus] ✅ Block ${blockHash} at height ${index} already committed (proposer path)`);
|
|
6487
|
+
}
|
|
6488
|
+
// Broadcast committed block so syncing / non-participating nodes can catch up
|
|
6489
|
+
try {
|
|
6490
|
+
const blockData = await globalThis.peernet.get(blockHash, 'block');
|
|
6491
|
+
globalThis.peernet.publish('add-block', blockData);
|
|
6492
|
+
globalThis.pubsub.publish('add-block', blockData);
|
|
6493
|
+
}
|
|
6494
|
+
catch (e) {
|
|
6495
|
+
debug('[consensus] Failed to broadcast committed block:', e?.message);
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6498
|
+
}
|
|
6499
|
+
catch (e) {
|
|
6500
|
+
debug('[consensus] Error handling precommit:', e?.message);
|
|
6501
|
+
}
|
|
6502
|
+
};
|
|
6503
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
6226
6504
|
this.#addTransaction = async (message) => {
|
|
6227
6505
|
const transaction = new TransactionMessage(message);
|
|
6228
6506
|
const hash = await transaction.hash();
|
|
@@ -6246,17 +6524,149 @@ class Chain extends VersionControl {
|
|
|
6246
6524
|
#sleep(ms) {
|
|
6247
6525
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6248
6526
|
}
|
|
6527
|
+
async #recordPeerFailure(peerId, reason) {
|
|
6528
|
+
if (!this.#peerReputations.has(peerId)) {
|
|
6529
|
+
this.#peerReputations.set(peerId, { score: 0, failures: [] });
|
|
6530
|
+
}
|
|
6531
|
+
const rep = this.#peerReputations.get(peerId);
|
|
6532
|
+
rep.score -= 1;
|
|
6533
|
+
rep.failures.push(`${Date.now()}: ${reason}`);
|
|
6534
|
+
if (rep.failures.length > this.#maxPeerFailures) {
|
|
6535
|
+
rep.failures.shift();
|
|
6536
|
+
}
|
|
6537
|
+
if (rep.score < this.#minPeerScore) {
|
|
6538
|
+
console.warn(`[peer-ban] Peer ${peerId} banned after ${rep.failures.length} failures`);
|
|
6539
|
+
// Disconnect and don't reconnect
|
|
6540
|
+
try {
|
|
6541
|
+
await globalThis.peernet.disconnect(peerId);
|
|
6542
|
+
}
|
|
6543
|
+
catch (e) {
|
|
6544
|
+
debug(`Failed to disconnect peer ${peerId}`);
|
|
6545
|
+
}
|
|
6546
|
+
}
|
|
6547
|
+
}
|
|
6548
|
+
#isJailed(address) {
|
|
6549
|
+
return typeof address === 'string' && this.#jail.has(address);
|
|
6550
|
+
}
|
|
6551
|
+
async #getConsensusValidators(nextBlockIndex) {
|
|
6552
|
+
const localBlock = await this.lastBlock;
|
|
6553
|
+
const localIndex = localBlock?.index !== undefined ? Number(localBlock.index) : -1;
|
|
6554
|
+
if (Array.isArray(localBlock?.validators) &&
|
|
6555
|
+
localBlock.validators.length > 0 &&
|
|
6556
|
+
(nextBlockIndex === undefined || nextBlockIndex === localIndex + 1)) {
|
|
6557
|
+
return [
|
|
6558
|
+
...new Set(localBlock.validators
|
|
6559
|
+
.map((validator) => validator.address)
|
|
6560
|
+
.filter((address) => Boolean(address)))
|
|
6561
|
+
].sort();
|
|
6562
|
+
}
|
|
6563
|
+
const validators = (await this.staticCall(addresses.validators, 'validators'));
|
|
6564
|
+
return [...new Set(validators)].sort();
|
|
6565
|
+
}
|
|
6566
|
+
#validateBlockValidators(blockMessage) {
|
|
6567
|
+
const validators = blockMessage.decoded.validators || [];
|
|
6568
|
+
if (!Array.isArray(validators) || validators.length === 0) {
|
|
6569
|
+
throw new Error(`Block ${blockMessage.decoded.index} does not include validators`);
|
|
6570
|
+
}
|
|
6571
|
+
// Validate protocol version compatibility
|
|
6572
|
+
if (!blockMessage.decoded.protocolVersion || typeof blockMessage.decoded.protocolVersion !== 'string') {
|
|
6573
|
+
throw new Error(`Block ${blockMessage.decoded.index} does not have a valid protocolVersion field`);
|
|
6574
|
+
}
|
|
6575
|
+
if (!this.isVersionCompatible(blockMessage.decoded.protocolVersion)) {
|
|
6576
|
+
throw new Error(`Block ${blockMessage.decoded.index} uses incompatible protocol version: ${blockMessage.decoded.protocolVersion} ` +
|
|
6577
|
+
`(local: ${this.version}). Major.minor version must match.`);
|
|
6578
|
+
}
|
|
6579
|
+
// Validate producer field
|
|
6580
|
+
if (!blockMessage.decoded.producer || typeof blockMessage.decoded.producer !== 'string') {
|
|
6581
|
+
throw new Error(`Block ${blockMessage.decoded.index} does not have a valid producer field`);
|
|
6582
|
+
}
|
|
6583
|
+
// Validate producerProof field
|
|
6584
|
+
if (!blockMessage.decoded.producerProof || typeof blockMessage.decoded.producerProof !== 'string') {
|
|
6585
|
+
throw new Error(`Block ${blockMessage.decoded.index} does not have a valid producerProof field`);
|
|
6586
|
+
}
|
|
6587
|
+
// Verify producer is in validators list
|
|
6588
|
+
const producerIsValidator = validators.some((v) => v.address === blockMessage.decoded.producer);
|
|
6589
|
+
if (!producerIsValidator) {
|
|
6590
|
+
throw new Error(`Block ${blockMessage.decoded.index} producer ${blockMessage.decoded.producer} is not in validators list`);
|
|
6591
|
+
}
|
|
6592
|
+
const addresses = validators.map((validator) => validator.address);
|
|
6593
|
+
if (addresses.some((address) => typeof address !== 'string' || address.length === 0)) {
|
|
6594
|
+
throw new Error(`Block ${blockMessage.decoded.index} includes an invalid validator address`);
|
|
6595
|
+
}
|
|
6596
|
+
const canonicalAddresses = [...addresses].sort();
|
|
6597
|
+
if (canonicalAddresses.some((address, index) => address !== addresses[index])) {
|
|
6598
|
+
throw new Error(`Block ${blockMessage.decoded.index} validators are not canonically sorted`);
|
|
6599
|
+
}
|
|
6600
|
+
if (new Set(addresses).size !== addresses.length) {
|
|
6601
|
+
throw new Error(`Block ${blockMessage.decoded.index} validators contain duplicates`);
|
|
6602
|
+
}
|
|
6603
|
+
const validatorCount = BigInt(validators.length);
|
|
6604
|
+
const expectedReward = blockMessage.decoded.fees / validatorCount + blockMessage.decoded.reward / validatorCount;
|
|
6605
|
+
for (const validator of validators) {
|
|
6606
|
+
if (validator.reward !== expectedReward) {
|
|
6607
|
+
throw new Error(`Block ${blockMessage.decoded.index} has an invalid reward for validator ${validator.address}: ` +
|
|
6608
|
+
`expected ${expectedReward}, got ${validator.reward}`);
|
|
6609
|
+
}
|
|
6610
|
+
}
|
|
6611
|
+
}
|
|
6612
|
+
/** Check if the next block will cross an epoch boundary (block-based timing) */
|
|
6613
|
+
#isEpochBoundary(blockHeight) {
|
|
6614
|
+
return (blockHeight + 1) % this.#epochLength === 0;
|
|
6615
|
+
}
|
|
6616
|
+
/** Handle epoch transition when a block crosses epoch boundary */
|
|
6617
|
+
async #handleEpochBoundary(blockHeight) {
|
|
6618
|
+
if (!this.#isEpochBoundary(blockHeight))
|
|
6619
|
+
return;
|
|
6620
|
+
// Epoch boundary crossed: update epoch start, reset round, trigger validator rotation
|
|
6621
|
+
this.#currentEpochStartHeight = blockHeight + 1;
|
|
6622
|
+
this.#consensusRound = 0;
|
|
6623
|
+
debug(`[consensus] Epoch boundary at block ${blockHeight}: new epoch starts at height ${this.#currentEpochStartHeight}`);
|
|
6624
|
+
// If we're participating as a validator, trigger immediate epoch to determine new proposer
|
|
6625
|
+
if (this.#participating && !this.#runningEpoch) {
|
|
6626
|
+
await this.#runEpoch();
|
|
6627
|
+
}
|
|
6628
|
+
}
|
|
6249
6629
|
async #runEpoch() {
|
|
6250
6630
|
if (this.#runningEpoch)
|
|
6251
6631
|
return;
|
|
6252
6632
|
this.#runningEpoch = true;
|
|
6253
6633
|
console.log('epoch');
|
|
6254
|
-
const validators = await this
|
|
6634
|
+
const validators = await this.#getConsensusValidators();
|
|
6255
6635
|
console.log({ validators });
|
|
6636
|
+
if (this.#isJailed(peernet.selectedAccount)) {
|
|
6637
|
+
this.#runningEpoch = false;
|
|
6638
|
+
return;
|
|
6639
|
+
}
|
|
6256
6640
|
if (!validators.includes(peernet.selectedAccount)) {
|
|
6257
6641
|
this.#runningEpoch = false;
|
|
6258
6642
|
return;
|
|
6259
6643
|
}
|
|
6644
|
+
// Phase 1: Deterministic proposer selection
|
|
6645
|
+
// proposer = validators[(nextBlockIndex + round) % validators.length]
|
|
6646
|
+
const localBlock = await this.lastBlock;
|
|
6647
|
+
const nextIndex = (localBlock?.index !== undefined ? Number(localBlock.index) : -1) + 1;
|
|
6648
|
+
const proposerIdx = (nextIndex + this.#consensusRound) % validators.length;
|
|
6649
|
+
const isProposer = validators[proposerIdx] === peernet.selectedAccount;
|
|
6650
|
+
if (!isProposer) {
|
|
6651
|
+
// Non-proposer: start round-advance timer in case proposer is unresponsive
|
|
6652
|
+
if (!this.#roundTimer) {
|
|
6653
|
+
this.#roundTimer = setTimeout(async () => {
|
|
6654
|
+
this.#roundTimer = null;
|
|
6655
|
+
this.#consensusRound++;
|
|
6656
|
+
debug(`[consensus] Round timed out, advancing to round ${this.#consensusRound}`);
|
|
6657
|
+
this.#runningEpoch = false;
|
|
6658
|
+
if (this.#participating)
|
|
6659
|
+
await this.#runEpoch();
|
|
6660
|
+
}, this.#slotTime);
|
|
6661
|
+
}
|
|
6662
|
+
this.#runningEpoch = false;
|
|
6663
|
+
return;
|
|
6664
|
+
}
|
|
6665
|
+
// We are the proposer — clear any stale round-advance timer
|
|
6666
|
+
if (this.#roundTimer) {
|
|
6667
|
+
clearTimeout(this.#roundTimer);
|
|
6668
|
+
this.#roundTimer = null;
|
|
6669
|
+
}
|
|
6260
6670
|
const start = Date.now();
|
|
6261
6671
|
try {
|
|
6262
6672
|
await this.#createBlock();
|
|
@@ -6341,6 +6751,10 @@ class Chain extends VersionControl {
|
|
|
6341
6751
|
globalThis.peernet.subscribe('send-transaction', this.#sendTransaction.bind(this));
|
|
6342
6752
|
globalThis.peernet.subscribe('add-transaction', this.#addTransaction.bind(this));
|
|
6343
6753
|
globalThis.peernet.subscribe('validator:timeout', this.#validatorTimeout.bind(this));
|
|
6754
|
+
// Tendermint consensus topics
|
|
6755
|
+
globalThis.peernet.subscribe('consensus:propose', this.#handleProposal.bind(this));
|
|
6756
|
+
globalThis.peernet.subscribe('consensus:prevote', this.#handlePrevote.bind(this));
|
|
6757
|
+
globalThis.peernet.subscribe('consensus:precommit', this.#handlePrecommit.bind(this));
|
|
6344
6758
|
globalThis.pubsub.subscribe('peer:connected', this.#peerConnected.bind(this));
|
|
6345
6759
|
globalThis.pubsub.publish('chain:ready', true);
|
|
6346
6760
|
console.log('[chain] init:done');
|
|
@@ -6356,11 +6770,44 @@ class Chain extends VersionControl {
|
|
|
6356
6770
|
await globalThis.transactionPoolStore.delete(hash);
|
|
6357
6771
|
}
|
|
6358
6772
|
async #validatorTimeout(validatorInfo) {
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6773
|
+
const address = validatorInfo?.address;
|
|
6774
|
+
if (!address)
|
|
6775
|
+
return;
|
|
6776
|
+
const timeout = Math.min(Math.max(Number(validatorInfo.timeout) || 0, 0), 60 * 60 * 1000);
|
|
6777
|
+
const existingRelease = this.#jailReleaseTimers.get(address);
|
|
6778
|
+
if (existingRelease)
|
|
6779
|
+
clearTimeout(existingRelease);
|
|
6780
|
+
this.#jail.add(address);
|
|
6781
|
+
const releaseTimer = setTimeout(() => {
|
|
6782
|
+
this.#jail.delete(address);
|
|
6783
|
+
this.#jailReleaseTimers.delete(address);
|
|
6784
|
+
}, timeout);
|
|
6785
|
+
this.#jailReleaseTimers.set(address, releaseTimer);
|
|
6786
|
+
}
|
|
6787
|
+
// ── Tendermint consensus handlers ────────────────────────────────────────
|
|
6788
|
+
/**
|
|
6789
|
+
* Publish a prevote or precommit. Idempotent — will not cast the same
|
|
6790
|
+
* vote twice for the same height:round.
|
|
6791
|
+
*/
|
|
6792
|
+
#castVote;
|
|
6793
|
+
/**
|
|
6794
|
+
* Phase 2 — receive a block proposal from the designated proposer.
|
|
6795
|
+
* Validates the proposer is correct for height/round, fetches + validates
|
|
6796
|
+
* the block from peernet, then casts a prevote.
|
|
6797
|
+
*/
|
|
6798
|
+
#handleProposal;
|
|
6799
|
+
/**
|
|
6800
|
+
* Phase 2 — collect prevotes. Once 2f+1 prevotes are seen for a block,
|
|
6801
|
+
* cast a precommit.
|
|
6802
|
+
*/
|
|
6803
|
+
#handlePrevote;
|
|
6804
|
+
/**
|
|
6805
|
+
* Phase 3 — collect precommits. Once 2f+1 precommits are seen for a block,
|
|
6806
|
+
* commit it: non-proposers call #addBlock, then broadcast on add-block for
|
|
6807
|
+
* syncing nodes.
|
|
6808
|
+
*/
|
|
6809
|
+
#handlePrecommit;
|
|
6810
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
6364
6811
|
#addTransaction;
|
|
6365
6812
|
async #prepareRequest(request) {
|
|
6366
6813
|
let node = await new globalThis.peernet.protos['peernet-request']({ request });
|
|
@@ -6416,17 +6863,7 @@ class Chain extends VersionControl {
|
|
|
6416
6863
|
debug(`peer connected: ${peerId}`);
|
|
6417
6864
|
const peer = peernet.getConnection(peerId);
|
|
6418
6865
|
debug(`peer connected with version ${peer.version}`);
|
|
6419
|
-
|
|
6420
|
-
// for now just do nothing if version doesn't match
|
|
6421
|
-
debug(`peer connected with version ${peer.version}`);
|
|
6422
|
-
const compatibleVersion = () => {
|
|
6423
|
-
if (!peer.version || !this.version)
|
|
6424
|
-
return false;
|
|
6425
|
-
const [peerMajor, peerMinor] = peer.version.split('.');
|
|
6426
|
-
const [localMajor, localMinor] = this.version.split('.');
|
|
6427
|
-
return peerMajor === localMajor && peerMinor === localMinor;
|
|
6428
|
-
};
|
|
6429
|
-
if (!compatibleVersion()) {
|
|
6866
|
+
if (!this.isVersionCompatible(peer.version)) {
|
|
6430
6867
|
debug(`versions don't match`);
|
|
6431
6868
|
return;
|
|
6432
6869
|
}
|
|
@@ -6440,9 +6877,19 @@ class Chain extends VersionControl {
|
|
|
6440
6877
|
catch (error) {
|
|
6441
6878
|
const peerName = peer?.peerId || peer?.id || peer?.address || peerId || 'unknown';
|
|
6442
6879
|
debug(`lastBlock request failed: ${peerName}:`, error?.message ?? error);
|
|
6880
|
+
await this.#recordPeerFailure(peerId, `lastBlock request failed: ${error?.message ?? error}`);
|
|
6443
6881
|
return;
|
|
6444
6882
|
}
|
|
6883
|
+
// CRITICAL: Validate the peer's claimed block height is not unreasonably ahead of our local chain
|
|
6884
|
+
// This prevents Byzantine nodes from claiming a fake chain length to steer our sync
|
|
6445
6885
|
const localBlock = await this.lastBlock;
|
|
6886
|
+
const MAX_SYNC_AHEAD = 100_000;
|
|
6887
|
+
if (lastBlock?.index > (localBlock?.index ?? 0) + MAX_SYNC_AHEAD) {
|
|
6888
|
+
const peerName = peer?.peerId || peer?.id || peer?.address || peerId || 'unknown';
|
|
6889
|
+
debug(`Peer ${peerName} claims unreasonable block height ${lastBlock.index} (local: ${localBlock?.index ?? 0})`);
|
|
6890
|
+
await this.#recordPeerFailure(peerId, `unreasonable lastBlock index: ${lastBlock.index}`);
|
|
6891
|
+
return;
|
|
6892
|
+
}
|
|
6446
6893
|
if (!lastBlock || !lastBlock.hash || lastBlock.hash === '0x0') {
|
|
6447
6894
|
debug(`peer has no lastBlock: ${peerId}`);
|
|
6448
6895
|
return;
|
|
@@ -6460,14 +6907,20 @@ class Chain extends VersionControl {
|
|
|
6460
6907
|
console.log(e);
|
|
6461
6908
|
}
|
|
6462
6909
|
}
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6910
|
+
const MAX_WANTLIST_SIZE = 1000;
|
|
6911
|
+
if (knownBlocksResponse.blocks) {
|
|
6912
|
+
const remaining = MAX_WANTLIST_SIZE - this.wantList.length;
|
|
6913
|
+
if (remaining > 0) {
|
|
6914
|
+
for (const hash of knownBlocksResponse.blocks.slice(0, remaining)) {
|
|
6915
|
+
this.wantList.push(hash);
|
|
6916
|
+
}
|
|
6466
6917
|
}
|
|
6918
|
+
}
|
|
6467
6919
|
}
|
|
6468
6920
|
catch (error) {
|
|
6469
6921
|
const peerName = peer?.peerId || peer?.id || peer?.address || peerId || 'unknown';
|
|
6470
6922
|
debug(`knownBlocks request failed: ${peerName}:`, error?.message ?? error);
|
|
6923
|
+
await this.#recordPeerFailure(peerId, `knownBlocks request failed: ${error?.message ?? error}`);
|
|
6471
6924
|
return;
|
|
6472
6925
|
}
|
|
6473
6926
|
}
|
|
@@ -6510,6 +6963,7 @@ class Chain extends VersionControl {
|
|
|
6510
6963
|
catch (error) {
|
|
6511
6964
|
const peerName = peer?.peerId || peer?.id || peer?.address || peerId || 'unknown';
|
|
6512
6965
|
debug(`stateInfo/syncChain failed: ${peerName}:`, error?.message ?? error);
|
|
6966
|
+
await this.#recordPeerFailure(peerId, `stateInfo/syncChain failed: ${error?.message ?? error}`);
|
|
6513
6967
|
return;
|
|
6514
6968
|
}
|
|
6515
6969
|
}
|
|
@@ -6546,26 +7000,55 @@ class Chain extends VersionControl {
|
|
|
6546
7000
|
const receivedEncoded = block instanceof BlockMessage ? block.encoded : block;
|
|
6547
7001
|
const blockMessage = await new BlockMessage(block);
|
|
6548
7002
|
const hash = await blockMessage.hash();
|
|
6549
|
-
//
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
}
|
|
7003
|
+
// CRITICAL: VALIDATE BEFORE TOUCHING STATE
|
|
7004
|
+
// 1. Check for duplicate blocks at same height
|
|
7005
|
+
const blockIndex = Number(blockMessage.decoded.index);
|
|
7006
|
+
const existingBlockAtHeight = this.#blocks[blockIndex];
|
|
7007
|
+
if (existingBlockAtHeight) {
|
|
7008
|
+
if (existingBlockAtHeight.hash !== hash) {
|
|
7009
|
+
console.error(`[CONSENSUS ALERT] Conflicting blocks at height ${blockIndex}:`);
|
|
7010
|
+
console.error(` Local: ${existingBlockAtHeight.hash}`);
|
|
7011
|
+
console.error(` Remote: ${hash}`);
|
|
7012
|
+
throw new Error(`Block conflict detected at index ${blockIndex}`);
|
|
6558
7013
|
}
|
|
6559
|
-
|
|
6560
|
-
|
|
7014
|
+
// Already have this exact block, skip
|
|
7015
|
+
debug(`Block already in store: ${hash}`);
|
|
7016
|
+
return;
|
|
7017
|
+
}
|
|
7018
|
+
// 2. Verify previous hash chain integrity
|
|
7019
|
+
if (blockIndex > 0) {
|
|
7020
|
+
const previousBlockInfo = this.#blocks[blockIndex - 1];
|
|
7021
|
+
if (!previousBlockInfo) {
|
|
7022
|
+
throw new Error(`Missing parent block at index ${blockIndex - 1}`);
|
|
6561
7023
|
}
|
|
6562
|
-
|
|
6563
|
-
|
|
7024
|
+
if (previousBlockInfo.hash !== blockMessage.decoded.previousHash) {
|
|
7025
|
+
throw new Error(`previousHash mismatch at index ${blockIndex}: ` +
|
|
7026
|
+
`expected ${previousBlockInfo.hash}, got ${blockMessage.decoded.previousHash}`);
|
|
6564
7027
|
}
|
|
6565
7028
|
}
|
|
6566
|
-
else {
|
|
6567
|
-
|
|
7029
|
+
else if (blockMessage.decoded.previousHash !== '0x0') {
|
|
7030
|
+
throw new Error(`Genesis block (index 0) must have previousHash='0x0'`);
|
|
7031
|
+
}
|
|
7032
|
+
// 3. Verify data integrity
|
|
7033
|
+
const canonicalEncoded = blockMessage.encoded;
|
|
7034
|
+
const byteLengthMatch = receivedEncoded.length === canonicalEncoded.length;
|
|
7035
|
+
if (!byteLengthMatch) {
|
|
7036
|
+
throw new Error(`[FATAL] Block data size mismatch: received ${receivedEncoded.length} bytes ` +
|
|
7037
|
+
`but canonical encoding is ${canonicalEncoded.length} bytes for block #${blockMessage.decoded.index}`);
|
|
7038
|
+
}
|
|
7039
|
+
let mismatch = false;
|
|
7040
|
+
for (let i = 0; i < receivedEncoded.length; i++) {
|
|
7041
|
+
if (receivedEncoded[i] !== canonicalEncoded[i]) {
|
|
7042
|
+
mismatch = true;
|
|
7043
|
+
break;
|
|
7044
|
+
}
|
|
7045
|
+
}
|
|
7046
|
+
if (mismatch) {
|
|
7047
|
+
throw new Error(`[FATAL] Block data corrupted in transit for block #${blockIndex} hash ${hash}`);
|
|
6568
7048
|
}
|
|
7049
|
+
console.log(`[chain] ✅ Block data integrity verified: ${hash}`);
|
|
7050
|
+
this.#validateBlockValidators(blockMessage);
|
|
7051
|
+
// NOW SAFE TO PROCEED with transaction processing
|
|
6569
7052
|
const transactions = await Promise.all(blockMessage.decoded.transactions
|
|
6570
7053
|
// @ts-ignore
|
|
6571
7054
|
.map(async (hash) => {
|
|
@@ -6574,24 +7057,34 @@ class Chain extends VersionControl {
|
|
|
6574
7057
|
return new TransactionMessage(data);
|
|
6575
7058
|
}));
|
|
6576
7059
|
await globalThis.blockStore.put(hash, blockMessage.encoded);
|
|
7060
|
+
// Cache block for conflict detection
|
|
7061
|
+
this.#blocks[blockIndex] = {
|
|
7062
|
+
hash,
|
|
7063
|
+
...blockMessage.decoded
|
|
7064
|
+
};
|
|
6577
7065
|
debug(`added block: ${hash}`);
|
|
6578
7066
|
let promises = [];
|
|
6579
7067
|
let contracts = [];
|
|
6580
|
-
|
|
6581
|
-
const
|
|
6582
|
-
|
|
7068
|
+
// Combine and sort all transactions deterministically
|
|
7069
|
+
const allTransactions = transactions.sort((a, b) => {
|
|
7070
|
+
// Primary: by priority (true first)
|
|
7071
|
+
if (a.decoded.priority !== b.decoded.priority) {
|
|
7072
|
+
return (b.decoded.priority ? 1 : 0) - (a.decoded.priority ? 1 : 0);
|
|
7073
|
+
}
|
|
7074
|
+
// Secondary: by nonce
|
|
7075
|
+
const nonceDiff = (a.decoded?.nonce ?? 0) - (b.decoded?.nonce ?? 0);
|
|
7076
|
+
if (nonceDiff !== 0)
|
|
7077
|
+
return nonceDiff;
|
|
7078
|
+
// Tertiary: in stable order (insertion order preserved)
|
|
7079
|
+
return 0;
|
|
7080
|
+
});
|
|
7081
|
+
// Execute sequentially (NOT concurrently) to ensure deterministic state
|
|
7082
|
+
for (const transaction of allTransactions) {
|
|
6583
7083
|
if (!contracts.includes(transaction.decoded.to)) {
|
|
6584
7084
|
contracts.push(transaction.decoded.to);
|
|
6585
7085
|
}
|
|
6586
|
-
if (transaction.decoded.priority)
|
|
6587
|
-
priorityransactions.push(transaction);
|
|
6588
|
-
else
|
|
6589
|
-
normalTransactions.push(transaction);
|
|
6590
|
-
}
|
|
6591
|
-
for (const transaction of priorityransactions.sort((a, b) => a.decoded.nonce - b.decoded.nonce)) {
|
|
6592
7086
|
await this.#handleTransaction(transaction, []);
|
|
6593
7087
|
}
|
|
6594
|
-
await Promise.all(normalTransactions.map((transaction) => this.#handleTransaction(transaction, [])));
|
|
6595
7088
|
// for (let transaction of transactionsMessages) {
|
|
6596
7089
|
// // await transactionStore.put(transaction.hash, transaction.encoded)
|
|
6597
7090
|
// if (!contracts.includes(transaction.to)) {
|
|
@@ -6616,6 +7109,15 @@ class Chain extends VersionControl {
|
|
|
6616
7109
|
await Promise.all(Object.entries(noncesByAddress).map(([from, nonce]) => globalThis.accountsStore.put(from, String(nonce))));
|
|
6617
7110
|
if ((await this.lastBlock).index < Number(blockMessage.decoded.index)) {
|
|
6618
7111
|
await this.machine.addLoadedBlock({ ...blockMessage.decoded, loaded: true, hash: await blockMessage.hash() });
|
|
7112
|
+
// Record validator snapshot at this block height for future consensus queries
|
|
7113
|
+
try {
|
|
7114
|
+
await this.call(addresses.validators, 'recordValidatorSnapshot', [blockMessage.decoded.index]);
|
|
7115
|
+
}
|
|
7116
|
+
catch (snapshotError) {
|
|
7117
|
+
debug(`failed to record validator snapshot: ${snapshotError?.message ?? snapshotError}`);
|
|
7118
|
+
}
|
|
7119
|
+
// Check if this block crosses epoch boundary and handle transition
|
|
7120
|
+
await this.#handleEpochBoundary(Number(blockMessage.decoded.index));
|
|
6619
7121
|
await this.updateState(blockMessage);
|
|
6620
7122
|
}
|
|
6621
7123
|
globalThis.pubsub.publish('block-processed', blockMessage.decoded);
|
|
@@ -6707,23 +7209,31 @@ class Chain extends VersionControl {
|
|
|
6707
7209
|
timestamp,
|
|
6708
7210
|
previousHash: '',
|
|
6709
7211
|
reward: BigInt(150),
|
|
6710
|
-
index: 0
|
|
7212
|
+
index: 0,
|
|
7213
|
+
producer: '',
|
|
7214
|
+
producerProof: '',
|
|
7215
|
+
protocolVersion: this.version
|
|
6711
7216
|
};
|
|
6712
7217
|
const latestTransactions = await this.machine.latestTransactions();
|
|
6713
7218
|
// exclude failing tx
|
|
6714
7219
|
transactions = await this.promiseTransactions(transactions);
|
|
6715
|
-
|
|
6716
|
-
const
|
|
6717
|
-
|
|
6718
|
-
if (
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
7220
|
+
// Combine priority and normal transactions, then sort deterministically
|
|
7221
|
+
const allTransactions = transactions.sort((a, b) => {
|
|
7222
|
+
// Primary: by priority (true first)
|
|
7223
|
+
if (a.decoded.priority !== b.decoded.priority) {
|
|
7224
|
+
return (b.decoded.priority ? 1 : 0) - (a.decoded.priority ? 1 : 0);
|
|
7225
|
+
}
|
|
7226
|
+
// Secondary: by nonce
|
|
7227
|
+
const nonceDiff = (a.decoded?.nonce ?? 0) - (b.decoded?.nonce ?? 0);
|
|
7228
|
+
if (nonceDiff !== 0)
|
|
7229
|
+
return nonceDiff;
|
|
7230
|
+
// Tertiary: in stable order (insertion order preserved)
|
|
7231
|
+
return 0;
|
|
7232
|
+
});
|
|
7233
|
+
// Execute sequentially (NOT concurrently) to ensure deterministic state
|
|
7234
|
+
for (const transaction of allTransactions) {
|
|
6724
7235
|
await this.#handleTransaction(transaction, latestTransactions, block);
|
|
6725
7236
|
}
|
|
6726
|
-
await Promise.all(normalTransactions.map((transaction) => this.#handleTransaction(transaction, latestTransactions, block)));
|
|
6727
7237
|
// don't add empty block
|
|
6728
7238
|
if (block.transactions.length === 0)
|
|
6729
7239
|
return;
|
|
@@ -6741,7 +7251,7 @@ class Chain extends VersionControl {
|
|
|
6741
7251
|
}
|
|
6742
7252
|
for (const validator of validators) {
|
|
6743
7253
|
const peer = peers[validator];
|
|
6744
|
-
if (peer && peer.connected && peer.version
|
|
7254
|
+
if (peer && peer.connected && this.isVersionCompatible(peer.version)) {
|
|
6745
7255
|
let data = await new BWRequestMessage();
|
|
6746
7256
|
const node = await globalThis.peernet.prepareMessage(data.encoded);
|
|
6747
7257
|
try {
|
|
@@ -6781,26 +7291,54 @@ class Chain extends VersionControl {
|
|
|
6781
7291
|
// block.timestamp = Date.now()
|
|
6782
7292
|
// block.reward = block.reward.toString()
|
|
6783
7293
|
// block.fees = block.fees.toString()
|
|
7294
|
+
// CRITICAL FIX: Sort validators deterministically to avoid encoding divergence
|
|
7295
|
+
// Use canonical validator set from contract, sorted by address
|
|
7296
|
+
const canonicalValidators = (await this.staticCall(addresses.validators, 'validators'));
|
|
7297
|
+
const sortedValidators = [...canonicalValidators].sort();
|
|
7298
|
+
// Apply reward to all canonical validators (deterministic)
|
|
7299
|
+
block.validators = sortedValidators.map((validatorAddress) => ({
|
|
7300
|
+
address: validatorAddress,
|
|
7301
|
+
reward: block.fees / BigInt(sortedValidators.length) + block.reward / BigInt(sortedValidators.length)
|
|
7302
|
+
}));
|
|
6784
7303
|
try {
|
|
6785
7304
|
await Promise.all(block.transactions.map(async (transaction) => {
|
|
6786
7305
|
await globalThis.transactionStore.put(transaction, await transactionPoolStore.get(transaction));
|
|
6787
7306
|
await globalThis.transactionPoolStore.delete(transaction);
|
|
6788
7307
|
}));
|
|
7308
|
+
// Set producer to current account
|
|
7309
|
+
block.producer = globalThis.peernet.selectedAccount || '';
|
|
7310
|
+
// Sign block hash to authenticate producer (producer must be the proposer)
|
|
7311
|
+
if (block.producer && this.keypair) {
|
|
7312
|
+
const blockHashInput = JSON.stringify({
|
|
7313
|
+
index: block.index,
|
|
7314
|
+
previousHash: block.previousHash,
|
|
7315
|
+
timestamp: block.timestamp,
|
|
7316
|
+
validators: block.validators.map((v) => v.address).sort()
|
|
7317
|
+
});
|
|
7318
|
+
block.producerProof = await signTransaction(blockHashInput, this.keypair);
|
|
7319
|
+
}
|
|
6789
7320
|
let blockMessage = await new BlockMessage(block);
|
|
6790
7321
|
const hash = await blockMessage.hash();
|
|
6791
7322
|
await globalThis.peernet.put(hash, blockMessage.encoded, 'block');
|
|
6792
7323
|
await this.machine.addLoadedBlock({ ...blockMessage.decoded, loaded: true, hash: await blockMessage.hash() });
|
|
6793
7324
|
await this.updateState(blockMessage);
|
|
6794
7325
|
debug(`created block: ${hash} @${block.index}`);
|
|
6795
|
-
//
|
|
6796
|
-
console.log(`[
|
|
7326
|
+
// Phase 2: announce proposal for consensus voting instead of direct add-block
|
|
7327
|
+
console.log(`[consensus] 📤 Proposing block #${block.index} | hash: ${hash} | round: ${this.#consensusRound}`);
|
|
7328
|
+
const proposalPayload = new TextEncoder().encode(JSON.stringify({
|
|
7329
|
+
blockHash: hash,
|
|
7330
|
+
index: block.index,
|
|
7331
|
+
round: this.#consensusRound,
|
|
7332
|
+
from: peernet.selectedAccount
|
|
7333
|
+
}));
|
|
6797
7334
|
try {
|
|
6798
|
-
globalThis.peernet.publish('
|
|
7335
|
+
globalThis.peernet.publish('consensus:propose', proposalPayload);
|
|
6799
7336
|
}
|
|
6800
7337
|
catch (publishError) {
|
|
6801
|
-
debug('peernet publish failed:
|
|
7338
|
+
debug('peernet publish failed: consensus:propose', publishError?.message ?? publishError);
|
|
6802
7339
|
}
|
|
6803
|
-
|
|
7340
|
+
// Proposer casts their own prevote immediately
|
|
7341
|
+
await this.#castVote('prevote', hash, block.index, this.#consensusRound);
|
|
6804
7342
|
}
|
|
6805
7343
|
catch (error) {
|
|
6806
7344
|
console.log(error);
|