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