@leofcoin/chain 1.9.24 → 1.10.1
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/{browser-8BFql6K9-BMXXABIY.js → browser-BKlRwB0A-CKYDFr2l.js} +2 -2
- package/exports/browser/{browser-FVp_QbaL-C-GWIuv6.js → browser-Dx3X9MHO-DjR7iqyW.js} +2 -2
- package/exports/browser/chain.js +5166 -957
- package/exports/browser/{client-lPe0SUWx-DwxXCP5p.js → client-eYBPUrVq-CukSyZyC.js} +19 -14
- package/exports/browser/{constants-COpvMqFX.js → constants-CUoFxEp2.js} +195 -88
- package/exports/browser/{index-D6qd-AUn-cJS9xh8w.js → index-B91mcwxm-D4wHXLVh.js} +2 -2
- package/exports/browser/{messages-BfDvXW-x-BaHit_or.js → messages-D0Goitr6-BWN8uKkb.js} +2 -2
- package/exports/browser/{node-browser-Dc4HZiX0.js → node-browser-JBfPNMJs.js} +2020 -1910
- package/exports/browser/node-browser.js +2 -2
- package/exports/browser/workers/block-worker.js +46 -1
- package/exports/browser/workers/machine-worker.js +32 -61
- package/exports/browser/workers/{worker-Bsi6vKgF-Bsi6vKgF.js → worker-jeGMMCte-BwFbyiks.js} +17 -92
- package/exports/chain.d.ts +10 -0
- package/exports/chain.js +291 -194
- package/exports/{constants-C83ZCYKa.js → constants-D6gWzJZg.js} +2 -1
- package/exports/node.d.ts +15 -0
- package/exports/node.js +7 -5
- package/exports/workers/block-worker.js +46 -1
- package/exports/workers/machine-worker.js +32 -61
- package/exports/workers/{worker-Bsi6vKgF-Bsi6vKgF.js → worker-jeGMMCte-BwFbyiks.js} +17 -92
- package/package.json +13 -8
package/exports/chain.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { createDebugger } from '@vandeurenglenn/debug';
|
|
2
2
|
import { Codec } from '@leofcoin/codec-format-interface';
|
|
3
3
|
import { jsonStringifyBigInt, jsonParseBigInt, formatBytes, parseUnits, formatUnits } from '@leofcoin/utils';
|
|
4
|
-
import { TransactionMessage, BlockMessage, ContractMessage, LastBlockMessage, PrevoteMessage, PrecommitMessage, ProposalMessage, BWMessage, StateMessage
|
|
4
|
+
import { TransactionMessage, BlockMessage, ContractMessage, LastBlockMessage, PrevoteMessage, PrecommitMessage, ProposalMessage, BWMessage, StateMessage } from '@leofcoin/messages';
|
|
5
5
|
import addresses, { contractFactory } from '@leofcoin/addresses';
|
|
6
|
-
import { calculateFee, createContractMessage, signTransaction, contractFactoryMessage, nativeTokenMessage, validatorsMessage, nameServiceMessage } from '@leofcoin/lib';
|
|
6
|
+
import { createTransactionHash, calculateFee, createContractMessage, signTransaction, contractFactoryMessage, nativeTokenMessage, validatorsMessage, nameServiceMessage } from '@leofcoin/lib';
|
|
7
|
+
import MultiWallet from '@leofcoin/multi-wallet';
|
|
8
|
+
import { fromBase58 } from '@vandeurenglenn/typed-array-utils';
|
|
7
9
|
import semver from 'semver';
|
|
8
10
|
import { randombytes } from '@leofcoin/crypto';
|
|
9
11
|
import EasyWorker from '@vandeurenglenn/easy-worker';
|
|
10
12
|
import { ContractDeploymentError, ExecutionError, isResolveError, ResolveError, isExecutionError } from '@leofcoin/errors';
|
|
11
|
-
import { P as PROTOCOL_VERSION, R as REACHED_ONE_ZERO_ZERO } from './constants-
|
|
13
|
+
import { P as PROTOCOL_VERSION, R as REACHED_ONE_ZERO_ZERO } from './constants-D6gWzJZg.js';
|
|
12
14
|
import '@leofcoin/networks';
|
|
13
15
|
|
|
14
16
|
const limit = 1800;
|
|
@@ -51,8 +53,14 @@ class Transaction extends Protocol {
|
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
removePendingNonce(address, nonce) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
const nonces = this.#pendingNonces.get(address);
|
|
57
|
+
if (!nonces) return;
|
|
58
|
+
nonces.delete(nonce);
|
|
59
|
+
if (this.#maxPendingNonce.get(address) === nonce) {
|
|
60
|
+
let max = -1;
|
|
61
|
+
for (const n of nonces) if (n > max) max = n;
|
|
62
|
+
if (max === -1) this.#maxPendingNonce.delete(address);
|
|
63
|
+
else this.#maxPendingNonce.set(address, max);
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
66
|
getPendingNonces(address) {
|
|
@@ -121,19 +129,33 @@ class Transaction extends Protocol {
|
|
|
121
129
|
transactions = transactions.filter((tx) => tx.decoded.from === address);
|
|
122
130
|
transactions = await this.promiseTransactionsContent(transactions);
|
|
123
131
|
if (this.lastBlock?.hash && transactions.length === 0 && this.lastBlock.hash !== "0x0") {
|
|
124
|
-
let
|
|
132
|
+
let blockHash = this.lastBlock.hash;
|
|
125
133
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
while (blockHash && blockHash !== "0x0" && transactions.length === 0) {
|
|
135
|
+
let rawBlock;
|
|
136
|
+
try {
|
|
137
|
+
rawBlock = await globalThis.blockStore.get(blockHash);
|
|
138
|
+
} catch {
|
|
139
|
+
rawBlock = await globalThis.peernet.get(blockHash, "block");
|
|
140
|
+
}
|
|
141
|
+
if (!rawBlock) break;
|
|
142
|
+
const block = await new BlockMessage(rawBlock);
|
|
143
|
+
const blockTransactions = await Promise.all(
|
|
144
|
+
block.decoded.transactions.map(async (hash) => {
|
|
145
|
+
let rawTransaction;
|
|
146
|
+
try {
|
|
147
|
+
rawTransaction = await globalThis.transactionStore.get(hash);
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
if (!rawTransaction) rawTransaction = await globalThis.peernet.get(hash, "transaction");
|
|
151
|
+
return rawTransaction ? new TransactionMessage(rawTransaction) : void 0;
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
transactions = blockTransactions.filter((transaction) => Boolean(transaction)).filter((transaction) => transaction.decoded.from === address).map((transaction) => transaction.decoded);
|
|
155
|
+
blockHash = block.decoded.previousHash;
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
return 0;
|
|
137
159
|
}
|
|
138
160
|
}
|
|
139
161
|
if (transactions.length === 0) return 0;
|
|
@@ -189,6 +211,22 @@ class Transaction extends Protocol {
|
|
|
189
211
|
async createTransactionMessage(transaction, signature) {
|
|
190
212
|
return new TransactionMessage({ ...transaction, signature });
|
|
191
213
|
}
|
|
214
|
+
async validateTransactionSignature(message) {
|
|
215
|
+
if (!this.isTransactionMessage(message)) message = await new TransactionMessage(message);
|
|
216
|
+
const { from, signature } = message.decoded;
|
|
217
|
+
if (!from || typeof from !== "string") throw new Error("transaction sender required");
|
|
218
|
+
if (!signature || typeof signature !== "string") throw new Error("transaction not signed");
|
|
219
|
+
try {
|
|
220
|
+
const network = globalThis.peernet?.network || "leofcoin";
|
|
221
|
+
const verifier = new MultiWallet(network);
|
|
222
|
+
await verifier.fromAddress(from, null, network);
|
|
223
|
+
const valid = await verifier.verify(fromBase58(signature), await createTransactionHash(message));
|
|
224
|
+
if (!valid) throw new Error("signature does not match transaction sender");
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (error?.message === "signature does not match transaction sender") throw error;
|
|
227
|
+
throw new Error(`invalid transaction signature: ${error?.message ?? error}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
192
230
|
async createTransaction(transaction) {
|
|
193
231
|
return {
|
|
194
232
|
...transaction,
|
|
@@ -198,8 +236,8 @@ class Transaction extends Protocol {
|
|
|
198
236
|
}
|
|
199
237
|
async sendTransaction(message) {
|
|
200
238
|
if (!this.isTransactionMessage(message)) message = await new TransactionMessage(message);
|
|
201
|
-
if (!message.decoded.signature) throw new Error(`transaction not signed`);
|
|
202
239
|
if (message.decoded.nonce === void 0) throw new Error(`nonce required`);
|
|
240
|
+
await this.validateTransactionSignature(message);
|
|
203
241
|
await this.validateNonce(message.decoded.from, message.decoded.nonce);
|
|
204
242
|
const hash = await message.hash();
|
|
205
243
|
try {
|
|
@@ -416,6 +454,7 @@ class ContractRegistry {
|
|
|
416
454
|
*/
|
|
417
455
|
clear() {
|
|
418
456
|
this.baseContracts.clear();
|
|
457
|
+
this.contractNames.clear();
|
|
419
458
|
this.contractDependencies.clear();
|
|
420
459
|
}
|
|
421
460
|
/**
|
|
@@ -503,7 +542,7 @@ class Contract extends Transaction {
|
|
|
503
542
|
*/
|
|
504
543
|
async deployDerivedContract(signer, contract, baseContractIdentifier, constructorParameters = []) {
|
|
505
544
|
if (baseContractIdentifier) {
|
|
506
|
-
const baseContract = contractRegistry.getBaseContract(baseContractIdentifier);
|
|
545
|
+
const baseContract = contractRegistry.getBaseContract(baseContractIdentifier) ?? contractRegistry.getBaseContractByHash(baseContractIdentifier);
|
|
507
546
|
if (!baseContract) {
|
|
508
547
|
throw new Error(
|
|
509
548
|
`Base contract '${baseContractIdentifier}' not found. Register it first using registerBaseContract.`
|
|
@@ -639,7 +678,8 @@ class Machine {
|
|
|
639
678
|
break;
|
|
640
679
|
}
|
|
641
680
|
case "addToWantList": {
|
|
642
|
-
this.wantList.push(data.hash);
|
|
681
|
+
if (!this.wantList.includes(data.hash)) this.wantList.push(data.hash);
|
|
682
|
+
break;
|
|
643
683
|
}
|
|
644
684
|
case "ask": {
|
|
645
685
|
if (data.question === "contract" || data.question === "transaction") {
|
|
@@ -701,9 +741,11 @@ class Machine {
|
|
|
701
741
|
try {
|
|
702
742
|
const promises = [];
|
|
703
743
|
for (const address of await accountsStore.keys()) {
|
|
704
|
-
promises.push(
|
|
705
|
-
|
|
706
|
-
|
|
744
|
+
promises.push(
|
|
745
|
+
(async () => {
|
|
746
|
+
accounts[address] = await accountsStore.get(address);
|
|
747
|
+
})()
|
|
748
|
+
);
|
|
707
749
|
}
|
|
708
750
|
await Promise.all(promises);
|
|
709
751
|
} catch (error) {
|
|
@@ -712,9 +754,7 @@ class Machine {
|
|
|
712
754
|
const { from, to, amount, nonce } = transaction.decoded.data;
|
|
713
755
|
if (!accounts[from]) {
|
|
714
756
|
accounts[from] = nonce;
|
|
715
|
-
promises.push(
|
|
716
|
-
await accountsStore.put(from, nonce);
|
|
717
|
-
});
|
|
757
|
+
promises.push(accountsStore.put(from, nonce));
|
|
718
758
|
}
|
|
719
759
|
}
|
|
720
760
|
await Promise.all(promises);
|
|
@@ -1262,7 +1302,7 @@ class State extends Contract {
|
|
|
1262
1302
|
#lastBlockHandler;
|
|
1263
1303
|
#knownBlocksHandler;
|
|
1264
1304
|
async init() {
|
|
1265
|
-
|
|
1305
|
+
debug$1("State init start");
|
|
1266
1306
|
this.jobber = new Jobber(this.resolveTimeout);
|
|
1267
1307
|
await globalThis.peernet.addRequestHandler("lastBlock", this.#lastBlockHandler);
|
|
1268
1308
|
await globalThis.peernet.addRequestHandler("knownBlocks", this.#knownBlocksHandler);
|
|
@@ -1297,7 +1337,7 @@ class State extends Contract {
|
|
|
1297
1337
|
console.log("State init middle");
|
|
1298
1338
|
try {
|
|
1299
1339
|
console.log("fetching known blocks from blockStore");
|
|
1300
|
-
this.knownBlocks = await blockStore.keys();
|
|
1340
|
+
this.knownBlocks = await globalThis.blockStore.keys();
|
|
1301
1341
|
} catch (error) {
|
|
1302
1342
|
console.log("no local known blocks found");
|
|
1303
1343
|
}
|
|
@@ -1322,6 +1362,7 @@ class State extends Contract {
|
|
|
1322
1362
|
this.updateState(new BlockMessage(lastBlock));
|
|
1323
1363
|
}
|
|
1324
1364
|
this.#loaded = true;
|
|
1365
|
+
this.#chainState = "loaded";
|
|
1325
1366
|
} catch (error) {
|
|
1326
1367
|
console.log("e");
|
|
1327
1368
|
if (isResolveError(error)) {
|
|
@@ -1469,6 +1510,13 @@ class State extends Contract {
|
|
|
1469
1510
|
console.error(error);
|
|
1470
1511
|
}
|
|
1471
1512
|
try {
|
|
1513
|
+
if (!await globalThis.chainStore.has("lastBlock")) {
|
|
1514
|
+
if (this.knownBlocks.length === 0) {
|
|
1515
|
+
this.#syncState = "connectionless";
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
return this.restoreChain();
|
|
1519
|
+
}
|
|
1472
1520
|
const localBlock = await globalThis.chainStore.get("lastBlock");
|
|
1473
1521
|
const hash = new TextDecoder().decode(localBlock);
|
|
1474
1522
|
if (hash && hash !== "0x0") {
|
|
@@ -1480,10 +1528,15 @@ class State extends Contract {
|
|
|
1480
1528
|
this.#chainSyncing = false;
|
|
1481
1529
|
this.#syncState = "errored";
|
|
1482
1530
|
this.#resolveErrored = true;
|
|
1531
|
+
if (globalThis.peernet.peers.length === 0) return;
|
|
1483
1532
|
return this.restoreChain();
|
|
1484
1533
|
}
|
|
1485
1534
|
}
|
|
1486
1535
|
async restoreChain() {
|
|
1536
|
+
if (globalThis.peernet.peers.length === 0 && this.knownBlocks.length === 0) {
|
|
1537
|
+
this.#syncState = "connectionless";
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1487
1540
|
try {
|
|
1488
1541
|
const { hash } = await this.#getLatestBlock();
|
|
1489
1542
|
await globalThis.chainStore.put("lastBlock", hash);
|
|
@@ -1495,6 +1548,10 @@ class State extends Contract {
|
|
|
1495
1548
|
this.#resolveErrored = true;
|
|
1496
1549
|
this.#resolveErrorCount += 1;
|
|
1497
1550
|
this.#resolving = false;
|
|
1551
|
+
if (this.#resolveErrorCount >= 3) {
|
|
1552
|
+
this.#syncState = "errored";
|
|
1553
|
+
throw new ResolveError("unable to restore chain after 3 attempts", { cause: error });
|
|
1554
|
+
}
|
|
1498
1555
|
return this.restoreChain();
|
|
1499
1556
|
}
|
|
1500
1557
|
}
|
|
@@ -1549,17 +1606,17 @@ class State extends Contract {
|
|
|
1549
1606
|
return;
|
|
1550
1607
|
}
|
|
1551
1608
|
if (localIndex > remoteIndex) {
|
|
1609
|
+
const MAX_REORG_DEPTH = 6;
|
|
1610
|
+
const reorgDepth = localIndex - remoteIndex;
|
|
1611
|
+
if (reorgDepth > MAX_REORG_DEPTH) {
|
|
1612
|
+
console.warn(
|
|
1613
|
+
`[consensus-safety] Peer proposing reorg depth of ${reorgDepth} blocks (limit is ${MAX_REORG_DEPTH}). Rejecting to prevent DoS.`
|
|
1614
|
+
);
|
|
1615
|
+
throw new Error(`Excessive reorg depth: ${reorgDepth} blocks (max ${MAX_REORG_DEPTH})`);
|
|
1616
|
+
}
|
|
1552
1617
|
debug$1(`Local index ${localIndex} is ahead of remote ${remoteIndex}, skipping sync`);
|
|
1553
1618
|
return;
|
|
1554
1619
|
}
|
|
1555
|
-
const MAX_REORG_DEPTH = 6;
|
|
1556
|
-
const reorgDepth = localIndex - remoteIndex;
|
|
1557
|
-
if (reorgDepth > 0 && reorgDepth > MAX_REORG_DEPTH) {
|
|
1558
|
-
console.warn(
|
|
1559
|
-
`[consensus-safety] Peer proposing reorg depth of ${reorgDepth} blocks (limit is ${MAX_REORG_DEPTH}). Rejecting to prevent DoS.`
|
|
1560
|
-
);
|
|
1561
|
-
throw new Error(`Excessive reorg depth: ${reorgDepth} blocks (max ${MAX_REORG_DEPTH})`);
|
|
1562
|
-
}
|
|
1563
1620
|
if (localStateHash !== remoteBlockHash) {
|
|
1564
1621
|
if (this.wantList.length > 0) {
|
|
1565
1622
|
debug$1(`Fetching ${this.wantList.length} blocks before resolving`);
|
|
@@ -1657,7 +1714,7 @@ class State extends Contract {
|
|
|
1657
1714
|
}))
|
|
1658
1715
|
);
|
|
1659
1716
|
let latest = { index: 0, hash: "0x0", previousHash: "0x0" };
|
|
1660
|
-
promises = promises.sort((a, b) => b.index - a.index);
|
|
1717
|
+
promises = promises.sort((a, b) => Number(b.value.index) - Number(a.value.index));
|
|
1661
1718
|
if (promises.length > 0) latest = promises[0].value;
|
|
1662
1719
|
debug$1(`Latest block from peers: ${latest.hash} @${latest.index}`);
|
|
1663
1720
|
if (latest.hash && latest.hash !== "0x0") {
|
|
@@ -1673,7 +1730,7 @@ class State extends Contract {
|
|
|
1673
1730
|
});
|
|
1674
1731
|
let node2 = await globalThis.peernet.prepareMessage(data2);
|
|
1675
1732
|
try {
|
|
1676
|
-
let message2 = await peer.request(node2.
|
|
1733
|
+
let message2 = await peer.request(node2.encoded);
|
|
1677
1734
|
message2 = await new globalThis.peernet.protos["peernet-response"](message2);
|
|
1678
1735
|
const MAX_WANTLIST_SIZE = 1e3;
|
|
1679
1736
|
const incoming = message2.decoded.response.blocks.filter((block) => !this.knownBlocks.includes(block));
|
|
@@ -1841,6 +1898,10 @@ class VersionControl extends State {
|
|
|
1841
1898
|
async init() {
|
|
1842
1899
|
super.init && await super.init();
|
|
1843
1900
|
try {
|
|
1901
|
+
if (!await globalThis.chainStore.has("version")) {
|
|
1902
|
+
await this.#setCurrentVersion();
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1844
1905
|
const version = await globalThis.chainStore.get("version");
|
|
1845
1906
|
const storedVersion = new TextDecoder().decode(version);
|
|
1846
1907
|
console.log(storedVersion, this.#currentVersion);
|
|
@@ -2158,6 +2219,60 @@ class ConnectionMonitor {
|
|
|
2158
2219
|
}
|
|
2159
2220
|
}
|
|
2160
2221
|
|
|
2222
|
+
const quorumThreshold = (validatorCount) => {
|
|
2223
|
+
if (!Number.isSafeInteger(validatorCount) || validatorCount < 1) {
|
|
2224
|
+
throw new Error("validator count must be a positive safe integer");
|
|
2225
|
+
}
|
|
2226
|
+
return Math.floor(2 * validatorCount / 3) + 1;
|
|
2227
|
+
};
|
|
2228
|
+
|
|
2229
|
+
const validateChainLink = (localTip, incoming) => {
|
|
2230
|
+
const localHash = localTip?.hash || "0x0";
|
|
2231
|
+
const localIndex = Number(localTip?.index ?? -1);
|
|
2232
|
+
const hasCanonicalTip = localHash !== "0x0";
|
|
2233
|
+
if (hasCanonicalTip && incoming.index < localIndex) return "stale";
|
|
2234
|
+
if (hasCanonicalTip && incoming.index === localIndex) {
|
|
2235
|
+
if (incoming.hash === localHash) return "duplicate";
|
|
2236
|
+
throw new Error(`Block conflict detected at index ${incoming.index}`);
|
|
2237
|
+
}
|
|
2238
|
+
const expectedIndex = hasCanonicalTip ? localIndex + 1 : 0;
|
|
2239
|
+
if (incoming.index !== expectedIndex) {
|
|
2240
|
+
throw new Error(`Unexpected block index ${incoming.index}; expected ${expectedIndex}`);
|
|
2241
|
+
}
|
|
2242
|
+
if (incoming.previousHash !== localHash) {
|
|
2243
|
+
throw new Error(
|
|
2244
|
+
`previousHash mismatch at index ${incoming.index}: expected ${localHash}, got ${incoming.previousHash}`
|
|
2245
|
+
);
|
|
2246
|
+
}
|
|
2247
|
+
return "append";
|
|
2248
|
+
};
|
|
2249
|
+
|
|
2250
|
+
const consensusSignableData = (validatorsAddress, type, message) => ({
|
|
2251
|
+
from: String(message.from),
|
|
2252
|
+
to: validatorsAddress,
|
|
2253
|
+
method: `consensus:${type}`,
|
|
2254
|
+
params: [String(message.blockHash), String(message.index), String(message.round)],
|
|
2255
|
+
timestamp: 0
|
|
2256
|
+
});
|
|
2257
|
+
const signConsensusMessage = async (validatorsAddress, type, message, identity) => {
|
|
2258
|
+
if (!identity) throw new Error(`cannot sign ${type} without a local identity`);
|
|
2259
|
+
const signed = await signTransaction(consensusSignableData(validatorsAddress, type, message), identity);
|
|
2260
|
+
return signed.signature;
|
|
2261
|
+
};
|
|
2262
|
+
const verifyConsensusMessage = async (validatorsAddress, type, message, network) => {
|
|
2263
|
+
if (typeof message.from !== "string" || typeof message.signature !== "string" || !message.signature) return false;
|
|
2264
|
+
try {
|
|
2265
|
+
const verifier = new MultiWallet(network);
|
|
2266
|
+
await verifier.fromAddress(message.from, null, network);
|
|
2267
|
+
return await verifier.verify(
|
|
2268
|
+
fromBase58(message.signature),
|
|
2269
|
+
await createTransactionHash(consensusSignableData(validatorsAddress, type, message))
|
|
2270
|
+
);
|
|
2271
|
+
} catch {
|
|
2272
|
+
return false;
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
2275
|
+
|
|
2161
2276
|
const debug = createDebugger("leofcoin/chain");
|
|
2162
2277
|
class Chain extends VersionControl {
|
|
2163
2278
|
constructor(config) {
|
|
@@ -2189,6 +2304,8 @@ class Chain extends VersionControl {
|
|
|
2189
2304
|
// ── Tendermint consensus state ──────────────────────────────────────────────
|
|
2190
2305
|
/** Current consensus round (increments when proposer is unresponsive) */
|
|
2191
2306
|
this.#consensusRound = 0;
|
|
2307
|
+
/** Locally proposed block awaiting quorum; prevents duplicate proposals at one height. */
|
|
2308
|
+
this.#proposalInFlight = null;
|
|
2192
2309
|
/** Timer that advances #consensusRound when the proposer doesn't propose in time */
|
|
2193
2310
|
this.#roundTimer = null;
|
|
2194
2311
|
/** prevotes collected per `height:round:blockHash` key */
|
|
@@ -2197,6 +2314,8 @@ class Chain extends VersionControl {
|
|
|
2197
2314
|
this.#precommits = /* @__PURE__ */ new Map();
|
|
2198
2315
|
/** Index of the last block that reached 2f+1 precommits */
|
|
2199
2316
|
this.#committedHeight = -1;
|
|
2317
|
+
/** Heights currently applying to local state; prevents duplicate concurrent commits. */
|
|
2318
|
+
this.#committingHeights = /* @__PURE__ */ new Set();
|
|
2200
2319
|
/** Prevents casting duplicate prevote/precommit per height:round */
|
|
2201
2320
|
this.#castedVotes = /* @__PURE__ */ new Set();
|
|
2202
2321
|
this.ready = new Promise((resolve) => {
|
|
@@ -2210,9 +2329,10 @@ class Chain extends VersionControl {
|
|
|
2210
2329
|
this.#castVote = async (type, blockHash, index, round) => {
|
|
2211
2330
|
const voteKey = `${type}:${index}:${round}`;
|
|
2212
2331
|
if (this.#castedVotes.has(voteKey)) return;
|
|
2213
|
-
this.#castedVotes.add(voteKey);
|
|
2214
2332
|
const from = peernet.selectedAccount;
|
|
2215
|
-
const
|
|
2333
|
+
const unsignedVote = { blockHash, index: BigInt(index), round: BigInt(round), from };
|
|
2334
|
+
const voteData = { ...unsignedVote, signature: await this.#signConsensusMessage(type, unsignedVote) };
|
|
2335
|
+
this.#castedVotes.add(voteKey);
|
|
2216
2336
|
const Message = type === "prevote" ? PrevoteMessage : PrecommitMessage;
|
|
2217
2337
|
const message = new Message(voteData);
|
|
2218
2338
|
const payload = message.encoded;
|
|
@@ -2221,6 +2341,8 @@ class Chain extends VersionControl {
|
|
|
2221
2341
|
} catch (e) {
|
|
2222
2342
|
debug(`peernet publish failed: consensus:${type}`, e?.message ?? e);
|
|
2223
2343
|
}
|
|
2344
|
+
if (type === "prevote") await this.#handlePrevote(payload);
|
|
2345
|
+
else await this.#handlePrecommit(payload);
|
|
2224
2346
|
};
|
|
2225
2347
|
/**
|
|
2226
2348
|
* Phase 2 — receive a block proposal from the designated proposer.
|
|
@@ -2232,6 +2354,10 @@ class Chain extends VersionControl {
|
|
|
2232
2354
|
const message = new ProposalMessage(payload);
|
|
2233
2355
|
const msg = message.decoded;
|
|
2234
2356
|
const { blockHash, index, round, from } = msg;
|
|
2357
|
+
if (!await this.#verifyConsensusMessage("proposal", msg)) {
|
|
2358
|
+
debug(`[consensus] Ignoring proposal with invalid signature from ${from}`);
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2235
2361
|
const validators = await this.#getConsensusValidators(Number(index));
|
|
2236
2362
|
const expectedProposerIdx = Number((index + round) % BigInt(validators.length));
|
|
2237
2363
|
if (!validators[expectedProposerIdx] || validators[expectedProposerIdx] !== from) {
|
|
@@ -2277,6 +2403,10 @@ class Chain extends VersionControl {
|
|
|
2277
2403
|
const message = new PrevoteMessage(payload);
|
|
2278
2404
|
const msg = message.decoded;
|
|
2279
2405
|
const { blockHash, index, round, from } = msg;
|
|
2406
|
+
if (!await this.#verifyConsensusMessage("prevote", msg)) {
|
|
2407
|
+
debug(`[consensus] Ignoring prevote with invalid signature from ${from}`);
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2280
2410
|
const validators = await this.#getConsensusValidators(Number(index));
|
|
2281
2411
|
if (!validators.includes(from)) return;
|
|
2282
2412
|
const localBlock = await this.lastBlock;
|
|
@@ -2285,7 +2415,7 @@ class Chain extends VersionControl {
|
|
|
2285
2415
|
const voteKey = `${index}:${round}:${blockHash}`;
|
|
2286
2416
|
if (!this.#prevotes.has(voteKey)) this.#prevotes.set(voteKey, /* @__PURE__ */ new Set());
|
|
2287
2417
|
this.#prevotes.get(voteKey).add(from);
|
|
2288
|
-
const threshold =
|
|
2418
|
+
const threshold = quorumThreshold(validators.length);
|
|
2289
2419
|
const voteCount = this.#prevotes.get(voteKey).size;
|
|
2290
2420
|
debug(`[consensus] Prevotes ${voteKey}: ${voteCount}/${validators.length} (need ${threshold})`);
|
|
2291
2421
|
if (voteCount >= threshold && validators.includes(peernet.selectedAccount) && !this.#isJailed(peernet.selectedAccount)) {
|
|
@@ -2305,46 +2435,48 @@ class Chain extends VersionControl {
|
|
|
2305
2435
|
const message = new PrecommitMessage(payload);
|
|
2306
2436
|
const msg = message.decoded;
|
|
2307
2437
|
const { blockHash, index, round, from } = msg;
|
|
2438
|
+
if (!await this.#verifyConsensusMessage("precommit", msg)) {
|
|
2439
|
+
debug(`[consensus] Ignoring precommit with invalid signature from ${from}`);
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2308
2442
|
const validators = await this.#getConsensusValidators(Number(index));
|
|
2309
2443
|
if (!validators.includes(from)) return;
|
|
2310
2444
|
if (index <= BigInt(this.#committedHeight)) return;
|
|
2311
2445
|
const voteKey = `${index}:${round}:${blockHash}`;
|
|
2312
2446
|
if (!this.#precommits.has(voteKey)) this.#precommits.set(voteKey, /* @__PURE__ */ new Set());
|
|
2313
2447
|
this.#precommits.get(voteKey).add(from);
|
|
2314
|
-
const threshold =
|
|
2448
|
+
const threshold = quorumThreshold(validators.length);
|
|
2315
2449
|
const voteCount = this.#precommits.get(voteKey).size;
|
|
2316
2450
|
debug(`[consensus] Precommits ${voteKey}: ${voteCount}/${validators.length} (need ${threshold})`);
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
this.#
|
|
2320
|
-
for (const key of [...this.#prevotes.keys()]) {
|
|
2321
|
-
if (BigInt(key.split(":")[0]) <= index) this.#prevotes.delete(key);
|
|
2322
|
-
}
|
|
2323
|
-
for (const key of [...this.#precommits.keys()]) {
|
|
2324
|
-
if (BigInt(key.split(":")[0]) <= index) this.#precommits.delete(key);
|
|
2325
|
-
}
|
|
2326
|
-
for (const key of [...this.#castedVotes]) {
|
|
2327
|
-
if (BigInt(key.split(":")[1]) <= index) this.#castedVotes.delete(key);
|
|
2328
|
-
}
|
|
2329
|
-
const currentBlock = await this.lastBlock;
|
|
2330
|
-
const currentIndex = currentBlock?.index !== void 0 ? currentBlock.index : -1n;
|
|
2331
|
-
if (index > currentIndex) {
|
|
2332
|
-
debug(`[consensus] \u2705 Committing block ${blockHash} at height ${index}`);
|
|
2333
|
-
try {
|
|
2334
|
-
const blockData = await globalThis.peernet.get(blockHash, "block");
|
|
2335
|
-
await this.#addBlock(blockData);
|
|
2336
|
-
} catch (e) {
|
|
2337
|
-
debug(`[consensus] Failed to commit block ${blockHash}:`, e?.message);
|
|
2338
|
-
}
|
|
2339
|
-
} else {
|
|
2340
|
-
debug(`[consensus] \u2705 Block ${blockHash} at height ${index} already committed (proposer path)`);
|
|
2341
|
-
}
|
|
2451
|
+
const numericIndex = Number(index);
|
|
2452
|
+
if (voteCount >= threshold && index > BigInt(this.#committedHeight) && !this.#committingHeights.has(numericIndex)) {
|
|
2453
|
+
this.#committingHeights.add(numericIndex);
|
|
2342
2454
|
try {
|
|
2455
|
+
debug(`[consensus] \u2705 Committing block ${blockHash} at height ${index}`);
|
|
2343
2456
|
const blockData = await globalThis.peernet.get(blockHash, "block");
|
|
2457
|
+
await this.#addBlock(blockData);
|
|
2458
|
+
this.#committedHeight = numericIndex;
|
|
2459
|
+
this.#consensusRound = 0;
|
|
2460
|
+
if (this.#proposalInFlight?.hash === blockHash) this.#proposalInFlight = null;
|
|
2461
|
+
if (this.#roundTimer) {
|
|
2462
|
+
clearTimeout(this.#roundTimer);
|
|
2463
|
+
this.#roundTimer = null;
|
|
2464
|
+
}
|
|
2465
|
+
for (const key of [...this.#prevotes.keys()]) {
|
|
2466
|
+
if (BigInt(key.split(":")[0]) <= index) this.#prevotes.delete(key);
|
|
2467
|
+
}
|
|
2468
|
+
for (const key of [...this.#precommits.keys()]) {
|
|
2469
|
+
if (BigInt(key.split(":")[0]) <= index) this.#precommits.delete(key);
|
|
2470
|
+
}
|
|
2471
|
+
for (const key of [...this.#castedVotes]) {
|
|
2472
|
+
if (BigInt(key.split(":")[1]) <= index) this.#castedVotes.delete(key);
|
|
2473
|
+
}
|
|
2344
2474
|
globalThis.peernet.publish("add-block", blockData);
|
|
2345
2475
|
globalThis.pubsub.publish("add-block", blockData);
|
|
2346
2476
|
} catch (e) {
|
|
2347
|
-
debug(
|
|
2477
|
+
debug(`[consensus] Failed to commit block ${blockHash}:`, e?.message);
|
|
2478
|
+
} finally {
|
|
2479
|
+
this.#committingHeights.delete(numericIndex);
|
|
2348
2480
|
}
|
|
2349
2481
|
}
|
|
2350
2482
|
} catch (e) {
|
|
@@ -2379,10 +2511,12 @@ class Chain extends VersionControl {
|
|
|
2379
2511
|
#minPeerScore;
|
|
2380
2512
|
#maxPeerFailures;
|
|
2381
2513
|
#consensusRound;
|
|
2514
|
+
#proposalInFlight;
|
|
2382
2515
|
#roundTimer;
|
|
2383
2516
|
#prevotes;
|
|
2384
2517
|
#precommits;
|
|
2385
2518
|
#committedHeight;
|
|
2519
|
+
#committingHeights;
|
|
2386
2520
|
#castedVotes;
|
|
2387
2521
|
// ────────────────────────────────────────────────────────────────────────────
|
|
2388
2522
|
#connectionMonitor;
|
|
@@ -2422,6 +2556,18 @@ class Chain extends VersionControl {
|
|
|
2422
2556
|
#isJailed(address) {
|
|
2423
2557
|
return typeof address === "string" && this.#jail.has(address);
|
|
2424
2558
|
}
|
|
2559
|
+
async #signConsensusMessage(type, message) {
|
|
2560
|
+
const identity = globalThis.peernet?.identity;
|
|
2561
|
+
return signConsensusMessage(addresses.validators, type, message, identity);
|
|
2562
|
+
}
|
|
2563
|
+
async #verifyConsensusMessage(type, message) {
|
|
2564
|
+
return verifyConsensusMessage(
|
|
2565
|
+
addresses.validators,
|
|
2566
|
+
type,
|
|
2567
|
+
message,
|
|
2568
|
+
globalThis.peernet?.network || "leofcoin"
|
|
2569
|
+
);
|
|
2570
|
+
}
|
|
2425
2571
|
async #getConsensusValidators(nextBlockIndex) {
|
|
2426
2572
|
const localBlock = await this.lastBlock;
|
|
2427
2573
|
const localIndex = localBlock?.index !== void 0 ? Number(localBlock.index) : -1;
|
|
@@ -2435,7 +2581,7 @@ class Chain extends VersionControl {
|
|
|
2435
2581
|
const validators = await this.staticCall(addresses.validators, "validators");
|
|
2436
2582
|
return [...new Set(validators)].sort();
|
|
2437
2583
|
}
|
|
2438
|
-
#validateBlockValidators(blockMessage) {
|
|
2584
|
+
async #validateBlockValidators(blockMessage) {
|
|
2439
2585
|
const validators = blockMessage.decoded.validators || [];
|
|
2440
2586
|
if (!Array.isArray(validators) || validators.length === 0) {
|
|
2441
2587
|
throw new Error(`Block ${blockMessage.decoded.index} does not include validators`);
|
|
@@ -2460,15 +2606,31 @@ class Chain extends VersionControl {
|
|
|
2460
2606
|
`Block ${blockMessage.decoded.index} producer ${blockMessage.decoded.producer} is not in validators list`
|
|
2461
2607
|
);
|
|
2462
2608
|
}
|
|
2463
|
-
const
|
|
2464
|
-
|
|
2609
|
+
const unsignedBlockMessage = new BlockMessage({ ...blockMessage.decoded, producerProof: "" });
|
|
2610
|
+
const unsignedBlockHash = await unsignedBlockMessage.hash();
|
|
2611
|
+
const proofPayload = await createTransactionHash({
|
|
2612
|
+
from: blockMessage.decoded.producer,
|
|
2613
|
+
to: addresses.validators,
|
|
2614
|
+
method: "produceBlock",
|
|
2615
|
+
params: [unsignedBlockHash],
|
|
2616
|
+
timestamp: Number(blockMessage.decoded.timestamp)
|
|
2617
|
+
});
|
|
2618
|
+
const network = globalThis.peernet?.network || "leofcoin";
|
|
2619
|
+
const verifier = new MultiWallet(network);
|
|
2620
|
+
await verifier.fromAddress(blockMessage.decoded.producer, null, network);
|
|
2621
|
+
const validProducerProof = await verifier.verify(fromBase58(blockMessage.decoded.producerProof), proofPayload);
|
|
2622
|
+
if (!validProducerProof) {
|
|
2623
|
+
throw new Error(`Block ${blockMessage.decoded.index} has an invalid producerProof`);
|
|
2624
|
+
}
|
|
2625
|
+
const validatorAddresses = validators.map((validator) => validator.address);
|
|
2626
|
+
if (validatorAddresses.some((address) => typeof address !== "string" || address.length === 0)) {
|
|
2465
2627
|
throw new Error(`Block ${blockMessage.decoded.index} includes an invalid validator address`);
|
|
2466
2628
|
}
|
|
2467
|
-
const canonicalAddresses = [...
|
|
2468
|
-
if (canonicalAddresses.some((address, index) => address !==
|
|
2629
|
+
const canonicalAddresses = [...validatorAddresses].sort();
|
|
2630
|
+
if (canonicalAddresses.some((address, index) => address !== validatorAddresses[index])) {
|
|
2469
2631
|
throw new Error(`Block ${blockMessage.decoded.index} validators are not canonically sorted`);
|
|
2470
2632
|
}
|
|
2471
|
-
if (new Set(
|
|
2633
|
+
if (new Set(validatorAddresses).size !== validatorAddresses.length) {
|
|
2472
2634
|
throw new Error(`Block ${blockMessage.decoded.index} validators contain duplicates`);
|
|
2473
2635
|
}
|
|
2474
2636
|
const validatorCount = BigInt(validators.length);
|
|
@@ -2545,7 +2707,7 @@ class Chain extends VersionControl {
|
|
|
2545
2707
|
const hasMore = await this.hasTransactionToHandle();
|
|
2546
2708
|
if (!hasMore && remaining > 0) await this.#sleep(remaining);
|
|
2547
2709
|
this.#runningEpoch = false;
|
|
2548
|
-
if (hasMore) return this.#runEpoch();
|
|
2710
|
+
if (hasMore && !this.#proposalInFlight) return this.#runEpoch();
|
|
2549
2711
|
}
|
|
2550
2712
|
async #setup() {
|
|
2551
2713
|
const contracts = [
|
|
@@ -2981,29 +3143,15 @@ class Chain extends VersionControl {
|
|
|
2981
3143
|
const blockMessage = await new BlockMessage(block);
|
|
2982
3144
|
const hash = await blockMessage.hash();
|
|
2983
3145
|
const blockIndex = Number(blockMessage.decoded.index);
|
|
2984
|
-
const
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
}
|
|
2991
|
-
debug(`Block already in store: ${hash}`);
|
|
3146
|
+
const linkResult = validateChainLink(await this.lastBlock, {
|
|
3147
|
+
index: blockIndex,
|
|
3148
|
+
hash,
|
|
3149
|
+
previousHash: String(blockMessage.decoded.previousHash)
|
|
3150
|
+
});
|
|
3151
|
+
if (linkResult !== "append") {
|
|
3152
|
+
debug(`Ignoring ${linkResult} block ${hash} at height ${blockIndex}`);
|
|
2992
3153
|
return;
|
|
2993
3154
|
}
|
|
2994
|
-
if (blockIndex > 0) {
|
|
2995
|
-
const previousBlockInfo = this.#blocks[blockIndex - 1];
|
|
2996
|
-
if (!previousBlockInfo) {
|
|
2997
|
-
throw new Error(`Missing parent block at index ${blockIndex - 1}`);
|
|
2998
|
-
}
|
|
2999
|
-
if (previousBlockInfo.hash !== blockMessage.decoded.previousHash) {
|
|
3000
|
-
throw new Error(
|
|
3001
|
-
`previousHash mismatch at index ${blockIndex}: expected ${previousBlockInfo.hash}, got ${blockMessage.decoded.previousHash}`
|
|
3002
|
-
);
|
|
3003
|
-
}
|
|
3004
|
-
} else if (blockMessage.decoded.previousHash !== "0x0") {
|
|
3005
|
-
throw new Error(`Genesis block (index 0) must have previousHash='0x0'`);
|
|
3006
|
-
}
|
|
3007
3155
|
const canonicalEncoded = blockMessage.encoded;
|
|
3008
3156
|
const byteLengthMatch = receivedEncoded.length === canonicalEncoded.length;
|
|
3009
3157
|
if (!byteLengthMatch) {
|
|
@@ -3022,14 +3170,19 @@ class Chain extends VersionControl {
|
|
|
3022
3170
|
throw new Error(`[FATAL] Block data corrupted in transit for block #${blockIndex} hash ${hash}`);
|
|
3023
3171
|
}
|
|
3024
3172
|
console.log(`[chain] \u2705 Block data integrity verified: ${hash}`);
|
|
3025
|
-
this.#validateBlockValidators(blockMessage);
|
|
3173
|
+
await this.#validateBlockValidators(blockMessage);
|
|
3026
3174
|
const transactions = await Promise.all(
|
|
3027
3175
|
blockMessage.decoded.transactions.map(async (hash2) => {
|
|
3028
3176
|
const data = await peernet.get(hash2, "transaction");
|
|
3029
|
-
await transactionPoolStore.has(hash2) && await transactionPoolStore.delete(hash2);
|
|
3030
3177
|
return new TransactionMessage(data);
|
|
3031
3178
|
})
|
|
3032
3179
|
);
|
|
3180
|
+
await Promise.all(transactions.map((transaction) => this.validateTransactionSignature(transaction)));
|
|
3181
|
+
await Promise.all(
|
|
3182
|
+
blockMessage.decoded.transactions.map(async (transactionHash) => {
|
|
3183
|
+
if (await transactionPoolStore.has(transactionHash)) await transactionPoolStore.delete(transactionHash);
|
|
3184
|
+
})
|
|
3185
|
+
);
|
|
3033
3186
|
await globalThis.blockStore.put(hash, blockMessage.encoded);
|
|
3034
3187
|
this.#blocks[blockIndex] = {
|
|
3035
3188
|
hash,
|
|
@@ -3046,26 +3199,11 @@ class Chain extends VersionControl {
|
|
|
3046
3199
|
if (nonceDiff !== 0) return nonceDiff;
|
|
3047
3200
|
return 0;
|
|
3048
3201
|
});
|
|
3049
|
-
const txsByAddress = /* @__PURE__ */ new Map();
|
|
3050
3202
|
for (const transaction of allTransactions) {
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
}
|
|
3055
|
-
txsByAddress.get(sender).push(transaction);
|
|
3203
|
+
if (!contracts.includes(transaction.decoded.to)) contracts.push(transaction.decoded.to);
|
|
3204
|
+
this.removePendingNonce(transaction.decoded.from, transaction.decoded.nonce);
|
|
3205
|
+
await this.#handleTransaction(transaction, []);
|
|
3056
3206
|
}
|
|
3057
|
-
const addressGroups = [...txsByAddress.values()];
|
|
3058
|
-
await Promise.all(
|
|
3059
|
-
addressGroups.map(async (addressTxs) => {
|
|
3060
|
-
for (const transaction of addressTxs) {
|
|
3061
|
-
if (!contracts.includes(transaction.decoded.to)) {
|
|
3062
|
-
contracts.push(transaction.decoded.to);
|
|
3063
|
-
}
|
|
3064
|
-
this.removePendingNonce(transaction.decoded.from, transaction.decoded.nonce);
|
|
3065
|
-
await this.#handleTransaction(transaction, []);
|
|
3066
|
-
}
|
|
3067
|
-
})
|
|
3068
|
-
);
|
|
3069
3207
|
try {
|
|
3070
3208
|
promises = await Promise.allSettled(promises);
|
|
3071
3209
|
const noncesByAddress = {};
|
|
@@ -3098,7 +3236,6 @@ class Chain extends VersionControl {
|
|
|
3098
3236
|
}
|
|
3099
3237
|
}
|
|
3100
3238
|
async participate(address) {
|
|
3101
|
-
this.#participating = true;
|
|
3102
3239
|
try {
|
|
3103
3240
|
if (!await this.staticCall(addresses.validators, "has", [address])) {
|
|
3104
3241
|
const rawTransaction = {
|
|
@@ -3110,18 +3247,20 @@ class Chain extends VersionControl {
|
|
|
3110
3247
|
timestamp: Date.now()
|
|
3111
3248
|
};
|
|
3112
3249
|
const transaction = await signTransaction(rawTransaction, globalThis.peernet.identity);
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
console.error(error);
|
|
3117
|
-
}
|
|
3250
|
+
await this.sendTransaction(transaction);
|
|
3251
|
+
this.#participating = false;
|
|
3252
|
+
return false;
|
|
3118
3253
|
}
|
|
3119
3254
|
} catch (error) {
|
|
3120
|
-
|
|
3255
|
+
this.#participating = false;
|
|
3256
|
+
throw new Error(`validator participation failed: ${error?.message ?? error}`);
|
|
3121
3257
|
}
|
|
3258
|
+
this.#participating = true;
|
|
3122
3259
|
if (await this.hasTransactionToHandle() && !this.#runningEpoch && this.#participating) await this.#runEpoch();
|
|
3260
|
+
return true;
|
|
3123
3261
|
}
|
|
3124
3262
|
async #handleTransaction(transaction, latestTransactions, block) {
|
|
3263
|
+
await this.validateTransactionSignature(transaction);
|
|
3125
3264
|
const hash = await transaction.hash();
|
|
3126
3265
|
const doubleTransactions = [];
|
|
3127
3266
|
if (latestTransactions.includes(hash) || await transactionStore.has(hash)) {
|
|
@@ -3153,12 +3292,13 @@ class Chain extends VersionControl {
|
|
|
3153
3292
|
}
|
|
3154
3293
|
// todo filter tx that need to wait on prev nonce
|
|
3155
3294
|
async #createBlock(limit = this.transactionLimit) {
|
|
3295
|
+
if (this.#proposalInFlight) return;
|
|
3156
3296
|
console.log(await globalThis.transactionPoolStore.size());
|
|
3157
3297
|
if (await globalThis.transactionPoolStore.size() === 0) return;
|
|
3158
3298
|
let transactions = await globalThis.transactionPoolStore.values(this.transactionLimit);
|
|
3159
3299
|
if (Object.keys(transactions)?.length === 0) return;
|
|
3160
3300
|
const timestamp = Date.now();
|
|
3161
|
-
|
|
3301
|
+
const block = {
|
|
3162
3302
|
transactions: [],
|
|
3163
3303
|
validators: [],
|
|
3164
3304
|
fees: BigInt(0),
|
|
@@ -3180,70 +3320,17 @@ class Chain extends VersionControl {
|
|
|
3180
3320
|
if (nonceDiff !== 0) return nonceDiff;
|
|
3181
3321
|
return 0;
|
|
3182
3322
|
});
|
|
3183
|
-
const txsByAddress = /* @__PURE__ */ new Map();
|
|
3184
3323
|
for (const transaction of allTransactions) {
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3324
|
+
await this.validateTransactionSignature(transaction);
|
|
3325
|
+
const hash = await transaction.hash();
|
|
3326
|
+
if (latestTransactions.includes(hash) || await globalThis.transactionStore.has(hash)) {
|
|
3327
|
+
continue;
|
|
3188
3328
|
}
|
|
3189
|
-
|
|
3329
|
+
block.transactions.push(hash);
|
|
3330
|
+
block.fees += BigInt(await calculateFee(transaction.decoded));
|
|
3331
|
+
await globalThis.peernet.put(hash, transaction.encoded, "transaction");
|
|
3190
3332
|
}
|
|
3191
|
-
const addressGroups = [...txsByAddress.values()];
|
|
3192
|
-
await Promise.all(
|
|
3193
|
-
addressGroups.map(async (addressTxs) => {
|
|
3194
|
-
for (const transaction of addressTxs) {
|
|
3195
|
-
await this.#handleTransaction(transaction, latestTransactions, block);
|
|
3196
|
-
}
|
|
3197
|
-
})
|
|
3198
|
-
);
|
|
3199
3333
|
if (block.transactions.length === 0) return;
|
|
3200
|
-
const validators = await this.staticCall(addresses.validators, "validators");
|
|
3201
|
-
const peers = {};
|
|
3202
|
-
for (const entry of globalThis.peernet.peers) {
|
|
3203
|
-
peers[entry[0]] = entry[1];
|
|
3204
|
-
}
|
|
3205
|
-
const finalizeBWAndBroadcast = async () => {
|
|
3206
|
-
const bwPromises = validators.map(async (validator) => {
|
|
3207
|
-
const peer = peers[validator];
|
|
3208
|
-
if (peer && peer.connected && this.isVersionCompatible(peer.version)) {
|
|
3209
|
-
try {
|
|
3210
|
-
let data = await new BWRequestMessage();
|
|
3211
|
-
const node = await globalThis.peernet.prepareMessage(data.encoded);
|
|
3212
|
-
const bw = await peer.request(node.encoded);
|
|
3213
|
-
return {
|
|
3214
|
-
address: validator,
|
|
3215
|
-
bw: bw.up + bw.down
|
|
3216
|
-
};
|
|
3217
|
-
} catch (error) {
|
|
3218
|
-
const peerId = peer?.peerId || peer?.id || peer?.address || "unknown";
|
|
3219
|
-
debug(`bw request failed: ${peerId}:`, error?.message ?? error);
|
|
3220
|
-
return null;
|
|
3221
|
-
}
|
|
3222
|
-
} else if (globalThis.peernet.selectedAccount === validator) {
|
|
3223
|
-
return {
|
|
3224
|
-
address: globalThis.peernet.selectedAccount,
|
|
3225
|
-
bw: globalThis.peernet.bw.up + globalThis.peernet.bw.down
|
|
3226
|
-
};
|
|
3227
|
-
}
|
|
3228
|
-
return null;
|
|
3229
|
-
});
|
|
3230
|
-
const bwResults = await Promise.allSettled(bwPromises);
|
|
3231
|
-
for (const result of bwResults) {
|
|
3232
|
-
if (result.status === "fulfilled" && result.value) {
|
|
3233
|
-
block.validators.push(result.value);
|
|
3234
|
-
}
|
|
3235
|
-
}
|
|
3236
|
-
};
|
|
3237
|
-
finalizeBWAndBroadcast().catch((error) => {
|
|
3238
|
-
debug(`background BW finalization failed:`, error?.message ?? error);
|
|
3239
|
-
});
|
|
3240
|
-
block.validators = block.validators.map((validator) => {
|
|
3241
|
-
validator.reward = block.fees;
|
|
3242
|
-
validator.reward += block.reward;
|
|
3243
|
-
validator.reward /= BigInt(block.validators.length);
|
|
3244
|
-
delete validator.bw;
|
|
3245
|
-
return validator;
|
|
3246
|
-
});
|
|
3247
3334
|
const localBlock = await this.lastBlock;
|
|
3248
3335
|
block.index = localBlock.index;
|
|
3249
3336
|
if (block.index === void 0) block.index = 0;
|
|
@@ -3251,17 +3338,12 @@ class Chain extends VersionControl {
|
|
|
3251
3338
|
block.previousHash = localBlock.hash || "0x0";
|
|
3252
3339
|
const canonicalValidators = await this.staticCall(addresses.validators, "validators");
|
|
3253
3340
|
const sortedValidators = [...canonicalValidators].sort();
|
|
3341
|
+
if (sortedValidators.length === 0) throw new Error("cannot produce a block without validators");
|
|
3254
3342
|
block.validators = sortedValidators.map((validatorAddress) => ({
|
|
3255
3343
|
address: validatorAddress,
|
|
3256
3344
|
reward: block.fees / BigInt(sortedValidators.length) + block.reward / BigInt(sortedValidators.length)
|
|
3257
3345
|
}));
|
|
3258
3346
|
try {
|
|
3259
|
-
await Promise.all(
|
|
3260
|
-
block.transactions.map(async (transaction) => {
|
|
3261
|
-
await globalThis.transactionStore.put(transaction, await transactionPoolStore.get(transaction));
|
|
3262
|
-
await globalThis.transactionPoolStore.delete(transaction);
|
|
3263
|
-
})
|
|
3264
|
-
);
|
|
3265
3347
|
block.producer = globalThis.peernet.selectedAccount || "";
|
|
3266
3348
|
const producerSigner = globalThis.peernet?.identity;
|
|
3267
3349
|
if (block.producer && producerSigner) {
|
|
@@ -3279,19 +3361,23 @@ class Chain extends VersionControl {
|
|
|
3279
3361
|
);
|
|
3280
3362
|
block.producerProof = signedProof.signature;
|
|
3281
3363
|
}
|
|
3364
|
+
if (!block.producer || !block.producerProof) throw new Error("block producer identity is not available");
|
|
3282
3365
|
let blockMessage = await new BlockMessage(block);
|
|
3283
3366
|
const hash = await blockMessage.hash();
|
|
3284
3367
|
await globalThis.peernet.put(hash, blockMessage.encoded, "block");
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
debug(`created block: ${hash} @${block.index}`);
|
|
3368
|
+
this.#proposalInFlight = { hash, index: block.index, round: this.#consensusRound };
|
|
3369
|
+
debug(`proposed block: ${hash} @${block.index}`);
|
|
3288
3370
|
console.log(`[consensus] \u{1F4E4} Proposing block #${block.index} | hash: ${hash} | round: ${this.#consensusRound}`);
|
|
3289
|
-
const
|
|
3371
|
+
const unsignedProposal = {
|
|
3290
3372
|
blockHash: hash,
|
|
3291
3373
|
index: BigInt(block.index),
|
|
3292
3374
|
round: BigInt(this.#consensusRound),
|
|
3293
3375
|
from: peernet.selectedAccount
|
|
3294
3376
|
};
|
|
3377
|
+
const proposalData = {
|
|
3378
|
+
...unsignedProposal,
|
|
3379
|
+
signature: await this.#signConsensusMessage("proposal", unsignedProposal)
|
|
3380
|
+
};
|
|
3295
3381
|
const proposalMessage = new ProposalMessage(proposalData);
|
|
3296
3382
|
const proposalPayload = proposalMessage.encoded;
|
|
3297
3383
|
try {
|
|
@@ -3300,6 +3386,16 @@ class Chain extends VersionControl {
|
|
|
3300
3386
|
debug("peernet publish failed: consensus:propose", publishError?.message ?? publishError);
|
|
3301
3387
|
}
|
|
3302
3388
|
await this.#castVote("prevote", hash, block.index, this.#consensusRound);
|
|
3389
|
+
if (!this.#roundTimer && this.#proposalInFlight?.hash === hash) {
|
|
3390
|
+
this.#roundTimer = setTimeout(async () => {
|
|
3391
|
+
this.#roundTimer = null;
|
|
3392
|
+
if (this.#proposalInFlight?.hash !== hash) return;
|
|
3393
|
+
this.#proposalInFlight = null;
|
|
3394
|
+
this.#consensusRound += 1;
|
|
3395
|
+
this.#runningEpoch = false;
|
|
3396
|
+
if (this.#participating) await this.#runEpoch();
|
|
3397
|
+
}, this.#slotTime);
|
|
3398
|
+
}
|
|
3303
3399
|
} catch (error) {
|
|
3304
3400
|
console.log(error);
|
|
3305
3401
|
throw new Error(`invalid block ${block}`);
|
|
@@ -3307,6 +3403,7 @@ class Chain extends VersionControl {
|
|
|
3307
3403
|
}
|
|
3308
3404
|
async #sendTransaction(transaction) {
|
|
3309
3405
|
transaction = await new TransactionMessage(transaction.encoded || transaction);
|
|
3406
|
+
await this.validateTransactionSignature(transaction);
|
|
3310
3407
|
const hash = await transaction.hash();
|
|
3311
3408
|
try {
|
|
3312
3409
|
const has = await globalThis.transactionPoolStore.has(hash);
|