@leofcoin/chain 1.8.0 → 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.
- package/exports/browser/chain.js +279 -199
- package/exports/chain.js +279 -200
- package/exports/state.d.ts +17 -9
- package/package.json +1 -1
package/exports/browser/chain.js
CHANGED
|
@@ -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
|
|
4658
|
-
return this.#
|
|
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:
|
|
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
|
-
|
|
4779
|
-
await globalThis.peernet.addRequestHandler('
|
|
4780
|
-
await globalThis.peernet.addRequestHandler('
|
|
4781
|
-
|
|
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
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
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
|
-
|
|
4793
|
-
|
|
4799
|
+
else {
|
|
4800
|
+
localBlock = { index: 0, hash: '0x0', previousHash: '0x0' };
|
|
4794
4801
|
}
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
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
|
-
|
|
4800
|
-
|
|
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
|
-
|
|
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.
|
|
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,40 +4873,41 @@ class State extends Contract {
|
|
|
4845
4873
|
return block;
|
|
4846
4874
|
}
|
|
4847
4875
|
async #resolveBlock(hash) {
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
}
|
|
4851
|
-
this.#resolvingBlocks.add(hash);
|
|
4876
|
+
let index = this.#blockHashMap.get(hash);
|
|
4877
|
+
let localHash = '0x0';
|
|
4852
4878
|
try {
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
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
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
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 fetchedTransactions = await Promise.all(transactionsToFetch.map((txHash) => globalThis.peernet.get(txHash, 'transaction')));
|
|
4873
|
-
// Batch store all transactions
|
|
4874
|
-
await Promise.all(transactionsToFetch.map((txHash, i) => globalThis.transactionStore.put(txHash, fetchedTransactions[i])));
|
|
4893
|
+
else {
|
|
4894
|
+
// Previous block already exists or is genesis, stop resolving
|
|
4895
|
+
return;
|
|
4875
4896
|
}
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
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);
|
|
4880
4905
|
}
|
|
4906
|
+
;
|
|
4907
|
+
(await transactionPoolStore.has(hash)) && (await transactionPoolStore.delete(hash));
|
|
4881
4908
|
}));
|
|
4909
|
+
index = block.decoded.index;
|
|
4910
|
+
const size = block.encoded.length > 0 ? block.encoded.length : block.encoded.byteLength;
|
|
4882
4911
|
this.#totalSize += size;
|
|
4883
4912
|
this.#blocks[index] = { hash, ...block.decoded };
|
|
4884
4913
|
this.#blockHashMap.set(hash, index);
|
|
@@ -4888,147 +4917,187 @@ class State extends Contract {
|
|
|
4888
4917
|
this.#lastResolvedTime = Date.now();
|
|
4889
4918
|
}
|
|
4890
4919
|
catch (error) {
|
|
4891
|
-
throw new ResolveError(`block: ${hash}`);
|
|
4892
|
-
}
|
|
4893
|
-
finally {
|
|
4894
|
-
this.#resolvingBlocks.delete(hash);
|
|
4895
|
-
}
|
|
4896
|
-
}
|
|
4897
|
-
async #buildBlockChain(latestHash, maxBlocks = 1000) {
|
|
4898
|
-
const chain = [];
|
|
4899
|
-
let currentHash = latestHash;
|
|
4900
|
-
let attempts = 0;
|
|
4901
|
-
while (currentHash !== '0x0' && chain.length < maxBlocks && attempts < maxBlocks + 5) {
|
|
4902
|
-
attempts++;
|
|
4903
|
-
// Check if we already have this block
|
|
4904
|
-
if (this.#blockHashMap.has(currentHash)) {
|
|
4905
|
-
const block = this.#blocks[this.#blockHashMap.get(currentHash)];
|
|
4906
|
-
if (block) {
|
|
4907
|
-
chain.push(currentHash);
|
|
4908
|
-
currentHash = block.previousHash;
|
|
4909
|
-
continue;
|
|
4910
|
-
}
|
|
4911
|
-
}
|
|
4912
|
-
chain.push(currentHash);
|
|
4913
|
-
// Try to get the block to find previous hash
|
|
4914
|
-
try {
|
|
4915
|
-
const block = await this.getAndPutBlock(currentHash);
|
|
4916
|
-
currentHash = block.decoded.previousHash;
|
|
4917
|
-
}
|
|
4918
|
-
catch (error) {
|
|
4919
|
-
debug$1(`Could not fetch block ${currentHash} to determine chain: ${error}`);
|
|
4920
|
-
break;
|
|
4921
|
-
}
|
|
4922
|
-
}
|
|
4923
|
-
return chain;
|
|
4924
|
-
}
|
|
4925
|
-
async #resolveBlocksInParallel(hashes) {
|
|
4926
|
-
// Resolve blocks in parallel with concurrency limit
|
|
4927
|
-
const resolving = [];
|
|
4928
|
-
let index = 0;
|
|
4929
|
-
const resolveNext = async () => {
|
|
4930
|
-
while (index < hashes.length) {
|
|
4931
|
-
const hash = hashes[index++];
|
|
4932
|
-
try {
|
|
4933
|
-
await this.#resolveBlock(hash);
|
|
4934
|
-
}
|
|
4935
|
-
catch (error) {
|
|
4936
|
-
debug$1(`Failed to resolve block ${hash}: ${error}`);
|
|
4937
|
-
this.#blockResolveQueue.push({ hash, retries: 0 });
|
|
4938
|
-
}
|
|
4939
|
-
}
|
|
4940
|
-
};
|
|
4941
|
-
// Start concurrent resolution tasks
|
|
4942
|
-
for (let i = 0; i < Math.min(this.#maxConcurrentResolves, hashes.length); i++) {
|
|
4943
|
-
resolving.push(resolveNext());
|
|
4920
|
+
throw new ResolveError(`block: ${hash}@${index}`);
|
|
4944
4921
|
}
|
|
4945
|
-
|
|
4922
|
+
return;
|
|
4946
4923
|
}
|
|
4947
4924
|
async resolveBlock(hash) {
|
|
4948
|
-
if (!hash
|
|
4949
|
-
|
|
4950
|
-
if (
|
|
4925
|
+
if (!hash)
|
|
4926
|
+
throw new Error(`expected hash, got: ${hash}`);
|
|
4927
|
+
if (hash === '0x0')
|
|
4951
4928
|
return;
|
|
4952
|
-
this.#
|
|
4953
|
-
|
|
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();
|
|
4954
4934
|
try {
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
if (
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
await this.jobber.destroy();
|
|
4962
|
-
// Run the parallel resolution inside a timed jobber task
|
|
4963
|
-
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;
|
|
4964
4941
|
}
|
|
4942
|
+
if (!this.#blockHashMap.has(this.#lastResolved.previousHash) && this.#lastResolved.previousHash !== '0x0')
|
|
4943
|
+
return this.resolveBlock(this.#lastResolved.previousHash);
|
|
4965
4944
|
}
|
|
4966
4945
|
catch (error) {
|
|
4967
|
-
console.
|
|
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;
|
|
4968
4952
|
this.wantList.push(hash);
|
|
4969
|
-
|
|
4970
|
-
finally {
|
|
4971
|
-
this.#syncing = false;
|
|
4953
|
+
throw new ResolveError(`block: ${hash}`, { cause: error });
|
|
4972
4954
|
}
|
|
4973
4955
|
}
|
|
4974
4956
|
async resolveBlocks() {
|
|
4975
|
-
if
|
|
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()');
|
|
4976
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
|
+
}
|
|
4977
4970
|
try {
|
|
4978
4971
|
const localBlock = await globalThis.chainStore.get('lastBlock');
|
|
4979
4972
|
const hash = new TextDecoder().decode(localBlock);
|
|
4980
4973
|
if (hash && hash !== '0x0') {
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
await this.jobber.destroy();
|
|
4984
|
-
// Build chain and resolve in parallel under jobber control
|
|
4985
|
-
const run = async () => {
|
|
4986
|
-
const chain = await this.#buildBlockChain(hash);
|
|
4987
|
-
if (chain.length > 0) {
|
|
4988
|
-
await this.#resolveBlocksInParallel(chain);
|
|
4989
|
-
}
|
|
4990
|
-
};
|
|
4991
|
-
await this.jobber.add(run);
|
|
4974
|
+
debug$1(`Resolving blocks from hash: ${hash}`);
|
|
4975
|
+
await this.resolveBlock(hash);
|
|
4992
4976
|
}
|
|
4993
4977
|
}
|
|
4994
4978
|
catch (error) {
|
|
4995
|
-
|
|
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);
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
catch (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);
|
|
4996
5002
|
}
|
|
4997
5003
|
}
|
|
4998
5004
|
async syncChain(lastBlock) {
|
|
4999
|
-
if
|
|
5000
|
-
|
|
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
|
+
}
|
|
5001
5019
|
if (!lastBlock)
|
|
5002
5020
|
lastBlock = await this.#getLatestBlock();
|
|
5003
|
-
|
|
5021
|
+
if (globalThis.peernet.peers.length === 0)
|
|
5022
|
+
return 'connectionless';
|
|
5004
5023
|
try {
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
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'));
|
|
5008
5065
|
}
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
const localIndex = (await this.lastBlock).index || -1;
|
|
5012
|
-
const start = Math.max(0, localIndex + 1);
|
|
5013
|
-
if (this.#machine && blocks.length > start) {
|
|
5014
|
-
await this.#loadBlocks(blocks.slice(start));
|
|
5066
|
+
catch (error) {
|
|
5067
|
+
debug$1(`No local state hash found: ${error}`);
|
|
5015
5068
|
}
|
|
5016
|
-
|
|
5017
|
-
|
|
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`);
|
|
5018
5096
|
}
|
|
5019
|
-
this.#syncErrorCount = 0;
|
|
5020
|
-
this.#syncing = false;
|
|
5021
|
-
return 'synced';
|
|
5022
5097
|
}
|
|
5023
5098
|
catch (error) {
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
this.#syncing = false;
|
|
5027
|
-
return this.syncChain(lastBlock);
|
|
5028
|
-
}
|
|
5029
|
-
this.#syncErrorCount = 0;
|
|
5030
|
-
this.#syncing = false;
|
|
5031
|
-
return 'errored';
|
|
5099
|
+
console.log(error);
|
|
5100
|
+
throw error;
|
|
5032
5101
|
}
|
|
5033
5102
|
}
|
|
5034
5103
|
async #getLatestBlock() {
|
|
@@ -5064,9 +5133,7 @@ class State extends Contract {
|
|
|
5064
5133
|
debug$1(`Latest block from peers: ${latest.hash} @${latest.index}`);
|
|
5065
5134
|
if (latest.hash && latest.hash !== '0x0') {
|
|
5066
5135
|
let message = await globalThis.peernet.get(latest.hash, 'block');
|
|
5067
|
-
debug$1({ message });
|
|
5068
5136
|
message = await new BlockMessage(message);
|
|
5069
|
-
debug$1({ message });
|
|
5070
5137
|
const hash = await message.hash();
|
|
5071
5138
|
if (hash !== latest.hash)
|
|
5072
5139
|
throw new Error('invalid block @getLatestBlock');
|
|
@@ -5079,7 +5146,7 @@ class State extends Contract {
|
|
|
5079
5146
|
let node = await globalThis.peernet.prepareMessage(data);
|
|
5080
5147
|
let message = await peer.request(node.encode());
|
|
5081
5148
|
message = await new globalThis.peernet.protos['peernet-response'](message);
|
|
5082
|
-
this.wantList.push(...message.decoded.response);
|
|
5149
|
+
this.wantList.push(...message.decoded.response.blocks.filter((block) => !this.knownBlocks.includes(block)));
|
|
5083
5150
|
}
|
|
5084
5151
|
}
|
|
5085
5152
|
return latest;
|
|
@@ -5199,13 +5266,26 @@ class State extends Contract {
|
|
|
5199
5266
|
});
|
|
5200
5267
|
}
|
|
5201
5268
|
get canSync() {
|
|
5202
|
-
|
|
5269
|
+
if (this.#chainSyncing)
|
|
5270
|
+
return false;
|
|
5271
|
+
return true;
|
|
5203
5272
|
}
|
|
5204
5273
|
get shouldSync() {
|
|
5205
|
-
if (this.#
|
|
5274
|
+
if (this.#chainSyncing)
|
|
5206
5275
|
return false;
|
|
5276
|
+
// Check if we have any connected peers with the same version
|
|
5207
5277
|
const compatiblePeers = Object.values(globalThis.peernet.connections || {}).filter((peer) => peer.connected && peer.version === this.version);
|
|
5208
|
-
|
|
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;
|
|
5209
5289
|
}
|
|
5210
5290
|
async #waitForPeers(timeoutMs = 30000) {
|
|
5211
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
|
|
799
|
-
return this.#
|
|
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:
|
|
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
|
-
|
|
920
|
-
await globalThis.peernet.addRequestHandler('
|
|
921
|
-
await globalThis.peernet.addRequestHandler('
|
|
922
|
-
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
934
|
-
|
|
939
|
+
else {
|
|
940
|
+
localBlock = { index: 0, hash: '0x0', previousHash: '0x0' };
|
|
935
941
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
-
|
|
941
|
-
|
|
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
|
-
|
|
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.
|
|
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,40 +1013,41 @@ class State extends Contract {
|
|
|
986
1013
|
return block;
|
|
987
1014
|
}
|
|
988
1015
|
async #resolveBlock(hash) {
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
}
|
|
992
|
-
this.#resolvingBlocks.add(hash);
|
|
1016
|
+
let index = this.#blockHashMap.get(hash);
|
|
1017
|
+
let localHash = '0x0';
|
|
993
1018
|
try {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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 fetchedTransactions = await Promise.all(transactionsToFetch.map((txHash) => globalThis.peernet.get(txHash, 'transaction')));
|
|
1014
|
-
// Batch store all transactions
|
|
1015
|
-
await Promise.all(transactionsToFetch.map((txHash, i) => globalThis.transactionStore.put(txHash, fetchedTransactions[i])));
|
|
1033
|
+
else {
|
|
1034
|
+
// Previous block already exists or is genesis, stop resolving
|
|
1035
|
+
return;
|
|
1016
1036
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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);
|
|
1021
1045
|
}
|
|
1046
|
+
;
|
|
1047
|
+
(await transactionPoolStore.has(hash)) && (await transactionPoolStore.delete(hash));
|
|
1022
1048
|
}));
|
|
1049
|
+
index = block.decoded.index;
|
|
1050
|
+
const size = block.encoded.length > 0 ? block.encoded.length : block.encoded.byteLength;
|
|
1023
1051
|
this.#totalSize += size;
|
|
1024
1052
|
this.#blocks[index] = { hash, ...block.decoded };
|
|
1025
1053
|
this.#blockHashMap.set(hash, index);
|
|
@@ -1029,147 +1057,187 @@ class State extends Contract {
|
|
|
1029
1057
|
this.#lastResolvedTime = Date.now();
|
|
1030
1058
|
}
|
|
1031
1059
|
catch (error) {
|
|
1032
|
-
throw new ResolveError(`block: ${hash}`);
|
|
1033
|
-
}
|
|
1034
|
-
finally {
|
|
1035
|
-
this.#resolvingBlocks.delete(hash);
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
async #buildBlockChain(latestHash, maxBlocks = 1000) {
|
|
1039
|
-
const chain = [];
|
|
1040
|
-
let currentHash = latestHash;
|
|
1041
|
-
let attempts = 0;
|
|
1042
|
-
while (currentHash !== '0x0' && chain.length < maxBlocks && attempts < maxBlocks + 5) {
|
|
1043
|
-
attempts++;
|
|
1044
|
-
// Check if we already have this block
|
|
1045
|
-
if (this.#blockHashMap.has(currentHash)) {
|
|
1046
|
-
const block = this.#blocks[this.#blockHashMap.get(currentHash)];
|
|
1047
|
-
if (block) {
|
|
1048
|
-
chain.push(currentHash);
|
|
1049
|
-
currentHash = block.previousHash;
|
|
1050
|
-
continue;
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
chain.push(currentHash);
|
|
1054
|
-
// Try to get the block to find previous hash
|
|
1055
|
-
try {
|
|
1056
|
-
const block = await this.getAndPutBlock(currentHash);
|
|
1057
|
-
currentHash = block.decoded.previousHash;
|
|
1058
|
-
}
|
|
1059
|
-
catch (error) {
|
|
1060
|
-
debug$1(`Could not fetch block ${currentHash} to determine chain: ${error}`);
|
|
1061
|
-
break;
|
|
1062
|
-
}
|
|
1060
|
+
throw new ResolveError(`block: ${hash}@${index}`);
|
|
1063
1061
|
}
|
|
1064
|
-
return
|
|
1065
|
-
}
|
|
1066
|
-
async #resolveBlocksInParallel(hashes) {
|
|
1067
|
-
// Resolve blocks in parallel with concurrency limit
|
|
1068
|
-
const resolving = [];
|
|
1069
|
-
let index = 0;
|
|
1070
|
-
const resolveNext = async () => {
|
|
1071
|
-
while (index < hashes.length) {
|
|
1072
|
-
const hash = hashes[index++];
|
|
1073
|
-
try {
|
|
1074
|
-
await this.#resolveBlock(hash);
|
|
1075
|
-
}
|
|
1076
|
-
catch (error) {
|
|
1077
|
-
debug$1(`Failed to resolve block ${hash}: ${error}`);
|
|
1078
|
-
this.#blockResolveQueue.push({ hash, retries: 0 });
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
};
|
|
1082
|
-
// Start concurrent resolution tasks
|
|
1083
|
-
for (let i = 0; i < Math.min(this.#maxConcurrentResolves, hashes.length); i++) {
|
|
1084
|
-
resolving.push(resolveNext());
|
|
1085
|
-
}
|
|
1086
|
-
await Promise.all(resolving);
|
|
1062
|
+
return;
|
|
1087
1063
|
}
|
|
1088
1064
|
async resolveBlock(hash) {
|
|
1089
|
-
if (!hash
|
|
1090
|
-
|
|
1091
|
-
if (
|
|
1065
|
+
if (!hash)
|
|
1066
|
+
throw new Error(`expected hash, got: ${hash}`);
|
|
1067
|
+
if (hash === '0x0')
|
|
1092
1068
|
return;
|
|
1093
|
-
this.#
|
|
1094
|
-
|
|
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();
|
|
1095
1074
|
try {
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
if (
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
await this.jobber.destroy();
|
|
1103
|
-
// Run the parallel resolution inside a timed jobber task
|
|
1104
|
-
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;
|
|
1105
1081
|
}
|
|
1082
|
+
if (!this.#blockHashMap.has(this.#lastResolved.previousHash) && this.#lastResolved.previousHash !== '0x0')
|
|
1083
|
+
return this.resolveBlock(this.#lastResolved.previousHash);
|
|
1106
1084
|
}
|
|
1107
1085
|
catch (error) {
|
|
1108
|
-
console.
|
|
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;
|
|
1109
1092
|
this.wantList.push(hash);
|
|
1110
|
-
|
|
1111
|
-
finally {
|
|
1112
|
-
this.#syncing = false;
|
|
1093
|
+
throw new ResolveError(`block: ${hash}`, { cause: error });
|
|
1113
1094
|
}
|
|
1114
1095
|
}
|
|
1115
1096
|
async resolveBlocks() {
|
|
1116
|
-
if
|
|
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()');
|
|
1117
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
|
+
}
|
|
1118
1110
|
try {
|
|
1119
1111
|
const localBlock = await globalThis.chainStore.get('lastBlock');
|
|
1120
1112
|
const hash = new TextDecoder().decode(localBlock);
|
|
1121
1113
|
if (hash && hash !== '0x0') {
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1114
|
+
debug$1(`Resolving blocks from hash: ${hash}`);
|
|
1115
|
+
await this.resolveBlock(hash);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
catch (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
1133
|
}
|
|
1134
1134
|
}
|
|
1135
1135
|
catch (error) {
|
|
1136
|
-
|
|
1136
|
+
console.log(error);
|
|
1137
|
+
this.#resolveErrored = true;
|
|
1138
|
+
this.#resolveErrorCount += 1;
|
|
1139
|
+
this.#resolving = false;
|
|
1140
|
+
return this.restoreChain();
|
|
1141
|
+
// console.log(e);
|
|
1137
1142
|
}
|
|
1138
1143
|
}
|
|
1139
1144
|
async syncChain(lastBlock) {
|
|
1140
|
-
if
|
|
1141
|
-
|
|
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
|
+
}
|
|
1142
1159
|
if (!lastBlock)
|
|
1143
1160
|
lastBlock = await this.#getLatestBlock();
|
|
1144
|
-
|
|
1161
|
+
if (globalThis.peernet.peers.length === 0)
|
|
1162
|
+
return 'connectionless';
|
|
1145
1163
|
try {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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'));
|
|
1149
1205
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
const localIndex = (await this.lastBlock).index || -1;
|
|
1153
|
-
const start = Math.max(0, localIndex + 1);
|
|
1154
|
-
if (this.#machine && blocks.length > start) {
|
|
1155
|
-
await this.#loadBlocks(blocks.slice(start));
|
|
1206
|
+
catch (error) {
|
|
1207
|
+
debug$1(`No local state hash found: ${error}`);
|
|
1156
1208
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
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`);
|
|
1159
1236
|
}
|
|
1160
|
-
this.#syncErrorCount = 0;
|
|
1161
|
-
this.#syncing = false;
|
|
1162
|
-
return 'synced';
|
|
1163
1237
|
}
|
|
1164
1238
|
catch (error) {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
this.#syncing = false;
|
|
1168
|
-
return this.syncChain(lastBlock);
|
|
1169
|
-
}
|
|
1170
|
-
this.#syncErrorCount = 0;
|
|
1171
|
-
this.#syncing = false;
|
|
1172
|
-
return 'errored';
|
|
1239
|
+
console.log(error);
|
|
1240
|
+
throw error;
|
|
1173
1241
|
}
|
|
1174
1242
|
}
|
|
1175
1243
|
async #getLatestBlock() {
|
|
@@ -1205,9 +1273,7 @@ class State extends Contract {
|
|
|
1205
1273
|
debug$1(`Latest block from peers: ${latest.hash} @${latest.index}`);
|
|
1206
1274
|
if (latest.hash && latest.hash !== '0x0') {
|
|
1207
1275
|
let message = await globalThis.peernet.get(latest.hash, 'block');
|
|
1208
|
-
debug$1({ message });
|
|
1209
1276
|
message = await new BlockMessage(message);
|
|
1210
|
-
debug$1({ message });
|
|
1211
1277
|
const hash = await message.hash();
|
|
1212
1278
|
if (hash !== latest.hash)
|
|
1213
1279
|
throw new Error('invalid block @getLatestBlock');
|
|
@@ -1220,7 +1286,7 @@ class State extends Contract {
|
|
|
1220
1286
|
let node = await globalThis.peernet.prepareMessage(data);
|
|
1221
1287
|
let message = await peer.request(node.encode());
|
|
1222
1288
|
message = await new globalThis.peernet.protos['peernet-response'](message);
|
|
1223
|
-
this.wantList.push(...message.decoded.response);
|
|
1289
|
+
this.wantList.push(...message.decoded.response.blocks.filter((block) => !this.knownBlocks.includes(block)));
|
|
1224
1290
|
}
|
|
1225
1291
|
}
|
|
1226
1292
|
return latest;
|
|
@@ -1340,13 +1406,26 @@ class State extends Contract {
|
|
|
1340
1406
|
});
|
|
1341
1407
|
}
|
|
1342
1408
|
get canSync() {
|
|
1343
|
-
|
|
1409
|
+
if (this.#chainSyncing)
|
|
1410
|
+
return false;
|
|
1411
|
+
return true;
|
|
1344
1412
|
}
|
|
1345
1413
|
get shouldSync() {
|
|
1346
|
-
if (this.#
|
|
1414
|
+
if (this.#chainSyncing)
|
|
1347
1415
|
return false;
|
|
1416
|
+
// Check if we have any connected peers with the same version
|
|
1348
1417
|
const compatiblePeers = Object.values(globalThis.peernet.connections || {}).filter((peer) => peer.connected && peer.version === this.version);
|
|
1349
|
-
|
|
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;
|
|
1350
1429
|
}
|
|
1351
1430
|
async #waitForPeers(timeoutMs = 30000) {
|
|
1352
1431
|
return new Promise((resolve) => {
|
package/exports/state.d.ts
CHANGED
|
@@ -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
|
|
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:
|
|
51
|
-
resolveBlocks(): Promise<
|
|
52
|
-
|
|
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<
|
|
63
|
+
triggerSync(): Promise<SyncState>;
|
|
57
64
|
triggerLoad(): Promise<void>;
|
|
58
65
|
}
|
|
66
|
+
export {};
|