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