@leofcoin/chain 1.10.17 → 1.10.18

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.
@@ -3696,6 +3696,12 @@ class Machine {
3696
3696
  pubsub.publish(data.id, { error: data.message });
3697
3697
  break;
3698
3698
  }
3699
+ case "addLoadedBlockError":
3700
+ case "collectFeeError":
3701
+ case "settleRewardsError": {
3702
+ pubsub.publish(data.id, { error: new Error(data.message) });
3703
+ break;
3704
+ }
3699
3705
  case "debug": {
3700
3706
  if (data.message.includes("loaded transactions for block:")) {
3701
3707
  pubsub.publish("block-loaded", data.message.replace("loaded transactions for block: ", "").split(" @")[0]);
@@ -3745,9 +3751,9 @@ class Machine {
3745
3751
  }
3746
3752
  }
3747
3753
  }
3748
- async updateState() {
3754
+ async updateState(strict = false) {
3749
3755
  try {
3750
- const lastBlock = await this.lastBlock;
3756
+ const lastBlock = strict ? await this.getCanonicalTip() : await this.lastBlock;
3751
3757
  const lastBlockIndex = Number(lastBlock.index);
3752
3758
  const statesLastBlockIndex = Number(this.states.lastBlock.index);
3753
3759
  if (lastBlockIndex > statesLastBlockIndex || lastBlock.hash !== this.states.lastBlock.hash && Number(lastBlockIndex) === 0) {
@@ -3806,7 +3812,7 @@ class Machine {
3806
3812
  return JSON.stringify(sorted, replacer);
3807
3813
  };
3808
3814
  const tasks = [
3809
- stateStore.put("lastBlock", sortedStringify(await this.lastBlock, jsonStringifyBigInt)),
3815
+ stateStore.put("lastBlock", sortedStringify(lastBlock, jsonStringifyBigInt)),
3810
3816
  stateStore.put("states", sortedStringify(state, jsonStringifyBigInt)),
3811
3817
  stateStore.put("accounts", sortedStringify(accounts, jsonStringifyBigInt)),
3812
3818
  stateStore.put(
@@ -3833,6 +3839,7 @@ class Machine {
3833
3839
  }
3834
3840
  } catch (error) {
3835
3841
  console.error(error);
3842
+ if (strict) throw error;
3836
3843
  }
3837
3844
  }
3838
3845
  async #init(blocks) {
@@ -4099,6 +4106,9 @@ ${error.message}`);
4099
4106
  return this.states.lastBlock;
4100
4107
  });
4101
4108
  }
4109
+ getCanonicalTip() {
4110
+ return this.#askWorker("lastBlock");
4111
+ }
4102
4112
  get lastBlockHeight() {
4103
4113
  return this.#askWorker("lastBlockHeight").catch((error) => {
4104
4114
  debug$2("lastBlockHeight fallback after worker timeout:", error?.message ?? error);
@@ -4243,6 +4253,56 @@ const decodeResponsePayload = async (result, depth = 0) => {
4243
4253
  return result;
4244
4254
  };
4245
4255
 
4256
+ class CanonicalStateQueue {
4257
+ #tail = Promise.resolve();
4258
+ #failed = false;
4259
+ #failure;
4260
+ run(operation) {
4261
+ const result = this.#tail.then(async () => {
4262
+ if (this.#failed) {
4263
+ throw new Error("Canonical state application halted after a failed mutation; restart and recover state", {
4264
+ cause: this.#failure
4265
+ });
4266
+ }
4267
+ let mutating = false;
4268
+ try {
4269
+ return await operation({ markMutating: () => {
4270
+ mutating = true;
4271
+ } });
4272
+ } catch (error) {
4273
+ if (mutating) {
4274
+ this.#failed = true;
4275
+ this.#failure = error;
4276
+ }
4277
+ throw error;
4278
+ }
4279
+ });
4280
+ this.#tail = result.then(() => void 0, () => void 0);
4281
+ return result;
4282
+ }
4283
+ }
4284
+
4285
+ const validateChainLink = (localTip, incoming) => {
4286
+ const localHash = localTip?.hash || "0x0";
4287
+ const localIndex = Number(localTip?.index ?? -1);
4288
+ const hasCanonicalTip = localHash !== "0x0";
4289
+ if (hasCanonicalTip && incoming.index < localIndex) return "stale";
4290
+ if (hasCanonicalTip && incoming.index === localIndex) {
4291
+ if (incoming.hash === localHash) return "duplicate";
4292
+ throw new Error(`Block conflict detected at index ${incoming.index}`);
4293
+ }
4294
+ const expectedIndex = hasCanonicalTip ? localIndex + 1 : 0;
4295
+ if (incoming.index !== expectedIndex) {
4296
+ throw new Error(`Unexpected block index ${incoming.index}; expected ${expectedIndex}`);
4297
+ }
4298
+ if (incoming.previousHash !== localHash) {
4299
+ throw new Error(
4300
+ `previousHash mismatch at index ${incoming.index}: expected ${localHash}, got ${incoming.previousHash}`
4301
+ );
4302
+ }
4303
+ return "append";
4304
+ };
4305
+
4246
4306
  const debug$1 = createDebugger("leofcoin/state");
4247
4307
  class State extends Contract {
4248
4308
  constructor(config) {
@@ -4257,6 +4317,7 @@ class State extends Contract {
4257
4317
  this.#blocks = [];
4258
4318
  this.knownBlocks = [];
4259
4319
  this.#totalSize = 0;
4320
+ this.#canonicalStateQueue = new CanonicalStateQueue();
4260
4321
  this.#loaded = false;
4261
4322
  this.#resolvingHashes = /* @__PURE__ */ new Set();
4262
4323
  this._wantList = [];
@@ -4299,6 +4360,13 @@ class State extends Contract {
4299
4360
  #blocks;
4300
4361
  #totalSize;
4301
4362
  #machine;
4363
+ #canonicalStateQueue;
4364
+ withCanonicalState(operation) {
4365
+ return this.#canonicalStateQueue.run(operation);
4366
+ }
4367
+ canonicalTip() {
4368
+ return this.#machine ? this.#machine.getCanonicalTip() : Promise.resolve({ index: -1, hash: "0x0", previousHash: "0x0" });
4369
+ }
4302
4370
  #loaded;
4303
4371
  #resolvingHashes;
4304
4372
  /**
@@ -4451,7 +4519,7 @@ class State extends Contract {
4451
4519
  this.#machine = machine;
4452
4520
  const lastBlock = await this.#machine.lastBlock;
4453
4521
  if (lastBlock.hash !== "0x0") {
4454
- this.updateState(new BlockMessage(lastBlock));
4522
+ await this.updateState(new BlockMessage(lastBlock));
4455
4523
  }
4456
4524
  this.#loaded = true;
4457
4525
  this.#chainState = "loaded";
@@ -4474,9 +4542,10 @@ class State extends Contract {
4474
4542
  await machine.ready;
4475
4543
  this.#machine = machine;
4476
4544
  }
4477
- await this.#machine.updateState();
4545
+ await this.#machine.updateState(true);
4478
4546
  } catch (error) {
4479
4547
  console.error(error);
4548
+ throw error;
4480
4549
  }
4481
4550
  }
4482
4551
  getLatestBlock() {
@@ -4665,11 +4734,16 @@ class State extends Contract {
4665
4734
  console.error(error);
4666
4735
  }
4667
4736
  if (!lastBlock) lastBlock = await this.#getLatestBlock();
4668
- if (globalThis.peernet.peers.length === 0) return "connectionless";
4737
+ if (globalThis.peernet.peers.length === 0) {
4738
+ this.#chainSyncing = false;
4739
+ this.#syncState = "connectionless";
4740
+ return this.#syncState;
4741
+ }
4669
4742
  try {
4670
- await this.#syncChain(lastBlock);
4743
+ await this.withCanonicalState((transition) => this.#syncChain(lastBlock, transition));
4671
4744
  } catch (error) {
4672
4745
  this.#syncErrorCount += 1;
4746
+ this.#chainSyncing = false;
4673
4747
  if (this.#syncErrorCount < 3) return this.syncChain(lastBlock);
4674
4748
  this.#syncErrorCount = 0;
4675
4749
  this.#chainSyncing = false;
@@ -4683,9 +4757,9 @@ class State extends Contract {
4683
4757
  this.#syncState = "synced";
4684
4758
  return this.#syncState;
4685
4759
  }
4686
- async #syncChain(lastBlock) {
4760
+ async #syncChain(lastBlock, transition) {
4687
4761
  try {
4688
- const localBlock = await this.lastBlock;
4762
+ const localBlock = await this.canonicalTip();
4689
4763
  const localIndex = localBlock ? Number(localBlock.index) : -1;
4690
4764
  const remoteIndex = Number(lastBlock.index);
4691
4765
  const remoteBlockHash = lastBlock.hash;
@@ -4707,6 +4781,9 @@ class State extends Contract {
4707
4781
  debug$1(`Remote block hash is 0x0, skipping sync`);
4708
4782
  return;
4709
4783
  }
4784
+ if (localBlock?.hash !== "0x0" && localIndex === remoteIndex && localBlock.hash !== remoteBlockHash) {
4785
+ throw new Error(`Block conflict detected at index ${remoteIndex}`);
4786
+ }
4710
4787
  if (localIndex > remoteIndex) {
4711
4788
  const MAX_REORG_DEPTH = 6;
4712
4789
  const reorgDepth = localIndex - remoteIndex;
@@ -4746,9 +4823,14 @@ class State extends Contract {
4746
4823
  debug$1(`Loading blocks from index ${localIndex + 1} to ${remoteIndex}`);
4747
4824
  const start = localIndex + 1;
4748
4825
  if (this.#machine && blocks.length > start) {
4749
- await this.#loadBlocks(blocks.slice(start));
4826
+ await this.#loadBlocks(blocks.slice(start), transition);
4750
4827
  }
4751
4828
  if (blocks.length > 0) {
4829
+ const appliedTip = await this.canonicalTip();
4830
+ if (appliedTip.hash !== remoteBlockHash || Number(appliedTip.index) !== remoteIndex) {
4831
+ throw new Error(`Sync did not apply canonical target ${remoteBlockHash} at index ${remoteIndex}`);
4832
+ }
4833
+ transition.markMutating();
4752
4834
  await this.updateState(new BlockMessage(blocks[blocks.length - 1]));
4753
4835
  }
4754
4836
  } else {
@@ -4868,14 +4950,14 @@ class State extends Contract {
4868
4950
  console.log(error);
4869
4951
  }
4870
4952
  console.log(error);
4871
- return false;
4953
+ throw error;
4872
4954
  }
4873
4955
  }
4874
4956
  /**
4875
4957
  *
4876
4958
  * @param {Block[]} blocks
4877
4959
  */
4878
- async #loadBlocks(blocks) {
4960
+ async #loadBlocks(blocks, transition) {
4879
4961
  this.#chainState = "loading";
4880
4962
  if (blocks.some((block) => !block)) {
4881
4963
  throw new Error("missing block data during load; chain resolution incomplete");
@@ -4886,10 +4968,21 @@ class State extends Contract {
4886
4968
  for (const block of blocks) {
4887
4969
  if (block && !block.loaded) {
4888
4970
  try {
4971
+ const link = validateChainLink(await this.canonicalTip(), {
4972
+ index: Number(block.index),
4973
+ hash: block.hash,
4974
+ previousHash: String(block.previousHash)
4975
+ });
4976
+ if (link !== "append") {
4977
+ this.#blocks[block.index].loaded = true;
4978
+ debug$1(`Ignoring ${link} loaded block ${block.hash} at height ${block.index}`);
4979
+ continue;
4980
+ }
4889
4981
  debug$1(`loading block: ${Number(block.index)} ${block.hash}`);
4890
4982
  let transactions = await this.#loadBlockTransactions(block.transactions || []);
4891
4983
  const validators = block.validators.map(({ address }) => address);
4892
4984
  debug$1(`loading transactions: ${transactions.length} for block ${block.index}`);
4985
+ transition.markMutating();
4893
4986
  for (const transaction of transactions) {
4894
4987
  const hash = await transaction.hash();
4895
4988
  if (poolTransactionKeys.has(hash)) await globalThis.transactionPoolStore.delete(hash);
@@ -4922,10 +5015,11 @@ class State extends Contract {
4922
5015
  ...distributeAmount(policy.subsidy, validators, Number(block.index) % validators.length)
4923
5016
  ]);
4924
5017
  }
4925
- this.#blocks[block.index].loaded = true;
4926
5018
  debug$1(`executed transactions for block ${block.index}`);
5019
+ const registered = await this.#machine.addLoadedBlock({ ...block, loaded: true });
5020
+ if (registered !== true) throw new Error(`Loaded block registration failed after execution: ${block.hash}`);
5021
+ this.#blocks[block.index].loaded = true;
4927
5022
  if (Number(block.index) === 0) this.#loaded = true;
4928
- await this.#machine.addLoadedBlock(block);
4929
5023
  debug$1(`loaded block: ${block.hash} @${Number(block.index)}`);
4930
5024
  globalThis.pubsub.publish("block-loaded", { ...block });
4931
5025
  } catch (error) {
@@ -4933,6 +5027,7 @@ class State extends Contract {
4933
5027
  for (const transaction of block.transactions) {
4934
5028
  this.wantList.push(transaction);
4935
5029
  }
5030
+ throw error;
4936
5031
  }
4937
5032
  }
4938
5033
  }
@@ -5046,27 +5141,6 @@ class VersionControl extends State {
5046
5141
  }
5047
5142
  }
5048
5143
 
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
5144
  const validateCanonicalValidatorSet = (blockIndex, expectedValidators, validators) => {
5071
5145
  const actualValidators = validators.map(({ address }) => address);
5072
5146
  const canonicalExpected = [...new Set(expectedValidators)].sort();
@@ -5642,7 +5716,9 @@ class Chain extends VersionControl {
5642
5716
  `[consensus] Epoch boundary at block ${blockHeight}: new epoch starts at height ${this.#currentEpochStartHeight}`
5643
5717
  );
5644
5718
  if (this.#participating && !this.#runningEpoch) {
5645
- await this.#runEpoch();
5719
+ void this.#runEpoch().catch((error) => {
5720
+ debug(`epoch scheduling failed: ${error?.message ?? error}`);
5721
+ });
5646
5722
  }
5647
5723
  }
5648
5724
  async #runEpoch() {
@@ -6111,11 +6187,14 @@ class Chain extends VersionControl {
6111
6187
  }
6112
6188
  }
6113
6189
  async #addBlock(block) {
6190
+ return this.withCanonicalState((transition) => this.#applyBlock(block, transition));
6191
+ }
6192
+ async #applyBlock(block, transition) {
6114
6193
  const receivedEncoded = block instanceof BlockMessage ? block.encoded : block;
6115
6194
  const blockMessage = await new BlockMessage(block);
6116
6195
  const hash = await blockMessage.hash();
6117
6196
  const blockIndex = Number(blockMessage.decoded.index);
6118
- const linkResult = validateChainLink(await this.lastBlock, {
6197
+ const linkResult = validateChainLink(await this.canonicalTip(), {
6119
6198
  index: blockIndex,
6120
6199
  hash,
6121
6200
  previousHash: String(blockMessage.decoded.previousHash)
@@ -6144,6 +6223,7 @@ class Chain extends VersionControl {
6144
6223
  console.log(`[chain] \u2705 Block data integrity verified: ${hash}`);
6145
6224
  await this.#validateBlockValidators(blockMessage);
6146
6225
  const transactions = await this.#resolveBlockTransactions(blockMessage);
6226
+ transition.markMutating();
6147
6227
  await Promise.all(
6148
6228
  blockMessage.decoded.transactions.map(async (transactionHash) => {
6149
6229
  if (await transactionPoolStore.has(transactionHash)) await transactionPoolStore.delete(transactionHash);
@@ -6154,9 +6234,6 @@ class Chain extends VersionControl {
6154
6234
  hash,
6155
6235
  ...blockMessage.decoded
6156
6236
  };
6157
- debug(`added block: ${hash}`);
6158
- let promises = [];
6159
- let contracts = [];
6160
6237
  const allTransactions = transactions.sort((a, b) => {
6161
6238
  if (a.decoded.priority !== b.decoded.priority) {
6162
6239
  return (b.decoded.priority ? 1 : 0) - (a.decoded.priority ? 1 : 0);
@@ -6167,7 +6244,6 @@ class Chain extends VersionControl {
6167
6244
  });
6168
6245
  const monetaryPolicy = await this.#monetaryPolicy(blockMessage.decoded.protocolVersion);
6169
6246
  for (const transaction of allTransactions) {
6170
- if (!contracts.includes(transaction.decoded.to)) contracts.push(transaction.decoded.to);
6171
6247
  this.removePendingNonce(transaction.decoded.from, transaction.decoded.nonce);
6172
6248
  const transactionHash = await transaction.hash();
6173
6249
  const validatorAddresses = blockMessage.decoded.validators.map(({ address }) => address);
@@ -6185,36 +6261,36 @@ class Chain extends VersionControl {
6185
6261
  ...distributeAmount(monetaryPolicy.subsidy, validators, blockIndex % validators.length)
6186
6262
  ]);
6187
6263
  }
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);
6264
+ const noncesByAddress = {};
6265
+ for (let transaction of transactions) {
6266
+ globalThis.pubsub.publish("transaction-processed", transaction.encoded);
6267
+ if (transaction.decoded.to === globalThis.peernet.selectedAccount)
6268
+ globalThis.pubsub.publish("account-transaction-processed", transaction.encoded);
6269
+ if (!noncesByAddress[transaction.decoded.from] || noncesByAddress?.[transaction.decoded.from] < transaction.decoded.nonce) {
6270
+ noncesByAddress[transaction.decoded.from] = transaction.decoded.nonce;
6211
6271
  }
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
6272
  }
6273
+ await Promise.all(
6274
+ Object.entries(noncesByAddress).map(([from, nonce]) => globalThis.accountsStore.put(from, String(nonce)))
6275
+ );
6276
+ if (validateChainLink(await this.canonicalTip(), {
6277
+ index: blockIndex,
6278
+ hash,
6279
+ previousHash: String(blockMessage.decoded.previousHash)
6280
+ }) !== "append") {
6281
+ throw new Error(`Canonical tip changed during block application: ${hash}`);
6282
+ }
6283
+ const registered = await this.machine.addLoadedBlock({ ...blockMessage.decoded, loaded: true, hash });
6284
+ if (registered !== true) throw new Error(`Block registration failed after execution: ${hash}`);
6285
+ try {
6286
+ await this.call(addresses.validators, "recordValidatorSnapshot", [blockMessage.decoded.index]);
6287
+ } catch (snapshotError) {
6288
+ debug(`failed to record validator snapshot: ${snapshotError?.message ?? snapshotError}`);
6289
+ }
6290
+ await this.updateState(blockMessage);
6291
+ debug(`added block: ${hash}`);
6292
+ globalThis.pubsub.publish("block-processed", blockMessage.decoded);
6293
+ await this.#handleEpochBoundary(blockIndex);
6218
6294
  }
6219
6295
  async participate(address) {
6220
6296
  try {
@@ -6278,10 +6354,6 @@ class Chain extends VersionControl {
6278
6354
  executionTimestamp: block?.timestamp ?? transaction.decoded.timestamp,
6279
6355
  feePayments
6280
6356
  });
6281
- if (block) {
6282
- block.transactions.push(hash);
6283
- block.fees = block.fees += await calculateFee(transaction.decoded);
6284
- }
6285
6357
  await globalThis.accountsStore.put(
6286
6358
  transaction.decoded.from,
6287
6359
  new TextEncoder().encode(String(transaction.decoded.nonce))
@@ -6293,6 +6365,7 @@ class Chain extends VersionControl {
6293
6365
  console.log(hash);
6294
6366
  peernet.publish("invalid-transaction", hash);
6295
6367
  await globalThis.transactionPoolStore.delete(e.hash);
6368
+ if (block) throw e;
6296
6369
  }
6297
6370
  }
6298
6371
  // 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/exports/chain.js CHANGED
@@ -659,6 +659,12 @@ class Machine {
659
659
  pubsub.publish(data.id, { error: data.message });
660
660
  break;
661
661
  }
662
+ case "addLoadedBlockError":
663
+ case "collectFeeError":
664
+ case "settleRewardsError": {
665
+ pubsub.publish(data.id, { error: new Error(data.message) });
666
+ break;
667
+ }
662
668
  case "debug": {
663
669
  if (data.message.includes("loaded transactions for block:")) {
664
670
  pubsub.publish("block-loaded", data.message.replace("loaded transactions for block: ", "").split(" @")[0]);
@@ -708,9 +714,9 @@ class Machine {
708
714
  }
709
715
  }
710
716
  }
711
- async updateState() {
717
+ async updateState(strict = false) {
712
718
  try {
713
- const lastBlock = await this.lastBlock;
719
+ const lastBlock = strict ? await this.getCanonicalTip() : await this.lastBlock;
714
720
  const lastBlockIndex = Number(lastBlock.index);
715
721
  const statesLastBlockIndex = Number(this.states.lastBlock.index);
716
722
  if (lastBlockIndex > statesLastBlockIndex || lastBlock.hash !== this.states.lastBlock.hash && Number(lastBlockIndex) === 0) {
@@ -769,7 +775,7 @@ class Machine {
769
775
  return JSON.stringify(sorted, replacer);
770
776
  };
771
777
  const tasks = [
772
- stateStore.put("lastBlock", sortedStringify(await this.lastBlock, jsonStringifyBigInt)),
778
+ stateStore.put("lastBlock", sortedStringify(lastBlock, jsonStringifyBigInt)),
773
779
  stateStore.put("states", sortedStringify(state, jsonStringifyBigInt)),
774
780
  stateStore.put("accounts", sortedStringify(accounts, jsonStringifyBigInt)),
775
781
  stateStore.put(
@@ -796,6 +802,7 @@ class Machine {
796
802
  }
797
803
  } catch (error) {
798
804
  console.error(error);
805
+ if (strict) throw error;
799
806
  }
800
807
  }
801
808
  async #init(blocks) {
@@ -1062,6 +1069,9 @@ ${error.message}`);
1062
1069
  return this.states.lastBlock;
1063
1070
  });
1064
1071
  }
1072
+ getCanonicalTip() {
1073
+ return this.#askWorker("lastBlock");
1074
+ }
1065
1075
  get lastBlockHeight() {
1066
1076
  return this.#askWorker("lastBlockHeight").catch((error) => {
1067
1077
  debug$2("lastBlockHeight fallback after worker timeout:", error?.message ?? error);
@@ -1206,6 +1216,56 @@ const decodeResponsePayload = async (result, depth = 0) => {
1206
1216
  return result;
1207
1217
  };
1208
1218
 
1219
+ class CanonicalStateQueue {
1220
+ #tail = Promise.resolve();
1221
+ #failed = false;
1222
+ #failure;
1223
+ run(operation) {
1224
+ const result = this.#tail.then(async () => {
1225
+ if (this.#failed) {
1226
+ throw new Error("Canonical state application halted after a failed mutation; restart and recover state", {
1227
+ cause: this.#failure
1228
+ });
1229
+ }
1230
+ let mutating = false;
1231
+ try {
1232
+ return await operation({ markMutating: () => {
1233
+ mutating = true;
1234
+ } });
1235
+ } catch (error) {
1236
+ if (mutating) {
1237
+ this.#failed = true;
1238
+ this.#failure = error;
1239
+ }
1240
+ throw error;
1241
+ }
1242
+ });
1243
+ this.#tail = result.then(() => void 0, () => void 0);
1244
+ return result;
1245
+ }
1246
+ }
1247
+
1248
+ const validateChainLink = (localTip, incoming) => {
1249
+ const localHash = localTip?.hash || "0x0";
1250
+ const localIndex = Number(localTip?.index ?? -1);
1251
+ const hasCanonicalTip = localHash !== "0x0";
1252
+ if (hasCanonicalTip && incoming.index < localIndex) return "stale";
1253
+ if (hasCanonicalTip && incoming.index === localIndex) {
1254
+ if (incoming.hash === localHash) return "duplicate";
1255
+ throw new Error(`Block conflict detected at index ${incoming.index}`);
1256
+ }
1257
+ const expectedIndex = hasCanonicalTip ? localIndex + 1 : 0;
1258
+ if (incoming.index !== expectedIndex) {
1259
+ throw new Error(`Unexpected block index ${incoming.index}; expected ${expectedIndex}`);
1260
+ }
1261
+ if (incoming.previousHash !== localHash) {
1262
+ throw new Error(
1263
+ `previousHash mismatch at index ${incoming.index}: expected ${localHash}, got ${incoming.previousHash}`
1264
+ );
1265
+ }
1266
+ return "append";
1267
+ };
1268
+
1209
1269
  const debug$1 = createDebugger("leofcoin/state");
1210
1270
  class State extends Contract {
1211
1271
  constructor(config) {
@@ -1220,6 +1280,7 @@ class State extends Contract {
1220
1280
  this.#blocks = [];
1221
1281
  this.knownBlocks = [];
1222
1282
  this.#totalSize = 0;
1283
+ this.#canonicalStateQueue = new CanonicalStateQueue();
1223
1284
  this.#loaded = false;
1224
1285
  this.#resolvingHashes = /* @__PURE__ */ new Set();
1225
1286
  this._wantList = [];
@@ -1262,6 +1323,13 @@ class State extends Contract {
1262
1323
  #blocks;
1263
1324
  #totalSize;
1264
1325
  #machine;
1326
+ #canonicalStateQueue;
1327
+ withCanonicalState(operation) {
1328
+ return this.#canonicalStateQueue.run(operation);
1329
+ }
1330
+ canonicalTip() {
1331
+ return this.#machine ? this.#machine.getCanonicalTip() : Promise.resolve({ index: -1, hash: "0x0", previousHash: "0x0" });
1332
+ }
1265
1333
  #loaded;
1266
1334
  #resolvingHashes;
1267
1335
  /**
@@ -1414,7 +1482,7 @@ class State extends Contract {
1414
1482
  this.#machine = machine;
1415
1483
  const lastBlock = await this.#machine.lastBlock;
1416
1484
  if (lastBlock.hash !== "0x0") {
1417
- this.updateState(new BlockMessage(lastBlock));
1485
+ await this.updateState(new BlockMessage(lastBlock));
1418
1486
  }
1419
1487
  this.#loaded = true;
1420
1488
  this.#chainState = "loaded";
@@ -1437,9 +1505,10 @@ class State extends Contract {
1437
1505
  await machine.ready;
1438
1506
  this.#machine = machine;
1439
1507
  }
1440
- await this.#machine.updateState();
1508
+ await this.#machine.updateState(true);
1441
1509
  } catch (error) {
1442
1510
  console.error(error);
1511
+ throw error;
1443
1512
  }
1444
1513
  }
1445
1514
  getLatestBlock() {
@@ -1628,11 +1697,16 @@ class State extends Contract {
1628
1697
  console.error(error);
1629
1698
  }
1630
1699
  if (!lastBlock) lastBlock = await this.#getLatestBlock();
1631
- if (globalThis.peernet.peers.length === 0) return "connectionless";
1700
+ if (globalThis.peernet.peers.length === 0) {
1701
+ this.#chainSyncing = false;
1702
+ this.#syncState = "connectionless";
1703
+ return this.#syncState;
1704
+ }
1632
1705
  try {
1633
- await this.#syncChain(lastBlock);
1706
+ await this.withCanonicalState((transition) => this.#syncChain(lastBlock, transition));
1634
1707
  } catch (error) {
1635
1708
  this.#syncErrorCount += 1;
1709
+ this.#chainSyncing = false;
1636
1710
  if (this.#syncErrorCount < 3) return this.syncChain(lastBlock);
1637
1711
  this.#syncErrorCount = 0;
1638
1712
  this.#chainSyncing = false;
@@ -1646,9 +1720,9 @@ class State extends Contract {
1646
1720
  this.#syncState = "synced";
1647
1721
  return this.#syncState;
1648
1722
  }
1649
- async #syncChain(lastBlock) {
1723
+ async #syncChain(lastBlock, transition) {
1650
1724
  try {
1651
- const localBlock = await this.lastBlock;
1725
+ const localBlock = await this.canonicalTip();
1652
1726
  const localIndex = localBlock ? Number(localBlock.index) : -1;
1653
1727
  const remoteIndex = Number(lastBlock.index);
1654
1728
  const remoteBlockHash = lastBlock.hash;
@@ -1670,6 +1744,9 @@ class State extends Contract {
1670
1744
  debug$1(`Remote block hash is 0x0, skipping sync`);
1671
1745
  return;
1672
1746
  }
1747
+ if (localBlock?.hash !== "0x0" && localIndex === remoteIndex && localBlock.hash !== remoteBlockHash) {
1748
+ throw new Error(`Block conflict detected at index ${remoteIndex}`);
1749
+ }
1673
1750
  if (localIndex > remoteIndex) {
1674
1751
  const MAX_REORG_DEPTH = 6;
1675
1752
  const reorgDepth = localIndex - remoteIndex;
@@ -1709,9 +1786,14 @@ class State extends Contract {
1709
1786
  debug$1(`Loading blocks from index ${localIndex + 1} to ${remoteIndex}`);
1710
1787
  const start = localIndex + 1;
1711
1788
  if (this.#machine && blocks.length > start) {
1712
- await this.#loadBlocks(blocks.slice(start));
1789
+ await this.#loadBlocks(blocks.slice(start), transition);
1713
1790
  }
1714
1791
  if (blocks.length > 0) {
1792
+ const appliedTip = await this.canonicalTip();
1793
+ if (appliedTip.hash !== remoteBlockHash || Number(appliedTip.index) !== remoteIndex) {
1794
+ throw new Error(`Sync did not apply canonical target ${remoteBlockHash} at index ${remoteIndex}`);
1795
+ }
1796
+ transition.markMutating();
1715
1797
  await this.updateState(new BlockMessage(blocks[blocks.length - 1]));
1716
1798
  }
1717
1799
  } else {
@@ -1831,14 +1913,14 @@ class State extends Contract {
1831
1913
  console.log(error);
1832
1914
  }
1833
1915
  console.log(error);
1834
- return false;
1916
+ throw error;
1835
1917
  }
1836
1918
  }
1837
1919
  /**
1838
1920
  *
1839
1921
  * @param {Block[]} blocks
1840
1922
  */
1841
- async #loadBlocks(blocks) {
1923
+ async #loadBlocks(blocks, transition) {
1842
1924
  this.#chainState = "loading";
1843
1925
  if (blocks.some((block) => !block)) {
1844
1926
  throw new Error("missing block data during load; chain resolution incomplete");
@@ -1849,10 +1931,21 @@ class State extends Contract {
1849
1931
  for (const block of blocks) {
1850
1932
  if (block && !block.loaded) {
1851
1933
  try {
1934
+ const link = validateChainLink(await this.canonicalTip(), {
1935
+ index: Number(block.index),
1936
+ hash: block.hash,
1937
+ previousHash: String(block.previousHash)
1938
+ });
1939
+ if (link !== "append") {
1940
+ this.#blocks[block.index].loaded = true;
1941
+ debug$1(`Ignoring ${link} loaded block ${block.hash} at height ${block.index}`);
1942
+ continue;
1943
+ }
1852
1944
  debug$1(`loading block: ${Number(block.index)} ${block.hash}`);
1853
1945
  let transactions = await this.#loadBlockTransactions(block.transactions || []);
1854
1946
  const validators = block.validators.map(({ address }) => address);
1855
1947
  debug$1(`loading transactions: ${transactions.length} for block ${block.index}`);
1948
+ transition.markMutating();
1856
1949
  for (const transaction of transactions) {
1857
1950
  const hash = await transaction.hash();
1858
1951
  if (poolTransactionKeys.has(hash)) await globalThis.transactionPoolStore.delete(hash);
@@ -1885,10 +1978,11 @@ class State extends Contract {
1885
1978
  ...distributeAmount(policy.subsidy, validators, Number(block.index) % validators.length)
1886
1979
  ]);
1887
1980
  }
1888
- this.#blocks[block.index].loaded = true;
1889
1981
  debug$1(`executed transactions for block ${block.index}`);
1982
+ const registered = await this.#machine.addLoadedBlock({ ...block, loaded: true });
1983
+ if (registered !== true) throw new Error(`Loaded block registration failed after execution: ${block.hash}`);
1984
+ this.#blocks[block.index].loaded = true;
1890
1985
  if (Number(block.index) === 0) this.#loaded = true;
1891
- await this.#machine.addLoadedBlock(block);
1892
1986
  debug$1(`loaded block: ${block.hash} @${Number(block.index)}`);
1893
1987
  globalThis.pubsub.publish("block-loaded", { ...block });
1894
1988
  } catch (error) {
@@ -1896,6 +1990,7 @@ class State extends Contract {
1896
1990
  for (const transaction of block.transactions) {
1897
1991
  this.wantList.push(transaction);
1898
1992
  }
1993
+ throw error;
1899
1994
  }
1900
1995
  }
1901
1996
  }
@@ -2009,27 +2104,6 @@ class VersionControl extends State {
2009
2104
  }
2010
2105
  }
2011
2106
 
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
2107
  const validateCanonicalValidatorSet = (blockIndex, expectedValidators, validators) => {
2034
2108
  const actualValidators = validators.map(({ address }) => address);
2035
2109
  const canonicalExpected = [...new Set(expectedValidators)].sort();
@@ -2592,7 +2666,9 @@ class Chain extends VersionControl {
2592
2666
  `[consensus] Epoch boundary at block ${blockHeight}: new epoch starts at height ${this.#currentEpochStartHeight}`
2593
2667
  );
2594
2668
  if (this.#participating && !this.#runningEpoch) {
2595
- await this.#runEpoch();
2669
+ void this.#runEpoch().catch((error) => {
2670
+ debug(`epoch scheduling failed: ${error?.message ?? error}`);
2671
+ });
2596
2672
  }
2597
2673
  }
2598
2674
  async #runEpoch() {
@@ -3061,11 +3137,14 @@ class Chain extends VersionControl {
3061
3137
  }
3062
3138
  }
3063
3139
  async #addBlock(block) {
3140
+ return this.withCanonicalState((transition) => this.#applyBlock(block, transition));
3141
+ }
3142
+ async #applyBlock(block, transition) {
3064
3143
  const receivedEncoded = block instanceof BlockMessage ? block.encoded : block;
3065
3144
  const blockMessage = await new BlockMessage(block);
3066
3145
  const hash = await blockMessage.hash();
3067
3146
  const blockIndex = Number(blockMessage.decoded.index);
3068
- const linkResult = validateChainLink(await this.lastBlock, {
3147
+ const linkResult = validateChainLink(await this.canonicalTip(), {
3069
3148
  index: blockIndex,
3070
3149
  hash,
3071
3150
  previousHash: String(blockMessage.decoded.previousHash)
@@ -3094,6 +3173,7 @@ class Chain extends VersionControl {
3094
3173
  console.log(`[chain] \u2705 Block data integrity verified: ${hash}`);
3095
3174
  await this.#validateBlockValidators(blockMessage);
3096
3175
  const transactions = await this.#resolveBlockTransactions(blockMessage);
3176
+ transition.markMutating();
3097
3177
  await Promise.all(
3098
3178
  blockMessage.decoded.transactions.map(async (transactionHash) => {
3099
3179
  if (await transactionPoolStore.has(transactionHash)) await transactionPoolStore.delete(transactionHash);
@@ -3104,9 +3184,6 @@ class Chain extends VersionControl {
3104
3184
  hash,
3105
3185
  ...blockMessage.decoded
3106
3186
  };
3107
- debug(`added block: ${hash}`);
3108
- let promises = [];
3109
- let contracts = [];
3110
3187
  const allTransactions = transactions.sort((a, b) => {
3111
3188
  if (a.decoded.priority !== b.decoded.priority) {
3112
3189
  return (b.decoded.priority ? 1 : 0) - (a.decoded.priority ? 1 : 0);
@@ -3117,7 +3194,6 @@ class Chain extends VersionControl {
3117
3194
  });
3118
3195
  const monetaryPolicy = await this.#monetaryPolicy(blockMessage.decoded.protocolVersion);
3119
3196
  for (const transaction of allTransactions) {
3120
- if (!contracts.includes(transaction.decoded.to)) contracts.push(transaction.decoded.to);
3121
3197
  this.removePendingNonce(transaction.decoded.from, transaction.decoded.nonce);
3122
3198
  const transactionHash = await transaction.hash();
3123
3199
  const validatorAddresses = blockMessage.decoded.validators.map(({ address }) => address);
@@ -3135,36 +3211,36 @@ class Chain extends VersionControl {
3135
3211
  ...distributeAmount(monetaryPolicy.subsidy, validators, blockIndex % validators.length)
3136
3212
  ]);
3137
3213
  }
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);
3214
+ const noncesByAddress = {};
3215
+ for (let transaction of transactions) {
3216
+ globalThis.pubsub.publish("transaction-processed", transaction.encoded);
3217
+ if (transaction.decoded.to === globalThis.peernet.selectedAccount)
3218
+ globalThis.pubsub.publish("account-transaction-processed", transaction.encoded);
3219
+ if (!noncesByAddress[transaction.decoded.from] || noncesByAddress?.[transaction.decoded.from] < transaction.decoded.nonce) {
3220
+ noncesByAddress[transaction.decoded.from] = transaction.decoded.nonce;
3161
3221
  }
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
3222
  }
3223
+ await Promise.all(
3224
+ Object.entries(noncesByAddress).map(([from, nonce]) => globalThis.accountsStore.put(from, String(nonce)))
3225
+ );
3226
+ if (validateChainLink(await this.canonicalTip(), {
3227
+ index: blockIndex,
3228
+ hash,
3229
+ previousHash: String(blockMessage.decoded.previousHash)
3230
+ }) !== "append") {
3231
+ throw new Error(`Canonical tip changed during block application: ${hash}`);
3232
+ }
3233
+ const registered = await this.machine.addLoadedBlock({ ...blockMessage.decoded, loaded: true, hash });
3234
+ if (registered !== true) throw new Error(`Block registration failed after execution: ${hash}`);
3235
+ try {
3236
+ await this.call(addresses.validators, "recordValidatorSnapshot", [blockMessage.decoded.index]);
3237
+ } catch (snapshotError) {
3238
+ debug(`failed to record validator snapshot: ${snapshotError?.message ?? snapshotError}`);
3239
+ }
3240
+ await this.updateState(blockMessage);
3241
+ debug(`added block: ${hash}`);
3242
+ globalThis.pubsub.publish("block-processed", blockMessage.decoded);
3243
+ await this.#handleEpochBoundary(blockIndex);
3168
3244
  }
3169
3245
  async participate(address) {
3170
3246
  try {
@@ -3228,10 +3304,6 @@ class Chain extends VersionControl {
3228
3304
  executionTimestamp: block?.timestamp ?? transaction.decoded.timestamp,
3229
3305
  feePayments
3230
3306
  });
3231
- if (block) {
3232
- block.transactions.push(hash);
3233
- block.fees = block.fees += await calculateFee(transaction.decoded);
3234
- }
3235
3307
  await globalThis.accountsStore.put(
3236
3308
  transaction.decoded.from,
3237
3309
  new TextEncoder().encode(String(transaction.decoded.nonce))
@@ -3243,6 +3315,7 @@ class Chain extends VersionControl {
3243
3315
  console.log(hash);
3244
3316
  peernet.publish("invalid-transaction", hash);
3245
3317
  await globalThis.transactionPoolStore.delete(e.hash);
3318
+ if (block) throw e;
3246
3319
  }
3247
3320
  }
3248
3321
  // 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.18",
4
4
  "description": "Official javascript implementation",
5
5
  "private": false,
6
6
  "exports": {