@leofcoin/chain 1.8.1 → 1.8.2

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.
@@ -4128,6 +4128,7 @@ class ExecutionError extends LeofcoinError {
4128
4128
  class ContractDeploymentError extends LeofcoinError {
4129
4129
  name = 'ContractDeploymentError';
4130
4130
  }
4131
+ const isResolveError = (error) => error.name === 'ResolveError';
4131
4132
  const isExecutionError = (error) => error.name === 'ExecutionError';
4132
4133
 
4133
4134
  // import State from './state'
@@ -4630,34 +4631,42 @@ class Jobber {
4630
4631
 
4631
4632
  const debug$1 = createDebugger('leofcoin/state');
4632
4633
  class State extends Contract {
4634
+ #resolveErrored;
4635
+ #lastResolvedTime;
4636
+ #lastResolved;
4637
+ #resolving;
4638
+ #resolveErrorCount;
4639
+ #syncState;
4640
+ #chainState;
4641
+ #lastBlockInQue;
4642
+ #syncErrorCount;
4633
4643
  #blockHashMap;
4644
+ #chainSyncing;
4634
4645
  #blocks;
4646
+ #totalSize;
4635
4647
  #machine;
4636
4648
  #loaded;
4637
- // Sync state
4638
- #syncing;
4639
- #syncErrorCount;
4640
- // Block resolution state
4641
- #resolvingBlocks;
4642
- #maxConcurrentResolves;
4643
- #totalSize;
4644
- #lastResolved;
4645
- #lastResolvedTime;
4646
- #blockResolveQueue;
4647
- #chainState;
4648
4649
  /**
4649
4650
  * contains transactions we need before we can successfully load
4650
4651
  */
4651
4652
  get wantList() {
4652
4653
  return this.#machine?.wantList ?? this._wantList;
4653
4654
  }
4655
+ get state() {
4656
+ return {
4657
+ sync: this.#syncState,
4658
+ chain: this.#chainState
4659
+ };
4660
+ }
4661
+ get blockHashMap() {
4662
+ return this.#blockHashMap.entries();
4663
+ }
4654
4664
  get loaded() {
4655
4665
  return this.#loaded;
4656
4666
  }
4657
- get isSyncing() {
4658
- return this.#syncing;
4667
+ get resolving() {
4668
+ return this.#resolving;
4659
4669
  }
4660
- // Delegate to machine
4661
4670
  get contracts() {
4662
4671
  return this.#machine.contracts;
4663
4672
  }
@@ -4700,38 +4709,35 @@ class State extends Contract {
4700
4709
  get lastBlockHeight() {
4701
4710
  return this.#machine ? this.#machine.lastBlockHeight : 0;
4702
4711
  }
4703
- get totalSize() {
4704
- return this.#totalSize;
4705
- }
4706
- get machine() {
4707
- return this.#machine;
4708
- }
4709
- get blockHashMap() {
4710
- return this.#blockHashMap.entries();
4711
- }
4712
4712
  getBlock(index) {
4713
4713
  return this.#machine.getBlock(index);
4714
4714
  }
4715
4715
  getBlocks(from, to) {
4716
4716
  return this.#machine.getBlocks(from, to);
4717
4717
  }
4718
+ get totalSize() {
4719
+ return this.#totalSize;
4720
+ }
4721
+ get machine() {
4722
+ return this.#machine;
4723
+ }
4718
4724
  constructor(config) {
4719
4725
  super(config);
4726
+ this.#lastResolvedTime = 0;
4727
+ this.#resolving = false;
4728
+ this.#resolveErrorCount = 0;
4729
+ this.#chainState = 'loading';
4730
+ this.#syncErrorCount = 0;
4720
4731
  this.#blockHashMap = new Map();
4732
+ this.#chainSyncing = false;
4721
4733
  this.#blocks = [];
4722
- this.#loaded = false;
4723
- // Sync state
4724
- this.#syncing = false;
4725
- this.#syncErrorCount = 0;
4726
- // Block resolution state
4727
- this.#resolvingBlocks = new Set();
4728
- this.#maxConcurrentResolves = 10;
4729
4734
  this.knownBlocks = [];
4730
4735
  this.#totalSize = 0;
4736
+ this.#loaded = false;
4731
4737
  this._wantList = [];
4732
4738
  this.#chainStateHandler = () => {
4733
4739
  return new globalThis.peernet.protos['peernet-response']({
4734
- response: { syncing: this.#syncing, loaded: this.#loaded }
4740
+ response: this.#chainState
4735
4741
  });
4736
4742
  };
4737
4743
  this.#lastBlockHandler = async () => {
@@ -4773,45 +4779,67 @@ class State extends Contract {
4773
4779
  #lastBlockHandler;
4774
4780
  #knownBlocksHandler;
4775
4781
  async init() {
4776
- // Initialize jobber for timed, cancelable tasks
4777
4782
  this.jobber = new Jobber(this.resolveTimeout);
4778
- // Register request handlers
4779
- await globalThis.peernet.addRequestHandler('lastBlock', this.#lastBlockHandler.bind(this));
4780
- await globalThis.peernet.addRequestHandler('knownBlocks', this.#knownBlocksHandler.bind(this));
4781
- await globalThis.peernet.addRequestHandler('chainState', this.#chainStateHandler.bind(this));
4783
+ await globalThis.peernet.addRequestHandler('lastBlock', this.#lastBlockHandler);
4784
+ await globalThis.peernet.addRequestHandler('knownBlocks', this.#knownBlocksHandler);
4785
+ await globalThis.peernet.addRequestHandler('chainState', this.#chainStateHandler);
4786
+ let localBlockHash;
4787
+ let blockMessage;
4788
+ let localBlock;
4782
4789
  try {
4783
- // Load local block state
4784
- let localBlock = { index: 0, hash: '0x0', previousHash: '0x0' };
4785
- try {
4786
- const localBlockHash = new TextDecoder().decode(await globalThis.chainStore.get('lastBlock'));
4787
- if (localBlockHash && localBlockHash !== '0x0') {
4788
- const blockMessage = await new BlockMessage(await globalThis.peernet.get(localBlockHash, 'block'));
4790
+ const rawBlock = await globalThis.chainStore.has('lastBlock');
4791
+ if (rawBlock) {
4792
+ localBlockHash = new TextDecoder().decode(await globalThis.chainStore.get('lastBlock'));
4793
+ if (localBlockHash !== '0x0') {
4794
+ blockMessage = await globalThis.peernet.get(localBlockHash, 'block');
4795
+ blockMessage = await new BlockMessage(blockMessage);
4789
4796
  localBlock = { ...blockMessage.decoded, hash: localBlockHash };
4790
4797
  }
4791
4798
  }
4792
- catch (error) {
4793
- debug$1('No local block found');
4799
+ else {
4800
+ localBlock = { index: 0, hash: '0x0', previousHash: '0x0' };
4794
4801
  }
4795
- // Load known blocks
4796
- try {
4797
- this.knownBlocks = await globalThis.blockStore.keys();
4802
+ }
4803
+ catch {
4804
+ localBlock = { index: 0, hash: '0x0', previousHash: '0x0' };
4805
+ }
4806
+ try {
4807
+ this.knownBlocks = await blockStore.keys();
4808
+ }
4809
+ catch (error) {
4810
+ debug$1('no local known blocks found');
4811
+ }
4812
+ try {
4813
+ if (localBlock.hash && localBlock.hash !== '0x0') {
4814
+ try {
4815
+ const states = {
4816
+ lastBlock: JSON.parse(new TextDecoder().decode(await globalThis.stateStore.get('lastBlock')))
4817
+ };
4818
+ if (blockMessage.decoded.index > states.lastBlock.index)
4819
+ await this.resolveBlocks();
4820
+ }
4821
+ catch (error) {
4822
+ // no states found, try resolving blocks
4823
+ await this.resolveBlocks();
4824
+ }
4798
4825
  }
4799
- catch (error) {
4800
- debug$1('No known blocks found');
4826
+ else {
4827
+ await this.resolveBlocks();
4801
4828
  }
4802
- // Initialize machine and resolve blocks if needed
4803
4829
  this.#machine = await new Machine(this.#blocks);
4804
- if (localBlock.hash !== '0x0') {
4805
- await this.resolveBlock(localBlock.hash);
4806
- }
4807
4830
  const lastBlock = await this.#machine.lastBlock;
4808
4831
  if (lastBlock.hash !== '0x0') {
4809
- await this.updateState(new BlockMessage(lastBlock));
4832
+ this.updateState(new BlockMessage(lastBlock));
4810
4833
  }
4811
4834
  this.#loaded = true;
4835
+ // await this.#loadBlocks(this.#blocks)
4812
4836
  }
4813
4837
  catch (error) {
4814
- console.error('Failed to initialize state:', error);
4838
+ console.log('e');
4839
+ if (isResolveError(error)) {
4840
+ console.error(error);
4841
+ }
4842
+ console.log(error);
4815
4843
  }
4816
4844
  }
4817
4845
  async updateState(message) {
@@ -4845,47 +4873,41 @@ class State extends Contract {
4845
4873
  return block;
4846
4874
  }
4847
4875
  async #resolveBlock(hash) {
4848
- if (this.#resolvingBlocks.has(hash)) {
4849
- return; // Already resolving this block
4850
- }
4851
- this.#resolvingBlocks.add(hash);
4876
+ let index = this.#blockHashMap.get(hash);
4877
+ let localHash = '0x0';
4852
4878
  try {
4853
- let index = this.#blockHashMap.get(hash);
4854
- debug$1(`resolving block: ${hash} @${index !== undefined ? index : 'unknown'}`);
4855
- if (this.#blocks[index]) {
4856
- // Block already exists
4879
+ localHash = await globalThis.stateStore.get('lastBlock');
4880
+ }
4881
+ catch (error) {
4882
+ debug$1('no local state found');
4883
+ }
4884
+ if (this.#blocks[index]) {
4885
+ // Block already exists, check if we need to resolve previous blocks
4886
+ const previousHash = this.#blocks[index].previousHash;
4887
+ if (previousHash === localHash)
4857
4888
  return;
4889
+ if (previousHash !== '0x0') {
4890
+ // Previous block not in memory, recursively resolve it
4891
+ return this.resolveBlock(previousHash);
4858
4892
  }
4859
- const block = await this.getAndPutBlock(hash);
4860
- index = block.decoded.index;
4861
- const size = block.encoded.length > 0 ? block.encoded.length : block.encoded.byteLength;
4862
- // Batch transaction operations
4863
- const transactionsToFetch = [];
4864
- const transactionHashes = block.decoded.transactions || [];
4865
- for (const txHash of transactionHashes) {
4866
- if (!(await globalThis.transactionStore.has(txHash))) {
4867
- transactionsToFetch.push(txHash);
4868
- }
4869
- }
4870
- // Fetch all missing transactions in parallel
4871
- if (transactionsToFetch.length > 0) {
4872
- const fetchedResults = await Promise.allSettled(transactionsToFetch.map((txHash) => globalThis.peernet.get(txHash, 'transaction')));
4873
- // Batch store all transactions that were successfully fetched
4874
- for (let i = 0; i < fetchedResults.length; i++) {
4875
- if (fetchedResults[i].status === 'fulfilled') {
4876
- await globalThis.transactionStore.put(transactionsToFetch[i], fetchedResults[i].value);
4877
- }
4878
- else {
4879
- debug$1(`failed to fetch transaction ${transactionsToFetch[i]}: ${fetchedResults[i].reason?.message || fetchedResults[i].reason}`);
4880
- }
4881
- }
4893
+ else {
4894
+ // Previous block already exists or is genesis, stop resolving
4895
+ return;
4882
4896
  }
4883
- // Remove from pool
4884
- await Promise.all(transactionHashes.map(async (txHash) => {
4885
- if (await globalThis.transactionPoolStore.has(txHash)) {
4886
- await globalThis.transactionPoolStore.delete(txHash);
4897
+ }
4898
+ try {
4899
+ const block = await this.getAndPutBlock(hash);
4900
+ await Promise.all(block.decoded.transactions.map(async (hash) => {
4901
+ // should be in a transaction store already
4902
+ if (!(await transactionStore.has(hash))) {
4903
+ const data = await peernet.get(hash, 'transaction');
4904
+ await transactionStore.put(hash, data);
4887
4905
  }
4906
+ ;
4907
+ (await transactionPoolStore.has(hash)) && (await transactionPoolStore.delete(hash));
4888
4908
  }));
4909
+ index = block.decoded.index;
4910
+ const size = block.encoded.length > 0 ? block.encoded.length : block.encoded.byteLength;
4889
4911
  this.#totalSize += size;
4890
4912
  this.#blocks[index] = { hash, ...block.decoded };
4891
4913
  this.#blockHashMap.set(hash, index);
@@ -4895,147 +4917,187 @@ class State extends Contract {
4895
4917
  this.#lastResolvedTime = Date.now();
4896
4918
  }
4897
4919
  catch (error) {
4898
- throw new ResolveError(`block: ${hash}`);
4920
+ throw new ResolveError(`block: ${hash}@${index}`);
4899
4921
  }
4900
- finally {
4901
- this.#resolvingBlocks.delete(hash);
4902
- }
4903
- }
4904
- async #buildBlockChain(latestHash, maxBlocks = 1000) {
4905
- const chain = [];
4906
- let currentHash = latestHash;
4907
- let attempts = 0;
4908
- while (currentHash !== '0x0' && chain.length < maxBlocks && attempts < maxBlocks + 5) {
4909
- attempts++;
4910
- // Check if we already have this block
4911
- if (this.#blockHashMap.has(currentHash)) {
4912
- const block = this.#blocks[this.#blockHashMap.get(currentHash)];
4913
- if (block) {
4914
- chain.push(currentHash);
4915
- currentHash = block.previousHash;
4916
- continue;
4917
- }
4918
- }
4919
- chain.push(currentHash);
4920
- // Try to get the block to find previous hash
4921
- try {
4922
- const block = await this.getAndPutBlock(currentHash);
4923
- currentHash = block.decoded.previousHash;
4924
- }
4925
- catch (error) {
4926
- debug$1(`Could not fetch block ${currentHash} to determine chain: ${error}`);
4927
- break;
4928
- }
4929
- }
4930
- return chain;
4931
- }
4932
- async #resolveBlocksInParallel(hashes) {
4933
- // Resolve blocks in parallel with concurrency limit
4934
- const resolving = [];
4935
- let index = 0;
4936
- const resolveNext = async () => {
4937
- while (index < hashes.length) {
4938
- const hash = hashes[index++];
4939
- try {
4940
- await this.#resolveBlock(hash);
4941
- }
4942
- catch (error) {
4943
- debug$1(`Failed to resolve block ${hash}: ${error}`);
4944
- this.#blockResolveQueue.push({ hash, retries: 0 });
4945
- }
4946
- }
4947
- };
4948
- // Start concurrent resolution tasks
4949
- for (let i = 0; i < Math.min(this.#maxConcurrentResolves, hashes.length); i++) {
4950
- resolving.push(resolveNext());
4951
- }
4952
- await Promise.all(resolving);
4922
+ return;
4953
4923
  }
4954
4924
  async resolveBlock(hash) {
4955
- if (!hash || hash === '0x0')
4925
+ if (!hash)
4926
+ throw new Error(`expected hash, got: ${hash}`);
4927
+ if (hash === '0x0')
4956
4928
  return;
4957
- if (this.#syncing)
4958
- return;
4959
- this.#syncing = true;
4960
- this.#syncErrorCount = 0;
4929
+ if (this.#resolving)
4930
+ return 'already resolving';
4931
+ this.#resolving = true;
4932
+ if (this.jobber.busy && this.jobber.destroy)
4933
+ await this.jobber.destroy();
4961
4934
  try {
4962
- debug$1(`Building block chain from ${hash}`);
4963
- const blockChain = await this.#buildBlockChain(hash);
4964
- debug$1(`Built chain of ${blockChain.length} blocks`);
4965
- if (blockChain.length > 0) {
4966
- // If a previous resolve job is still running, cancel it
4967
- if (this.jobber?.busy && this.jobber.destroy)
4968
- await this.jobber.destroy();
4969
- // Run the parallel resolution inside a timed jobber task
4970
- await this.jobber.add(() => this.#resolveBlocksInParallel(blockChain));
4935
+ await this.jobber.add(() => this.#resolveBlock(hash));
4936
+ this.#resolving = false;
4937
+ const lastBlockHash = await globalThis.stateStore.get('lastBlock');
4938
+ if (lastBlockHash === hash) {
4939
+ this.#resolveErrored = false;
4940
+ return;
4971
4941
  }
4942
+ if (!this.#blockHashMap.has(this.#lastResolved.previousHash) && this.#lastResolved.previousHash !== '0x0')
4943
+ return this.resolveBlock(this.#lastResolved.previousHash);
4972
4944
  }
4973
4945
  catch (error) {
4974
- console.error('Block resolution failed:', error);
4946
+ console.log({ error });
4947
+ this.#resolveErrorCount += 1;
4948
+ this.#resolving = false;
4949
+ if (this.#resolveErrorCount < 3)
4950
+ return this.resolveBlock(hash);
4951
+ this.#resolveErrorCount = 0;
4975
4952
  this.wantList.push(hash);
4976
- }
4977
- finally {
4978
- this.#syncing = false;
4953
+ throw new ResolveError(`block: ${hash}`, { cause: error });
4979
4954
  }
4980
4955
  }
4981
4956
  async resolveBlocks() {
4982
- if (this.#syncing)
4957
+ // Don't re-resolve if already syncing or resolving
4958
+ if (this.#chainSyncing || this.#resolving) {
4959
+ debug$1('Already syncing or resolving, skipping resolveBlocks()');
4983
4960
  return;
4961
+ }
4962
+ try {
4963
+ if (this.jobber.busy && this.jobber.destroy) {
4964
+ await this.jobber.destroy();
4965
+ }
4966
+ }
4967
+ catch (error) {
4968
+ console.error(error);
4969
+ }
4984
4970
  try {
4985
4971
  const localBlock = await globalThis.chainStore.get('lastBlock');
4986
4972
  const hash = new TextDecoder().decode(localBlock);
4987
4973
  if (hash && hash !== '0x0') {
4988
- // Cancel any in-flight job before starting a new one
4989
- if (this.jobber?.busy && this.jobber.destroy)
4990
- await this.jobber.destroy();
4991
- // Build chain and resolve in parallel under jobber control
4992
- const run = async () => {
4993
- const chain = await this.#buildBlockChain(hash);
4994
- if (chain.length > 0) {
4995
- await this.#resolveBlocksInParallel(chain);
4996
- }
4997
- };
4998
- await this.jobber.add(run);
4974
+ debug$1(`Resolving blocks from hash: ${hash}`);
4975
+ await this.resolveBlock(hash);
4976
+ }
4977
+ }
4978
+ catch (error) {
4979
+ console.log(error);
4980
+ this.#chainSyncing = false;
4981
+ this.#syncState = 'errored';
4982
+ this.#resolveErrored = true;
4983
+ return this.restoreChain();
4984
+ // console.log(e);
4985
+ }
4986
+ }
4987
+ async restoreChain() {
4988
+ try {
4989
+ const { hash } = await this.#getLatestBlock();
4990
+ await globalThis.chainStore.put('lastBlock', hash);
4991
+ if (hash && hash !== '0x0') {
4992
+ await this.resolveBlock(hash);
4999
4993
  }
5000
4994
  }
5001
4995
  catch (error) {
5002
- debug$1('Failed to resolve blocks:', error);
4996
+ console.log(error);
4997
+ this.#resolveErrored = true;
4998
+ this.#resolveErrorCount += 1;
4999
+ this.#resolving = false;
5000
+ return this.restoreChain();
5001
+ // console.log(e);
5003
5002
  }
5004
5003
  }
5005
5004
  async syncChain(lastBlock) {
5006
- if (this.#syncing)
5007
- return 'syncing';
5005
+ console.log('check if can sync');
5006
+ if (!this.shouldSync)
5007
+ return;
5008
+ console.log('starting sync');
5009
+ this.#syncState = 'syncing';
5010
+ this.#chainSyncing = true;
5011
+ try {
5012
+ if (this.jobber.busy && this.jobber.destroy) {
5013
+ await this.jobber.destroy();
5014
+ }
5015
+ }
5016
+ catch (error) {
5017
+ console.error(error);
5018
+ }
5008
5019
  if (!lastBlock)
5009
5020
  lastBlock = await this.#getLatestBlock();
5010
- this.#syncing = true;
5021
+ if (globalThis.peernet.peers.length === 0)
5022
+ return 'connectionless';
5011
5023
  try {
5012
- if (globalThis.peernet.peers.length === 0) {
5013
- this.#syncing = false;
5014
- return 'connectionless';
5024
+ await this.#syncChain(lastBlock);
5025
+ }
5026
+ catch (error) {
5027
+ this.#syncErrorCount += 1;
5028
+ if (this.#syncErrorCount < 3)
5029
+ return this.syncChain(lastBlock);
5030
+ this.#syncErrorCount = 0;
5031
+ this.#chainSyncing = false;
5032
+ this.#syncState = 'errored';
5033
+ return this.#syncState;
5034
+ }
5035
+ if (lastBlock.index === this.#lastBlockInQue?.index)
5036
+ this.#lastBlockInQue = undefined;
5037
+ this.#syncErrorCount = 0;
5038
+ this.#chainSyncing = false;
5039
+ if (this.#lastBlockInQue)
5040
+ return this.syncChain(this.#lastBlockInQue);
5041
+ this.#syncState = 'synced';
5042
+ return this.#syncState;
5043
+ }
5044
+ async #syncChain(lastBlock) {
5045
+ try {
5046
+ // if (this.knownBlocks?.length === Number(lastBlock.index) + 1) {
5047
+ // let promises = []
5048
+ // promises = await Promise.allSettled(
5049
+ // this.knownBlocks.map(async (address) => {
5050
+ // const has = await globalThis.peernet.has(address)
5051
+ // return { has, address }
5052
+ // })
5053
+ // )
5054
+ // promises = promises.filter(({ status, value }) => status === 'fulfilled' && !value.has)
5055
+ // await Promise.allSettled(promises.map(({ value }) => this.getAndPutBlock(value.address)))
5056
+ // }
5057
+ const localBlock = await this.lastBlock;
5058
+ const localIndex = localBlock ? Number(localBlock.index) : -1;
5059
+ const remoteIndex = Number(lastBlock.index);
5060
+ const remoteBlockHash = lastBlock.hash;
5061
+ // Get the local state hash from chainStore
5062
+ let localStateHash = '0x0';
5063
+ try {
5064
+ localStateHash = new TextDecoder().decode(await globalThis.chainStore.get('lastBlock'));
5015
5065
  }
5016
- await this.resolveBlock(lastBlock.hash);
5017
- const blocks = this.#blocks;
5018
- const localIndex = (await this.lastBlock).index || -1;
5019
- const start = Math.max(0, localIndex + 1);
5020
- if (this.#machine && blocks.length > start) {
5021
- await this.#loadBlocks(blocks.slice(start));
5066
+ catch (error) {
5067
+ debug$1(`No local state hash found: ${error}`);
5022
5068
  }
5023
- if (blocks.length > 0) {
5024
- await this.updateState(new BlockMessage(blocks[blocks.length - 1]));
5069
+ debug$1(`Local block height: ${localIndex}, remote block height: ${remoteIndex}`);
5070
+ debug$1(`Local state hash: ${localStateHash}, remote block hash: ${remoteBlockHash}`);
5071
+ // Skip syncing if remote block hash is 0x0 (invalid state)
5072
+ if (remoteBlockHash === '0x0') {
5073
+ debug$1(`Remote block hash is 0x0, skipping sync`);
5074
+ return;
5075
+ }
5076
+ // Use state hash comparison: only resolve if remote hash differs from local state hash
5077
+ if (localStateHash !== remoteBlockHash) {
5078
+ // Remote block hash differs from our local state, need to resolve
5079
+ debug$1(`Resolving remote block: ${remoteBlockHash} @${remoteIndex} (differs from local state)`);
5080
+ await this.resolveBlock(remoteBlockHash);
5081
+ const blocksSynced = remoteIndex - localIndex;
5082
+ debug$1(`Resolved ${blocksSynced} new block(s)`);
5083
+ const blocks = this.#blocks;
5084
+ debug$1(`Loading blocks from index ${localIndex + 1} to ${remoteIndex}`);
5085
+ const start = localIndex + 1;
5086
+ if (this.#machine && blocks.length > start) {
5087
+ await this.#loadBlocks(blocks.slice(start));
5088
+ }
5089
+ // Update state with the latest block
5090
+ if (blocks.length > 0) {
5091
+ await this.updateState(new BlockMessage(blocks[blocks.length - 1]));
5092
+ }
5093
+ }
5094
+ else {
5095
+ debug$1(`Block already in local state. Remote hash: ${remoteBlockHash} matches local state`);
5025
5096
  }
5026
- this.#syncErrorCount = 0;
5027
- this.#syncing = false;
5028
- return 'synced';
5029
5097
  }
5030
5098
  catch (error) {
5031
- this.#syncErrorCount++;
5032
- if (this.#syncErrorCount < 3) {
5033
- this.#syncing = false;
5034
- return this.syncChain(lastBlock);
5035
- }
5036
- this.#syncErrorCount = 0;
5037
- this.#syncing = false;
5038
- return 'errored';
5099
+ console.log(error);
5100
+ throw error;
5039
5101
  }
5040
5102
  }
5041
5103
  async #getLatestBlock() {
@@ -5071,9 +5133,7 @@ class State extends Contract {
5071
5133
  debug$1(`Latest block from peers: ${latest.hash} @${latest.index}`);
5072
5134
  if (latest.hash && latest.hash !== '0x0') {
5073
5135
  let message = await globalThis.peernet.get(latest.hash, 'block');
5074
- debug$1({ message });
5075
5136
  message = await new BlockMessage(message);
5076
- debug$1({ message });
5077
5137
  const hash = await message.hash();
5078
5138
  if (hash !== latest.hash)
5079
5139
  throw new Error('invalid block @getLatestBlock');
@@ -5086,7 +5146,7 @@ class State extends Contract {
5086
5146
  let node = await globalThis.peernet.prepareMessage(data);
5087
5147
  let message = await peer.request(node.encode());
5088
5148
  message = await new globalThis.peernet.protos['peernet-response'](message);
5089
- this.wantList.push(...message.decoded.response);
5149
+ this.wantList.push(...message.decoded.response.blocks.filter((block) => !this.knownBlocks.includes(block)));
5090
5150
  }
5091
5151
  }
5092
5152
  return latest;
@@ -5206,13 +5266,26 @@ class State extends Contract {
5206
5266
  });
5207
5267
  }
5208
5268
  get canSync() {
5209
- return !this.#syncing;
5269
+ if (this.#chainSyncing)
5270
+ return false;
5271
+ return true;
5210
5272
  }
5211
5273
  get shouldSync() {
5212
- if (this.#syncing)
5274
+ if (this.#chainSyncing)
5213
5275
  return false;
5276
+ // Check if we have any connected peers with the same version
5214
5277
  const compatiblePeers = Object.values(globalThis.peernet.connections || {}).filter((peer) => peer.connected && peer.version === this.version);
5215
- return compatiblePeers.length > 0;
5278
+ if (compatiblePeers.length === 0) {
5279
+ debug$1('No compatible peers available for sync');
5280
+ return false;
5281
+ }
5282
+ if (!this.#chainSyncing ||
5283
+ this.#resolveErrored ||
5284
+ this.#syncState === 'errored' ||
5285
+ this.#syncState === 'connectionless' ||
5286
+ this.#lastResolvedTime + this.resolveTimeout > Date.now())
5287
+ return true;
5288
+ return false;
5216
5289
  }
5217
5290
  async #waitForPeers(timeoutMs = 30000) {
5218
5291
  return new Promise((resolve) => {
package/exports/chain.js CHANGED
@@ -6,7 +6,7 @@ import { calculateFee, createContractMessage, signTransaction, contractFactoryMe
6
6
  import semver from 'semver';
7
7
  import { randombytes } from '@leofcoin/crypto';
8
8
  import EasyWorker from '@vandeurenglenn/easy-worker';
9
- import { ContractDeploymentError, ExecutionError, ResolveError, isExecutionError } from '@leofcoin/errors';
9
+ import { ContractDeploymentError, ExecutionError, isResolveError, ResolveError, isExecutionError } from '@leofcoin/errors';
10
10
 
11
11
  const limit = 1800;
12
12
  const transactionLimit = 1000;
@@ -771,34 +771,42 @@ class Jobber {
771
771
 
772
772
  const debug$1 = createDebugger('leofcoin/state');
773
773
  class State extends Contract {
774
+ #resolveErrored;
775
+ #lastResolvedTime;
776
+ #lastResolved;
777
+ #resolving;
778
+ #resolveErrorCount;
779
+ #syncState;
780
+ #chainState;
781
+ #lastBlockInQue;
782
+ #syncErrorCount;
774
783
  #blockHashMap;
784
+ #chainSyncing;
775
785
  #blocks;
786
+ #totalSize;
776
787
  #machine;
777
788
  #loaded;
778
- // Sync state
779
- #syncing;
780
- #syncErrorCount;
781
- // Block resolution state
782
- #resolvingBlocks;
783
- #maxConcurrentResolves;
784
- #totalSize;
785
- #lastResolved;
786
- #lastResolvedTime;
787
- #blockResolveQueue;
788
- #chainState;
789
789
  /**
790
790
  * contains transactions we need before we can successfully load
791
791
  */
792
792
  get wantList() {
793
793
  return this.#machine?.wantList ?? this._wantList;
794
794
  }
795
+ get state() {
796
+ return {
797
+ sync: this.#syncState,
798
+ chain: this.#chainState
799
+ };
800
+ }
801
+ get blockHashMap() {
802
+ return this.#blockHashMap.entries();
803
+ }
795
804
  get loaded() {
796
805
  return this.#loaded;
797
806
  }
798
- get isSyncing() {
799
- return this.#syncing;
807
+ get resolving() {
808
+ return this.#resolving;
800
809
  }
801
- // Delegate to machine
802
810
  get contracts() {
803
811
  return this.#machine.contracts;
804
812
  }
@@ -841,38 +849,35 @@ class State extends Contract {
841
849
  get lastBlockHeight() {
842
850
  return this.#machine ? this.#machine.lastBlockHeight : 0;
843
851
  }
844
- get totalSize() {
845
- return this.#totalSize;
846
- }
847
- get machine() {
848
- return this.#machine;
849
- }
850
- get blockHashMap() {
851
- return this.#blockHashMap.entries();
852
- }
853
852
  getBlock(index) {
854
853
  return this.#machine.getBlock(index);
855
854
  }
856
855
  getBlocks(from, to) {
857
856
  return this.#machine.getBlocks(from, to);
858
857
  }
858
+ get totalSize() {
859
+ return this.#totalSize;
860
+ }
861
+ get machine() {
862
+ return this.#machine;
863
+ }
859
864
  constructor(config) {
860
865
  super(config);
866
+ this.#lastResolvedTime = 0;
867
+ this.#resolving = false;
868
+ this.#resolveErrorCount = 0;
869
+ this.#chainState = 'loading';
870
+ this.#syncErrorCount = 0;
861
871
  this.#blockHashMap = new Map();
872
+ this.#chainSyncing = false;
862
873
  this.#blocks = [];
863
- this.#loaded = false;
864
- // Sync state
865
- this.#syncing = false;
866
- this.#syncErrorCount = 0;
867
- // Block resolution state
868
- this.#resolvingBlocks = new Set();
869
- this.#maxConcurrentResolves = 10;
870
874
  this.knownBlocks = [];
871
875
  this.#totalSize = 0;
876
+ this.#loaded = false;
872
877
  this._wantList = [];
873
878
  this.#chainStateHandler = () => {
874
879
  return new globalThis.peernet.protos['peernet-response']({
875
- response: { syncing: this.#syncing, loaded: this.#loaded }
880
+ response: this.#chainState
876
881
  });
877
882
  };
878
883
  this.#lastBlockHandler = async () => {
@@ -914,45 +919,67 @@ class State extends Contract {
914
919
  #lastBlockHandler;
915
920
  #knownBlocksHandler;
916
921
  async init() {
917
- // Initialize jobber for timed, cancelable tasks
918
922
  this.jobber = new Jobber(this.resolveTimeout);
919
- // Register request handlers
920
- await globalThis.peernet.addRequestHandler('lastBlock', this.#lastBlockHandler.bind(this));
921
- await globalThis.peernet.addRequestHandler('knownBlocks', this.#knownBlocksHandler.bind(this));
922
- await globalThis.peernet.addRequestHandler('chainState', this.#chainStateHandler.bind(this));
923
+ await globalThis.peernet.addRequestHandler('lastBlock', this.#lastBlockHandler);
924
+ await globalThis.peernet.addRequestHandler('knownBlocks', this.#knownBlocksHandler);
925
+ await globalThis.peernet.addRequestHandler('chainState', this.#chainStateHandler);
926
+ let localBlockHash;
927
+ let blockMessage;
928
+ let localBlock;
923
929
  try {
924
- // Load local block state
925
- let localBlock = { index: 0, hash: '0x0', previousHash: '0x0' };
926
- try {
927
- const localBlockHash = new TextDecoder().decode(await globalThis.chainStore.get('lastBlock'));
928
- if (localBlockHash && localBlockHash !== '0x0') {
929
- const blockMessage = await new BlockMessage(await globalThis.peernet.get(localBlockHash, 'block'));
930
+ const rawBlock = await globalThis.chainStore.has('lastBlock');
931
+ if (rawBlock) {
932
+ localBlockHash = new TextDecoder().decode(await globalThis.chainStore.get('lastBlock'));
933
+ if (localBlockHash !== '0x0') {
934
+ blockMessage = await globalThis.peernet.get(localBlockHash, 'block');
935
+ blockMessage = await new BlockMessage(blockMessage);
930
936
  localBlock = { ...blockMessage.decoded, hash: localBlockHash };
931
937
  }
932
938
  }
933
- catch (error) {
934
- debug$1('No local block found');
939
+ else {
940
+ localBlock = { index: 0, hash: '0x0', previousHash: '0x0' };
935
941
  }
936
- // Load known blocks
937
- try {
938
- this.knownBlocks = await globalThis.blockStore.keys();
942
+ }
943
+ catch {
944
+ localBlock = { index: 0, hash: '0x0', previousHash: '0x0' };
945
+ }
946
+ try {
947
+ this.knownBlocks = await blockStore.keys();
948
+ }
949
+ catch (error) {
950
+ debug$1('no local known blocks found');
951
+ }
952
+ try {
953
+ if (localBlock.hash && localBlock.hash !== '0x0') {
954
+ try {
955
+ const states = {
956
+ lastBlock: JSON.parse(new TextDecoder().decode(await globalThis.stateStore.get('lastBlock')))
957
+ };
958
+ if (blockMessage.decoded.index > states.lastBlock.index)
959
+ await this.resolveBlocks();
960
+ }
961
+ catch (error) {
962
+ // no states found, try resolving blocks
963
+ await this.resolveBlocks();
964
+ }
939
965
  }
940
- catch (error) {
941
- debug$1('No known blocks found');
966
+ else {
967
+ await this.resolveBlocks();
942
968
  }
943
- // Initialize machine and resolve blocks if needed
944
969
  this.#machine = await new Machine(this.#blocks);
945
- if (localBlock.hash !== '0x0') {
946
- await this.resolveBlock(localBlock.hash);
947
- }
948
970
  const lastBlock = await this.#machine.lastBlock;
949
971
  if (lastBlock.hash !== '0x0') {
950
- await this.updateState(new BlockMessage(lastBlock));
972
+ this.updateState(new BlockMessage(lastBlock));
951
973
  }
952
974
  this.#loaded = true;
975
+ // await this.#loadBlocks(this.#blocks)
953
976
  }
954
977
  catch (error) {
955
- console.error('Failed to initialize state:', error);
978
+ console.log('e');
979
+ if (isResolveError(error)) {
980
+ console.error(error);
981
+ }
982
+ console.log(error);
956
983
  }
957
984
  }
958
985
  async updateState(message) {
@@ -986,47 +1013,41 @@ class State extends Contract {
986
1013
  return block;
987
1014
  }
988
1015
  async #resolveBlock(hash) {
989
- if (this.#resolvingBlocks.has(hash)) {
990
- return; // Already resolving this block
991
- }
992
- this.#resolvingBlocks.add(hash);
1016
+ let index = this.#blockHashMap.get(hash);
1017
+ let localHash = '0x0';
993
1018
  try {
994
- let index = this.#blockHashMap.get(hash);
995
- debug$1(`resolving block: ${hash} @${index !== undefined ? index : 'unknown'}`);
996
- if (this.#blocks[index]) {
997
- // Block already exists
1019
+ localHash = await globalThis.stateStore.get('lastBlock');
1020
+ }
1021
+ catch (error) {
1022
+ debug$1('no local state found');
1023
+ }
1024
+ if (this.#blocks[index]) {
1025
+ // Block already exists, check if we need to resolve previous blocks
1026
+ const previousHash = this.#blocks[index].previousHash;
1027
+ if (previousHash === localHash)
998
1028
  return;
1029
+ if (previousHash !== '0x0') {
1030
+ // Previous block not in memory, recursively resolve it
1031
+ return this.resolveBlock(previousHash);
999
1032
  }
1000
- const block = await this.getAndPutBlock(hash);
1001
- index = block.decoded.index;
1002
- const size = block.encoded.length > 0 ? block.encoded.length : block.encoded.byteLength;
1003
- // Batch transaction operations
1004
- const transactionsToFetch = [];
1005
- const transactionHashes = block.decoded.transactions || [];
1006
- for (const txHash of transactionHashes) {
1007
- if (!(await globalThis.transactionStore.has(txHash))) {
1008
- transactionsToFetch.push(txHash);
1009
- }
1010
- }
1011
- // Fetch all missing transactions in parallel
1012
- if (transactionsToFetch.length > 0) {
1013
- const fetchedResults = await Promise.allSettled(transactionsToFetch.map((txHash) => globalThis.peernet.get(txHash, 'transaction')));
1014
- // Batch store all transactions that were successfully fetched
1015
- for (let i = 0; i < fetchedResults.length; i++) {
1016
- if (fetchedResults[i].status === 'fulfilled') {
1017
- await globalThis.transactionStore.put(transactionsToFetch[i], fetchedResults[i].value);
1018
- }
1019
- else {
1020
- debug$1(`failed to fetch transaction ${transactionsToFetch[i]}: ${fetchedResults[i].reason?.message || fetchedResults[i].reason}`);
1021
- }
1022
- }
1033
+ else {
1034
+ // Previous block already exists or is genesis, stop resolving
1035
+ return;
1023
1036
  }
1024
- // Remove from pool
1025
- await Promise.all(transactionHashes.map(async (txHash) => {
1026
- if (await globalThis.transactionPoolStore.has(txHash)) {
1027
- await globalThis.transactionPoolStore.delete(txHash);
1037
+ }
1038
+ try {
1039
+ const block = await this.getAndPutBlock(hash);
1040
+ await Promise.all(block.decoded.transactions.map(async (hash) => {
1041
+ // should be in a transaction store already
1042
+ if (!(await transactionStore.has(hash))) {
1043
+ const data = await peernet.get(hash, 'transaction');
1044
+ await transactionStore.put(hash, data);
1028
1045
  }
1046
+ ;
1047
+ (await transactionPoolStore.has(hash)) && (await transactionPoolStore.delete(hash));
1029
1048
  }));
1049
+ index = block.decoded.index;
1050
+ const size = block.encoded.length > 0 ? block.encoded.length : block.encoded.byteLength;
1030
1051
  this.#totalSize += size;
1031
1052
  this.#blocks[index] = { hash, ...block.decoded };
1032
1053
  this.#blockHashMap.set(hash, index);
@@ -1036,147 +1057,187 @@ class State extends Contract {
1036
1057
  this.#lastResolvedTime = Date.now();
1037
1058
  }
1038
1059
  catch (error) {
1039
- throw new ResolveError(`block: ${hash}`);
1060
+ throw new ResolveError(`block: ${hash}@${index}`);
1040
1061
  }
1041
- finally {
1042
- this.#resolvingBlocks.delete(hash);
1043
- }
1044
- }
1045
- async #buildBlockChain(latestHash, maxBlocks = 1000) {
1046
- const chain = [];
1047
- let currentHash = latestHash;
1048
- let attempts = 0;
1049
- while (currentHash !== '0x0' && chain.length < maxBlocks && attempts < maxBlocks + 5) {
1050
- attempts++;
1051
- // Check if we already have this block
1052
- if (this.#blockHashMap.has(currentHash)) {
1053
- const block = this.#blocks[this.#blockHashMap.get(currentHash)];
1054
- if (block) {
1055
- chain.push(currentHash);
1056
- currentHash = block.previousHash;
1057
- continue;
1058
- }
1059
- }
1060
- chain.push(currentHash);
1061
- // Try to get the block to find previous hash
1062
- try {
1063
- const block = await this.getAndPutBlock(currentHash);
1064
- currentHash = block.decoded.previousHash;
1065
- }
1066
- catch (error) {
1067
- debug$1(`Could not fetch block ${currentHash} to determine chain: ${error}`);
1068
- break;
1069
- }
1070
- }
1071
- return chain;
1072
- }
1073
- async #resolveBlocksInParallel(hashes) {
1074
- // Resolve blocks in parallel with concurrency limit
1075
- const resolving = [];
1076
- let index = 0;
1077
- const resolveNext = async () => {
1078
- while (index < hashes.length) {
1079
- const hash = hashes[index++];
1080
- try {
1081
- await this.#resolveBlock(hash);
1082
- }
1083
- catch (error) {
1084
- debug$1(`Failed to resolve block ${hash}: ${error}`);
1085
- this.#blockResolveQueue.push({ hash, retries: 0 });
1086
- }
1087
- }
1088
- };
1089
- // Start concurrent resolution tasks
1090
- for (let i = 0; i < Math.min(this.#maxConcurrentResolves, hashes.length); i++) {
1091
- resolving.push(resolveNext());
1092
- }
1093
- await Promise.all(resolving);
1062
+ return;
1094
1063
  }
1095
1064
  async resolveBlock(hash) {
1096
- if (!hash || hash === '0x0')
1097
- return;
1098
- if (this.#syncing)
1065
+ if (!hash)
1066
+ throw new Error(`expected hash, got: ${hash}`);
1067
+ if (hash === '0x0')
1099
1068
  return;
1100
- this.#syncing = true;
1101
- this.#syncErrorCount = 0;
1069
+ if (this.#resolving)
1070
+ return 'already resolving';
1071
+ this.#resolving = true;
1072
+ if (this.jobber.busy && this.jobber.destroy)
1073
+ await this.jobber.destroy();
1102
1074
  try {
1103
- debug$1(`Building block chain from ${hash}`);
1104
- const blockChain = await this.#buildBlockChain(hash);
1105
- debug$1(`Built chain of ${blockChain.length} blocks`);
1106
- if (blockChain.length > 0) {
1107
- // If a previous resolve job is still running, cancel it
1108
- if (this.jobber?.busy && this.jobber.destroy)
1109
- await this.jobber.destroy();
1110
- // Run the parallel resolution inside a timed jobber task
1111
- await this.jobber.add(() => this.#resolveBlocksInParallel(blockChain));
1075
+ await this.jobber.add(() => this.#resolveBlock(hash));
1076
+ this.#resolving = false;
1077
+ const lastBlockHash = await globalThis.stateStore.get('lastBlock');
1078
+ if (lastBlockHash === hash) {
1079
+ this.#resolveErrored = false;
1080
+ return;
1112
1081
  }
1082
+ if (!this.#blockHashMap.has(this.#lastResolved.previousHash) && this.#lastResolved.previousHash !== '0x0')
1083
+ return this.resolveBlock(this.#lastResolved.previousHash);
1113
1084
  }
1114
1085
  catch (error) {
1115
- console.error('Block resolution failed:', error);
1086
+ console.log({ error });
1087
+ this.#resolveErrorCount += 1;
1088
+ this.#resolving = false;
1089
+ if (this.#resolveErrorCount < 3)
1090
+ return this.resolveBlock(hash);
1091
+ this.#resolveErrorCount = 0;
1116
1092
  this.wantList.push(hash);
1117
- }
1118
- finally {
1119
- this.#syncing = false;
1093
+ throw new ResolveError(`block: ${hash}`, { cause: error });
1120
1094
  }
1121
1095
  }
1122
1096
  async resolveBlocks() {
1123
- if (this.#syncing)
1097
+ // Don't re-resolve if already syncing or resolving
1098
+ if (this.#chainSyncing || this.#resolving) {
1099
+ debug$1('Already syncing or resolving, skipping resolveBlocks()');
1124
1100
  return;
1101
+ }
1102
+ try {
1103
+ if (this.jobber.busy && this.jobber.destroy) {
1104
+ await this.jobber.destroy();
1105
+ }
1106
+ }
1107
+ catch (error) {
1108
+ console.error(error);
1109
+ }
1125
1110
  try {
1126
1111
  const localBlock = await globalThis.chainStore.get('lastBlock');
1127
1112
  const hash = new TextDecoder().decode(localBlock);
1128
1113
  if (hash && hash !== '0x0') {
1129
- // Cancel any in-flight job before starting a new one
1130
- if (this.jobber?.busy && this.jobber.destroy)
1131
- await this.jobber.destroy();
1132
- // Build chain and resolve in parallel under jobber control
1133
- const run = async () => {
1134
- const chain = await this.#buildBlockChain(hash);
1135
- if (chain.length > 0) {
1136
- await this.#resolveBlocksInParallel(chain);
1137
- }
1138
- };
1139
- await this.jobber.add(run);
1114
+ debug$1(`Resolving blocks from hash: ${hash}`);
1115
+ await this.resolveBlock(hash);
1140
1116
  }
1141
1117
  }
1142
1118
  catch (error) {
1143
- debug$1('Failed to resolve blocks:', error);
1119
+ console.log(error);
1120
+ this.#chainSyncing = false;
1121
+ this.#syncState = 'errored';
1122
+ this.#resolveErrored = true;
1123
+ return this.restoreChain();
1124
+ // console.log(e);
1125
+ }
1126
+ }
1127
+ async restoreChain() {
1128
+ try {
1129
+ const { hash } = await this.#getLatestBlock();
1130
+ await globalThis.chainStore.put('lastBlock', hash);
1131
+ if (hash && hash !== '0x0') {
1132
+ await this.resolveBlock(hash);
1133
+ }
1134
+ }
1135
+ catch (error) {
1136
+ console.log(error);
1137
+ this.#resolveErrored = true;
1138
+ this.#resolveErrorCount += 1;
1139
+ this.#resolving = false;
1140
+ return this.restoreChain();
1141
+ // console.log(e);
1144
1142
  }
1145
1143
  }
1146
1144
  async syncChain(lastBlock) {
1147
- if (this.#syncing)
1148
- return 'syncing';
1145
+ console.log('check if can sync');
1146
+ if (!this.shouldSync)
1147
+ return;
1148
+ console.log('starting sync');
1149
+ this.#syncState = 'syncing';
1150
+ this.#chainSyncing = true;
1151
+ try {
1152
+ if (this.jobber.busy && this.jobber.destroy) {
1153
+ await this.jobber.destroy();
1154
+ }
1155
+ }
1156
+ catch (error) {
1157
+ console.error(error);
1158
+ }
1149
1159
  if (!lastBlock)
1150
1160
  lastBlock = await this.#getLatestBlock();
1151
- this.#syncing = true;
1161
+ if (globalThis.peernet.peers.length === 0)
1162
+ return 'connectionless';
1152
1163
  try {
1153
- if (globalThis.peernet.peers.length === 0) {
1154
- this.#syncing = false;
1155
- return 'connectionless';
1164
+ await this.#syncChain(lastBlock);
1165
+ }
1166
+ catch (error) {
1167
+ this.#syncErrorCount += 1;
1168
+ if (this.#syncErrorCount < 3)
1169
+ return this.syncChain(lastBlock);
1170
+ this.#syncErrorCount = 0;
1171
+ this.#chainSyncing = false;
1172
+ this.#syncState = 'errored';
1173
+ return this.#syncState;
1174
+ }
1175
+ if (lastBlock.index === this.#lastBlockInQue?.index)
1176
+ this.#lastBlockInQue = undefined;
1177
+ this.#syncErrorCount = 0;
1178
+ this.#chainSyncing = false;
1179
+ if (this.#lastBlockInQue)
1180
+ return this.syncChain(this.#lastBlockInQue);
1181
+ this.#syncState = 'synced';
1182
+ return this.#syncState;
1183
+ }
1184
+ async #syncChain(lastBlock) {
1185
+ try {
1186
+ // if (this.knownBlocks?.length === Number(lastBlock.index) + 1) {
1187
+ // let promises = []
1188
+ // promises = await Promise.allSettled(
1189
+ // this.knownBlocks.map(async (address) => {
1190
+ // const has = await globalThis.peernet.has(address)
1191
+ // return { has, address }
1192
+ // })
1193
+ // )
1194
+ // promises = promises.filter(({ status, value }) => status === 'fulfilled' && !value.has)
1195
+ // await Promise.allSettled(promises.map(({ value }) => this.getAndPutBlock(value.address)))
1196
+ // }
1197
+ const localBlock = await this.lastBlock;
1198
+ const localIndex = localBlock ? Number(localBlock.index) : -1;
1199
+ const remoteIndex = Number(lastBlock.index);
1200
+ const remoteBlockHash = lastBlock.hash;
1201
+ // Get the local state hash from chainStore
1202
+ let localStateHash = '0x0';
1203
+ try {
1204
+ localStateHash = new TextDecoder().decode(await globalThis.chainStore.get('lastBlock'));
1156
1205
  }
1157
- await this.resolveBlock(lastBlock.hash);
1158
- const blocks = this.#blocks;
1159
- const localIndex = (await this.lastBlock).index || -1;
1160
- const start = Math.max(0, localIndex + 1);
1161
- if (this.#machine && blocks.length > start) {
1162
- await this.#loadBlocks(blocks.slice(start));
1206
+ catch (error) {
1207
+ debug$1(`No local state hash found: ${error}`);
1163
1208
  }
1164
- if (blocks.length > 0) {
1165
- await this.updateState(new BlockMessage(blocks[blocks.length - 1]));
1209
+ debug$1(`Local block height: ${localIndex}, remote block height: ${remoteIndex}`);
1210
+ debug$1(`Local state hash: ${localStateHash}, remote block hash: ${remoteBlockHash}`);
1211
+ // Skip syncing if remote block hash is 0x0 (invalid state)
1212
+ if (remoteBlockHash === '0x0') {
1213
+ debug$1(`Remote block hash is 0x0, skipping sync`);
1214
+ return;
1215
+ }
1216
+ // Use state hash comparison: only resolve if remote hash differs from local state hash
1217
+ if (localStateHash !== remoteBlockHash) {
1218
+ // Remote block hash differs from our local state, need to resolve
1219
+ debug$1(`Resolving remote block: ${remoteBlockHash} @${remoteIndex} (differs from local state)`);
1220
+ await this.resolveBlock(remoteBlockHash);
1221
+ const blocksSynced = remoteIndex - localIndex;
1222
+ debug$1(`Resolved ${blocksSynced} new block(s)`);
1223
+ const blocks = this.#blocks;
1224
+ debug$1(`Loading blocks from index ${localIndex + 1} to ${remoteIndex}`);
1225
+ const start = localIndex + 1;
1226
+ if (this.#machine && blocks.length > start) {
1227
+ await this.#loadBlocks(blocks.slice(start));
1228
+ }
1229
+ // Update state with the latest block
1230
+ if (blocks.length > 0) {
1231
+ await this.updateState(new BlockMessage(blocks[blocks.length - 1]));
1232
+ }
1233
+ }
1234
+ else {
1235
+ debug$1(`Block already in local state. Remote hash: ${remoteBlockHash} matches local state`);
1166
1236
  }
1167
- this.#syncErrorCount = 0;
1168
- this.#syncing = false;
1169
- return 'synced';
1170
1237
  }
1171
1238
  catch (error) {
1172
- this.#syncErrorCount++;
1173
- if (this.#syncErrorCount < 3) {
1174
- this.#syncing = false;
1175
- return this.syncChain(lastBlock);
1176
- }
1177
- this.#syncErrorCount = 0;
1178
- this.#syncing = false;
1179
- return 'errored';
1239
+ console.log(error);
1240
+ throw error;
1180
1241
  }
1181
1242
  }
1182
1243
  async #getLatestBlock() {
@@ -1212,9 +1273,7 @@ class State extends Contract {
1212
1273
  debug$1(`Latest block from peers: ${latest.hash} @${latest.index}`);
1213
1274
  if (latest.hash && latest.hash !== '0x0') {
1214
1275
  let message = await globalThis.peernet.get(latest.hash, 'block');
1215
- debug$1({ message });
1216
1276
  message = await new BlockMessage(message);
1217
- debug$1({ message });
1218
1277
  const hash = await message.hash();
1219
1278
  if (hash !== latest.hash)
1220
1279
  throw new Error('invalid block @getLatestBlock');
@@ -1227,7 +1286,7 @@ class State extends Contract {
1227
1286
  let node = await globalThis.peernet.prepareMessage(data);
1228
1287
  let message = await peer.request(node.encode());
1229
1288
  message = await new globalThis.peernet.protos['peernet-response'](message);
1230
- this.wantList.push(...message.decoded.response);
1289
+ this.wantList.push(...message.decoded.response.blocks.filter((block) => !this.knownBlocks.includes(block)));
1231
1290
  }
1232
1291
  }
1233
1292
  return latest;
@@ -1347,13 +1406,26 @@ class State extends Contract {
1347
1406
  });
1348
1407
  }
1349
1408
  get canSync() {
1350
- return !this.#syncing;
1409
+ if (this.#chainSyncing)
1410
+ return false;
1411
+ return true;
1351
1412
  }
1352
1413
  get shouldSync() {
1353
- if (this.#syncing)
1414
+ if (this.#chainSyncing)
1354
1415
  return false;
1416
+ // Check if we have any connected peers with the same version
1355
1417
  const compatiblePeers = Object.values(globalThis.peernet.connections || {}).filter((peer) => peer.connected && peer.version === this.version);
1356
- return compatiblePeers.length > 0;
1418
+ if (compatiblePeers.length === 0) {
1419
+ debug$1('No compatible peers available for sync');
1420
+ return false;
1421
+ }
1422
+ if (!this.#chainSyncing ||
1423
+ this.#resolveErrored ||
1424
+ this.#syncState === 'errored' ||
1425
+ this.#syncState === 'connectionless' ||
1426
+ this.#lastResolvedTime + this.resolveTimeout > Date.now())
1427
+ return true;
1428
+ return false;
1357
1429
  }
1358
1430
  async #waitForPeers(timeoutMs = 30000) {
1359
1431
  return new Promise((resolve) => {
@@ -3,17 +3,24 @@ import Contract from './contract.js';
3
3
  import Machine from './machine.js';
4
4
  import Jobber from './jobs/jobber.js';
5
5
  import { BlockHash } from './types.js';
6
+ declare type SyncState = 'syncing' | 'synced' | 'errored' | 'connectionless';
7
+ declare type ChainState = 'loading' | 'loaded';
6
8
  export default class State extends Contract {
7
9
  #private;
8
- jobber: Jobber;
9
10
  knownBlocks: BlockHash[];
11
+ jobber: Jobber;
10
12
  _wantList: any[];
11
13
  /**
12
14
  * contains transactions we need before we can successfully load
13
15
  */
14
16
  get wantList(): string[];
17
+ get state(): {
18
+ sync: SyncState;
19
+ chain: ChainState;
20
+ };
21
+ get blockHashMap(): MapIterator<[any, any]>;
15
22
  get loaded(): boolean;
16
- get isSyncing(): boolean;
23
+ get resolving(): boolean;
17
24
  get contracts(): Promise<any>;
18
25
  get totalContracts(): Promise<any>;
19
26
  get nativeCalls(): Promise<any>;
@@ -32,11 +39,10 @@ export default class State extends Contract {
32
39
  previousHash: string;
33
40
  };
34
41
  get lastBlockHeight(): Promise<any> | 0;
35
- get totalSize(): number;
36
- get machine(): Machine;
37
- get blockHashMap(): MapIterator<[any, any]>;
38
42
  getBlock(index: any): Promise<any>;
39
43
  getBlocks(from?: any, to?: any): Promise<[]>;
44
+ get totalSize(): number;
45
+ get machine(): Machine;
40
46
  constructor(config: any);
41
47
  clearPool(): Promise<void>;
42
48
  /**
@@ -47,12 +53,14 @@ export default class State extends Contract {
47
53
  updateState(message: BlockMessage): Promise<void>;
48
54
  getLatestBlock(): Promise<BlockMessage['decoded']>;
49
55
  getAndPutBlock(hash: string): Promise<BlockMessage>;
50
- resolveBlock(hash: string): Promise<void>;
51
- resolveBlocks(): Promise<void>;
52
- syncChain(lastBlock?: any): Promise<'syncing' | 'synced' | 'errored' | 'connectionless'>;
56
+ resolveBlock(hash: any): any;
57
+ resolveBlocks(): Promise<any>;
58
+ restoreChain(): any;
59
+ syncChain(lastBlock?: any): Promise<SyncState>;
53
60
  promiseRequests(promises: any): Promise<unknown>;
54
61
  get canSync(): boolean;
55
62
  get shouldSync(): boolean;
56
- triggerSync(): Promise<"syncing" | "synced" | "errored" | "connectionless">;
63
+ triggerSync(): Promise<SyncState>;
57
64
  triggerLoad(): Promise<void>;
58
65
  }
66
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leofcoin/chain",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
4
4
  "description": "Official javascript implementation",
5
5
  "private": false,
6
6
  "exports": {