@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.
@@ -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-BTdMMS4w.js';
1
+ import { t as toBase58, T as TransactionMessage, C as ContractMessage, R as RawTransactionMessage, B as BlockMessage, L as LastBlockMessage, P as PROTOCOL_VERSION, a as REACHED_ONE_ZERO_ZERO, b as PrevoteMessage, c as PrecommitMessage, d as ProposalMessage, e as BWMessage, S as StateMessage, f as BWRequestMessage } from './constants-D_XqG46B.js';
2
2
  import { log } from 'console';
3
3
  import { log as log$1 } from 'node:console';
4
4
 
@@ -3912,11 +3912,25 @@ class Transaction extends Protocol {
3912
3912
  return Number(nonce);
3913
3913
  }
3914
3914
  async validateNonce(address, nonce) {
3915
- const previousNonce = await this.getNonce(address);
3916
- if (previousNonce > nonce)
3917
- throw new Error(`a transaction with a higher nonce already exists`);
3918
- if (previousNonce === nonce)
3915
+ // Compare only against the COMMITTED nonce (accountsStore), not the pool max.
3916
+ // The pool may hold many future nonces from batch sends — rejecting lower nonces
3917
+ // because a higher one is already queued would break concurrent batch submission.
3918
+ let committedNonce;
3919
+ try {
3920
+ if (await globalThis.accountsStore.has(address)) {
3921
+ const raw = await globalThis.accountsStore.get(address);
3922
+ committedNonce = Number(new TextDecoder().decode(raw));
3923
+ }
3924
+ else {
3925
+ committedNonce = await this.#getNonceFallback(address);
3926
+ }
3927
+ }
3928
+ catch {
3929
+ committedNonce = 0;
3930
+ }
3931
+ if (committedNonce >= nonce)
3919
3932
  throw new Error(`a transaction with the same nonce already exists`);
3933
+ // Only reject exact duplicates already in the pool (not "higher nonce" rejections)
3920
3934
  let transactions = await globalThis.transactionPoolStore.values();
3921
3935
  transactions = await this.promiseTransactions(transactions);
3922
3936
  transactions = transactions.filter((tx) => tx.decoded.from === address);
@@ -5041,11 +5055,6 @@ class Jobber {
5041
5055
  }
5042
5056
  }
5043
5057
 
5044
- utils.addCodec({
5045
- name: 'last-block-message',
5046
- codec: 0x6c626d,
5047
- hashAlg: 'keccak-256'
5048
- });
5049
5058
  const debug$1 = createDebugger('leofcoin/state');
5050
5059
  class State extends Contract {
5051
5060
  #resolveErrored;
@@ -5607,6 +5616,8 @@ class State extends Contract {
5607
5616
  }
5608
5617
  async #getLatestBlock() {
5609
5618
  let promises = [];
5619
+ const connectedPeers = Object.values(globalThis.peernet.connections || {}).filter((peer) => peer.connected);
5620
+ let compatiblePeerCount = 0;
5610
5621
  let data = await new globalThis.peernet.protos['peernet-request']({
5611
5622
  request: 'lastBlock'
5612
5623
  });
@@ -5614,15 +5625,8 @@ class State extends Contract {
5614
5625
  for (const id in globalThis.peernet.connections) {
5615
5626
  // @ts-ignore
5616
5627
  const peer = globalThis.peernet.connections[id];
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()) {
5628
+ if (peer.connected && this.isVersionCompatible(peer.version)) {
5629
+ compatiblePeerCount += 1;
5626
5630
  const task = async () => {
5627
5631
  try {
5628
5632
  const result = await peer.request(node.encoded);
@@ -5639,9 +5643,15 @@ class State extends Contract {
5639
5643
  promises.push(task());
5640
5644
  }
5641
5645
  }
5646
+ if (connectedPeers.length > 0 && compatiblePeerCount === 0) {
5647
+ throw new ResolveError(`latestBlock: no compatible peers found for local version ${this.version} among ${connectedPeers.length} connected peers`);
5648
+ }
5642
5649
  // @ts-ignore
5643
5650
  console.log({ promises });
5644
5651
  promises = (await this.promiseRequests(promises));
5652
+ if (compatiblePeerCount > 0 && promises.length === 0) {
5653
+ throw new ResolveError('latestBlock: no responses from compatible peers');
5654
+ }
5645
5655
  console.log({ promises });
5646
5656
  let latest = { index: 0, hash: '0x0', previousHash: '0x0' };
5647
5657
  promises = promises.sort((a, b) => b.index - a.index);
@@ -5656,15 +5666,7 @@ class State extends Contract {
5656
5666
  throw new Error('invalid block @getLatestBlock');
5657
5667
  latest = { ...message.decoded, hash };
5658
5668
  const peer = promises[0].peer;
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()) {
5669
+ if (peer.connected && this.isVersionCompatible(peer.version)) {
5668
5670
  let data = await new globalThis.peernet.protos['peernet-request']({
5669
5671
  request: 'knownBlocks'
5670
5672
  });
@@ -5812,7 +5814,7 @@ class State extends Contract {
5812
5814
  if (this.#chainSyncing)
5813
5815
  return false;
5814
5816
  // Check if we have any connected peers with the same version
5815
- const compatiblePeers = Object.values(globalThis.peernet.connections || {}).filter((peer) => peer.connected && peer.version === this.version);
5817
+ const compatiblePeers = Object.values(globalThis.peernet.connections || {}).filter((peer) => peer.connected && this.isVersionCompatible(peer.version));
5816
5818
  if (compatiblePeers.length === 0) {
5817
5819
  debug$1('No compatible peers available for sync');
5818
5820
  return false;
@@ -5828,7 +5830,7 @@ class State extends Contract {
5828
5830
  async #waitForPeers(timeoutMs = 30000) {
5829
5831
  return new Promise((resolve) => {
5830
5832
  const checkPeers = () => {
5831
- const peers = Object.values(globalThis.peernet.connections || {}).filter((peer) => peer.connected && peer.version === this.version);
5833
+ const peers = Object.values(globalThis.peernet.connections || {}).filter((peer) => peer.connected && this.isVersionCompatible(peer.version));
5832
5834
  if (peers.length > 0) {
5833
5835
  resolve(true);
5834
5836
  }
@@ -6147,7 +6149,7 @@ class ConnectionMonitor {
6147
6149
  const networkName = globalThis.peernet?.network;
6148
6150
  if (networkName && typeof networkName === 'string') {
6149
6151
  // Try to import network config
6150
- const { default: networks } = await import('./constants-BTdMMS4w.js').then(function (n) { return n.n; });
6152
+ const { default: networks } = await import('./constants-D_XqG46B.js').then(function (n) { return n.n; });
6151
6153
  const [mainKey, subKey] = networkName.split(':');
6152
6154
  const networkConfig = networks?.[mainKey]?.[subKey];
6153
6155
  if (networkConfig?.stars && Array.isArray(networkConfig.stars)) {
@@ -6239,16 +6241,6 @@ class ConnectionMonitor {
6239
6241
  }
6240
6242
  }
6241
6243
 
6242
- utils.addCodec({
6243
- name: 'last-block-message',
6244
- codec: 0x6c626d,
6245
- hashAlg: 'keccak-256'
6246
- });
6247
- utils.addCodec({
6248
- name: 'last-block-request-message',
6249
- codec: 0x6c62726d,
6250
- hashAlg: 'keccak-256'
6251
- });
6252
6244
  const debug = createDebugger('leofcoin/chain');
6253
6245
  // check if browser or local
6254
6246
  class Chain extends VersionControl {
@@ -6342,7 +6334,10 @@ class Chain extends VersionControl {
6342
6334
  return;
6343
6335
  this.#castedVotes.add(voteKey);
6344
6336
  const from = peernet.selectedAccount;
6345
- const payload = new TextEncoder().encode(JSON.stringify({ blockHash, index, round, from }));
6337
+ const voteData = { blockHash, index: BigInt(index), round: BigInt(round), from };
6338
+ const Message = type === 'prevote' ? PrevoteMessage : PrecommitMessage;
6339
+ const message = new Message(voteData);
6340
+ const payload = message.encoded;
6346
6341
  try {
6347
6342
  globalThis.peernet.publish(`consensus:${type}`, payload);
6348
6343
  }
@@ -6357,16 +6352,17 @@ class Chain extends VersionControl {
6357
6352
  */
6358
6353
  this.#handleProposal = async (payload) => {
6359
6354
  try {
6360
- const msg = JSON.parse(new TextDecoder().decode(payload));
6355
+ const message = new ProposalMessage(payload);
6356
+ const msg = message.decoded;
6361
6357
  const { blockHash, index, round, from } = msg;
6362
- const validators = await this.#getConsensusValidators(index);
6363
- const expectedProposerIdx = (index + round) % validators.length;
6358
+ const validators = await this.#getConsensusValidators(Number(index));
6359
+ const expectedProposerIdx = Number((index + round) % BigInt(validators.length));
6364
6360
  if (!validators[expectedProposerIdx] || validators[expectedProposerIdx] !== from) {
6365
6361
  debug(`[consensus] Proposal from wrong proposer at height ${index} round ${round}`);
6366
6362
  return;
6367
6363
  }
6368
6364
  const localBlock = await this.lastBlock;
6369
- const localIndex = localBlock?.index !== undefined ? Number(localBlock.index) : -1;
6365
+ const localIndex = localBlock?.index !== undefined ? localBlock.index : -1n;
6370
6366
  if (index <= localIndex) {
6371
6367
  debug(`[consensus] Ignoring stale proposal at height ${index} (local: ${localIndex})`);
6372
6368
  return;
@@ -6385,13 +6381,13 @@ class Chain extends VersionControl {
6385
6381
  debug(`[consensus] Cannot fetch proposed block ${blockHash}:`, e?.message);
6386
6382
  return;
6387
6383
  }
6388
- this.#consensusRound = round;
6384
+ this.#consensusRound = Number(round);
6389
6385
  if (this.#roundTimer) {
6390
6386
  clearTimeout(this.#roundTimer);
6391
6387
  this.#roundTimer = null;
6392
6388
  }
6393
6389
  if (validators.includes(peernet.selectedAccount) && !this.#isJailed(peernet.selectedAccount)) {
6394
- await this.#castVote('prevote', blockHash, index, round);
6390
+ await this.#castVote('prevote', blockHash, Number(index), Number(round));
6395
6391
  }
6396
6392
  }
6397
6393
  catch (e) {
@@ -6404,13 +6400,14 @@ class Chain extends VersionControl {
6404
6400
  */
6405
6401
  this.#handlePrevote = async (payload) => {
6406
6402
  try {
6407
- const msg = JSON.parse(new TextDecoder().decode(payload));
6403
+ const message = new PrevoteMessage(payload);
6404
+ const msg = message.decoded;
6408
6405
  const { blockHash, index, round, from } = msg;
6409
- const validators = await this.#getConsensusValidators(index);
6406
+ const validators = await this.#getConsensusValidators(Number(index));
6410
6407
  if (!validators.includes(from))
6411
6408
  return;
6412
6409
  const localBlock = await this.lastBlock;
6413
- const localIndex = localBlock?.index !== undefined ? Number(localBlock.index) : -1;
6410
+ const localIndex = localBlock?.index !== undefined ? localBlock.index : -1n;
6414
6411
  if (index <= localIndex)
6415
6412
  return;
6416
6413
  const voteKey = `${index}:${round}:${blockHash}`;
@@ -6423,7 +6420,7 @@ class Chain extends VersionControl {
6423
6420
  if (voteCount >= threshold &&
6424
6421
  validators.includes(peernet.selectedAccount) &&
6425
6422
  !this.#isJailed(peernet.selectedAccount)) {
6426
- await this.#castVote('precommit', blockHash, index, round);
6423
+ await this.#castVote('precommit', blockHash, Number(index), Number(round));
6427
6424
  }
6428
6425
  }
6429
6426
  catch (e) {
@@ -6437,12 +6434,13 @@ class Chain extends VersionControl {
6437
6434
  */
6438
6435
  this.#handlePrecommit = async (payload) => {
6439
6436
  try {
6440
- const msg = JSON.parse(new TextDecoder().decode(payload));
6437
+ const message = new PrecommitMessage(payload);
6438
+ const msg = message.decoded;
6441
6439
  const { blockHash, index, round, from } = msg;
6442
- const validators = await this.#getConsensusValidators(index);
6440
+ const validators = await this.#getConsensusValidators(Number(index));
6443
6441
  if (!validators.includes(from))
6444
6442
  return;
6445
- if (index <= this.#committedHeight)
6443
+ if (index <= BigInt(this.#committedHeight))
6446
6444
  return;
6447
6445
  const voteKey = `${index}:${round}:${blockHash}`;
6448
6446
  if (!this.#precommits.has(voteKey))
@@ -6451,27 +6449,27 @@ class Chain extends VersionControl {
6451
6449
  const threshold = Math.ceil((2 * validators.length) / 3);
6452
6450
  const voteCount = this.#precommits.get(voteKey).size;
6453
6451
  debug(`[consensus] Precommits ${voteKey}: ${voteCount}/${validators.length} (need ${threshold})`);
6454
- if (voteCount >= threshold && index > this.#committedHeight) {
6455
- this.#committedHeight = index;
6452
+ if (voteCount >= threshold && index > BigInt(this.#committedHeight)) {
6453
+ this.#committedHeight = Number(index);
6456
6454
  this.#consensusRound = 0;
6457
6455
  // Prune vote state for committed and older heights
6458
6456
  for (const key of [...this.#prevotes.keys()]) {
6459
- if (Number(key.split(':')[0]) <= index)
6457
+ if (BigInt(key.split(':')[0]) <= index)
6460
6458
  this.#prevotes.delete(key);
6461
6459
  }
6462
6460
  for (const key of [...this.#precommits.keys()]) {
6463
- if (Number(key.split(':')[0]) <= index)
6461
+ if (BigInt(key.split(':')[0]) <= index)
6464
6462
  this.#precommits.delete(key);
6465
6463
  }
6466
6464
  for (const key of [...this.#castedVotes]) {
6467
- if (Number(key.split(':')[1]) <= index)
6465
+ if (BigInt(key.split(':')[1]) <= index)
6468
6466
  this.#castedVotes.delete(key);
6469
6467
  }
6470
6468
  // Non-proposers add the block to local state now.
6471
6469
  // Proposers already committed state in #createBlock() and their
6472
6470
  // lastBlock.index === index, so the guard below skips them.
6473
6471
  const currentBlock = await this.lastBlock;
6474
- const currentIndex = currentBlock?.index !== undefined ? Number(currentBlock.index) : -1;
6472
+ const currentIndex = currentBlock?.index !== undefined ? currentBlock.index : -1n;
6475
6473
  if (index > currentIndex) {
6476
6474
  debug(`[consensus] ✅ Committing block ${blockHash} at height ${index}`);
6477
6475
  try {
@@ -6744,7 +6742,9 @@ class Chain extends VersionControl {
6744
6742
  await globalThis.peernet.addRequestHandler('transactionPool', this.#transactionPoolHandler.bind(this));
6745
6743
  await globalThis.peernet.addRequestHandler('version', this.#versionHandler.bind(this));
6746
6744
  await globalThis.peernet.addRequestHandler('stateInfo', () => {
6747
- return new globalThis.peernet.protos['peernet-response']({ response: this.machine.states.info });
6745
+ return new globalThis.peernet.protos['peernet-response']({
6746
+ response: new StateMessage(this.machine.states.info).encoded
6747
+ });
6748
6748
  });
6749
6749
  globalThis.peernet.subscribe('add-block', this.#addBlock.bind(this));
6750
6750
  globalThis.peernet.subscribe('invalid-transaction', this.#invalidTransaction.bind(this));
@@ -6832,13 +6832,8 @@ class Chain extends VersionControl {
6832
6832
  async getPeerTransactionPool(peer) {
6833
6833
  let transactionsInPool = await this.#makeRequest(peer, 'transactionPool');
6834
6834
  if (transactionsInPool instanceof Uint8Array) {
6835
- try {
6836
- const text = new TextDecoder().decode(transactionsInPool);
6837
- transactionsInPool = JSON.parse(text);
6838
- }
6839
- catch (e) {
6840
- return [];
6841
- }
6835
+ debug('transactionPool response must be decoded array payload');
6836
+ return [];
6842
6837
  }
6843
6838
  if (!Array.isArray(transactionsInPool))
6844
6839
  return [];
@@ -6862,9 +6857,41 @@ class Chain extends VersionControl {
6862
6857
  async #peerConnected(peerId) {
6863
6858
  debug(`peer connected: ${peerId}`);
6864
6859
  const peer = peernet.getConnection(peerId);
6860
+ if (!peer) {
6861
+ debug(`peer not found: ${peerId}`);
6862
+ return;
6863
+ }
6864
+ if (!peer.version) {
6865
+ try {
6866
+ let versionResponse = await this.#makeRequest(peer, 'version');
6867
+ if (versionResponse instanceof Uint8Array) {
6868
+ versionResponse = new TextDecoder().decode(versionResponse);
6869
+ }
6870
+ if (typeof versionResponse === 'string') {
6871
+ peer.version = versionResponse;
6872
+ }
6873
+ else if (versionResponse &&
6874
+ typeof versionResponse === 'object' &&
6875
+ typeof versionResponse.version === 'string') {
6876
+ peer.version = versionResponse.version;
6877
+ }
6878
+ if (!peer.version || typeof peer.version !== 'string') {
6879
+ const reason = `invalid version response from peer ${peerId}`;
6880
+ debug(reason);
6881
+ await this.#recordPeerFailure(peerId, reason);
6882
+ return;
6883
+ }
6884
+ }
6885
+ catch (error) {
6886
+ debug(`failed to request version from peer ${peerId}:`, error?.message ?? error);
6887
+ return;
6888
+ }
6889
+ }
6865
6890
  debug(`peer connected with version ${peer.version}`);
6866
6891
  if (!this.isVersionCompatible(peer.version)) {
6867
- debug(`versions don't match`);
6892
+ const mismatchReason = `incompatible peer version ${peer.version} (local: ${this.version})`;
6893
+ console.error(`[chain] ${mismatchReason}`);
6894
+ await this.#recordPeerFailure(peerId, mismatchReason);
6868
6895
  return;
6869
6896
  }
6870
6897
  let lastBlock;
@@ -6884,7 +6911,7 @@ class Chain extends VersionControl {
6884
6911
  // This prevents Byzantine nodes from claiming a fake chain length to steer our sync
6885
6912
  const localBlock = await this.lastBlock;
6886
6913
  const MAX_SYNC_AHEAD = 100_000;
6887
- if (lastBlock?.index > (localBlock?.index ?? 0) + MAX_SYNC_AHEAD) {
6914
+ if (lastBlock?.index > BigInt(localBlock?.index ?? 0) + BigInt(MAX_SYNC_AHEAD)) {
6888
6915
  const peerName = peer?.peerId || peer?.id || peer?.address || peerId || 'unknown';
6889
6916
  debug(`Peer ${peerName} claims unreasonable block height ${lastBlock.index} (local: ${localBlock?.index ?? 0})`);
6890
6917
  await this.#recordPeerFailure(peerId, `unreasonable lastBlock index: ${lastBlock.index}`);
@@ -6900,15 +6927,13 @@ class Chain extends VersionControl {
6900
6927
  try {
6901
6928
  let knownBlocksResponse = await this.#makeRequest(peer, 'knownBlocks');
6902
6929
  if (knownBlocksResponse instanceof Uint8Array) {
6903
- try {
6904
- knownBlocksResponse = JSON.parse(new TextDecoder().decode(knownBlocksResponse));
6905
- }
6906
- catch (e) {
6907
- console.log(e);
6908
- }
6930
+ const reason = `knownBlocks must be object response, got raw bytes from ${peerId}`;
6931
+ debug(reason);
6932
+ await this.#recordPeerFailure(peerId, reason);
6933
+ return;
6909
6934
  }
6910
6935
  const MAX_WANTLIST_SIZE = 1000;
6911
- if (knownBlocksResponse.blocks) {
6936
+ if (knownBlocksResponse && Array.isArray(knownBlocksResponse.blocks)) {
6912
6937
  const remaining = MAX_WANTLIST_SIZE - this.wantList.length;
6913
6938
  if (remaining > 0) {
6914
6939
  for (const hash of knownBlocksResponse.blocks.slice(0, remaining)) {
@@ -6950,12 +6975,7 @@ class Chain extends VersionControl {
6950
6975
  try {
6951
6976
  let stateInfo = await this.#makeRequest(peer, 'stateInfo');
6952
6977
  if (stateInfo instanceof Uint8Array) {
6953
- try {
6954
- stateInfo = JSON.parse(new TextDecoder().decode(stateInfo));
6955
- }
6956
- catch (e) {
6957
- console.log(e);
6958
- }
6978
+ stateInfo = new StateMessage(stateInfo).decoded;
6959
6979
  }
6960
6980
  await this.syncChain(lastBlock);
6961
6981
  this.machine.states.info = stateInfo;
@@ -6973,7 +6993,7 @@ class Chain extends VersionControl {
6973
6993
  return new globalThis.peernet.protos['peernet-response']({ response: pool });
6974
6994
  }
6975
6995
  async #versionHandler() {
6976
- return new globalThis.peernet.protos['peernet-response']({ response: { version: this.version } });
6996
+ return new globalThis.peernet.protos['peernet-response']({ response: this.version });
6977
6997
  }
6978
6998
  async #executeTransaction({ hash, from, to, method, params, nonce }) {
6979
6999
  try {
@@ -7325,12 +7345,14 @@ class Chain extends VersionControl {
7325
7345
  debug(`created block: ${hash} @${block.index}`);
7326
7346
  // Phase 2: announce proposal for consensus voting instead of direct add-block
7327
7347
  console.log(`[consensus] 📤 Proposing block #${block.index} | hash: ${hash} | round: ${this.#consensusRound}`);
7328
- const proposalPayload = new TextEncoder().encode(JSON.stringify({
7348
+ const proposalData = {
7329
7349
  blockHash: hash,
7330
- index: block.index,
7331
- round: this.#consensusRound,
7350
+ index: BigInt(block.index),
7351
+ round: BigInt(this.#consensusRound),
7332
7352
  from: peernet.selectedAccount
7333
- }));
7353
+ };
7354
+ const proposalMessage = new ProposalMessage(proposalData);
7355
+ const proposalPayload = proposalMessage.encoded;
7334
7356
  try {
7335
7357
  globalThis.peernet.publish('consensus:propose', proposalPayload);
7336
7358
  }