@leofcoin/chain 1.10.17 → 1.10.19

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 { s as signTransaction, c as MultiWallet, d as createTransactionHash } from './index-DmuI-29u.js';
1
+ import { s as signTransaction, c as MultiWallet, d as createTransactionHash } from './index-DCbLvMKL.js';
2
2
  import { f as fromBase58 } from './index-CvKt4UDE.js';
3
3
  import './browser-crypto-CH0ua22L.js';
4
4
  import './sha2-BKcEJzgd.js';
@@ -1,6 +1,6 @@
1
1
  import { F as FormatInterface, p as proto, a as proto$1, b as proto$2, T as TransactionMessage, j as jsonStringifyBigInt, c as jsonParseBigInt, C as ContractMessage, d as Codec } from './browser-crypto-CH0ua22L.js';
2
2
  import { B as BlockMessage, L as LastBlockMessage, P as PROTOCOL_VERSION, R as REACHED_ONE_ZERO_ZERO, C as ConnectionMonitor, a as BWMessage, S as StateMessage } from './connection-monitor-D-krNiRH.js';
3
- import { M as MAX_TRANSACTION_BYTES, a as MAX_BLOCK_TRANSACTION_BYTES, b as MAX_BLOCK_TRANSACTIONS, c as MultiWallet, d as createTransactionHash, e as calculateFee, f as createContractMessage, s as signTransaction, r as randombytes, g as distributeTransactionFee, h as supportsTransactionFees, i as supportsMonetaryPolicy, v as validateBlockResourceLimits, j as calculateMonetaryPolicy, k as distributeAmount, l as aggregateValidatorFees, m as MONETARY_POLICY_AUTHORITY, n as contractFactoryMessage, o as nativeTokenMessage, p as validatorsMessage, q as nameServiceMessage, t as parseUnits, u as formatUnits } from './index-DmuI-29u.js';
3
+ import { M as MAX_TRANSACTION_BYTES, a as MAX_BLOCK_TRANSACTION_BYTES, b as MAX_BLOCK_TRANSACTIONS, c as MultiWallet, d as createTransactionHash, e as calculateFee, f as createContractMessage, s as signTransaction, r as randombytes, g as distributeTransactionFee, h as supportsTransactionFees, i as supportsMonetaryPolicy, v as validateBlockResourceLimits, j as calculateMonetaryPolicy, k as distributeAmount, l as aggregateValidatorFees, m as MONETARY_POLICY_AUTHORITY, n as contractFactoryMessage, o as nativeTokenMessage, p as validatorsMessage, q as nameServiceMessage, t as parseUnits, u as formatUnits } from './index-DCbLvMKL.js';
4
4
  import { f as fromBase58 } from './index-CvKt4UDE.js';
5
5
  import { g as getDefaultExportFromCjs } from './_commonjsHelpers-B85MJLTf.js';
6
6
  import { q as quorumThreshold } from './quorum-S_qdgiAY.js';
@@ -3233,7 +3233,8 @@ class ContractRegistry {
3233
3233
  */
3234
3234
  async registerNameInNameService(name, hash, chain, signer) {
3235
3235
  ({
3236
- from: await signer.address});
3236
+ from: await signer.address,
3237
+ to: addresses.nameService});
3237
3238
  }
3238
3239
  /**
3239
3240
  * Get a base contract by name
@@ -3696,6 +3697,12 @@ class Machine {
3696
3697
  pubsub.publish(data.id, { error: data.message });
3697
3698
  break;
3698
3699
  }
3700
+ case "addLoadedBlockError":
3701
+ case "collectFeeError":
3702
+ case "settleRewardsError": {
3703
+ pubsub.publish(data.id, { error: new Error(data.message) });
3704
+ break;
3705
+ }
3699
3706
  case "debug": {
3700
3707
  if (data.message.includes("loaded transactions for block:")) {
3701
3708
  pubsub.publish("block-loaded", data.message.replace("loaded transactions for block: ", "").split(" @")[0]);
@@ -3745,9 +3752,9 @@ class Machine {
3745
3752
  }
3746
3753
  }
3747
3754
  }
3748
- async updateState() {
3755
+ async updateState(strict = false) {
3749
3756
  try {
3750
- const lastBlock = await this.lastBlock;
3757
+ const lastBlock = strict ? await this.getCanonicalTip() : await this.lastBlock;
3751
3758
  const lastBlockIndex = Number(lastBlock.index);
3752
3759
  const statesLastBlockIndex = Number(this.states.lastBlock.index);
3753
3760
  if (lastBlockIndex > statesLastBlockIndex || lastBlock.hash !== this.states.lastBlock.hash && Number(lastBlockIndex) === 0) {
@@ -3806,7 +3813,7 @@ class Machine {
3806
3813
  return JSON.stringify(sorted, replacer);
3807
3814
  };
3808
3815
  const tasks = [
3809
- stateStore.put("lastBlock", sortedStringify(await this.lastBlock, jsonStringifyBigInt)),
3816
+ stateStore.put("lastBlock", sortedStringify(lastBlock, jsonStringifyBigInt)),
3810
3817
  stateStore.put("states", sortedStringify(state, jsonStringifyBigInt)),
3811
3818
  stateStore.put("accounts", sortedStringify(accounts, jsonStringifyBigInt)),
3812
3819
  stateStore.put(
@@ -3833,6 +3840,7 @@ class Machine {
3833
3840
  }
3834
3841
  } catch (error) {
3835
3842
  console.error(error);
3843
+ if (strict) throw error;
3836
3844
  }
3837
3845
  }
3838
3846
  async #init(blocks) {
@@ -4099,6 +4107,9 @@ ${error.message}`);
4099
4107
  return this.states.lastBlock;
4100
4108
  });
4101
4109
  }
4110
+ getCanonicalTip() {
4111
+ return this.#askWorker("lastBlock");
4112
+ }
4102
4113
  get lastBlockHeight() {
4103
4114
  return this.#askWorker("lastBlockHeight").catch((error) => {
4104
4115
  debug$2("lastBlockHeight fallback after worker timeout:", error?.message ?? error);
@@ -4243,6 +4254,56 @@ const decodeResponsePayload = async (result, depth = 0) => {
4243
4254
  return result;
4244
4255
  };
4245
4256
 
4257
+ class CanonicalStateQueue {
4258
+ #tail = Promise.resolve();
4259
+ #failed = false;
4260
+ #failure;
4261
+ run(operation) {
4262
+ const result = this.#tail.then(async () => {
4263
+ if (this.#failed) {
4264
+ throw new Error("Canonical state application halted after a failed mutation; restart and recover state", {
4265
+ cause: this.#failure
4266
+ });
4267
+ }
4268
+ let mutating = false;
4269
+ try {
4270
+ return await operation({ markMutating: () => {
4271
+ mutating = true;
4272
+ } });
4273
+ } catch (error) {
4274
+ if (mutating) {
4275
+ this.#failed = true;
4276
+ this.#failure = error;
4277
+ }
4278
+ throw error;
4279
+ }
4280
+ });
4281
+ this.#tail = result.then(() => void 0, () => void 0);
4282
+ return result;
4283
+ }
4284
+ }
4285
+
4286
+ const validateChainLink = (localTip, incoming) => {
4287
+ const localHash = localTip?.hash || "0x0";
4288
+ const localIndex = Number(localTip?.index ?? -1);
4289
+ const hasCanonicalTip = localHash !== "0x0";
4290
+ if (hasCanonicalTip && incoming.index < localIndex) return "stale";
4291
+ if (hasCanonicalTip && incoming.index === localIndex) {
4292
+ if (incoming.hash === localHash) return "duplicate";
4293
+ throw new Error(`Block conflict detected at index ${incoming.index}`);
4294
+ }
4295
+ const expectedIndex = hasCanonicalTip ? localIndex + 1 : 0;
4296
+ if (incoming.index !== expectedIndex) {
4297
+ throw new Error(`Unexpected block index ${incoming.index}; expected ${expectedIndex}`);
4298
+ }
4299
+ if (incoming.previousHash !== localHash) {
4300
+ throw new Error(
4301
+ `previousHash mismatch at index ${incoming.index}: expected ${localHash}, got ${incoming.previousHash}`
4302
+ );
4303
+ }
4304
+ return "append";
4305
+ };
4306
+
4246
4307
  const debug$1 = createDebugger("leofcoin/state");
4247
4308
  class State extends Contract {
4248
4309
  constructor(config) {
@@ -4257,6 +4318,7 @@ class State extends Contract {
4257
4318
  this.#blocks = [];
4258
4319
  this.knownBlocks = [];
4259
4320
  this.#totalSize = 0;
4321
+ this.#canonicalStateQueue = new CanonicalStateQueue();
4260
4322
  this.#loaded = false;
4261
4323
  this.#resolvingHashes = /* @__PURE__ */ new Set();
4262
4324
  this._wantList = [];
@@ -4299,6 +4361,13 @@ class State extends Contract {
4299
4361
  #blocks;
4300
4362
  #totalSize;
4301
4363
  #machine;
4364
+ #canonicalStateQueue;
4365
+ withCanonicalState(operation) {
4366
+ return this.#canonicalStateQueue.run(operation);
4367
+ }
4368
+ canonicalTip() {
4369
+ return this.#machine ? this.#machine.getCanonicalTip() : Promise.resolve({ index: -1, hash: "0x0", previousHash: "0x0" });
4370
+ }
4302
4371
  #loaded;
4303
4372
  #resolvingHashes;
4304
4373
  /**
@@ -4451,7 +4520,7 @@ class State extends Contract {
4451
4520
  this.#machine = machine;
4452
4521
  const lastBlock = await this.#machine.lastBlock;
4453
4522
  if (lastBlock.hash !== "0x0") {
4454
- this.updateState(new BlockMessage(lastBlock));
4523
+ await this.updateState(new BlockMessage(lastBlock));
4455
4524
  }
4456
4525
  this.#loaded = true;
4457
4526
  this.#chainState = "loaded";
@@ -4474,9 +4543,10 @@ class State extends Contract {
4474
4543
  await machine.ready;
4475
4544
  this.#machine = machine;
4476
4545
  }
4477
- await this.#machine.updateState();
4546
+ await this.#machine.updateState(true);
4478
4547
  } catch (error) {
4479
4548
  console.error(error);
4549
+ throw error;
4480
4550
  }
4481
4551
  }
4482
4552
  getLatestBlock() {
@@ -4665,11 +4735,16 @@ class State extends Contract {
4665
4735
  console.error(error);
4666
4736
  }
4667
4737
  if (!lastBlock) lastBlock = await this.#getLatestBlock();
4668
- if (globalThis.peernet.peers.length === 0) return "connectionless";
4738
+ if (globalThis.peernet.peers.length === 0) {
4739
+ this.#chainSyncing = false;
4740
+ this.#syncState = "connectionless";
4741
+ return this.#syncState;
4742
+ }
4669
4743
  try {
4670
- await this.#syncChain(lastBlock);
4744
+ await this.withCanonicalState((transition) => this.#syncChain(lastBlock, transition));
4671
4745
  } catch (error) {
4672
4746
  this.#syncErrorCount += 1;
4747
+ this.#chainSyncing = false;
4673
4748
  if (this.#syncErrorCount < 3) return this.syncChain(lastBlock);
4674
4749
  this.#syncErrorCount = 0;
4675
4750
  this.#chainSyncing = false;
@@ -4683,9 +4758,9 @@ class State extends Contract {
4683
4758
  this.#syncState = "synced";
4684
4759
  return this.#syncState;
4685
4760
  }
4686
- async #syncChain(lastBlock) {
4761
+ async #syncChain(lastBlock, transition) {
4687
4762
  try {
4688
- const localBlock = await this.lastBlock;
4763
+ const localBlock = await this.canonicalTip();
4689
4764
  const localIndex = localBlock ? Number(localBlock.index) : -1;
4690
4765
  const remoteIndex = Number(lastBlock.index);
4691
4766
  const remoteBlockHash = lastBlock.hash;
@@ -4707,6 +4782,9 @@ class State extends Contract {
4707
4782
  debug$1(`Remote block hash is 0x0, skipping sync`);
4708
4783
  return;
4709
4784
  }
4785
+ if (localBlock?.hash !== "0x0" && localIndex === remoteIndex && localBlock.hash !== remoteBlockHash) {
4786
+ throw new Error(`Block conflict detected at index ${remoteIndex}`);
4787
+ }
4710
4788
  if (localIndex > remoteIndex) {
4711
4789
  const MAX_REORG_DEPTH = 6;
4712
4790
  const reorgDepth = localIndex - remoteIndex;
@@ -4746,9 +4824,14 @@ class State extends Contract {
4746
4824
  debug$1(`Loading blocks from index ${localIndex + 1} to ${remoteIndex}`);
4747
4825
  const start = localIndex + 1;
4748
4826
  if (this.#machine && blocks.length > start) {
4749
- await this.#loadBlocks(blocks.slice(start));
4827
+ await this.#loadBlocks(blocks.slice(start), transition);
4750
4828
  }
4751
4829
  if (blocks.length > 0) {
4830
+ const appliedTip = await this.canonicalTip();
4831
+ if (appliedTip.hash !== remoteBlockHash || Number(appliedTip.index) !== remoteIndex) {
4832
+ throw new Error(`Sync did not apply canonical target ${remoteBlockHash} at index ${remoteIndex}`);
4833
+ }
4834
+ transition.markMutating();
4752
4835
  await this.updateState(new BlockMessage(blocks[blocks.length - 1]));
4753
4836
  }
4754
4837
  } else {
@@ -4868,14 +4951,14 @@ class State extends Contract {
4868
4951
  console.log(error);
4869
4952
  }
4870
4953
  console.log(error);
4871
- return false;
4954
+ throw error;
4872
4955
  }
4873
4956
  }
4874
4957
  /**
4875
4958
  *
4876
4959
  * @param {Block[]} blocks
4877
4960
  */
4878
- async #loadBlocks(blocks) {
4961
+ async #loadBlocks(blocks, transition) {
4879
4962
  this.#chainState = "loading";
4880
4963
  if (blocks.some((block) => !block)) {
4881
4964
  throw new Error("missing block data during load; chain resolution incomplete");
@@ -4886,10 +4969,21 @@ class State extends Contract {
4886
4969
  for (const block of blocks) {
4887
4970
  if (block && !block.loaded) {
4888
4971
  try {
4972
+ const link = validateChainLink(await this.canonicalTip(), {
4973
+ index: Number(block.index),
4974
+ hash: block.hash,
4975
+ previousHash: String(block.previousHash)
4976
+ });
4977
+ if (link !== "append") {
4978
+ this.#blocks[block.index].loaded = true;
4979
+ debug$1(`Ignoring ${link} loaded block ${block.hash} at height ${block.index}`);
4980
+ continue;
4981
+ }
4889
4982
  debug$1(`loading block: ${Number(block.index)} ${block.hash}`);
4890
4983
  let transactions = await this.#loadBlockTransactions(block.transactions || []);
4891
4984
  const validators = block.validators.map(({ address }) => address);
4892
4985
  debug$1(`loading transactions: ${transactions.length} for block ${block.index}`);
4986
+ transition.markMutating();
4893
4987
  for (const transaction of transactions) {
4894
4988
  const hash = await transaction.hash();
4895
4989
  if (poolTransactionKeys.has(hash)) await globalThis.transactionPoolStore.delete(hash);
@@ -4922,10 +5016,11 @@ class State extends Contract {
4922
5016
  ...distributeAmount(policy.subsidy, validators, Number(block.index) % validators.length)
4923
5017
  ]);
4924
5018
  }
4925
- this.#blocks[block.index].loaded = true;
4926
5019
  debug$1(`executed transactions for block ${block.index}`);
5020
+ const registered = await this.#machine.addLoadedBlock({ ...block, loaded: true });
5021
+ if (registered !== true) throw new Error(`Loaded block registration failed after execution: ${block.hash}`);
5022
+ this.#blocks[block.index].loaded = true;
4927
5023
  if (Number(block.index) === 0) this.#loaded = true;
4928
- await this.#machine.addLoadedBlock(block);
4929
5024
  debug$1(`loaded block: ${block.hash} @${Number(block.index)}`);
4930
5025
  globalThis.pubsub.publish("block-loaded", { ...block });
4931
5026
  } catch (error) {
@@ -4933,6 +5028,7 @@ class State extends Contract {
4933
5028
  for (const transaction of block.transactions) {
4934
5029
  this.wantList.push(transaction);
4935
5030
  }
5031
+ throw error;
4936
5032
  }
4937
5033
  }
4938
5034
  }
@@ -5046,27 +5142,6 @@ class VersionControl extends State {
5046
5142
  }
5047
5143
  }
5048
5144
 
5049
- const validateChainLink = (localTip, incoming) => {
5050
- const localHash = localTip?.hash || "0x0";
5051
- const localIndex = Number(localTip?.index ?? -1);
5052
- const hasCanonicalTip = localHash !== "0x0";
5053
- if (hasCanonicalTip && incoming.index < localIndex) return "stale";
5054
- if (hasCanonicalTip && incoming.index === localIndex) {
5055
- if (incoming.hash === localHash) return "duplicate";
5056
- throw new Error(`Block conflict detected at index ${incoming.index}`);
5057
- }
5058
- const expectedIndex = hasCanonicalTip ? localIndex + 1 : 0;
5059
- if (incoming.index !== expectedIndex) {
5060
- throw new Error(`Unexpected block index ${incoming.index}; expected ${expectedIndex}`);
5061
- }
5062
- if (incoming.previousHash !== localHash) {
5063
- throw new Error(
5064
- `previousHash mismatch at index ${incoming.index}: expected ${localHash}, got ${incoming.previousHash}`
5065
- );
5066
- }
5067
- return "append";
5068
- };
5069
-
5070
5145
  const validateCanonicalValidatorSet = (blockIndex, expectedValidators, validators) => {
5071
5146
  const actualValidators = validators.map(({ address }) => address);
5072
5147
  const canonicalExpected = [...new Set(expectedValidators)].sort();
@@ -5186,6 +5261,73 @@ const assertHashFormatState = async (chainStore, stateStore) => {
5186
5261
  }
5187
5262
  };
5188
5263
 
5264
+ const CHAIN_ID_SCHEMA = "leofcoin:chain-identity:v1";
5265
+ const GENESIS_CONTRACT_NAMES = ["contractFactory", "nativeToken", "nameService", "validators"];
5266
+ const assertChainId = (value) => {
5267
+ if (typeof value !== "string" || !/^lfc:[0-9a-f]{64}$/.test(value))
5268
+ throw new Error("Invalid chain ID; expected lfc: followed by 64 lowercase hex characters");
5269
+ return value;
5270
+ };
5271
+ const createChainIdentity = async (network, contracts) => {
5272
+ if (typeof network !== "string" || !/^[a-z0-9][a-z0-9:-]{0,79}$/.test(network))
5273
+ throw new Error("Invalid genesis network namespace");
5274
+ const genesisContracts = {};
5275
+ for (const name of GENESIS_CONTRACT_NAMES) {
5276
+ const hash = contracts?.[name];
5277
+ if (typeof hash !== "string" || !/^[A-Z2-7]{32,128}$/.test(hash))
5278
+ throw new Error(`Invalid genesis contract hash: ${name}`);
5279
+ genesisContracts[name] = hash;
5280
+ }
5281
+ const bytes = new TextEncoder().encode(JSON.stringify({ schema: CHAIN_ID_SCHEMA, network, genesisContracts }));
5282
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
5283
+ const chainId = "lfc:" + [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
5284
+ return { schema: CHAIN_ID_SCHEMA, chainId, network, genesisContracts };
5285
+ };
5286
+ const verifyChainIdentity = async (value) => {
5287
+ const identity = value;
5288
+ if (!identity || identity.schema !== CHAIN_ID_SCHEMA)
5289
+ throw new Error("Unsupported chain identity response");
5290
+ const verified = await createChainIdentity(identity.network, identity.genesisContracts);
5291
+ if (verified.chainId !== assertChainId(identity.chainId))
5292
+ throw new Error("Chain ID does not match its genesis contracts");
5293
+ return verified;
5294
+ };
5295
+ const findChainPresence = async (chainId, peerIds, requestIdentity, timeoutMs = 3e3) => {
5296
+ assertChainId(chainId);
5297
+ const peers = [...new Set(peerIds)].slice(0, 16);
5298
+ const matches = [];
5299
+ let responded = 0;
5300
+ let unavailable = 0;
5301
+ await Promise.all(peers.map(async (peerId) => {
5302
+ let timer;
5303
+ try {
5304
+ const result = await Promise.race([
5305
+ Promise.resolve().then(() => requestIdentity(peerId)),
5306
+ new Promise((_, reject) => {
5307
+ timer = setTimeout(() => reject(new Error("Chain identity request timed out")), timeoutMs);
5308
+ })
5309
+ ]);
5310
+ const identity = await verifyChainIdentity(result);
5311
+ responded++;
5312
+ if (identity.chainId === chainId)
5313
+ matches.push(peerId);
5314
+ } catch {
5315
+ unavailable++;
5316
+ } finally {
5317
+ clearTimeout(timer);
5318
+ }
5319
+ }));
5320
+ return {
5321
+ chainId,
5322
+ status: matches.length ? "found" : !peers.length || unavailable ? "unknown" : "not-observed",
5323
+ checkedPeers: peers.length,
5324
+ respondedPeers: responded,
5325
+ unavailablePeers: unavailable,
5326
+ matchingPeers: matches.sort(),
5327
+ scope: "reachable peers only; offline or undiscovered chains may still exist"
5328
+ };
5329
+ };
5330
+
5189
5331
  const debug = createDebugger("leofcoin/chain");
5190
5332
  class Chain extends VersionControl {
5191
5333
  constructor(config) {
@@ -5453,6 +5595,32 @@ class Chain extends VersionControl {
5453
5595
  get nativeToken() {
5454
5596
  return addresses.nativeToken;
5455
5597
  }
5598
+ /** Immutable bundled genesis identity, independent of the current chain tip. */
5599
+ getChainIdentity() {
5600
+ return createChainIdentity(globalThis.peernet?.network || "leofcoin:peach", addresses);
5601
+ }
5602
+ async findChain(chainId) {
5603
+ assertChainId(chainId);
5604
+ const local = await this.getChainIdentity();
5605
+ if (local.chainId === chainId) return {
5606
+ chainId,
5607
+ status: "found",
5608
+ foundViaLocalNode: true,
5609
+ checkedPeers: 0,
5610
+ respondedPeers: 0,
5611
+ unavailablePeers: 0,
5612
+ matchingPeers: [],
5613
+ scope: "local node; not proof of a globally available or finalized chain"
5614
+ };
5615
+ const peers = Object.entries(globalThis.peernet?.connections || {}).filter(([, peer]) => peer.connected && typeof peer.request === "function");
5616
+ return {
5617
+ ...await findChainPresence(chainId, peers.map(([id]) => id), async (id) => {
5618
+ const peer = peers.find(([peerId]) => peerId === id)?.[1];
5619
+ return this.#makeRequest(peer, "chainIdentity");
5620
+ }),
5621
+ foundViaLocalNode: false
5622
+ };
5623
+ }
5456
5624
  get validators() {
5457
5625
  return [...this.#validators];
5458
5626
  }
@@ -5642,7 +5810,9 @@ class Chain extends VersionControl {
5642
5810
  `[consensus] Epoch boundary at block ${blockHeight}: new epoch starts at height ${this.#currentEpochStartHeight}`
5643
5811
  );
5644
5812
  if (this.#participating && !this.#runningEpoch) {
5645
- await this.#runEpoch();
5813
+ void this.#runEpoch().catch((error) => {
5814
+ debug(`epoch scheduling failed: ${error?.message ?? error}`);
5815
+ });
5646
5816
  }
5647
5817
  }
5648
5818
  async #runEpoch() {
@@ -5745,6 +5915,10 @@ class Chain extends VersionControl {
5745
5915
  });
5746
5916
  await globalThis.peernet.addRequestHandler("transactionPool", this.#transactionPoolHandler.bind(this));
5747
5917
  await globalThis.peernet.addRequestHandler("version", this.#versionHandler.bind(this));
5918
+ await globalThis.peernet.addRequestHandler(
5919
+ "chainIdentity",
5920
+ async () => new globalThis.peernet.protos["peernet-response"]({ response: await this.getChainIdentity() })
5921
+ );
5748
5922
  await globalThis.peernet.addRequestHandler("stateInfo", async () => {
5749
5923
  const lastblock = await this.lastBlock || { index: 0, hash: "0x0", previousHash: "0x0" };
5750
5924
  const values = this.machine?.states?.info || {};
@@ -6111,11 +6285,14 @@ class Chain extends VersionControl {
6111
6285
  }
6112
6286
  }
6113
6287
  async #addBlock(block) {
6288
+ return this.withCanonicalState((transition) => this.#applyBlock(block, transition));
6289
+ }
6290
+ async #applyBlock(block, transition) {
6114
6291
  const receivedEncoded = block instanceof BlockMessage ? block.encoded : block;
6115
6292
  const blockMessage = await new BlockMessage(block);
6116
6293
  const hash = await blockMessage.hash();
6117
6294
  const blockIndex = Number(blockMessage.decoded.index);
6118
- const linkResult = validateChainLink(await this.lastBlock, {
6295
+ const linkResult = validateChainLink(await this.canonicalTip(), {
6119
6296
  index: blockIndex,
6120
6297
  hash,
6121
6298
  previousHash: String(blockMessage.decoded.previousHash)
@@ -6144,6 +6321,7 @@ class Chain extends VersionControl {
6144
6321
  console.log(`[chain] \u2705 Block data integrity verified: ${hash}`);
6145
6322
  await this.#validateBlockValidators(blockMessage);
6146
6323
  const transactions = await this.#resolveBlockTransactions(blockMessage);
6324
+ transition.markMutating();
6147
6325
  await Promise.all(
6148
6326
  blockMessage.decoded.transactions.map(async (transactionHash) => {
6149
6327
  if (await transactionPoolStore.has(transactionHash)) await transactionPoolStore.delete(transactionHash);
@@ -6154,9 +6332,6 @@ class Chain extends VersionControl {
6154
6332
  hash,
6155
6333
  ...blockMessage.decoded
6156
6334
  };
6157
- debug(`added block: ${hash}`);
6158
- let promises = [];
6159
- let contracts = [];
6160
6335
  const allTransactions = transactions.sort((a, b) => {
6161
6336
  if (a.decoded.priority !== b.decoded.priority) {
6162
6337
  return (b.decoded.priority ? 1 : 0) - (a.decoded.priority ? 1 : 0);
@@ -6167,7 +6342,6 @@ class Chain extends VersionControl {
6167
6342
  });
6168
6343
  const monetaryPolicy = await this.#monetaryPolicy(blockMessage.decoded.protocolVersion);
6169
6344
  for (const transaction of allTransactions) {
6170
- if (!contracts.includes(transaction.decoded.to)) contracts.push(transaction.decoded.to);
6171
6345
  this.removePendingNonce(transaction.decoded.from, transaction.decoded.nonce);
6172
6346
  const transactionHash = await transaction.hash();
6173
6347
  const validatorAddresses = blockMessage.decoded.validators.map(({ address }) => address);
@@ -6185,36 +6359,36 @@ class Chain extends VersionControl {
6185
6359
  ...distributeAmount(monetaryPolicy.subsidy, validators, blockIndex % validators.length)
6186
6360
  ]);
6187
6361
  }
6188
- try {
6189
- promises = await Promise.allSettled(promises);
6190
- const noncesByAddress = {};
6191
- for (let transaction of transactions) {
6192
- globalThis.pubsub.publish("transaction-processed", transaction.encoded);
6193
- if (transaction.decoded.to === globalThis.peernet.selectedAccount)
6194
- globalThis.pubsub.publish("account-transaction-processed", transaction.encoded);
6195
- if (!noncesByAddress[transaction.decoded.from] || noncesByAddress?.[transaction.decoded.from] < transaction.decoded.nonce) {
6196
- noncesByAddress[transaction.decoded.from] = transaction.decoded.nonce;
6197
- }
6198
- }
6199
- await Promise.all(
6200
- Object.entries(noncesByAddress).map(([from, nonce]) => globalThis.accountsStore.put(from, String(nonce)))
6201
- );
6202
- if ((await this.lastBlock).index < Number(blockMessage.decoded.index)) {
6203
- await this.machine.addLoadedBlock({ ...blockMessage.decoded, loaded: true, hash: await blockMessage.hash() });
6204
- try {
6205
- await this.call(addresses.validators, "recordValidatorSnapshot", [blockMessage.decoded.index]);
6206
- } catch (snapshotError) {
6207
- debug(`failed to record validator snapshot: ${snapshotError?.message ?? snapshotError}`);
6208
- }
6209
- await this.#handleEpochBoundary(Number(blockMessage.decoded.index));
6210
- await this.updateState(blockMessage);
6362
+ const noncesByAddress = {};
6363
+ for (let transaction of transactions) {
6364
+ globalThis.pubsub.publish("transaction-processed", transaction.encoded);
6365
+ if (transaction.decoded.to === globalThis.peernet.selectedAccount)
6366
+ globalThis.pubsub.publish("account-transaction-processed", transaction.encoded);
6367
+ if (!noncesByAddress[transaction.decoded.from] || noncesByAddress?.[transaction.decoded.from] < transaction.decoded.nonce) {
6368
+ noncesByAddress[transaction.decoded.from] = transaction.decoded.nonce;
6211
6369
  }
6212
- globalThis.pubsub.publish("block-processed", blockMessage.decoded);
6213
- } catch (error) {
6214
- console.log(error.hash);
6215
- console.log("errrrr");
6216
- await transactionPoolStore.delete(error.hash);
6217
6370
  }
6371
+ await Promise.all(
6372
+ Object.entries(noncesByAddress).map(([from, nonce]) => globalThis.accountsStore.put(from, String(nonce)))
6373
+ );
6374
+ if (validateChainLink(await this.canonicalTip(), {
6375
+ index: blockIndex,
6376
+ hash,
6377
+ previousHash: String(blockMessage.decoded.previousHash)
6378
+ }) !== "append") {
6379
+ throw new Error(`Canonical tip changed during block application: ${hash}`);
6380
+ }
6381
+ const registered = await this.machine.addLoadedBlock({ ...blockMessage.decoded, loaded: true, hash });
6382
+ if (registered !== true) throw new Error(`Block registration failed after execution: ${hash}`);
6383
+ try {
6384
+ await this.call(addresses.validators, "recordValidatorSnapshot", [blockMessage.decoded.index]);
6385
+ } catch (snapshotError) {
6386
+ debug(`failed to record validator snapshot: ${snapshotError?.message ?? snapshotError}`);
6387
+ }
6388
+ await this.updateState(blockMessage);
6389
+ debug(`added block: ${hash}`);
6390
+ globalThis.pubsub.publish("block-processed", blockMessage.decoded);
6391
+ await this.#handleEpochBoundary(blockIndex);
6218
6392
  }
6219
6393
  async participate(address) {
6220
6394
  try {
@@ -6278,10 +6452,6 @@ class Chain extends VersionControl {
6278
6452
  executionTimestamp: block?.timestamp ?? transaction.decoded.timestamp,
6279
6453
  feePayments
6280
6454
  });
6281
- if (block) {
6282
- block.transactions.push(hash);
6283
- block.fees = block.fees += await calculateFee(transaction.decoded);
6284
- }
6285
6455
  await globalThis.accountsStore.put(
6286
6456
  transaction.decoded.from,
6287
6457
  new TextEncoder().encode(String(transaction.decoded.nonce))
@@ -6293,6 +6463,7 @@ class Chain extends VersionControl {
6293
6463
  console.log(hash);
6294
6464
  peernet.publish("invalid-transaction", hash);
6295
6465
  await globalThis.transactionPoolStore.delete(e.hash);
6466
+ if (block) throw e;
6296
6467
  }
6297
6468
  }
6298
6469
  // todo filter tx that need to wait on prev nonce
@@ -959,6 +959,7 @@ var bytecodes = {
959
959
  nameService,
960
960
  validators
961
961
  };
962
+
962
963
  const contractFactoryMessage = bytecodes.contractFactory;
963
964
  const nativeTokenMessage = bytecodes.nativeToken;
964
965
  const nameServiceMessage = bytecodes.nameService;
@@ -8847,11 +8847,25 @@ const _ = {
8847
8847
  addLoadedBlock: (block) => {
8848
8848
  const size = formatBytes(block.length);
8849
8849
  block = JSON.parse(block, jsonParseBigInt$1);
8850
- // if (block.decoded) block = { ...block.decoded, hash: await new BlockMessage(block).hash() }
8851
- // if (blocks[block.index - 1]) {
8852
- // console.warn(`block ${block.index} already loaded, skipping`)
8853
- // return false
8854
- // }
8850
+ const index = Number(block.index);
8851
+ const tipIndex = Number(lastBlock.index);
8852
+ if (!['number', 'bigint'].includes(typeof block.index) ||
8853
+ !Number.isSafeInteger(index) || index < 0 || typeof block.hash !== 'string' || !block.hash) {
8854
+ throw new Error('Invalid loaded block identity');
8855
+ }
8856
+ if (lastBlock.hash !== '0x0') {
8857
+ if (index < tipIndex)
8858
+ return false;
8859
+ if (index === tipIndex) {
8860
+ if (block.hash === lastBlock.hash)
8861
+ return false;
8862
+ throw new Error(`Loaded block conflict at index ${index}`);
8863
+ }
8864
+ }
8865
+ const expectedIndex = lastBlock.hash === '0x0' ? 0 : tipIndex + 1;
8866
+ if (index !== expectedIndex || block.previousHash !== lastBlock.hash) {
8867
+ throw new Error(`Loaded block does not extend canonical tip at index ${index}`);
8868
+ }
8855
8869
  blocks[block.index.toString()] = block;
8856
8870
  lastBlock = blocks[blocks.length - 1];
8857
8871
  totalBlocks += 1n;
package/exports/chain.js CHANGED
@@ -14,6 +14,7 @@ import { P as PROTOCOL_VERSION, R as REACHED_ONE_ZERO_ZERO } from './constants-D
14
14
  import ConnectionMonitor from './connection-monitor.js';
15
15
  import { q as quorumThreshold } from './quorum-S_qdgiAY.js';
16
16
  import { assertHashFormatState } from './hash-format-state.js';
17
+ import { createChainIdentity, assertChainId, findChainPresence } from '@leofcoin/lib/chain-identity';
17
18
  import '@leofcoin/networks';
18
19
 
19
20
  const limit = 1800;
@@ -659,6 +660,12 @@ class Machine {
659
660
  pubsub.publish(data.id, { error: data.message });
660
661
  break;
661
662
  }
663
+ case "addLoadedBlockError":
664
+ case "collectFeeError":
665
+ case "settleRewardsError": {
666
+ pubsub.publish(data.id, { error: new Error(data.message) });
667
+ break;
668
+ }
662
669
  case "debug": {
663
670
  if (data.message.includes("loaded transactions for block:")) {
664
671
  pubsub.publish("block-loaded", data.message.replace("loaded transactions for block: ", "").split(" @")[0]);
@@ -708,9 +715,9 @@ class Machine {
708
715
  }
709
716
  }
710
717
  }
711
- async updateState() {
718
+ async updateState(strict = false) {
712
719
  try {
713
- const lastBlock = await this.lastBlock;
720
+ const lastBlock = strict ? await this.getCanonicalTip() : await this.lastBlock;
714
721
  const lastBlockIndex = Number(lastBlock.index);
715
722
  const statesLastBlockIndex = Number(this.states.lastBlock.index);
716
723
  if (lastBlockIndex > statesLastBlockIndex || lastBlock.hash !== this.states.lastBlock.hash && Number(lastBlockIndex) === 0) {
@@ -769,7 +776,7 @@ class Machine {
769
776
  return JSON.stringify(sorted, replacer);
770
777
  };
771
778
  const tasks = [
772
- stateStore.put("lastBlock", sortedStringify(await this.lastBlock, jsonStringifyBigInt)),
779
+ stateStore.put("lastBlock", sortedStringify(lastBlock, jsonStringifyBigInt)),
773
780
  stateStore.put("states", sortedStringify(state, jsonStringifyBigInt)),
774
781
  stateStore.put("accounts", sortedStringify(accounts, jsonStringifyBigInt)),
775
782
  stateStore.put(
@@ -796,6 +803,7 @@ class Machine {
796
803
  }
797
804
  } catch (error) {
798
805
  console.error(error);
806
+ if (strict) throw error;
799
807
  }
800
808
  }
801
809
  async #init(blocks) {
@@ -1062,6 +1070,9 @@ ${error.message}`);
1062
1070
  return this.states.lastBlock;
1063
1071
  });
1064
1072
  }
1073
+ getCanonicalTip() {
1074
+ return this.#askWorker("lastBlock");
1075
+ }
1065
1076
  get lastBlockHeight() {
1066
1077
  return this.#askWorker("lastBlockHeight").catch((error) => {
1067
1078
  debug$2("lastBlockHeight fallback after worker timeout:", error?.message ?? error);
@@ -1206,6 +1217,56 @@ const decodeResponsePayload = async (result, depth = 0) => {
1206
1217
  return result;
1207
1218
  };
1208
1219
 
1220
+ class CanonicalStateQueue {
1221
+ #tail = Promise.resolve();
1222
+ #failed = false;
1223
+ #failure;
1224
+ run(operation) {
1225
+ const result = this.#tail.then(async () => {
1226
+ if (this.#failed) {
1227
+ throw new Error("Canonical state application halted after a failed mutation; restart and recover state", {
1228
+ cause: this.#failure
1229
+ });
1230
+ }
1231
+ let mutating = false;
1232
+ try {
1233
+ return await operation({ markMutating: () => {
1234
+ mutating = true;
1235
+ } });
1236
+ } catch (error) {
1237
+ if (mutating) {
1238
+ this.#failed = true;
1239
+ this.#failure = error;
1240
+ }
1241
+ throw error;
1242
+ }
1243
+ });
1244
+ this.#tail = result.then(() => void 0, () => void 0);
1245
+ return result;
1246
+ }
1247
+ }
1248
+
1249
+ const validateChainLink = (localTip, incoming) => {
1250
+ const localHash = localTip?.hash || "0x0";
1251
+ const localIndex = Number(localTip?.index ?? -1);
1252
+ const hasCanonicalTip = localHash !== "0x0";
1253
+ if (hasCanonicalTip && incoming.index < localIndex) return "stale";
1254
+ if (hasCanonicalTip && incoming.index === localIndex) {
1255
+ if (incoming.hash === localHash) return "duplicate";
1256
+ throw new Error(`Block conflict detected at index ${incoming.index}`);
1257
+ }
1258
+ const expectedIndex = hasCanonicalTip ? localIndex + 1 : 0;
1259
+ if (incoming.index !== expectedIndex) {
1260
+ throw new Error(`Unexpected block index ${incoming.index}; expected ${expectedIndex}`);
1261
+ }
1262
+ if (incoming.previousHash !== localHash) {
1263
+ throw new Error(
1264
+ `previousHash mismatch at index ${incoming.index}: expected ${localHash}, got ${incoming.previousHash}`
1265
+ );
1266
+ }
1267
+ return "append";
1268
+ };
1269
+
1209
1270
  const debug$1 = createDebugger("leofcoin/state");
1210
1271
  class State extends Contract {
1211
1272
  constructor(config) {
@@ -1220,6 +1281,7 @@ class State extends Contract {
1220
1281
  this.#blocks = [];
1221
1282
  this.knownBlocks = [];
1222
1283
  this.#totalSize = 0;
1284
+ this.#canonicalStateQueue = new CanonicalStateQueue();
1223
1285
  this.#loaded = false;
1224
1286
  this.#resolvingHashes = /* @__PURE__ */ new Set();
1225
1287
  this._wantList = [];
@@ -1262,6 +1324,13 @@ class State extends Contract {
1262
1324
  #blocks;
1263
1325
  #totalSize;
1264
1326
  #machine;
1327
+ #canonicalStateQueue;
1328
+ withCanonicalState(operation) {
1329
+ return this.#canonicalStateQueue.run(operation);
1330
+ }
1331
+ canonicalTip() {
1332
+ return this.#machine ? this.#machine.getCanonicalTip() : Promise.resolve({ index: -1, hash: "0x0", previousHash: "0x0" });
1333
+ }
1265
1334
  #loaded;
1266
1335
  #resolvingHashes;
1267
1336
  /**
@@ -1414,7 +1483,7 @@ class State extends Contract {
1414
1483
  this.#machine = machine;
1415
1484
  const lastBlock = await this.#machine.lastBlock;
1416
1485
  if (lastBlock.hash !== "0x0") {
1417
- this.updateState(new BlockMessage(lastBlock));
1486
+ await this.updateState(new BlockMessage(lastBlock));
1418
1487
  }
1419
1488
  this.#loaded = true;
1420
1489
  this.#chainState = "loaded";
@@ -1437,9 +1506,10 @@ class State extends Contract {
1437
1506
  await machine.ready;
1438
1507
  this.#machine = machine;
1439
1508
  }
1440
- await this.#machine.updateState();
1509
+ await this.#machine.updateState(true);
1441
1510
  } catch (error) {
1442
1511
  console.error(error);
1512
+ throw error;
1443
1513
  }
1444
1514
  }
1445
1515
  getLatestBlock() {
@@ -1628,11 +1698,16 @@ class State extends Contract {
1628
1698
  console.error(error);
1629
1699
  }
1630
1700
  if (!lastBlock) lastBlock = await this.#getLatestBlock();
1631
- if (globalThis.peernet.peers.length === 0) return "connectionless";
1701
+ if (globalThis.peernet.peers.length === 0) {
1702
+ this.#chainSyncing = false;
1703
+ this.#syncState = "connectionless";
1704
+ return this.#syncState;
1705
+ }
1632
1706
  try {
1633
- await this.#syncChain(lastBlock);
1707
+ await this.withCanonicalState((transition) => this.#syncChain(lastBlock, transition));
1634
1708
  } catch (error) {
1635
1709
  this.#syncErrorCount += 1;
1710
+ this.#chainSyncing = false;
1636
1711
  if (this.#syncErrorCount < 3) return this.syncChain(lastBlock);
1637
1712
  this.#syncErrorCount = 0;
1638
1713
  this.#chainSyncing = false;
@@ -1646,9 +1721,9 @@ class State extends Contract {
1646
1721
  this.#syncState = "synced";
1647
1722
  return this.#syncState;
1648
1723
  }
1649
- async #syncChain(lastBlock) {
1724
+ async #syncChain(lastBlock, transition) {
1650
1725
  try {
1651
- const localBlock = await this.lastBlock;
1726
+ const localBlock = await this.canonicalTip();
1652
1727
  const localIndex = localBlock ? Number(localBlock.index) : -1;
1653
1728
  const remoteIndex = Number(lastBlock.index);
1654
1729
  const remoteBlockHash = lastBlock.hash;
@@ -1670,6 +1745,9 @@ class State extends Contract {
1670
1745
  debug$1(`Remote block hash is 0x0, skipping sync`);
1671
1746
  return;
1672
1747
  }
1748
+ if (localBlock?.hash !== "0x0" && localIndex === remoteIndex && localBlock.hash !== remoteBlockHash) {
1749
+ throw new Error(`Block conflict detected at index ${remoteIndex}`);
1750
+ }
1673
1751
  if (localIndex > remoteIndex) {
1674
1752
  const MAX_REORG_DEPTH = 6;
1675
1753
  const reorgDepth = localIndex - remoteIndex;
@@ -1709,9 +1787,14 @@ class State extends Contract {
1709
1787
  debug$1(`Loading blocks from index ${localIndex + 1} to ${remoteIndex}`);
1710
1788
  const start = localIndex + 1;
1711
1789
  if (this.#machine && blocks.length > start) {
1712
- await this.#loadBlocks(blocks.slice(start));
1790
+ await this.#loadBlocks(blocks.slice(start), transition);
1713
1791
  }
1714
1792
  if (blocks.length > 0) {
1793
+ const appliedTip = await this.canonicalTip();
1794
+ if (appliedTip.hash !== remoteBlockHash || Number(appliedTip.index) !== remoteIndex) {
1795
+ throw new Error(`Sync did not apply canonical target ${remoteBlockHash} at index ${remoteIndex}`);
1796
+ }
1797
+ transition.markMutating();
1715
1798
  await this.updateState(new BlockMessage(blocks[blocks.length - 1]));
1716
1799
  }
1717
1800
  } else {
@@ -1831,14 +1914,14 @@ class State extends Contract {
1831
1914
  console.log(error);
1832
1915
  }
1833
1916
  console.log(error);
1834
- return false;
1917
+ throw error;
1835
1918
  }
1836
1919
  }
1837
1920
  /**
1838
1921
  *
1839
1922
  * @param {Block[]} blocks
1840
1923
  */
1841
- async #loadBlocks(blocks) {
1924
+ async #loadBlocks(blocks, transition) {
1842
1925
  this.#chainState = "loading";
1843
1926
  if (blocks.some((block) => !block)) {
1844
1927
  throw new Error("missing block data during load; chain resolution incomplete");
@@ -1849,10 +1932,21 @@ class State extends Contract {
1849
1932
  for (const block of blocks) {
1850
1933
  if (block && !block.loaded) {
1851
1934
  try {
1935
+ const link = validateChainLink(await this.canonicalTip(), {
1936
+ index: Number(block.index),
1937
+ hash: block.hash,
1938
+ previousHash: String(block.previousHash)
1939
+ });
1940
+ if (link !== "append") {
1941
+ this.#blocks[block.index].loaded = true;
1942
+ debug$1(`Ignoring ${link} loaded block ${block.hash} at height ${block.index}`);
1943
+ continue;
1944
+ }
1852
1945
  debug$1(`loading block: ${Number(block.index)} ${block.hash}`);
1853
1946
  let transactions = await this.#loadBlockTransactions(block.transactions || []);
1854
1947
  const validators = block.validators.map(({ address }) => address);
1855
1948
  debug$1(`loading transactions: ${transactions.length} for block ${block.index}`);
1949
+ transition.markMutating();
1856
1950
  for (const transaction of transactions) {
1857
1951
  const hash = await transaction.hash();
1858
1952
  if (poolTransactionKeys.has(hash)) await globalThis.transactionPoolStore.delete(hash);
@@ -1885,10 +1979,11 @@ class State extends Contract {
1885
1979
  ...distributeAmount(policy.subsidy, validators, Number(block.index) % validators.length)
1886
1980
  ]);
1887
1981
  }
1888
- this.#blocks[block.index].loaded = true;
1889
1982
  debug$1(`executed transactions for block ${block.index}`);
1983
+ const registered = await this.#machine.addLoadedBlock({ ...block, loaded: true });
1984
+ if (registered !== true) throw new Error(`Loaded block registration failed after execution: ${block.hash}`);
1985
+ this.#blocks[block.index].loaded = true;
1890
1986
  if (Number(block.index) === 0) this.#loaded = true;
1891
- await this.#machine.addLoadedBlock(block);
1892
1987
  debug$1(`loaded block: ${block.hash} @${Number(block.index)}`);
1893
1988
  globalThis.pubsub.publish("block-loaded", { ...block });
1894
1989
  } catch (error) {
@@ -1896,6 +1991,7 @@ class State extends Contract {
1896
1991
  for (const transaction of block.transactions) {
1897
1992
  this.wantList.push(transaction);
1898
1993
  }
1994
+ throw error;
1899
1995
  }
1900
1996
  }
1901
1997
  }
@@ -2009,27 +2105,6 @@ class VersionControl extends State {
2009
2105
  }
2010
2106
  }
2011
2107
 
2012
- const validateChainLink = (localTip, incoming) => {
2013
- const localHash = localTip?.hash || "0x0";
2014
- const localIndex = Number(localTip?.index ?? -1);
2015
- const hasCanonicalTip = localHash !== "0x0";
2016
- if (hasCanonicalTip && incoming.index < localIndex) return "stale";
2017
- if (hasCanonicalTip && incoming.index === localIndex) {
2018
- if (incoming.hash === localHash) return "duplicate";
2019
- throw new Error(`Block conflict detected at index ${incoming.index}`);
2020
- }
2021
- const expectedIndex = hasCanonicalTip ? localIndex + 1 : 0;
2022
- if (incoming.index !== expectedIndex) {
2023
- throw new Error(`Unexpected block index ${incoming.index}; expected ${expectedIndex}`);
2024
- }
2025
- if (incoming.previousHash !== localHash) {
2026
- throw new Error(
2027
- `previousHash mismatch at index ${incoming.index}: expected ${localHash}, got ${incoming.previousHash}`
2028
- );
2029
- }
2030
- return "append";
2031
- };
2032
-
2033
2108
  const validateCanonicalValidatorSet = (blockIndex, expectedValidators, validators) => {
2034
2109
  const actualValidators = validators.map(({ address }) => address);
2035
2110
  const canonicalExpected = [...new Set(expectedValidators)].sort();
@@ -2403,6 +2478,32 @@ class Chain extends VersionControl {
2403
2478
  get nativeToken() {
2404
2479
  return addresses.nativeToken;
2405
2480
  }
2481
+ /** Immutable bundled genesis identity, independent of the current chain tip. */
2482
+ getChainIdentity() {
2483
+ return createChainIdentity(globalThis.peernet?.network || "leofcoin:peach", addresses);
2484
+ }
2485
+ async findChain(chainId) {
2486
+ assertChainId(chainId);
2487
+ const local = await this.getChainIdentity();
2488
+ if (local.chainId === chainId) return {
2489
+ chainId,
2490
+ status: "found",
2491
+ foundViaLocalNode: true,
2492
+ checkedPeers: 0,
2493
+ respondedPeers: 0,
2494
+ unavailablePeers: 0,
2495
+ matchingPeers: [],
2496
+ scope: "local node; not proof of a globally available or finalized chain"
2497
+ };
2498
+ const peers = Object.entries(globalThis.peernet?.connections || {}).filter(([, peer]) => peer.connected && typeof peer.request === "function");
2499
+ return {
2500
+ ...await findChainPresence(chainId, peers.map(([id]) => id), async (id) => {
2501
+ const peer = peers.find(([peerId]) => peerId === id)?.[1];
2502
+ return this.#makeRequest(peer, "chainIdentity");
2503
+ }),
2504
+ foundViaLocalNode: false
2505
+ };
2506
+ }
2406
2507
  get validators() {
2407
2508
  return [...this.#validators];
2408
2509
  }
@@ -2592,7 +2693,9 @@ class Chain extends VersionControl {
2592
2693
  `[consensus] Epoch boundary at block ${blockHeight}: new epoch starts at height ${this.#currentEpochStartHeight}`
2593
2694
  );
2594
2695
  if (this.#participating && !this.#runningEpoch) {
2595
- await this.#runEpoch();
2696
+ void this.#runEpoch().catch((error) => {
2697
+ debug(`epoch scheduling failed: ${error?.message ?? error}`);
2698
+ });
2596
2699
  }
2597
2700
  }
2598
2701
  async #runEpoch() {
@@ -2695,6 +2798,10 @@ class Chain extends VersionControl {
2695
2798
  });
2696
2799
  await globalThis.peernet.addRequestHandler("transactionPool", this.#transactionPoolHandler.bind(this));
2697
2800
  await globalThis.peernet.addRequestHandler("version", this.#versionHandler.bind(this));
2801
+ await globalThis.peernet.addRequestHandler(
2802
+ "chainIdentity",
2803
+ async () => new globalThis.peernet.protos["peernet-response"]({ response: await this.getChainIdentity() })
2804
+ );
2698
2805
  await globalThis.peernet.addRequestHandler("stateInfo", async () => {
2699
2806
  const lastblock = await this.lastBlock || { index: 0, hash: "0x0", previousHash: "0x0" };
2700
2807
  const values = this.machine?.states?.info || {};
@@ -3061,11 +3168,14 @@ class Chain extends VersionControl {
3061
3168
  }
3062
3169
  }
3063
3170
  async #addBlock(block) {
3171
+ return this.withCanonicalState((transition) => this.#applyBlock(block, transition));
3172
+ }
3173
+ async #applyBlock(block, transition) {
3064
3174
  const receivedEncoded = block instanceof BlockMessage ? block.encoded : block;
3065
3175
  const blockMessage = await new BlockMessage(block);
3066
3176
  const hash = await blockMessage.hash();
3067
3177
  const blockIndex = Number(blockMessage.decoded.index);
3068
- const linkResult = validateChainLink(await this.lastBlock, {
3178
+ const linkResult = validateChainLink(await this.canonicalTip(), {
3069
3179
  index: blockIndex,
3070
3180
  hash,
3071
3181
  previousHash: String(blockMessage.decoded.previousHash)
@@ -3094,6 +3204,7 @@ class Chain extends VersionControl {
3094
3204
  console.log(`[chain] \u2705 Block data integrity verified: ${hash}`);
3095
3205
  await this.#validateBlockValidators(blockMessage);
3096
3206
  const transactions = await this.#resolveBlockTransactions(blockMessage);
3207
+ transition.markMutating();
3097
3208
  await Promise.all(
3098
3209
  blockMessage.decoded.transactions.map(async (transactionHash) => {
3099
3210
  if (await transactionPoolStore.has(transactionHash)) await transactionPoolStore.delete(transactionHash);
@@ -3104,9 +3215,6 @@ class Chain extends VersionControl {
3104
3215
  hash,
3105
3216
  ...blockMessage.decoded
3106
3217
  };
3107
- debug(`added block: ${hash}`);
3108
- let promises = [];
3109
- let contracts = [];
3110
3218
  const allTransactions = transactions.sort((a, b) => {
3111
3219
  if (a.decoded.priority !== b.decoded.priority) {
3112
3220
  return (b.decoded.priority ? 1 : 0) - (a.decoded.priority ? 1 : 0);
@@ -3117,7 +3225,6 @@ class Chain extends VersionControl {
3117
3225
  });
3118
3226
  const monetaryPolicy = await this.#monetaryPolicy(blockMessage.decoded.protocolVersion);
3119
3227
  for (const transaction of allTransactions) {
3120
- if (!contracts.includes(transaction.decoded.to)) contracts.push(transaction.decoded.to);
3121
3228
  this.removePendingNonce(transaction.decoded.from, transaction.decoded.nonce);
3122
3229
  const transactionHash = await transaction.hash();
3123
3230
  const validatorAddresses = blockMessage.decoded.validators.map(({ address }) => address);
@@ -3135,36 +3242,36 @@ class Chain extends VersionControl {
3135
3242
  ...distributeAmount(monetaryPolicy.subsidy, validators, blockIndex % validators.length)
3136
3243
  ]);
3137
3244
  }
3138
- try {
3139
- promises = await Promise.allSettled(promises);
3140
- const noncesByAddress = {};
3141
- for (let transaction of transactions) {
3142
- globalThis.pubsub.publish("transaction-processed", transaction.encoded);
3143
- if (transaction.decoded.to === globalThis.peernet.selectedAccount)
3144
- globalThis.pubsub.publish("account-transaction-processed", transaction.encoded);
3145
- if (!noncesByAddress[transaction.decoded.from] || noncesByAddress?.[transaction.decoded.from] < transaction.decoded.nonce) {
3146
- noncesByAddress[transaction.decoded.from] = transaction.decoded.nonce;
3147
- }
3148
- }
3149
- await Promise.all(
3150
- Object.entries(noncesByAddress).map(([from, nonce]) => globalThis.accountsStore.put(from, String(nonce)))
3151
- );
3152
- if ((await this.lastBlock).index < Number(blockMessage.decoded.index)) {
3153
- await this.machine.addLoadedBlock({ ...blockMessage.decoded, loaded: true, hash: await blockMessage.hash() });
3154
- try {
3155
- await this.call(addresses.validators, "recordValidatorSnapshot", [blockMessage.decoded.index]);
3156
- } catch (snapshotError) {
3157
- debug(`failed to record validator snapshot: ${snapshotError?.message ?? snapshotError}`);
3158
- }
3159
- await this.#handleEpochBoundary(Number(blockMessage.decoded.index));
3160
- await this.updateState(blockMessage);
3245
+ const noncesByAddress = {};
3246
+ for (let transaction of transactions) {
3247
+ globalThis.pubsub.publish("transaction-processed", transaction.encoded);
3248
+ if (transaction.decoded.to === globalThis.peernet.selectedAccount)
3249
+ globalThis.pubsub.publish("account-transaction-processed", transaction.encoded);
3250
+ if (!noncesByAddress[transaction.decoded.from] || noncesByAddress?.[transaction.decoded.from] < transaction.decoded.nonce) {
3251
+ noncesByAddress[transaction.decoded.from] = transaction.decoded.nonce;
3161
3252
  }
3162
- globalThis.pubsub.publish("block-processed", blockMessage.decoded);
3163
- } catch (error) {
3164
- console.log(error.hash);
3165
- console.log("errrrr");
3166
- await transactionPoolStore.delete(error.hash);
3167
3253
  }
3254
+ await Promise.all(
3255
+ Object.entries(noncesByAddress).map(([from, nonce]) => globalThis.accountsStore.put(from, String(nonce)))
3256
+ );
3257
+ if (validateChainLink(await this.canonicalTip(), {
3258
+ index: blockIndex,
3259
+ hash,
3260
+ previousHash: String(blockMessage.decoded.previousHash)
3261
+ }) !== "append") {
3262
+ throw new Error(`Canonical tip changed during block application: ${hash}`);
3263
+ }
3264
+ const registered = await this.machine.addLoadedBlock({ ...blockMessage.decoded, loaded: true, hash });
3265
+ if (registered !== true) throw new Error(`Block registration failed after execution: ${hash}`);
3266
+ try {
3267
+ await this.call(addresses.validators, "recordValidatorSnapshot", [blockMessage.decoded.index]);
3268
+ } catch (snapshotError) {
3269
+ debug(`failed to record validator snapshot: ${snapshotError?.message ?? snapshotError}`);
3270
+ }
3271
+ await this.updateState(blockMessage);
3272
+ debug(`added block: ${hash}`);
3273
+ globalThis.pubsub.publish("block-processed", blockMessage.decoded);
3274
+ await this.#handleEpochBoundary(blockIndex);
3168
3275
  }
3169
3276
  async participate(address) {
3170
3277
  try {
@@ -3228,10 +3335,6 @@ class Chain extends VersionControl {
3228
3335
  executionTimestamp: block?.timestamp ?? transaction.decoded.timestamp,
3229
3336
  feePayments
3230
3337
  });
3231
- if (block) {
3232
- block.transactions.push(hash);
3233
- block.fees = block.fees += await calculateFee(transaction.decoded);
3234
- }
3235
3338
  await globalThis.accountsStore.put(
3236
3339
  transaction.decoded.from,
3237
3340
  new TextEncoder().encode(String(transaction.decoded.nonce))
@@ -3243,6 +3346,7 @@ class Chain extends VersionControl {
3243
3346
  console.log(hash);
3244
3347
  peernet.publish("invalid-transaction", hash);
3245
3348
  await globalThis.transactionPoolStore.delete(e.hash);
3349
+ if (block) throw e;
3246
3350
  }
3247
3351
  }
3248
3352
  // todo filter tx that need to wait on prev nonce
@@ -8847,11 +8847,25 @@ const _ = {
8847
8847
  addLoadedBlock: (block) => {
8848
8848
  const size = formatBytes(block.length);
8849
8849
  block = JSON.parse(block, jsonParseBigInt$1);
8850
- // if (block.decoded) block = { ...block.decoded, hash: await new BlockMessage(block).hash() }
8851
- // if (blocks[block.index - 1]) {
8852
- // console.warn(`block ${block.index} already loaded, skipping`)
8853
- // return false
8854
- // }
8850
+ const index = Number(block.index);
8851
+ const tipIndex = Number(lastBlock.index);
8852
+ if (!['number', 'bigint'].includes(typeof block.index) ||
8853
+ !Number.isSafeInteger(index) || index < 0 || typeof block.hash !== 'string' || !block.hash) {
8854
+ throw new Error('Invalid loaded block identity');
8855
+ }
8856
+ if (lastBlock.hash !== '0x0') {
8857
+ if (index < tipIndex)
8858
+ return false;
8859
+ if (index === tipIndex) {
8860
+ if (block.hash === lastBlock.hash)
8861
+ return false;
8862
+ throw new Error(`Loaded block conflict at index ${index}`);
8863
+ }
8864
+ }
8865
+ const expectedIndex = lastBlock.hash === '0x0' ? 0 : tipIndex + 1;
8866
+ if (index !== expectedIndex || block.previousHash !== lastBlock.hash) {
8867
+ throw new Error(`Loaded block does not extend canonical tip at index ${index}`);
8868
+ }
8855
8869
  blocks[block.index.toString()] = block;
8856
8870
  lastBlock = blocks[blocks.length - 1];
8857
8871
  totalBlocks += 1n;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leofcoin/chain",
3
- "version": "1.10.17",
3
+ "version": "1.10.19",
4
4
  "description": "Official javascript implementation",
5
5
  "private": false,
6
6
  "exports": {