@leofcoin/chain 1.9.2 → 1.9.4

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/chain.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createDebugger } from '@vandeurenglenn/debug';
2
2
  import { formatBytes, jsonStringifyBigInt, jsonParseBigInt, parseUnits, formatUnits } from '@leofcoin/utils';
3
- import { TransactionMessage, BlockMessage, ContractMessage, LastBlockMessage, BWMessage, BWRequestMessage } from '@leofcoin/messages';
3
+ import { TransactionMessage, BlockMessage, ContractMessage, LastBlockMessage, PrevoteMessage, PrecommitMessage, ProposalMessage, BWMessage, StateMessage, BWRequestMessage } from '@leofcoin/messages';
4
4
  import addresses, { contractFactory } from '@leofcoin/addresses';
5
5
  import { calculateFee, createContractMessage, signTransaction, contractFactoryMessage, nativeTokenMessage, validatorsMessage, nameServiceMessage } from '@leofcoin/lib';
6
6
  import semver from 'semver';
@@ -8,7 +8,6 @@ import { randombytes } from '@leofcoin/crypto';
8
8
  import EasyWorker from '@vandeurenglenn/easy-worker';
9
9
  import { ContractDeploymentError, ExecutionError, isResolveError, ResolveError, isExecutionError } from '@leofcoin/errors';
10
10
  import { log } from 'console';
11
- import codecs from '@leofcoin/codecs/utils';
12
11
  import { P as PROTOCOL_VERSION, R as REACHED_ONE_ZERO_ZERO } from './constants-eo0U5-D_.js';
13
12
  import { log as log$1 } from 'node:console';
14
13
  import '@leofcoin/networks';
@@ -154,11 +153,25 @@ class Transaction extends Protocol {
154
153
  return Number(nonce);
155
154
  }
156
155
  async validateNonce(address, nonce) {
157
- const previousNonce = await this.getNonce(address);
158
- if (previousNonce > nonce)
159
- throw new Error(`a transaction with a higher nonce already exists`);
160
- if (previousNonce === nonce)
156
+ // Compare only against the COMMITTED nonce (accountsStore), not the pool max.
157
+ // The pool may hold many future nonces from batch sends — rejecting lower nonces
158
+ // because a higher one is already queued would break concurrent batch submission.
159
+ let committedNonce;
160
+ try {
161
+ if (await globalThis.accountsStore.has(address)) {
162
+ const raw = await globalThis.accountsStore.get(address);
163
+ committedNonce = Number(new TextDecoder().decode(raw));
164
+ }
165
+ else {
166
+ committedNonce = await this.#getNonceFallback(address);
167
+ }
168
+ }
169
+ catch {
170
+ committedNonce = 0;
171
+ }
172
+ if (committedNonce >= nonce)
161
173
  throw new Error(`a transaction with the same nonce already exists`);
174
+ // Only reject exact duplicates already in the pool (not "higher nonce" rejections)
162
175
  let transactions = await globalThis.transactionPoolStore.values();
163
176
  transactions = await this.promiseTransactions(transactions);
164
177
  transactions = transactions.filter((tx) => tx.decoded.from === address);
@@ -1143,11 +1156,6 @@ class Jobber {
1143
1156
  }
1144
1157
  }
1145
1158
 
1146
- codecs.addCodec({
1147
- name: 'last-block-message',
1148
- codec: 0x6c626d,
1149
- hashAlg: 'keccak-256'
1150
- });
1151
1159
  const debug$1 = createDebugger('leofcoin/state');
1152
1160
  class State extends Contract {
1153
1161
  #resolveErrored;
@@ -1709,6 +1717,8 @@ class State extends Contract {
1709
1717
  }
1710
1718
  async #getLatestBlock() {
1711
1719
  let promises = [];
1720
+ const connectedPeers = Object.values(globalThis.peernet.connections || {}).filter((peer) => peer.connected);
1721
+ let compatiblePeerCount = 0;
1712
1722
  let data = await new globalThis.peernet.protos['peernet-request']({
1713
1723
  request: 'lastBlock'
1714
1724
  });
@@ -1716,15 +1726,8 @@ class State extends Contract {
1716
1726
  for (const id in globalThis.peernet.connections) {
1717
1727
  // @ts-ignore
1718
1728
  const peer = globalThis.peernet.connections[id];
1719
- // CRITICAL FIX: Use semver comparison (major.minor) not exact match
1720
- const isVersionCompatible = () => {
1721
- if (!peer.version || !this.version)
1722
- return false;
1723
- const [peerMajor, peerMinor] = peer.version.split('.');
1724
- const [localMajor, localMinor] = this.version.split('.');
1725
- return peerMajor === localMajor && peerMinor === localMinor;
1726
- };
1727
- if (peer.connected && isVersionCompatible()) {
1729
+ if (peer.connected && this.isVersionCompatible(peer.version)) {
1730
+ compatiblePeerCount += 1;
1728
1731
  const task = async () => {
1729
1732
  try {
1730
1733
  const result = await peer.request(node.encoded);
@@ -1741,9 +1744,15 @@ class State extends Contract {
1741
1744
  promises.push(task());
1742
1745
  }
1743
1746
  }
1747
+ if (connectedPeers.length > 0 && compatiblePeerCount === 0) {
1748
+ throw new ResolveError(`latestBlock: no compatible peers found for local version ${this.version} among ${connectedPeers.length} connected peers`);
1749
+ }
1744
1750
  // @ts-ignore
1745
1751
  console.log({ promises });
1746
1752
  promises = (await this.promiseRequests(promises));
1753
+ if (compatiblePeerCount > 0 && promises.length === 0) {
1754
+ throw new ResolveError('latestBlock: no responses from compatible peers');
1755
+ }
1747
1756
  console.log({ promises });
1748
1757
  let latest = { index: 0, hash: '0x0', previousHash: '0x0' };
1749
1758
  promises = promises.sort((a, b) => b.index - a.index);
@@ -1758,15 +1767,7 @@ class State extends Contract {
1758
1767
  throw new Error('invalid block @getLatestBlock');
1759
1768
  latest = { ...message.decoded, hash };
1760
1769
  const peer = promises[0].peer;
1761
- // CRITICAL FIX: Check version compatibility using semver
1762
- const isVersionCompatible = () => {
1763
- if (!peer.version || !this.version)
1764
- return false;
1765
- const [peerMajor, peerMinor] = peer.version.split('.');
1766
- const [localMajor, localMinor] = this.version.split('.');
1767
- return peerMajor === localMajor && peerMinor === localMinor;
1768
- };
1769
- if (peer.connected && isVersionCompatible()) {
1770
+ if (peer.connected && this.isVersionCompatible(peer.version)) {
1770
1771
  let data = await new globalThis.peernet.protos['peernet-request']({
1771
1772
  request: 'knownBlocks'
1772
1773
  });
@@ -1914,7 +1915,7 @@ class State extends Contract {
1914
1915
  if (this.#chainSyncing)
1915
1916
  return false;
1916
1917
  // Check if we have any connected peers with the same version
1917
- const compatiblePeers = Object.values(globalThis.peernet.connections || {}).filter((peer) => peer.connected && peer.version === this.version);
1918
+ const compatiblePeers = Object.values(globalThis.peernet.connections || {}).filter((peer) => peer.connected && this.isVersionCompatible(peer.version));
1918
1919
  if (compatiblePeers.length === 0) {
1919
1920
  debug$1('No compatible peers available for sync');
1920
1921
  return false;
@@ -1930,7 +1931,7 @@ class State extends Contract {
1930
1931
  async #waitForPeers(timeoutMs = 30000) {
1931
1932
  return new Promise((resolve) => {
1932
1933
  const checkPeers = () => {
1933
- const peers = Object.values(globalThis.peernet.connections || {}).filter((peer) => peer.connected && peer.version === this.version);
1934
+ const peers = Object.values(globalThis.peernet.connections || {}).filter((peer) => peer.connected && this.isVersionCompatible(peer.version));
1934
1935
  if (peers.length > 0) {
1935
1936
  resolve(true);
1936
1937
  }
@@ -2341,16 +2342,6 @@ class ConnectionMonitor {
2341
2342
  }
2342
2343
  }
2343
2344
 
2344
- codecs.addCodec({
2345
- name: 'last-block-message',
2346
- codec: 0x6c626d,
2347
- hashAlg: 'keccak-256'
2348
- });
2349
- codecs.addCodec({
2350
- name: 'last-block-request-message',
2351
- codec: 0x6c62726d,
2352
- hashAlg: 'keccak-256'
2353
- });
2354
2345
  const debug = createDebugger('leofcoin/chain');
2355
2346
  // check if browser or local
2356
2347
  class Chain extends VersionControl {
@@ -2444,7 +2435,10 @@ class Chain extends VersionControl {
2444
2435
  return;
2445
2436
  this.#castedVotes.add(voteKey);
2446
2437
  const from = peernet.selectedAccount;
2447
- const payload = new TextEncoder().encode(JSON.stringify({ blockHash, index, round, from }));
2438
+ const voteData = { blockHash, index: BigInt(index), round: BigInt(round), from };
2439
+ const Message = type === 'prevote' ? PrevoteMessage : PrecommitMessage;
2440
+ const message = new Message(voteData);
2441
+ const payload = message.encoded;
2448
2442
  try {
2449
2443
  globalThis.peernet.publish(`consensus:${type}`, payload);
2450
2444
  }
@@ -2459,16 +2453,17 @@ class Chain extends VersionControl {
2459
2453
  */
2460
2454
  this.#handleProposal = async (payload) => {
2461
2455
  try {
2462
- const msg = JSON.parse(new TextDecoder().decode(payload));
2456
+ const message = new ProposalMessage(payload);
2457
+ const msg = message.decoded;
2463
2458
  const { blockHash, index, round, from } = msg;
2464
- const validators = await this.#getConsensusValidators(index);
2465
- const expectedProposerIdx = (index + round) % validators.length;
2459
+ const validators = await this.#getConsensusValidators(Number(index));
2460
+ const expectedProposerIdx = Number((index + round) % BigInt(validators.length));
2466
2461
  if (!validators[expectedProposerIdx] || validators[expectedProposerIdx] !== from) {
2467
2462
  debug(`[consensus] Proposal from wrong proposer at height ${index} round ${round}`);
2468
2463
  return;
2469
2464
  }
2470
2465
  const localBlock = await this.lastBlock;
2471
- const localIndex = localBlock?.index !== undefined ? Number(localBlock.index) : -1;
2466
+ const localIndex = localBlock?.index !== undefined ? localBlock.index : -1n;
2472
2467
  if (index <= localIndex) {
2473
2468
  debug(`[consensus] Ignoring stale proposal at height ${index} (local: ${localIndex})`);
2474
2469
  return;
@@ -2487,13 +2482,13 @@ class Chain extends VersionControl {
2487
2482
  debug(`[consensus] Cannot fetch proposed block ${blockHash}:`, e?.message);
2488
2483
  return;
2489
2484
  }
2490
- this.#consensusRound = round;
2485
+ this.#consensusRound = Number(round);
2491
2486
  if (this.#roundTimer) {
2492
2487
  clearTimeout(this.#roundTimer);
2493
2488
  this.#roundTimer = null;
2494
2489
  }
2495
2490
  if (validators.includes(peernet.selectedAccount) && !this.#isJailed(peernet.selectedAccount)) {
2496
- await this.#castVote('prevote', blockHash, index, round);
2491
+ await this.#castVote('prevote', blockHash, Number(index), Number(round));
2497
2492
  }
2498
2493
  }
2499
2494
  catch (e) {
@@ -2506,13 +2501,14 @@ class Chain extends VersionControl {
2506
2501
  */
2507
2502
  this.#handlePrevote = async (payload) => {
2508
2503
  try {
2509
- const msg = JSON.parse(new TextDecoder().decode(payload));
2504
+ const message = new PrevoteMessage(payload);
2505
+ const msg = message.decoded;
2510
2506
  const { blockHash, index, round, from } = msg;
2511
- const validators = await this.#getConsensusValidators(index);
2507
+ const validators = await this.#getConsensusValidators(Number(index));
2512
2508
  if (!validators.includes(from))
2513
2509
  return;
2514
2510
  const localBlock = await this.lastBlock;
2515
- const localIndex = localBlock?.index !== undefined ? Number(localBlock.index) : -1;
2511
+ const localIndex = localBlock?.index !== undefined ? localBlock.index : -1n;
2516
2512
  if (index <= localIndex)
2517
2513
  return;
2518
2514
  const voteKey = `${index}:${round}:${blockHash}`;
@@ -2525,7 +2521,7 @@ class Chain extends VersionControl {
2525
2521
  if (voteCount >= threshold &&
2526
2522
  validators.includes(peernet.selectedAccount) &&
2527
2523
  !this.#isJailed(peernet.selectedAccount)) {
2528
- await this.#castVote('precommit', blockHash, index, round);
2524
+ await this.#castVote('precommit', blockHash, Number(index), Number(round));
2529
2525
  }
2530
2526
  }
2531
2527
  catch (e) {
@@ -2539,12 +2535,13 @@ class Chain extends VersionControl {
2539
2535
  */
2540
2536
  this.#handlePrecommit = async (payload) => {
2541
2537
  try {
2542
- const msg = JSON.parse(new TextDecoder().decode(payload));
2538
+ const message = new PrecommitMessage(payload);
2539
+ const msg = message.decoded;
2543
2540
  const { blockHash, index, round, from } = msg;
2544
- const validators = await this.#getConsensusValidators(index);
2541
+ const validators = await this.#getConsensusValidators(Number(index));
2545
2542
  if (!validators.includes(from))
2546
2543
  return;
2547
- if (index <= this.#committedHeight)
2544
+ if (index <= BigInt(this.#committedHeight))
2548
2545
  return;
2549
2546
  const voteKey = `${index}:${round}:${blockHash}`;
2550
2547
  if (!this.#precommits.has(voteKey))
@@ -2553,27 +2550,27 @@ class Chain extends VersionControl {
2553
2550
  const threshold = Math.ceil((2 * validators.length) / 3);
2554
2551
  const voteCount = this.#precommits.get(voteKey).size;
2555
2552
  debug(`[consensus] Precommits ${voteKey}: ${voteCount}/${validators.length} (need ${threshold})`);
2556
- if (voteCount >= threshold && index > this.#committedHeight) {
2557
- this.#committedHeight = index;
2553
+ if (voteCount >= threshold && index > BigInt(this.#committedHeight)) {
2554
+ this.#committedHeight = Number(index);
2558
2555
  this.#consensusRound = 0;
2559
2556
  // Prune vote state for committed and older heights
2560
2557
  for (const key of [...this.#prevotes.keys()]) {
2561
- if (Number(key.split(':')[0]) <= index)
2558
+ if (BigInt(key.split(':')[0]) <= index)
2562
2559
  this.#prevotes.delete(key);
2563
2560
  }
2564
2561
  for (const key of [...this.#precommits.keys()]) {
2565
- if (Number(key.split(':')[0]) <= index)
2562
+ if (BigInt(key.split(':')[0]) <= index)
2566
2563
  this.#precommits.delete(key);
2567
2564
  }
2568
2565
  for (const key of [...this.#castedVotes]) {
2569
- if (Number(key.split(':')[1]) <= index)
2566
+ if (BigInt(key.split(':')[1]) <= index)
2570
2567
  this.#castedVotes.delete(key);
2571
2568
  }
2572
2569
  // Non-proposers add the block to local state now.
2573
2570
  // Proposers already committed state in #createBlock() and their
2574
2571
  // lastBlock.index === index, so the guard below skips them.
2575
2572
  const currentBlock = await this.lastBlock;
2576
- const currentIndex = currentBlock?.index !== undefined ? Number(currentBlock.index) : -1;
2573
+ const currentIndex = currentBlock?.index !== undefined ? currentBlock.index : -1n;
2577
2574
  if (index > currentIndex) {
2578
2575
  debug(`[consensus] ✅ Committing block ${blockHash} at height ${index}`);
2579
2576
  try {
@@ -2846,7 +2843,9 @@ class Chain extends VersionControl {
2846
2843
  await globalThis.peernet.addRequestHandler('transactionPool', this.#transactionPoolHandler.bind(this));
2847
2844
  await globalThis.peernet.addRequestHandler('version', this.#versionHandler.bind(this));
2848
2845
  await globalThis.peernet.addRequestHandler('stateInfo', () => {
2849
- return new globalThis.peernet.protos['peernet-response']({ response: this.machine.states.info });
2846
+ return new globalThis.peernet.protos['peernet-response']({
2847
+ response: new StateMessage(this.machine.states.info).encoded
2848
+ });
2850
2849
  });
2851
2850
  globalThis.peernet.subscribe('add-block', this.#addBlock.bind(this));
2852
2851
  globalThis.peernet.subscribe('invalid-transaction', this.#invalidTransaction.bind(this));
@@ -2934,13 +2933,8 @@ class Chain extends VersionControl {
2934
2933
  async getPeerTransactionPool(peer) {
2935
2934
  let transactionsInPool = await this.#makeRequest(peer, 'transactionPool');
2936
2935
  if (transactionsInPool instanceof Uint8Array) {
2937
- try {
2938
- const text = new TextDecoder().decode(transactionsInPool);
2939
- transactionsInPool = JSON.parse(text);
2940
- }
2941
- catch (e) {
2942
- return [];
2943
- }
2936
+ debug('transactionPool response must be decoded array payload');
2937
+ return [];
2944
2938
  }
2945
2939
  if (!Array.isArray(transactionsInPool))
2946
2940
  return [];
@@ -2964,9 +2958,41 @@ class Chain extends VersionControl {
2964
2958
  async #peerConnected(peerId) {
2965
2959
  debug(`peer connected: ${peerId}`);
2966
2960
  const peer = peernet.getConnection(peerId);
2961
+ if (!peer) {
2962
+ debug(`peer not found: ${peerId}`);
2963
+ return;
2964
+ }
2965
+ if (!peer.version) {
2966
+ try {
2967
+ let versionResponse = await this.#makeRequest(peer, 'version');
2968
+ if (versionResponse instanceof Uint8Array) {
2969
+ versionResponse = new TextDecoder().decode(versionResponse);
2970
+ }
2971
+ if (typeof versionResponse === 'string') {
2972
+ peer.version = versionResponse;
2973
+ }
2974
+ else if (versionResponse &&
2975
+ typeof versionResponse === 'object' &&
2976
+ typeof versionResponse.version === 'string') {
2977
+ peer.version = versionResponse.version;
2978
+ }
2979
+ if (!peer.version || typeof peer.version !== 'string') {
2980
+ const reason = `invalid version response from peer ${peerId}`;
2981
+ debug(reason);
2982
+ await this.#recordPeerFailure(peerId, reason);
2983
+ return;
2984
+ }
2985
+ }
2986
+ catch (error) {
2987
+ debug(`failed to request version from peer ${peerId}:`, error?.message ?? error);
2988
+ return;
2989
+ }
2990
+ }
2967
2991
  debug(`peer connected with version ${peer.version}`);
2968
2992
  if (!this.isVersionCompatible(peer.version)) {
2969
- debug(`versions don't match`);
2993
+ const mismatchReason = `incompatible peer version ${peer.version} (local: ${this.version})`;
2994
+ console.error(`[chain] ${mismatchReason}`);
2995
+ await this.#recordPeerFailure(peerId, mismatchReason);
2970
2996
  return;
2971
2997
  }
2972
2998
  let lastBlock;
@@ -2986,7 +3012,7 @@ class Chain extends VersionControl {
2986
3012
  // This prevents Byzantine nodes from claiming a fake chain length to steer our sync
2987
3013
  const localBlock = await this.lastBlock;
2988
3014
  const MAX_SYNC_AHEAD = 100_000;
2989
- if (lastBlock?.index > (localBlock?.index ?? 0) + MAX_SYNC_AHEAD) {
3015
+ if (lastBlock?.index > BigInt(localBlock?.index ?? 0) + BigInt(MAX_SYNC_AHEAD)) {
2990
3016
  const peerName = peer?.peerId || peer?.id || peer?.address || peerId || 'unknown';
2991
3017
  debug(`Peer ${peerName} claims unreasonable block height ${lastBlock.index} (local: ${localBlock?.index ?? 0})`);
2992
3018
  await this.#recordPeerFailure(peerId, `unreasonable lastBlock index: ${lastBlock.index}`);
@@ -3002,15 +3028,13 @@ class Chain extends VersionControl {
3002
3028
  try {
3003
3029
  let knownBlocksResponse = await this.#makeRequest(peer, 'knownBlocks');
3004
3030
  if (knownBlocksResponse instanceof Uint8Array) {
3005
- try {
3006
- knownBlocksResponse = JSON.parse(new TextDecoder().decode(knownBlocksResponse));
3007
- }
3008
- catch (e) {
3009
- console.log(e);
3010
- }
3031
+ const reason = `knownBlocks must be object response, got raw bytes from ${peerId}`;
3032
+ debug(reason);
3033
+ await this.#recordPeerFailure(peerId, reason);
3034
+ return;
3011
3035
  }
3012
3036
  const MAX_WANTLIST_SIZE = 1000;
3013
- if (knownBlocksResponse.blocks) {
3037
+ if (knownBlocksResponse && Array.isArray(knownBlocksResponse.blocks)) {
3014
3038
  const remaining = MAX_WANTLIST_SIZE - this.wantList.length;
3015
3039
  if (remaining > 0) {
3016
3040
  for (const hash of knownBlocksResponse.blocks.slice(0, remaining)) {
@@ -3052,12 +3076,7 @@ class Chain extends VersionControl {
3052
3076
  try {
3053
3077
  let stateInfo = await this.#makeRequest(peer, 'stateInfo');
3054
3078
  if (stateInfo instanceof Uint8Array) {
3055
- try {
3056
- stateInfo = JSON.parse(new TextDecoder().decode(stateInfo));
3057
- }
3058
- catch (e) {
3059
- console.log(e);
3060
- }
3079
+ stateInfo = new StateMessage(stateInfo).decoded;
3061
3080
  }
3062
3081
  await this.syncChain(lastBlock);
3063
3082
  this.machine.states.info = stateInfo;
@@ -3075,7 +3094,7 @@ class Chain extends VersionControl {
3075
3094
  return new globalThis.peernet.protos['peernet-response']({ response: pool });
3076
3095
  }
3077
3096
  async #versionHandler() {
3078
- return new globalThis.peernet.protos['peernet-response']({ response: { version: this.version } });
3097
+ return new globalThis.peernet.protos['peernet-response']({ response: this.version });
3079
3098
  }
3080
3099
  async #executeTransaction({ hash, from, to, method, params, nonce }) {
3081
3100
  try {
@@ -3427,12 +3446,14 @@ class Chain extends VersionControl {
3427
3446
  debug(`created block: ${hash} @${block.index}`);
3428
3447
  // Phase 2: announce proposal for consensus voting instead of direct add-block
3429
3448
  console.log(`[consensus] 📤 Proposing block #${block.index} | hash: ${hash} | round: ${this.#consensusRound}`);
3430
- const proposalPayload = new TextEncoder().encode(JSON.stringify({
3449
+ const proposalData = {
3431
3450
  blockHash: hash,
3432
- index: block.index,
3433
- round: this.#consensusRound,
3451
+ index: BigInt(block.index),
3452
+ round: BigInt(this.#consensusRound),
3434
3453
  from: peernet.selectedAccount
3435
- }));
3454
+ };
3455
+ const proposalMessage = new ProposalMessage(proposalData);
3456
+ const proposalPayload = proposalMessage.encoded;
3436
3457
  try {
3437
3458
  globalThis.peernet.publish('consensus:propose', proposalPayload);
3438
3459
  }
@@ -1,4 +1,4 @@
1
- import { E as EasyWorker, B as BlockMessage } from './worker-BrtyXRJ7-BrtyXRJ7.js';
1
+ import { E as EasyWorker, B as BlockMessage } from './worker-Bsi6vKgF-Bsi6vKgF.js';
2
2
 
3
3
  const worker = new EasyWorker();
4
4
  globalThis.BigNumber = BigNumber;
@@ -1,4 +1,4 @@
1
- import { E as EasyWorker, C as ContractMessage, T as TransactionMessage } from './worker-BrtyXRJ7-BrtyXRJ7.js';
1
+ import { E as EasyWorker, C as ContractMessage, T as TransactionMessage } from './worker-Bsi6vKgF-Bsi6vKgF.js';
2
2
 
3
3
  /* Do NOT modify this file; see /src.ts/_admin/update-version.ts */
4
4
  /**