@leofcoin/chain 1.10.21 → 1.11.0

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.
Files changed (29) hide show
  1. package/README.md +95 -0
  2. package/exports/browser/beacon-wire.js +2 -2
  3. package/exports/browser/{browser-CCfR9KOM-BGMMnVsR.js → browser-CCfR9KOM-CNWKLusu.js} +3 -3
  4. package/exports/browser/{browser-crypto-CH0ua22L.js → browser-crypto-B_UJguhk.js} +453 -35
  5. package/exports/browser/chain.js +1275 -996
  6. package/exports/browser/client-DbZBfpG6-C1cXgvfr.js +5943 -0
  7. package/exports/browser/{connection-monitor-D-krNiRH.js → connection-monitor-BzbeYGEt.js} +153 -24
  8. package/exports/browser/{index-dTQVgkpf-DOL2rIjp.js → index-dTQVgkpf-CkoAiRNX.js} +3 -3
  9. package/exports/browser/{index-DCbLvMKL.js → index-phv5KvaE.js} +238 -234
  10. package/exports/browser/{messages-DCePyqKC-BSvJxDWZ.js → messages-DZYCxoZI-B472zldo.js} +3 -3
  11. package/exports/browser/{node-browser-Ha4Lvntj.js → node-browser-BmyPBkL5.js} +55 -11
  12. package/exports/browser/node-browser.js +3 -3
  13. package/exports/browser/workers/block-worker.js +8 -9
  14. package/exports/browser/workers/machine-worker.js +141 -1195
  15. package/exports/{workers/worker-BDTWOCzS-COrAszCt.js → browser/workers/proposal.proto-8zgMfutf-B3So_tnS.js} +910 -398
  16. package/exports/chain.js +296 -1078
  17. package/exports/connection-monitor-BdFLRh-J.js +465 -0
  18. package/exports/connection-monitor.js +2 -324
  19. package/exports/{constants-Dy9i9S_o.js → constants-BQ6f-_OT.js} +1 -1
  20. package/exports/hash-format-state.js +3 -3
  21. package/exports/machine.js +550 -0
  22. package/exports/node.js +35 -2
  23. package/exports/state-checkpoint-BiQ1EXzo.js +110 -0
  24. package/exports/state-checkpoint.js +3 -0
  25. package/exports/workers/block-worker.js +8 -9
  26. package/exports/workers/machine-worker.js +141 -1195
  27. package/exports/{browser/workers/worker-BDTWOCzS-COrAszCt.js → workers/proposal.proto-8zgMfutf-B3So_tnS.js} +910 -398
  28. package/package.json +9 -7
  29. package/exports/browser/client-DKKzPSlb-C_1ug6ux.js +0 -2401
package/exports/chain.js CHANGED
@@ -1,65 +1,74 @@
1
- import { createDebugger } from '@vandeurenglenn/debug';
1
+ import { decodeWire, encodeWire } from '@leofcoin/binary';
2
2
  import { Codec } from '@leofcoin/codec-format-interface';
3
- import { jsonStringifyBigInt, jsonParseBigInt, formatBytes, parseUnits, formatUnits } from '@leofcoin/utils';
4
- import { TransactionMessage, BlockMessage, ContractMessage, LastBlockMessage, PrevoteMessage, PrecommitMessage, ProposalMessage, BWMessage, StateMessage } from '@leofcoin/messages';
5
- import addresses, { contractFactory, nativeToken } from '@leofcoin/addresses';
6
- import { MAX_TRANSACTION_BYTES, MAX_BLOCK_TRANSACTION_BYTES, MAX_BLOCK_TRANSACTIONS, createTransactionHash, calculateFee, createContractMessage, signTransaction, distributeTransactionFee, supportsTransactionFees, supportsMonetaryPolicy, validateBlockResourceLimits, calculateMonetaryPolicy, distributeAmount, aggregateValidatorFees, MONETARY_POLICY_AUTHORITY, contractFactoryMessage, nativeTokenMessage, validatorsMessage, nameServiceMessage } from '@leofcoin/lib';
3
+ import { createDebugger } from '@vandeurenglenn/debug';
4
+ import { formatBytes, parseUnits, formatUnits } from '@leofcoin/utils';
5
+ import { TransactionMessage, BlockMessage, LastBlockMessage, PrevoteMessage, PrecommitMessage, ProposalMessage, ContractMessage, BWMessage, StateMessage } from '@leofcoin/messages';
6
+ import addresses from '@leofcoin/addresses';
7
+ import { MAX_TRANSACTION_BYTES, MAX_BLOCK_TRANSACTION_BYTES, MAX_BLOCK_TRANSACTIONS, createTransactionHash, calculateFee, createContractMessage, signTransaction, supportsTransactionFees, supportsMonetaryPolicy, validateBlockResourceLimits, aggregateValidatorFees, distributeAmount, MONETARY_POLICY_AUTHORITY, calculateMonetaryPolicy, contractFactoryMessage, nativeTokenMessage, validatorsMessage, nameServiceMessage, distributeTransactionFee } from '@leofcoin/lib';
7
8
  import MultiWallet from '@leofcoin/multi-wallet';
8
9
  import { fromBase58 } from '@vandeurenglenn/typed-array-utils';
9
10
  import semver from 'semver';
10
- import { randombytes } from '@leofcoin/crypto';
11
- import EasyWorker from '@vandeurenglenn/easy-worker';
12
- import { ContractDeploymentError, ExecutionError, isResolveError, ResolveError, isExecutionError } from '@leofcoin/errors';
13
- import { P as PROTOCOL_VERSION, R as REACHED_ONE_ZERO_ZERO } from './constants-Dy9i9S_o.js';
14
- import ConnectionMonitor from './connection-monitor.js';
11
+ import { P as Protocol, C as ConnectionMonitor } from './connection-monitor-BdFLRh-J.js';
12
+ import Machine from './machine.js';
13
+ import { createChainIdentity, assertChainId, findChainPresence } from '@leofcoin/lib/chain-identity';
14
+ import { r as readCheckpoint, a as recoveryHistory, b as restoreAccountNonces, v as validateChainLink } from './state-checkpoint-BiQ1EXzo.js';
15
+ import { ResolveError } from '@leofcoin/errors';
16
+ import { P as PROTOCOL_VERSION, R as REACHED_ONE_ZERO_ZERO } from './constants-BQ6f-_OT.js';
15
17
  import { q as quorumThreshold } from './quorum-S_qdgiAY.js';
16
18
  import { assertHashFormatState } from './hash-format-state.js';
17
- import { createChainIdentity, assertChainId, findChainPresence } from '@leofcoin/lib/chain-identity';
19
+ import '@leofcoin/crypto';
20
+ import '@leofcoin/workers/binary-worker';
18
21
  import '@leofcoin/networks';
19
22
 
20
- const limit = 1800;
21
- const transactionLimit = 2500;
22
- const requestTimeout = 3e4;
23
- const syncTimeout = 3e4;
24
- class Protocol {
25
- constructor(config) {
26
- this.resolveTimeout = 1e4;
27
- if (config?.resolveTimeout) this.resolveTimeout = config.resolveTimeout;
28
- }
29
- get limit() {
30
- return limit;
31
- }
32
- get transactionLimit() {
33
- return transactionLimit;
34
- }
35
- get requestTimeout() {
36
- return requestTimeout;
37
- }
38
- get syncTimeout() {
39
- return syncTimeout;
40
- }
41
- }
23
+ const decodeResponsePayload = async (result) => {
24
+ if (!(result instanceof Uint8Array)) throw new TypeError("Peer response must be Uint8Array");
25
+ const codec = new Codec(result);
26
+ if (codec.name !== "peernet-response") return result;
27
+ const ResponseMessage = globalThis.peernet?.protos?.["peernet-response"];
28
+ if (!ResponseMessage) throw new Error("peernet-response codec is not registered");
29
+ return new ResponseMessage(result).decoded.response;
30
+ };
42
31
 
43
- const waitForCanonicalTransaction = (pubsub, transactionPool, hash) => new Promise((resolve, reject) => {
44
- const completionTopic = `transaction.completed.${hash}`;
45
- const cleanup = () => {
46
- pubsub.unsubscribe("block-processed", blockProcessed);
47
- pubsub.unsubscribe(completionTopic, transactionCompleted);
32
+ const createCanonicalTransactionWaiter = (pubsub, transactionPool, hash) => {
33
+ let cleanup = () => {
48
34
  };
49
- const blockProcessed = (block) => {
50
- if (!Array.isArray(block?.transactions) || !block.transactions.includes(hash)) return;
51
- cleanup();
52
- resolve(hash);
35
+ let cancel = (_reason) => {
53
36
  };
54
- const transactionCompleted = async (result) => {
55
- if (result?.status === "fulfilled") return;
56
- await transactionPool.delete(hash);
57
- cleanup();
58
- reject({ hash, error: result?.error });
59
- };
60
- pubsub.subscribe("block-processed", blockProcessed);
61
- pubsub.subscribe(completionTopic, transactionCompleted);
62
- });
37
+ const wait = new Promise((resolve, reject) => {
38
+ const completionTopic = `transaction.completed.${hash}`;
39
+ let settled = false;
40
+ cancel = (reason) => {
41
+ if (settled) return;
42
+ settled = true;
43
+ reject({ hash, error: reason });
44
+ };
45
+ cleanup = () => {
46
+ pubsub.unsubscribe("block-processed", blockProcessed);
47
+ pubsub.unsubscribe(completionTopic, transactionCompleted);
48
+ };
49
+ const blockProcessed = (block) => {
50
+ if (settled || !Array.isArray(block?.transactions) || !block.transactions.includes(hash)) return;
51
+ settled = true;
52
+ resolve(hash);
53
+ };
54
+ const transactionCompleted = async (result) => {
55
+ if (settled || result?.status === "fulfilled") return;
56
+ settled = true;
57
+ try {
58
+ await transactionPool.delete(hash);
59
+ reject({ hash, error: result?.error });
60
+ } catch (error) {
61
+ reject({ hash, error, transactionError: result?.error });
62
+ }
63
+ };
64
+ pubsub.subscribe("block-processed", blockProcessed);
65
+ pubsub.subscribe(completionTopic, transactionCompleted);
66
+ });
67
+ const finalized = wait.finally(() => cleanup());
68
+ void finalized.catch(() => {
69
+ });
70
+ return { wait: finalized, cancel };
71
+ };
63
72
 
64
73
  class Transaction extends Protocol {
65
74
  #pendingNonces = /* @__PURE__ */ new Map();
@@ -260,9 +269,10 @@ class Transaction extends Protocol {
260
269
  await this.validateTransactionSignature(message);
261
270
  await this.validateNonce(message.decoded.from, message.decoded.nonce);
262
271
  const hash = await message.hash();
272
+ const fee = await calculateFee(message.decoded);
273
+ const waiter = createCanonicalTransactionWaiter(pubsub, transactionPoolStore, hash);
263
274
  try {
264
275
  let data;
265
- const wait = waitForCanonicalTransaction(pubsub, transactionPoolStore, hash);
266
276
  await globalThis.transactionPoolStore.put(hash, message.encoded);
267
277
  this.addPendingNonce(message.decoded.from, message.decoded.nonce);
268
278
  try {
@@ -270,12 +280,16 @@ class Transaction extends Protocol {
270
280
  } catch (publishError) {
271
281
  console.warn("peernet publish failed: add-transaction", publishError?.message ?? publishError);
272
282
  }
273
- const fee = await calculateFee(message.decoded);
274
- return { hash, data, fee, wait, message };
283
+ return { hash, data, fee, wait: waiter.wait, message };
275
284
  } catch (error) {
276
- console.log("remo");
277
- await transactionPoolStore.delete(hash);
278
- this.removePendingNonce(message.decoded.from, message.decoded.nonce);
285
+ waiter.cancel(error);
286
+ try {
287
+ await transactionPoolStore.delete(hash);
288
+ } catch (cleanupError) {
289
+ console.warn("failed to clean up rejected transaction:", cleanupError);
290
+ } finally {
291
+ this.removePendingNonce(message.decoded.from, message.decoded.nonce);
292
+ }
279
293
  throw error;
280
294
  }
281
295
  }
@@ -602,511 +616,6 @@ class Contract extends Transaction {
602
616
  }
603
617
  }
604
618
 
605
- const debug$2 = createDebugger("leofcoin/machine");
606
- class Machine {
607
- constructor(blocks) {
608
- this.states = {
609
- states: {},
610
- lastBlock: {
611
- index: -1,
612
- hash: ""
613
- },
614
- accounts: {},
615
- info: {
616
- nativeCalls: BigInt(0),
617
- nativeMints: BigInt(0),
618
- nativeBurns: BigInt(0),
619
- nativeTransfers: BigInt(0),
620
- totalBurnAmount: BigInt(0),
621
- totalMintAmount: BigInt(0),
622
- totalTransferAmount: BigInt(0),
623
- totalTransactions: BigInt(0),
624
- totalBlocks: BigInt(0)
625
- }
626
- };
627
- this.wantList = [];
628
- this.ready = new Promise((resolve) => {
629
- this.readyResolve = resolve;
630
- });
631
- this.init(blocks);
632
- }
633
- init(blocks) {
634
- return this.#init(blocks);
635
- }
636
- async #onmessage(data) {
637
- switch (data.type) {
638
- case "transactionLoaded": {
639
- const { from, nonce, hash } = data.result;
640
- await transactionPoolStore.has(hash) && await transactionPoolStore.delete(hash);
641
- try {
642
- const _nonce = await accountsStore.get(from);
643
- if (nonce > _nonce) await accountsStore.put(from, nonce);
644
- } catch (error) {
645
- await accountsStore.put(from, nonce);
646
- }
647
- break;
648
- }
649
- case "contractError": {
650
- console.warn(data.error);
651
- console.warn(`contract error: ${data.hash}`);
652
- await contractStore.delete(data.hash);
653
- break;
654
- }
655
- case "initError": {
656
- console.error(`init error: ${data.message}`);
657
- break;
658
- }
659
- case "executeError": {
660
- pubsub.publish(data.id, { error: data.message });
661
- break;
662
- }
663
- case "addLoadedBlockError":
664
- case "collectFeeError":
665
- case "settleRewardsError": {
666
- pubsub.publish(data.id, { error: new Error(data.message) });
667
- break;
668
- }
669
- case "debug": {
670
- if (data.message.includes("loaded transactions for block:")) {
671
- pubsub.publish("block-loaded", data.message.replace("loaded transactions for block: ", "").split(" @")[0]);
672
- }
673
- break;
674
- }
675
- case "error": {
676
- console.error(data.message);
677
- break;
678
- }
679
- case "machine-ready": {
680
- this.readyResolve(this);
681
- pubsub.publish("machine.ready", true);
682
- break;
683
- }
684
- case "response": {
685
- pubsub.publish(data.id, data.value || false);
686
- break;
687
- }
688
- case "addToWantList": {
689
- if (!this.wantList.includes(data.hash)) this.wantList.push(data.hash);
690
- break;
691
- }
692
- case "ask": {
693
- if (data.question === "contract" || data.question === "transaction") {
694
- try {
695
- const input = await peernet.get(data.input, data.question);
696
- if (input === void 0) {
697
- this.worker.postMessage({ id: data.id, error: `could not get ${data.question} @${data.input}` });
698
- this.wantList.push(data.input);
699
- } else {
700
- this.worker.postMessage({ id: data.id, input });
701
- }
702
- } catch (error) {
703
- console.error(error);
704
- this.worker.postMessage({ id: data.id, error: `could not get ${data.question} @${data.input}` });
705
- this.wantList.push(data.input);
706
- }
707
- } else if (data.question === "peers") {
708
- this.worker.postMessage({
709
- id: data.id,
710
- input: peernet.connections ? Object.entries(peernet.connections).map(([id, peer]) => peer.toJSON()) : []
711
- });
712
- } else {
713
- this.worker.postMessage({ id: data.id, input: data.input });
714
- }
715
- }
716
- }
717
- }
718
- async updateState(strict = false) {
719
- try {
720
- const lastBlock = strict ? await this.getCanonicalTip() : await this.lastBlock;
721
- const lastBlockIndex = Number(lastBlock.index);
722
- const statesLastBlockIndex = Number(this.states.lastBlock.index);
723
- if (lastBlockIndex > statesLastBlockIndex || lastBlock.hash !== this.states.lastBlock.hash && Number(lastBlockIndex) === 0) {
724
- const blocks = (await this.blocks).slice(statesLastBlockIndex);
725
- let transactions = [];
726
- for (const block of blocks) {
727
- if (block?.transactions) transactions = [...transactions, ...block.transactions];
728
- }
729
- transactions = await Promise.all(
730
- transactions.map(async (transaction) => new TransactionMessage(await transactionStore.get(transaction)))
731
- );
732
- const contractsToGet = transactions.reduce((set, current) => {
733
- const contract = current.decoded.to;
734
- if (!set.includes(contract)) set.push(contract);
735
- return set;
736
- }, []);
737
- const state = {};
738
- if (!contractsToGet.includes(addresses.contractFactory)) contractsToGet.push(addresses.contractFactory);
739
- if (!contractsToGet.includes(addresses.nativeToken)) contractsToGet.push(addresses.nativeToken);
740
- if (!contractsToGet.includes(addresses.nameService)) contractsToGet.push(addresses.nameService);
741
- if (!contractsToGet.includes(addresses.validators)) contractsToGet.push(addresses.validators);
742
- await Promise.all(
743
- contractsToGet.map(async (contract) => {
744
- const value = await this.#askWorker("get", { contract, method: "state", params: [] });
745
- state[contract] = value;
746
- })
747
- );
748
- let accounts = {};
749
- try {
750
- const promises = [];
751
- for (const address of await accountsStore.keys()) {
752
- promises.push(
753
- (async () => {
754
- accounts[address] = await accountsStore.get(address);
755
- })()
756
- );
757
- }
758
- await Promise.all(promises);
759
- } catch (error) {
760
- const promises = [];
761
- for (const transaction of transactions.reverse()) {
762
- const { from, to, amount, nonce } = transaction.decoded.data;
763
- if (!accounts[from]) {
764
- accounts[from] = nonce;
765
- promises.push(accountsStore.put(from, nonce));
766
- }
767
- }
768
- await Promise.all(promises);
769
- }
770
- const sortedStringify = (obj, replacer) => {
771
- const sorted = {};
772
- const keys = Object.keys(obj).sort();
773
- for (const key of keys) {
774
- sorted[key] = obj[key];
775
- }
776
- return JSON.stringify(sorted, replacer);
777
- };
778
- const tasks = [
779
- stateStore.put("lastBlock", sortedStringify(lastBlock, jsonStringifyBigInt)),
780
- stateStore.put("states", sortedStringify(state, jsonStringifyBigInt)),
781
- stateStore.put("accounts", sortedStringify(accounts, jsonStringifyBigInt)),
782
- stateStore.put(
783
- "info",
784
- sortedStringify(
785
- {
786
- nativeBurnAmount: await this.totalBurnAmount,
787
- nativeBurns: await this.nativeBurns,
788
- nativeCalls: await this.nativeCalls,
789
- nativeMints: await this.nativeMints,
790
- nativeTransfers: await this.nativeTransfers,
791
- totalBlocks: await blockStore.length,
792
- totalBurnAmount: await this.totalBurnAmount,
793
- totalMintAmount: await this.totalMintAmount,
794
- totalTransactions: await this.totalTransactions,
795
- totalTransferAmount: await this.totalTransferAmount
796
- },
797
- jsonStringifyBigInt
798
- )
799
- )
800
- // accountsStore.clear()
801
- ];
802
- await Promise.all(tasks);
803
- }
804
- } catch (error) {
805
- console.error(error);
806
- if (strict) throw error;
807
- }
808
- }
809
- async #init(blocks) {
810
- return new Promise(async (resolve) => {
811
- const machineReady = async () => {
812
- pubsub.unsubscribe("machine.ready", machineReady);
813
- await this.updateState();
814
- resolve(this);
815
- };
816
- pubsub.subscribe("machine.ready", machineReady);
817
- let pre;
818
- try {
819
- const importee = await import('url');
820
- const url = importee.default;
821
- if (url) pre = url.fileURLToPath(new URL(".", import.meta.url));
822
- } catch {
823
- pre = "./";
824
- }
825
- this.worker = await new EasyWorker(pre + "workers/machine-worker.js", {
826
- serialization: "advanced",
827
- type: "module"
828
- });
829
- this.worker.onmessage(this.#onmessage.bind(this));
830
- let rawLastBlock;
831
- let rawStates;
832
- let rawAccounts;
833
- let rawInfo;
834
- if (await stateStore.has("lastBlock")) {
835
- const decoder = new TextDecoder();
836
- const decode = (param) => decoder.decode(param);
837
- rawLastBlock = decode(await stateStore.get("lastBlock"));
838
- this.states.lastBlock = JSON.parse(rawLastBlock, jsonParseBigInt);
839
- try {
840
- rawStates = decode(await stateStore.get("states"));
841
- rawAccounts = decode(await stateStore.get("accounts"));
842
- rawInfo = decode(await stateStore.get("info"));
843
- this.states.states = JSON.parse(rawStates, jsonParseBigInt);
844
- this.states.accounts = JSON.parse(rawAccounts, jsonParseBigInt);
845
- this.states.info = JSON.parse(rawInfo, jsonParseBigInt);
846
- } catch (error) {
847
- console.error(error);
848
- this.states.accounts = {};
849
- this.states.info = {
850
- nativeCalls: BigInt(0),
851
- nativeMints: BigInt(0),
852
- nativeBurns: BigInt(0),
853
- nativeTransfers: BigInt(0),
854
- totalBurnAmount: BigInt(0),
855
- totalMintAmount: BigInt(0),
856
- totalTransferAmount: BigInt(0),
857
- totalTransactions: BigInt(0),
858
- totalBlocks: BigInt(0)
859
- };
860
- rawInfo = JSON.stringify(this.states.info, jsonStringifyBigInt);
861
- await stateStore.put("info", new TextEncoder().encode(rawInfo));
862
- }
863
- }
864
- const message = {
865
- type: "init",
866
- input: {
867
- blocks,
868
- fromState: this.states.lastBlock?.index >= 0,
869
- lastBlock: rawLastBlock,
870
- state: rawStates,
871
- info: rawInfo,
872
- // @ts-ignore
873
- peerid: peernet.peerId
874
- }
875
- };
876
- this.worker.postMessage(message);
877
- });
878
- }
879
- async #runContract(contractMessage) {
880
- const hash = await contractMessage.hash();
881
- return new Promise((resolve, reject) => {
882
- const id = randombytes(20).toString("hex");
883
- const onmessage = (message) => {
884
- pubsub.unsubscribe(id, onmessage);
885
- if (message?.error) reject(message.error);
886
- else resolve(message);
887
- };
888
- pubsub.subscribe(id, onmessage);
889
- this.worker.postMessage({
890
- type: "run",
891
- id,
892
- input: {
893
- decoded: contractMessage.decoded,
894
- encoded: contractMessage.encoded,
895
- hash
896
- }
897
- });
898
- });
899
- }
900
- /**
901
- *
902
- * @param {Address} contract
903
- * @param {String} method
904
- * @param {Array} parameters
905
- * @returns Promise<message>
906
- */
907
- async execute(contract, method, parameters, sender, execution) {
908
- try {
909
- if (contract === contractFactory && method === "registerContract") {
910
- if (await this.has(parameters[0])) throw new Error(`duplicate contract @${parameters[0]}`);
911
- let message;
912
- if (!await globalThis.contractStore.has(parameters[0])) {
913
- message = await peernet.get(parameters[0], "contract");
914
- if (!message) throw new Error(`contract ${parameters[0]} not available`);
915
- message = await new ContractMessage(message);
916
- await globalThis.contractStore.put(await message.hash(), message.encoded);
917
- }
918
- if (!message) {
919
- message = await globalThis.contractStore.get(parameters[0]);
920
- message = await new ContractMessage(message);
921
- }
922
- if (!await this.has(await message.hash())) await this.#runContract(message);
923
- }
924
- } catch (error) {
925
- throw new ContractDeploymentError(`contract deployment failed for ${parameters[0]}
926
- ${error.message}`);
927
- }
928
- return new Promise((resolve, reject) => {
929
- const id = randombytes(20).toString("hex");
930
- const timeout = setTimeout(() => {
931
- pubsub.unsubscribe(id, onmessage);
932
- reject(new Error(`Machine.execute timeout for ${contract}.${method}`));
933
- }, 3e4);
934
- const onmessage = (message) => {
935
- clearTimeout(timeout);
936
- pubsub.unsubscribe(id, onmessage);
937
- if (message?.error) reject(new ExecutionError(message.error));
938
- else resolve(message);
939
- };
940
- pubsub.subscribe(id, onmessage);
941
- this.worker.postMessage({
942
- type: "execute",
943
- id,
944
- input: {
945
- to: contract,
946
- contract,
947
- method,
948
- params: parameters,
949
- sender,
950
- executionSeed: execution?.seed,
951
- executionTimestamp: execution?.timestamp,
952
- executionRandomness: execution?.randomness
953
- }
954
- });
955
- });
956
- }
957
- collectFee(from, payments, burned) {
958
- return this.#askWorker("collectFee", { from, payments, burned });
959
- }
960
- settleRewards(rewards) {
961
- return this.#askWorker("settleRewards", { rewards });
962
- }
963
- get(contract, method, parameters) {
964
- return new Promise((resolve, reject) => {
965
- const id = randombytes(20).toString();
966
- const timeout = setTimeout(() => {
967
- pubsub.unsubscribe(id, onmessage);
968
- reject(new Error(`Machine.get timeout for ${contract}.${method}`));
969
- }, 3e4);
970
- const onmessage = (message) => {
971
- clearTimeout(timeout);
972
- pubsub.unsubscribe(id, onmessage);
973
- resolve(message);
974
- };
975
- pubsub.subscribe(id, onmessage);
976
- this.worker.postMessage({
977
- type: "get",
978
- id,
979
- input: {
980
- contract,
981
- method,
982
- params: parameters
983
- }
984
- });
985
- });
986
- }
987
- async has(address) {
988
- return new Promise((resolve, reject) => {
989
- const id = randombytes(20).toString("hex");
990
- const timeout = setTimeout(() => {
991
- pubsub.unsubscribe(id, onmessage);
992
- reject(new Error(`Machine.has timeout for ${address}`));
993
- }, 1e4);
994
- const onmessage = (message) => {
995
- clearTimeout(timeout);
996
- pubsub.unsubscribe(id, onmessage);
997
- if (message?.error) reject(message.error);
998
- else resolve(message);
999
- };
1000
- pubsub.subscribe(id, onmessage);
1001
- this.worker.postMessage({
1002
- type: "has",
1003
- id,
1004
- input: {
1005
- address
1006
- }
1007
- });
1008
- });
1009
- }
1010
- #askWorker(type, input) {
1011
- return new Promise((resolve, reject) => {
1012
- const id = randombytes(20).toString("hex");
1013
- const timeout = setTimeout(() => {
1014
- pubsub.unsubscribe(id, onmessage);
1015
- reject(new Error(`Machine.#askWorker timeout for ${type}`));
1016
- }, 3e4);
1017
- const onmessage = (message) => {
1018
- clearTimeout(timeout);
1019
- pubsub.unsubscribe(id, onmessage);
1020
- if (message?.error) reject(message.error);
1021
- else resolve(message);
1022
- };
1023
- pubsub.subscribe(id, onmessage);
1024
- this.worker.postMessage({
1025
- type,
1026
- id,
1027
- input
1028
- });
1029
- });
1030
- }
1031
- get contracts() {
1032
- return this.#askWorker("contracts");
1033
- }
1034
- get totalContracts() {
1035
- return this.#askWorker("totalContracts");
1036
- }
1037
- get nativeCalls() {
1038
- return this.#askWorker("nativeCalls");
1039
- }
1040
- get nativeMints() {
1041
- return this.#askWorker("nativeMints");
1042
- }
1043
- get nativeBurns() {
1044
- return this.#askWorker("nativeBurns");
1045
- }
1046
- get nativeTransfers() {
1047
- return this.#askWorker("nativeTransfers");
1048
- }
1049
- get totalBurnAmount() {
1050
- return this.#askWorker("totalBurnAmount");
1051
- }
1052
- get totalMintAmount() {
1053
- return this.#askWorker("totalMintAmount");
1054
- }
1055
- get totalTransferAmount() {
1056
- return this.#askWorker("totalTransferAmount");
1057
- }
1058
- get totalTransactions() {
1059
- return this.#askWorker("totalTransactions");
1060
- }
1061
- get totalBlocks() {
1062
- return this.#askWorker("totalBlocks");
1063
- }
1064
- get blocks() {
1065
- return this.getBlocks();
1066
- }
1067
- get lastBlock() {
1068
- return this.#askWorker("lastBlock").catch((error) => {
1069
- debug$2("lastBlock fallback after worker timeout:", error?.message ?? error);
1070
- return this.states.lastBlock;
1071
- });
1072
- }
1073
- getCanonicalTip() {
1074
- return this.#askWorker("lastBlock");
1075
- }
1076
- get lastBlockHeight() {
1077
- return this.#askWorker("lastBlockHeight").catch((error) => {
1078
- debug$2("lastBlockHeight fallback after worker timeout:", error?.message ?? error);
1079
- return Number(this.states.lastBlock?.index ?? 0);
1080
- });
1081
- }
1082
- getBlocks(from, to) {
1083
- return this.#askWorker("blocks", { from, to });
1084
- }
1085
- getBlock(index) {
1086
- return this.#askWorker("block", index);
1087
- }
1088
- async addLoadedBlock(block) {
1089
- debug$2(`adding loaded block: ${block.index}@${block.hash}`);
1090
- if (block.decoded) block = { ...block.decoded, hash: await block.hash() };
1091
- return this.#askWorker("addLoadedBlock", JSON.stringify(block, jsonStringifyBigInt));
1092
- }
1093
- async latestTransactions() {
1094
- return this.#askWorker("latestTransactions");
1095
- }
1096
- async delete(hash) {
1097
- return globalThis.contractStore.delete(hash);
1098
- }
1099
- /**
1100
- *
1101
- * @returns Promise
1102
- */
1103
- async deleteAll() {
1104
- let hashes = await globalThis.contractStore.keys();
1105
- hashes = Object.keys(hashes).map((hash) => this.delete(hash));
1106
- return Promise.all(hashes);
1107
- }
1108
- }
1109
-
1110
619
  class Jobber {
1111
620
  constructor(timeout) {
1112
621
  this.busy = false;
@@ -1136,91 +645,19 @@ class Jobber {
1136
645
  }
1137
646
  }
1138
647
 
1139
- const MAX_RESPONSE_DEPTH$1 = 3;
1140
- const asLastBlockMessage = async (blockInput, claimedHash) => {
1141
- const block = new BlockMessage(blockInput);
1142
- const hash = await block.hash();
1143
- if (claimedHash && hash !== claimedHash) {
1144
- throw new Error(`lastBlock hash mismatch: expected ${claimedHash}, got ${hash}`);
1145
- }
1146
- return new LastBlockMessage({ hash, index: block.decoded.index });
1147
- };
1148
- const resolveLegacyHash = async (hash) => {
1149
- const blockData = await globalThis.peernet?.get?.(hash, "block");
1150
- if (!blockData) throw new Error(`unable to resolve lastBlock hash: ${hash}`);
1151
- return asLastBlockMessage(blockData, hash);
1152
- };
1153
- const resolveLastBlockMessage = async (result, depth = 0) => {
1154
- if (depth > MAX_RESPONSE_DEPTH$1) throw new Error("lastBlock response nesting exceeds limit");
1155
- if (result instanceof Uint8Array) {
1156
- let codec;
1157
- try {
1158
- codec = new Codec(result);
1159
- } catch {
1160
- }
1161
- if (codec) {
1162
- if (codec.name === "peernet-response") {
1163
- const ResponseMessage = globalThis.peernet?.protos?.["peernet-response"];
1164
- if (!ResponseMessage) throw new Error("peernet-response codec is not registered");
1165
- const wrapped = new ResponseMessage(result);
1166
- if (wrapped?.decoded?.response === void 0) throw new Error("peernet-response has no response payload");
1167
- return resolveLastBlockMessage(wrapped.decoded.response, depth + 1);
1168
- }
1169
- if (codec.name === "last-block-message") return new LastBlockMessage(result);
1170
- if (codec.name === "block-message") return asLastBlockMessage(result);
1171
- }
1172
- const candidate = new TextDecoder().decode(result);
1173
- if (candidate) return resolveLegacyHash(candidate);
1174
- }
1175
- if (typeof result === "string") return resolveLegacyHash(result);
1176
- if (result && typeof result === "object") {
1177
- const response = result.decoded?.response ?? result.response;
1178
- if (response !== void 0) return resolveLastBlockMessage(response, depth + 1);
1179
- if ("hash" in result && "index" in result) return new LastBlockMessage(result);
1180
- if ("previousHash" in result && "index" in result) return asLastBlockMessage(result);
1181
- }
1182
- throw new Error(`invalid lastBlock payload: ${typeof result}`);
1183
- };
1184
-
1185
- const MAX_RESPONSE_DEPTH = 3;
1186
- const decodeResponsePayload = async (result, depth = 0) => {
1187
- if (depth > MAX_RESPONSE_DEPTH) throw new Error("peernet response nesting exceeds limit");
1188
- if (result instanceof Uint8Array) {
1189
- let codec;
1190
- try {
1191
- codec = new Codec(result);
1192
- } catch {
1193
- }
1194
- if (codec?.name === "peernet-response") {
1195
- const ResponseMessage = globalThis.peernet?.protos?.["peernet-response"];
1196
- if (!ResponseMessage) throw new Error("peernet-response codec is not registered");
1197
- const wrapped = new ResponseMessage(result);
1198
- return decodeResponsePayload(wrapped.decoded.response, depth + 1);
1199
- }
1200
- try {
1201
- return JSON.parse(new TextDecoder().decode(result));
1202
- } catch {
1203
- return result;
1204
- }
1205
- }
1206
- if (result && typeof result === "object") {
1207
- const response = result.decoded?.response ?? result.response;
1208
- if (response !== void 0) return decodeResponsePayload(response, depth + 1);
1209
- }
1210
- if (typeof result === "string") {
1211
- try {
1212
- return JSON.parse(result);
1213
- } catch {
1214
- return result;
1215
- }
1216
- }
1217
- return result;
648
+ const resolveLastBlockMessage = async (result) => {
649
+ if (!(result instanceof Uint8Array) || new Codec(result).name !== "last-block-message")
650
+ throw new TypeError("lastBlock response must be a binary last-block message");
651
+ return new LastBlockMessage(result);
1218
652
  };
1219
653
 
1220
654
  class CanonicalStateQueue {
1221
655
  #tail = Promise.resolve();
1222
656
  #failed = false;
1223
657
  #failure;
658
+ get failed() {
659
+ return this.#failed;
660
+ }
1224
661
  run(operation) {
1225
662
  const result = this.#tail.then(async () => {
1226
663
  if (this.#failed) {
@@ -1246,27 +683,50 @@ class CanonicalStateQueue {
1246
683
  }
1247
684
  }
1248
685
 
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
- );
686
+ const resolveTransactionReference = async (expectedHash, transactionData) => {
687
+ const transaction = transactionData instanceof TransactionMessage ? transactionData : new TransactionMessage(transactionData);
688
+ const actualHash = await transaction.hash();
689
+ if (actualHash !== expectedHash) {
690
+ throw new Error(`Transaction hash mismatch: expected ${expectedHash}, got ${actualHash}`);
1266
691
  }
1267
- return "append";
692
+ return transaction;
1268
693
  };
1269
694
 
695
+ function publishObserverEvent(bus, topic, payload, onError = (topic2, error) => console.warn(`[chain observer] ${topic2}:`, error)) {
696
+ const report = (error) => {
697
+ try {
698
+ onError(topic, error);
699
+ } catch {
700
+ }
701
+ };
702
+ const invoke = (callback) => {
703
+ try {
704
+ const result = callback();
705
+ if (result && typeof result.then === "function") Promise.resolve(result).catch(report);
706
+ } catch (error) {
707
+ report(error);
708
+ }
709
+ };
710
+ try {
711
+ const value = structuredClone(payload);
712
+ if (!(bus.subscribers instanceof Map)) {
713
+ invoke(() => bus.publish(topic, value));
714
+ return;
715
+ }
716
+ let subscriber = bus.subscribers.get(topic);
717
+ if (!subscriber) {
718
+ subscriber = { handlers: [], value: void 0 };
719
+ bus.subscribers.set(topic, subscriber);
720
+ }
721
+ const oldValue = subscriber.value;
722
+ if (!bus.verbose && oldValue === value) return;
723
+ subscriber.value = value;
724
+ for (const handler of [...subscriber.handlers]) invoke(() => handler.bound(value, oldValue));
725
+ } catch (error) {
726
+ report(error);
727
+ }
728
+ }
729
+
1270
730
  const debug$1 = createDebugger("leofcoin/state");
1271
731
  class State extends Contract {
1272
732
  constructor(config) {
@@ -1283,7 +743,7 @@ class State extends Contract {
1283
743
  this.#totalSize = 0;
1284
744
  this.#canonicalStateQueue = new CanonicalStateQueue();
1285
745
  this.#loaded = false;
1286
- this.#resolvingHashes = /* @__PURE__ */ new Set();
746
+ this.#resolvingHashes = /* @__PURE__ */ new Map();
1287
747
  this._wantList = [];
1288
748
  this.#chainStateHandler = () => {
1289
749
  return new globalThis.peernet.protos["peernet-response"]({
@@ -1300,15 +760,6 @@ class State extends Contract {
1300
760
  response: { blocks: await globalThis.blockStore.keys() }
1301
761
  });
1302
762
  };
1303
- this.#loadBlockTransactions = (transactions) => Promise.all(
1304
- transactions.map(async (transaction) => new TransactionMessage(await peernet.get(transaction, "transaction")))
1305
- );
1306
- this.#getLastTransactions = async () => {
1307
- let lastTransactions = (await Promise.all(
1308
- (await this.blocks).filter((block) => block.loaded).slice(-24).map((block) => this.#loadBlockTransactions(block.transactions))
1309
- )).reduce((all, transactions) => [...all, ...transactions], []);
1310
- return Promise.all(lastTransactions.map((transaction) => transaction.hash()));
1311
- };
1312
763
  }
1313
764
  #resolveErrored;
1314
765
  #lastResolvedTime;
@@ -1326,7 +777,13 @@ class State extends Contract {
1326
777
  #machine;
1327
778
  #canonicalStateQueue;
1328
779
  withCanonicalState(operation) {
1329
- return this.#canonicalStateQueue.run(operation);
780
+ return this.#canonicalStateQueue.run(operation).catch((error) => {
781
+ if (this.#canonicalStateQueue.failed) {
782
+ this.#loaded = false;
783
+ this.#syncState = "errored";
784
+ }
785
+ throw error;
786
+ });
1330
787
  }
1331
788
  canonicalTip() {
1332
789
  return this.#machine ? this.#machine.getCanonicalTip() : Promise.resolve({ index: -1, hash: "0x0", previousHash: "0x0" });
@@ -1426,91 +883,79 @@ class State extends Contract {
1426
883
  #lastBlockHandler;
1427
884
  #knownBlocksHandler;
1428
885
  async init() {
1429
- debug$1("State init start");
1430
886
  this.jobber = new Jobber(this.resolveTimeout);
1431
887
  await globalThis.peernet.addRequestHandler("lastBlock", this.#lastBlockHandler);
1432
888
  await globalThis.peernet.addRequestHandler("knownBlocks", this.#knownBlocksHandler);
1433
889
  await globalThis.peernet.addRequestHandler("chainState", this.#chainStateHandler);
1434
- let localBlockHash;
1435
- let blockMessage;
1436
- let localBlock;
1437
- console.log("State init before try-catch");
1438
890
  try {
1439
- const rawBlock = await globalThis.chainStore.has("lastBlock");
1440
- console.log("State init after has lastBlock check");
1441
- if (rawBlock) {
1442
- console.log("State init after has lastBlock found");
1443
- console.log(rawBlock);
1444
- localBlockHash = new TextDecoder().decode(await globalThis.chainStore.get("lastBlock"));
1445
- console.log(localBlockHash);
1446
- if (localBlockHash !== "0x0") {
1447
- blockMessage = await globalThis.blockStore.get(localBlockHash);
1448
- console.log(blockMessage);
1449
- blockMessage = await new BlockMessage(blockMessage);
1450
- localBlock = { ...blockMessage.decoded, hash: localBlockHash };
891
+ const identity = await createChainIdentity(globalThis.peernet?.network || "leofcoin:peach", addresses);
892
+ const required = [addresses.contractFactory, addresses.nativeToken, addresses.nameService, addresses.validators];
893
+ const checkpoint = await readCheckpoint(globalThis.stateStore, identity.chainId, required);
894
+ const checkpointTip = checkpoint?.snapshot.lastBlock ?? { index: -1, hash: "0x0" };
895
+ const marker = await globalThis.chainStore.has("lastBlock") ? new TextDecoder().decode(await globalThis.chainStore.get("lastBlock")) : "0x0";
896
+ const readBlock = async (hash) => await globalThis.blockStore.has(hash) ? globalThis.blockStore.get(hash) : globalThis.peernet.get(hash, "block");
897
+ let history = await recoveryHistory(checkpointTip.hash, readBlock);
898
+ if (marker !== "0x0" && marker !== checkpointTip.hash && !history.some((block) => block.hash === marker)) {
899
+ const candidate = await recoveryHistory(marker, readBlock);
900
+ if (checkpointTip.hash !== "0x0" && candidate[Number(checkpointTip.index)]?.hash !== checkpointTip.hash)
901
+ throw new Error("Persisted chain marker conflicts with state checkpoint");
902
+ history = candidate;
903
+ }
904
+ if (!checkpoint && await globalThis.stateStore.has("lastBlock")) {
905
+ const legacy = JSON.parse(new TextDecoder().decode(await globalThis.stateStore.get("lastBlock")));
906
+ if (legacy.hash && legacy.hash !== "0x0" && legacy.hash !== history.at(-1)?.hash) {
907
+ const candidate = await recoveryHistory(legacy.hash, readBlock);
908
+ const common = Math.min(history.length, candidate.length);
909
+ if (common && history[common - 1].hash !== candidate[common - 1].hash)
910
+ throw new Error("Legacy snapshot conflicts with canonical chain marker");
911
+ if (candidate.length > history.length) history = candidate;
1451
912
  }
1452
- console.log("State init after localBlock set");
1453
- } else {
1454
- localBlock = { index: 0, hash: "0x0", previousHash: "0x0" };
1455
913
  }
1456
- } catch {
1457
- console.log("e");
1458
- console.log("State init middle");
1459
- localBlock = { index: 0, hash: "0x0", previousHash: "0x0" };
1460
- }
1461
- console.log("State init middle");
1462
- try {
1463
- console.log("fetching known blocks from blockStore");
1464
- this.knownBlocks = await globalThis.blockStore.keys();
1465
- } catch (error) {
1466
- console.log("no local known blocks found");
1467
- }
1468
- try {
1469
- if (localBlock?.hash && localBlock.hash !== "0x0") {
1470
- try {
1471
- if (!await globalThis.stateStore.has("lastBlock")) {
1472
- await this.resolveBlocks();
1473
- }
1474
- } catch {
1475
- await this.resolveBlocks();
1476
- }
1477
- } else {
1478
- await this.resolveBlocks();
914
+ if (await globalThis.chainStore.has("recoveryTarget")) {
915
+ const target = new TextDecoder().decode(await globalThis.chainStore.get("recoveryTarget"));
916
+ const candidate = await recoveryHistory(target, readBlock);
917
+ const common = Math.min(history.length, candidate.length);
918
+ if (common && history[common - 1].hash !== candidate[common - 1].hash)
919
+ throw new Error("Recovery target conflicts with canonical checkpoint history");
920
+ if (candidate.length > history.length) history = candidate;
1479
921
  }
1480
- const machine = new Machine(this.#blocks);
1481
- console.log(machine);
922
+ this.#blocks = history;
923
+ this.#blockHashMap = new Map(history.map((block) => [block.hash, block.index]));
924
+ this.knownBlocks = await globalThis.blockStore.keys();
925
+ if (!checkpoint && !history.length && this.knownBlocks.length)
926
+ throw new Error("Local blocks exist without a canonical recovery anchor; preserve this root and rebuild in a separate root");
927
+ const prefix = history.slice(0, Number(checkpointTip.index) + 1).map((block) => ({ ...block, loaded: true }));
928
+ if (history.length > prefix.length) await globalThis.chainStore.put("recoveryTarget", history.at(-1).hash);
929
+ const machine = new Machine(prefix, checkpoint);
1482
930
  await machine.ready;
1483
931
  this.#machine = machine;
1484
- const lastBlock = await this.#machine.lastBlock;
1485
- if (lastBlock.hash !== "0x0") {
1486
- await this.updateState(new BlockMessage(lastBlock));
1487
- }
932
+ await restoreAccountNonces(globalThis.accountsStore, checkpoint?.snapshot.accounts ?? {});
933
+ for (const block of prefix) this.#blocks[Number(block.index)].loaded = true;
934
+ await this.withCanonicalState((transition) => this.#loadBlocks(history.slice(prefix.length), transition));
935
+ if (!checkpoint && !history.length) await this.#machine.updateState(true);
936
+ const tip = await this.canonicalTip();
937
+ await globalThis.chainStore.put("lastBlock", tip.hash);
938
+ await globalThis.chainStore.delete("recoveryTarget");
1488
939
  this.#loaded = true;
1489
940
  this.#chainState = "loaded";
1490
941
  } catch (error) {
1491
- console.log("e");
1492
- if (isResolveError(error)) {
1493
- console.error(error);
1494
- }
1495
- console.log(error);
942
+ this.#loaded = false;
943
+ this.#chainState = "loading";
944
+ this.#syncState = "errored";
945
+ throw error;
1496
946
  }
1497
947
  }
1498
948
  async updateState(message) {
1499
- try {
1500
- const hash = await message.hash();
1501
- await globalThis.chainStore.put("lastBlock", hash);
1502
- globalThis.pubsub.publish("lastBlock", message.encoded);
1503
- if (!this.#machine) {
1504
- const machine = new Machine(this.#blocks);
1505
- console.log(machine);
1506
- await machine.ready;
1507
- this.#machine = machine;
1508
- }
1509
- await this.#machine.updateState(true);
1510
- } catch (error) {
1511
- console.error(error);
1512
- throw error;
1513
- }
949
+ if (!this.#machine) throw new Error("Cannot checkpoint an uninitialized state machine");
950
+ const hash = await message.hash();
951
+ const tip = await this.canonicalTip();
952
+ if (tip.hash !== hash || Number(tip.index) !== Number(message.decoded.index))
953
+ throw new Error("Checkpoint target does not match canonical worker tip");
954
+ await this.#machine.updateState(true);
955
+ await globalThis.chainStore.put("lastBlock", hash);
956
+ if (await globalThis.chainStore.has("recoveryTarget") && new TextDecoder().decode(await globalThis.chainStore.get("recoveryTarget")) === hash)
957
+ await globalThis.chainStore.delete("recoveryTarget");
958
+ publishObserverEvent(globalThis.pubsub, "lastBlock", message.encoded);
1514
959
  }
1515
960
  getLatestBlock() {
1516
961
  return this.#getLatestBlock();
@@ -1539,7 +984,8 @@ class State extends Contract {
1539
984
  if (!exists) {
1540
985
  const data = await peernet.get(hash, "transaction");
1541
986
  if (!data) throw new Error(`missing transaction data for ${hash}`);
1542
- await transactionStore.put(hash, data);
987
+ const transaction = await resolveTransactionReference(hash, data);
988
+ await transactionStore.put(hash, transaction.encoded);
1543
989
  }
1544
990
  const inPool = await transactionPoolStore.has(hash);
1545
991
  if (inPool) await transactionPoolStore.delete(hash);
@@ -1547,84 +993,61 @@ class State extends Contract {
1547
993
  );
1548
994
  }
1549
995
  async #resolveBlock(hash) {
1550
- let index = this.#blockHashMap.get(hash);
1551
- let localHash = "0x0";
1552
- try {
1553
- const rawLocalHash = await globalThis.stateStore.get("lastBlock");
1554
- if (rawLocalHash) {
1555
- const decoded = JSON.parse(new TextDecoder().decode(rawLocalHash));
1556
- localHash = typeof decoded === "string" ? decoded : decoded?.hash ?? "0x0";
996
+ const localHash = (await this.canonicalTip()).hash;
997
+ const visited = /* @__PURE__ */ new Set();
998
+ while (hash !== "0x0" && hash !== localHash) {
999
+ if (visited.has(hash)) throw new ResolveError(`cyclic block ancestry at ${hash}`);
1000
+ visited.add(hash);
1001
+ const cachedIndex = this.#blockHashMap.get(hash);
1002
+ const cached = this.#blocks[cachedIndex];
1003
+ if (cached) {
1004
+ hash = cached.previousHash;
1005
+ continue;
1557
1006
  }
1558
- } catch (error) {
1559
- debug$1("no local state found");
1560
- }
1561
- if (this.#blocks[index]) {
1562
- const previousHash = this.#blocks[index].previousHash;
1563
- if (previousHash === localHash) return;
1564
- if (previousHash !== "0x0") {
1565
- return this.resolveBlock(previousHash);
1566
- } else {
1567
- return;
1007
+ try {
1008
+ const block = await this.getAndPutBlock(hash);
1009
+ await this.#resolveTransactions(block.decoded.transactions);
1010
+ const index = block.decoded.index;
1011
+ const size = block.encoded.byteLength;
1012
+ this.#totalSize += size;
1013
+ this.#blocks[index] = { hash, ...block.decoded };
1014
+ this.#blockHashMap.set(hash, index);
1015
+ debug$1(`resolved block: ${hash} @${index} ${formatBytes(size)}`);
1016
+ publishObserverEvent(globalThis.pubsub, "block-resolved", { hash, index });
1017
+ this.#lastResolved = this.#blocks[index];
1018
+ this.#lastResolvedTime = Date.now();
1019
+ hash = block.decoded.previousHash;
1020
+ } catch (error) {
1021
+ throw new ResolveError(`block: ${hash}`, { cause: error });
1568
1022
  }
1569
1023
  }
1570
- try {
1571
- const block = await this.getAndPutBlock(hash);
1572
- const promises = [];
1573
- if (block.decoded.previousHash !== "0x0" && block.decoded.previousHash !== localHash) {
1574
- promises.push(this.resolveBlock(block.decoded.previousHash));
1575
- }
1576
- promises.push(this.#resolveTransactions(block.decoded.transactions));
1577
- await Promise.all(promises);
1578
- index = block.decoded.index;
1579
- const size = block.encoded.length > 0 ? block.encoded.length : block.encoded.byteLength;
1580
- this.#totalSize += size;
1581
- this.#blocks[index] = { hash, ...block.decoded };
1582
- this.#blockHashMap.set(hash, index);
1583
- debug$1(`resolved block: ${hash} @${index} ${formatBytes(size)}`);
1584
- globalThis.pubsub.publish("block-resolved", { hash, index });
1585
- this.#lastResolved = this.#blocks[index];
1586
- this.#lastResolvedTime = Date.now();
1587
- } catch (error) {
1588
- throw new ResolveError(`block: ${hash}@${index}`, { cause: error });
1589
- }
1590
- return;
1591
1024
  }
1592
1025
  async resolveBlock(hash) {
1593
1026
  if (!hash) throw new Error(`expected hash, got: ${hash}`);
1594
1027
  if (hash === "0x0") return;
1595
- if (this.#resolvingHashes.has(hash)) return;
1596
- this.#resolvingHashes.add(hash);
1597
- const isEntering = this.#resolvingHashes.size === 1;
1028
+ const existing = this.#resolvingHashes.get(hash);
1029
+ if (existing) return existing;
1598
1030
  this.#resolving = true;
1599
- try {
1600
- if (isEntering) {
1601
- if (this.jobber.busy && this.jobber.destroy) await this.jobber.destroy();
1602
- await this.jobber.add(() => this.#resolveBlock(hash));
1603
- } else {
1604
- await this.#resolveBlock(hash);
1605
- }
1606
- try {
1607
- const lastBlockHash = await globalThis.stateStore.get("lastBlock");
1608
- if (lastBlockHash === hash) {
1031
+ const resolution = Promise.resolve().then(async () => {
1032
+ for (let attempt = 0; attempt < 3; attempt++) {
1033
+ try {
1034
+ await this.#resolveBlock(hash);
1609
1035
  this.#resolveErrored = false;
1610
1036
  return;
1037
+ } catch (error) {
1038
+ console.error(`block resolution attempt ${attempt + 1} failed for ${hash}`, error);
1039
+ if (attempt === 2) {
1040
+ if (!this.wantList.includes(hash)) this.wantList.push(hash);
1041
+ throw new ResolveError(`block: ${hash}`, { cause: error });
1042
+ }
1611
1043
  }
1612
- } catch (error) {
1613
1044
  }
1614
- } catch (error) {
1615
- console.log({ error });
1616
- this.#resolveErrorCount += 1;
1617
- if (this.#resolveErrorCount < 3) {
1618
- this.#resolvingHashes.delete(hash);
1619
- return this.resolveBlock(hash);
1620
- }
1621
- this.#resolveErrorCount = 0;
1622
- this.wantList.push(hash);
1623
- throw new ResolveError(`block: ${hash}`, { cause: error });
1624
- } finally {
1045
+ }).finally(() => {
1625
1046
  this.#resolvingHashes.delete(hash);
1626
1047
  if (this.#resolvingHashes.size === 0) this.#resolving = false;
1627
- }
1048
+ });
1049
+ this.#resolvingHashes.set(hash, resolution);
1050
+ return resolution;
1628
1051
  }
1629
1052
  async resolveBlocks() {
1630
1053
  if (this.#chainSyncing || this.#resolving) {
@@ -1727,7 +1150,7 @@ class State extends Contract {
1727
1150
  const localIndex = localBlock ? Number(localBlock.index) : -1;
1728
1151
  const remoteIndex = Number(lastBlock.index);
1729
1152
  const remoteBlockHash = lastBlock.hash;
1730
- globalThis.pubsub.publish("sync-progress", {
1153
+ publishObserverEvent(globalThis.pubsub, "sync-progress", {
1731
1154
  phase: "syncing",
1732
1155
  startIndex: localIndex,
1733
1156
  localIndex,
@@ -1761,26 +1184,9 @@ class State extends Contract {
1761
1184
  return;
1762
1185
  }
1763
1186
  if (localStateHash !== remoteBlockHash) {
1764
- if (this.wantList.length > 0) {
1765
- debug$1(`Fetching ${this.wantList.length} blocks before resolving`);
1766
- const getBatch = async (batch) => {
1767
- const blocks2 = await Promise.all(
1768
- batch.map(
1769
- (hash) => this.getAndPutBlock(hash).catch((e) => {
1770
- console.warn(`failed to fetch block ${hash}`, e);
1771
- })
1772
- )
1773
- );
1774
- const transactions = blocks2.filter((block) => Boolean(block)).flatMap((block) => block.decoded.transactions);
1775
- return this.#resolveTransactions(transactions);
1776
- };
1777
- for (let i = 0; i < this.wantList.length; i += 50) {
1778
- const batch = this.wantList.slice(i, i + 50);
1779
- await getBatch(batch);
1780
- }
1781
- }
1782
1187
  debug$1(`Resolving remote block: ${remoteBlockHash} @${remoteIndex} (differs from local state)`);
1783
1188
  await this.resolveBlock(remoteBlockHash);
1189
+ await globalThis.chainStore.put("recoveryTarget", remoteBlockHash);
1784
1190
  const blocksSynced = remoteIndex - localIndex;
1785
1191
  debug$1(`Resolved ${blocksSynced} new block(s)`);
1786
1192
  const blocks = this.#blocks;
@@ -1800,7 +1206,7 @@ class State extends Contract {
1800
1206
  } else {
1801
1207
  debug$1(`Block already in local state. Remote hash: ${remoteBlockHash} matches local state`);
1802
1208
  }
1803
- globalThis.pubsub.publish("sync-progress", {
1209
+ publishObserverEvent(globalThis.pubsub, "sync-progress", {
1804
1210
  phase: "synced",
1805
1211
  startIndex: localIndex,
1806
1212
  localIndex: remoteIndex,
@@ -1888,112 +1294,23 @@ class State extends Contract {
1888
1294
  }
1889
1295
  return latest;
1890
1296
  }
1891
- #loadBlockTransactions;
1892
- #getLastTransactions;
1893
- // todo throw error
1894
- async #_executeTransaction(transaction, validators, feesEnabled, burnBasisPoints, blockTimestamp) {
1895
- try {
1896
- const hash = await transaction.hash();
1897
- if (feesEnabled) {
1898
- const fee = BigInt(await calculateFee(transaction.decoded));
1899
- const { payments, burned } = distributeTransactionFee(fee, hash, validators, burnBasisPoints);
1900
- await this.#machine.collectFee(transaction.decoded.from, payments, burned);
1901
- }
1902
- await this.#machine.execute(
1903
- transaction.decoded.to,
1904
- transaction.decoded.method,
1905
- transaction.decoded.params,
1906
- transaction.decoded.from,
1907
- { seed: hash, timestamp: Number(blockTimestamp) }
1908
- );
1909
- } catch (error) {
1910
- console.log(error);
1911
- await globalThis.transactionPoolStore.delete(await transaction.hash());
1912
- console.log("removing invalid transaction");
1913
- if (isExecutionError(error)) {
1914
- console.log(error);
1915
- }
1916
- console.log(error);
1917
- throw error;
1918
- }
1297
+ async applyRecoveredBlock(block, transition) {
1298
+ throw new Error("Canonical block application is not implemented");
1919
1299
  }
1920
- /**
1921
- *
1922
- * @param {Block[]} blocks
1923
- */
1924
1300
  async #loadBlocks(blocks, transition) {
1925
1301
  this.#chainState = "loading";
1926
- if (blocks.some((block) => !block)) {
1927
- throw new Error("missing block data during load; chain resolution incomplete");
1928
- }
1929
- const poolTransactionKeys = new Set(await globalThis.transactionPoolStore.keys());
1930
- debug$1(`pool transactions: ${poolTransactionKeys.size}`);
1931
- debug$1(`loading ${blocks.length} blocks`);
1932
1302
  for (const block of blocks) {
1933
- if (block && !block.loaded) {
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
- }
1945
- debug$1(`loading block: ${Number(block.index)} ${block.hash}`);
1946
- let transactions = await this.#loadBlockTransactions(block.transactions || []);
1947
- const validators = block.validators.map(({ address }) => address);
1948
- debug$1(`loading transactions: ${transactions.length} for block ${block.index}`);
1949
- transition.markMutating();
1950
- for (const transaction of transactions) {
1951
- const hash = await transaction.hash();
1952
- if (poolTransactionKeys.has(hash)) await globalThis.transactionPoolStore.delete(hash);
1953
- }
1954
- transactions = transactions.sort((a, b) => {
1955
- if (a.decoded.priority !== b.decoded.priority) return a.decoded.priority ? -1 : 1;
1956
- const left = BigInt(a.decoded.nonce);
1957
- const right = BigInt(b.decoded.nonce);
1958
- return left < right ? -1 : left > right ? 1 : 0;
1959
- });
1960
- debug$1(`executing ${transactions.length} transactions for block ${block.index}`);
1961
- const feesEnabled = supportsTransactionFees(block.protocolVersion);
1962
- const monetaryPolicyEnabled = supportsMonetaryPolicy(block.protocolVersion);
1963
- if (monetaryPolicyEnabled) await validateBlockResourceLimits(transactions);
1964
- const policy = monetaryPolicyEnabled ? calculateMonetaryPolicy(
1965
- BigInt(await this.#machine.get(nativeToken, "totalSupply")),
1966
- BigInt(await this.#machine.get(nativeToken, "targetSupply"))
1967
- ) : { subsidy: 0n, burnBasisPoints: 1000n };
1968
- for (const transaction of transactions) {
1969
- await this.#_executeTransaction(
1970
- transaction,
1971
- validators,
1972
- feesEnabled,
1973
- policy.burnBasisPoints,
1974
- block.timestamp
1975
- );
1976
- }
1977
- if (monetaryPolicyEnabled && policy.subsidy > 0n) {
1978
- await this.#machine.settleRewards([
1979
- ...distributeAmount(policy.subsidy, validators, Number(block.index) % validators.length)
1980
- ]);
1981
- }
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;
1986
- if (Number(block.index) === 0) this.#loaded = true;
1987
- debug$1(`loaded block: ${block.hash} @${Number(block.index)}`);
1988
- globalThis.pubsub.publish("block-loaded", { ...block });
1989
- } catch (error) {
1990
- console.error(error);
1991
- for (const transaction of block.transactions) {
1992
- this.wantList.push(transaction);
1993
- }
1994
- throw error;
1995
- }
1996
- }
1303
+ if (!block) throw new Error("Missing block data during recovery");
1304
+ if (block.loaded) continue;
1305
+ const { hash, loaded, ...header } = block;
1306
+ const message = new BlockMessage(header);
1307
+ if (await message.hash() !== block.hash) throw new Error("Recovery block hash changed before application");
1308
+ await this.applyRecoveredBlock(message, transition);
1309
+ const tip = await this.canonicalTip();
1310
+ if (tip.hash !== block.hash || Number(tip.index) !== Number(block.index))
1311
+ throw new Error("Recovery did not apply the expected canonical block");
1312
+ block.loaded = true;
1313
+ publishObserverEvent(globalThis.pubsub, "block-loaded", { ...block });
1997
1314
  }
1998
1315
  this.#chainState = "loaded";
1999
1316
  return true;
@@ -2053,18 +1370,16 @@ class State extends Contract {
2053
1370
  return this.syncChain(latest);
2054
1371
  }
2055
1372
  async triggerLoad() {
2056
- if (this.#blocks?.length > 0) {
2057
- const machine = new Machine(this.#blocks);
2058
- console.log(machine);
2059
- await machine.ready;
2060
- this.#machine = machine;
2061
- }
1373
+ if (!this.#machine) throw new Error("Restart the node to initialize recovery");
1374
+ const tip = await this.canonicalTip();
1375
+ return this.withCanonicalState((transition) => this.#loadBlocks(this.#blocks.slice(Number(tip.index) + 1), transition));
2062
1376
  }
2063
1377
  }
2064
1378
 
2065
1379
  class VersionControl extends State {
2066
1380
  constructor(config) {
2067
1381
  super(config);
1382
+ this.version = PROTOCOL_VERSION;
2068
1383
  }
2069
1384
  #currentVersion = PROTOCOL_VERSION;
2070
1385
  #reachedOneZeroZero = REACHED_ONE_ZERO_ZERO;
@@ -2134,15 +1449,6 @@ const validateBlockEconomics = (block, calculatedFees, validatorFees = /* @__PUR
2134
1449
  }
2135
1450
  };
2136
1451
 
2137
- const resolveTransactionReference = async (expectedHash, transactionData) => {
2138
- const transaction = transactionData instanceof TransactionMessage ? transactionData : new TransactionMessage(transactionData);
2139
- const actualHash = await transaction.hash();
2140
- if (actualHash !== expectedHash) {
2141
- throw new Error(`Transaction hash mismatch: expected ${expectedHash}, got ${actualHash}`);
2142
- }
2143
- return transaction;
2144
- };
2145
-
2146
1452
  const consensusSignableData = (validatorsAddress, type, message) => ({
2147
1453
  from: String(message.from),
2148
1454
  to: validatorsAddress,
@@ -2810,10 +2116,10 @@ class Chain extends VersionControl {
2810
2116
  });
2811
2117
  });
2812
2118
  globalThis.peernet.subscribe("add-block", this.#addBlock.bind(this));
2813
- globalThis.peernet.subscribe("invalid-transaction", this.#invalidTransaction.bind(this));
2119
+ globalThis.peernet.subscribe("invalid-transaction", (bytes) => this.#invalidTransaction(decodeWire("invalid-transaction", bytes)));
2814
2120
  globalThis.peernet.subscribe("send-transaction", this.#sendTransaction.bind(this));
2815
2121
  globalThis.peernet.subscribe("add-transaction", this.#addTransaction.bind(this));
2816
- globalThis.peernet.subscribe("validator:timeout", this.#validatorTimeout.bind(this));
2122
+ globalThis.peernet.subscribe("validator:timeout", (bytes) => this.#validatorTimeout(decodeWire("validator:timeout", bytes)));
2817
2123
  globalThis.peernet.subscribe("consensus:propose", this.#handleProposal.bind(this));
2818
2124
  globalThis.peernet.subscribe("consensus:prevote", this.#handlePrevote.bind(this));
2819
2125
  globalThis.peernet.subscribe("consensus:precommit", this.#handlePrecommit.bind(this));
@@ -2859,52 +2165,7 @@ class Chain extends VersionControl {
2859
2165
  let response = await peer.request(node.encoded);
2860
2166
  console.log(`raw response for request ${request}:`, response);
2861
2167
  if (response === void 0 || response === null) return response;
2862
- const normalizeResponse = async (payload) => {
2863
- if (payload === void 0 || payload === null) return payload;
2864
- if (payload instanceof Uint8Array || typeof Buffer !== "undefined" && Buffer.isBuffer(payload)) {
2865
- let bytes = payload instanceof Uint8Array ? payload : new Uint8Array(payload);
2866
- for (let i = 0; i < 3; i += 1) {
2867
- try {
2868
- const codec = new Codec(bytes);
2869
- if (codec.name !== "peernet-response") break;
2870
- const wrapped = await new globalThis.peernet.protos["peernet-response"](bytes);
2871
- if (wrapped?.decoded?.response === void 0) break;
2872
- const next = wrapped.decoded.response;
2873
- if (next instanceof Uint8Array || typeof Buffer !== "undefined" && Buffer.isBuffer(next)) {
2874
- bytes = next instanceof Uint8Array ? next : new Uint8Array(next);
2875
- continue;
2876
- }
2877
- return normalizeResponse(next);
2878
- } catch {
2879
- break;
2880
- }
2881
- }
2882
- try {
2883
- return JSON.parse(new TextDecoder().decode(bytes));
2884
- } catch {
2885
- return bytes;
2886
- }
2887
- }
2888
- if (payload && typeof payload === "object") {
2889
- const objectPayload = payload;
2890
- const decoded = objectPayload.decoded;
2891
- if ("decoded" in objectPayload && decoded && "response" in decoded) {
2892
- return normalizeResponse(decoded.response);
2893
- }
2894
- if ("response" in objectPayload) {
2895
- return normalizeResponse(objectPayload.response);
2896
- }
2897
- }
2898
- if (typeof payload === "string") {
2899
- try {
2900
- return JSON.parse(payload);
2901
- } catch {
2902
- return payload;
2903
- }
2904
- }
2905
- return payload;
2906
- };
2907
- return await normalizeResponse(response);
2168
+ return decodeResponsePayload(response);
2908
2169
  } catch (error) {
2909
2170
  const peerId = peer?.peerId || peer?.id || peer?.address || "unknown";
2910
2171
  debug(`peernet request failed: ${request} -> ${peerId}:`, error?.message ?? error);
@@ -2912,60 +2173,11 @@ class Chain extends VersionControl {
2912
2173
  }
2913
2174
  }
2914
2175
  async #decodeKnownBlocksResponse(response) {
2915
- if (!response) return null;
2916
- if (Array.isArray(response)) {
2917
- return { blocks: response };
2918
- }
2919
- if (Array.isArray(response.blocks)) {
2920
- return { blocks: response.blocks };
2921
- }
2922
- if (response.response && Array.isArray(response.response.blocks)) {
2923
- return { blocks: response.response.blocks };
2924
- }
2925
- if (typeof response === "string") {
2926
- try {
2927
- const parsed = JSON.parse(response);
2928
- if (Array.isArray(parsed)) return { blocks: parsed };
2929
- if (parsed && Array.isArray(parsed.blocks)) return { blocks: parsed.blocks };
2930
- if (parsed?.response && Array.isArray(parsed.response.blocks)) return { blocks: parsed.response.blocks };
2931
- } catch {
2932
- return null;
2933
- }
2934
- return null;
2935
- }
2936
- if (!(response instanceof Uint8Array)) return null;
2937
- let payload = response;
2938
- for (let i = 0; i < 3; i++) {
2939
- if (!(payload instanceof Uint8Array)) break;
2940
- try {
2941
- const nestedResponse = await new globalThis.peernet.protos["peernet-response"](payload);
2942
- payload = nestedResponse?.decoded?.response;
2943
- } catch {
2944
- break;
2945
- }
2946
- }
2947
- if (Array.isArray(payload)) return { blocks: payload };
2948
- if (payload && Array.isArray(payload.blocks)) return { blocks: payload.blocks };
2949
- if (payload?.response && Array.isArray(payload.response.blocks)) return { blocks: payload.response.blocks };
2950
- try {
2951
- const decoded = new TextDecoder().decode(response);
2952
- const parsed = JSON.parse(decoded);
2953
- if (Array.isArray(parsed)) return { blocks: parsed };
2954
- if (parsed && Array.isArray(parsed.blocks)) return { blocks: parsed.blocks };
2955
- if (parsed?.response && Array.isArray(parsed.response.blocks)) return { blocks: parsed.response.blocks };
2956
- } catch {
2957
- return null;
2958
- }
2959
- return null;
2176
+ if (!Array.isArray(response) || response.length > 1e5 || response.some((hash) => typeof hash !== "string")) return null;
2177
+ return { blocks: response };
2960
2178
  }
2961
2179
  async getPeerTransactionPool(peer) {
2962
2180
  let transactionsInPool = await this.#makeRequest(peer, "transactionPool");
2963
- if (transactionsInPool instanceof Uint8Array) {
2964
- transactionsInPool = new TextDecoder().decode(transactionsInPool);
2965
- }
2966
- if (typeof transactionsInPool === "string") {
2967
- transactionsInPool = JSON.parse(transactionsInPool);
2968
- }
2969
2181
  if (!Array.isArray(transactionsInPool) || transactionsInPool.some((hash) => typeof hash !== "string")) {
2970
2182
  throw new Error("transactionPool response must be an array of transaction hashes");
2971
2183
  }
@@ -3090,13 +2302,6 @@ class Chain extends VersionControl {
3090
2302
  }
3091
2303
  }
3092
2304
  }
3093
- if (this.wantList.length > 0) {
3094
- const promises = await Promise.allSettled(this.wantList.map((hash) => peernet.get(hash, "block")));
3095
- for (let i = promises.length - 1; i >= 0; i -= 1) {
3096
- if (promises[i].status === "fulfilled") this.wantList.splice(i, 1);
3097
- }
3098
- if (this.wantList.length === 0) await this.triggerSync();
3099
- }
3100
2305
  try {
3101
2306
  let stateInfo = await this.#makeRequest(peer, "stateInfo");
3102
2307
  if (stateInfo instanceof Uint8Array) {
@@ -3110,7 +2315,6 @@ class Chain extends VersionControl {
3110
2315
  debug(
3111
2316
  `sync finished with peer ${peerId}: state=${this.syncState} localNow=${(await this.lastBlock)?.index ?? -1}`
3112
2317
  );
3113
- this.machine.states.info = stateInfo;
3114
2318
  } catch (error) {
3115
2319
  const peerName = peer?.peerId || peer?.id || peer?.address || peerId || "unknown";
3116
2320
  debug(`stateInfo/syncChain failed: ${peerName}:`, error?.message ?? error);
@@ -3148,7 +2352,7 @@ class Chain extends VersionControl {
3148
2352
  } catch (error) {
3149
2353
  await transactionPoolStore.delete(hash);
3150
2354
  try {
3151
- globalThis.peernet.publish("invalid-transaction", hash);
2355
+ globalThis.peernet.publish("invalid-transaction", encodeWire("invalid-transaction", hash));
3152
2356
  } catch (publishError) {
3153
2357
  debug("peernet publish failed: invalid-transaction", publishError?.message ?? publishError);
3154
2358
  }
@@ -3159,7 +2363,13 @@ class Chain extends VersionControl {
3159
2363
  async #addBlock(block) {
3160
2364
  return this.withCanonicalState((transition) => this.#applyBlock(block, transition));
3161
2365
  }
3162
- async #applyBlock(block, transition) {
2366
+ applyRecoveredBlock(block, transition) {
2367
+ return this.#applyBlock(block, transition, false);
2368
+ }
2369
+ #recordValidatorSnapshot(index) {
2370
+ return this.machine.execute(addresses.validators, "recordValidatorSnapshot", [index], globalThis.peernet.selectedAccount);
2371
+ }
2372
+ async #applyBlock(block, transition, schedule = true) {
3163
2373
  const receivedEncoded = block instanceof BlockMessage ? block.encoded : block;
3164
2374
  const blockMessage = await new BlockMessage(block);
3165
2375
  const hash = await blockMessage.hash();
@@ -3193,13 +2403,14 @@ class Chain extends VersionControl {
3193
2403
  console.log(`[chain] \u2705 Block data integrity verified: ${hash}`);
3194
2404
  await this.#validateBlockValidators(blockMessage);
3195
2405
  const transactions = await this.#resolveBlockTransactions(blockMessage);
2406
+ await globalThis.blockStore.put(hash, blockMessage.encoded);
2407
+ if (!await globalThis.chainStore.has("recoveryTarget")) await globalThis.chainStore.put("recoveryTarget", hash);
3196
2408
  transition.markMutating();
3197
2409
  await Promise.all(
3198
2410
  blockMessage.decoded.transactions.map(async (transactionHash) => {
3199
2411
  if (await transactionPoolStore.has(transactionHash)) await transactionPoolStore.delete(transactionHash);
3200
2412
  })
3201
2413
  );
3202
- await globalThis.blockStore.put(hash, blockMessage.encoded);
3203
2414
  this.#blocks[blockIndex] = {
3204
2415
  hash,
3205
2416
  ...blockMessage.decoded
@@ -3233,9 +2444,6 @@ class Chain extends VersionControl {
3233
2444
  }
3234
2445
  const noncesByAddress = {};
3235
2446
  for (let transaction of transactions) {
3236
- globalThis.pubsub.publish("transaction-processed", transaction.encoded);
3237
- if (transaction.decoded.to === globalThis.peernet.selectedAccount)
3238
- globalThis.pubsub.publish("account-transaction-processed", transaction.encoded);
3239
2447
  if (!noncesByAddress[transaction.decoded.from] || noncesByAddress?.[transaction.decoded.from] < transaction.decoded.nonce) {
3240
2448
  noncesByAddress[transaction.decoded.from] = transaction.decoded.nonce;
3241
2449
  }
@@ -3253,14 +2461,19 @@ class Chain extends VersionControl {
3253
2461
  const registered = await this.machine.addLoadedBlock({ ...blockMessage.decoded, loaded: true, hash });
3254
2462
  if (registered !== true) throw new Error(`Block registration failed after execution: ${hash}`);
3255
2463
  try {
3256
- await this.call(addresses.validators, "recordValidatorSnapshot", [blockMessage.decoded.index]);
2464
+ await this.#recordValidatorSnapshot(blockMessage.decoded.index);
3257
2465
  } catch (snapshotError) {
3258
2466
  debug(`failed to record validator snapshot: ${snapshotError?.message ?? snapshotError}`);
3259
2467
  }
3260
2468
  await this.updateState(blockMessage);
3261
2469
  debug(`added block: ${hash}`);
3262
- globalThis.pubsub.publish("block-processed", blockMessage.decoded);
3263
- await this.#handleEpochBoundary(blockIndex);
2470
+ for (const transaction of transactions) {
2471
+ publishObserverEvent(globalThis.pubsub, "transaction-processed", transaction.encoded);
2472
+ if (transaction.decoded.to === globalThis.peernet.selectedAccount)
2473
+ publishObserverEvent(globalThis.pubsub, "account-transaction-processed", transaction.encoded);
2474
+ }
2475
+ publishObserverEvent(globalThis.pubsub, "block-processed", blockMessage.decoded);
2476
+ if (schedule) await this.#handleEpochBoundary(blockIndex);
3264
2477
  }
3265
2478
  async participate(address) {
3266
2479
  try {
@@ -3308,15 +2521,14 @@ class Chain extends VersionControl {
3308
2521
  async #handleTransaction(transaction, latestTransactions, block, feePayments = { payments: [], burned: 0n }) {
3309
2522
  await this.validateTransactionSignature(transaction);
3310
2523
  const hash = await transaction.hash();
3311
- const doubleTransactions = [];
3312
- if (latestTransactions.includes(hash) || await transactionStore.has(hash)) {
3313
- doubleTransactions.push(hash);
3314
- }
3315
- if (doubleTransactions.length > 0) {
3316
- await globalThis.transactionPoolStore.delete(hash);
3317
- await globalThis.peernet.publish("invalid-transaction", hash);
3318
- return;
3319
- }
2524
+ if (latestTransactions.includes(hash)) throw new Error(`Duplicate canonical transaction: ${hash}`);
2525
+ const nonce = BigInt(transaction.decoded.nonce);
2526
+ let committed = 0n;
2527
+ if (await globalThis.accountsStore.has(transaction.decoded.from)) {
2528
+ const raw = await globalThis.accountsStore.get(transaction.decoded.from);
2529
+ committed = BigInt(typeof raw === "string" ? raw : new TextDecoder().decode(raw));
2530
+ }
2531
+ if (nonce <= committed) throw new Error(`Transaction nonce already committed: ${hash}`);
3320
2532
  try {
3321
2533
  const result = await this.#executeTransaction({
3322
2534
  ...transaction.decoded,
@@ -3333,7 +2545,7 @@ class Chain extends VersionControl {
3333
2545
  console.log("vvvvvv");
3334
2546
  console.log({ e });
3335
2547
  console.log(hash);
3336
- peernet.publish("invalid-transaction", hash);
2548
+ peernet.publish("invalid-transaction", encodeWire("invalid-transaction", hash));
3337
2549
  await globalThis.transactionPoolStore.delete(e.hash);
3338
2550
  if (block) throw e;
3339
2551
  }
@@ -3498,7 +2710,7 @@ class Chain extends VersionControl {
3498
2710
  } catch (e) {
3499
2711
  if (hash) {
3500
2712
  try {
3501
- await globalThis.peernet.publish("invalid-transaction", hash);
2713
+ await globalThis.peernet.publish("invalid-transaction", encodeWire("invalid-transaction", hash));
3502
2714
  } catch (publishError) {
3503
2715
  debug("peernet publish failed: invalid-transaction", publishError?.message ?? publishError);
3504
2716
  }
@@ -3512,7 +2724,8 @@ class Chain extends VersionControl {
3512
2724
  * error is thrown on error so undefined data doesn't mean there is an error...
3513
2725
  **/
3514
2726
  async sendTransaction(transaction) {
3515
- const transactionMessage = await new TransactionMessage({ ...transaction });
2727
+ const { hash: ignoredHash, ...payload } = transaction instanceof TransactionMessage ? transaction.decoded : transaction;
2728
+ const transactionMessage = await new TransactionMessage(payload);
3516
2729
  const event = await super.sendTransaction(transactionMessage);
3517
2730
  await this.#sendTransaction(transactionMessage.encoded);
3518
2731
  try {
@@ -3552,7 +2765,7 @@ class Chain extends VersionControl {
3552
2765
  * @returns
3553
2766
  */
3554
2767
  internalCall(sender, contract, method, parameters) {
3555
- return this.machine.execute(contract, method, parameters, sender);
2768
+ throw new Error("Direct sender impersonation is disabled; submit a signed transaction with sendTransaction()");
3556
2769
  }
3557
2770
  /**
3558
2771
  *
@@ -3561,18 +2774,23 @@ class Chain extends VersionControl {
3561
2774
  * @param {Array} parameters
3562
2775
  * @returns
3563
2776
  */
3564
- call(contract, method, parameters) {
3565
- return this.machine.execute(contract, method, parameters, globalThis.peernet.selectedAccount);
2777
+ async call(transaction, ...legacy) {
2778
+ const signed = transaction instanceof TransactionMessage ? transaction.decoded : transaction;
2779
+ if (legacy.length || !signed || typeof signed !== "object" || typeof signed.signature !== "string" || !signed.signature)
2780
+ throw new Error("call() requires a signed transaction object; use staticCall() for reads or sendTransaction() for writes");
2781
+ return this.sendTransaction(structuredClone(signed));
3566
2782
  }
3567
2783
  staticCall(contract, method, parameters) {
3568
2784
  return this.machine.get(contract, method, parameters);
3569
2785
  }
3570
- mint(to, amount) {
3571
- return this.call(addresses.nativeToken, "mint", [to, amount]);
2786
+ mint(...args) {
2787
+ throw new Error("Native minting is restricted to the monetary protocol");
3572
2788
  }
3573
- transfer(from, to, amount) {
3574
- if (from !== globalThis.peernet.selectedAccount) throw new Error("transfer sender must be the selected account");
3575
- return this.call(addresses.nativeToken, "transfer", [to, amount]);
2789
+ async transfer(transaction, ...legacy) {
2790
+ const signed = transaction instanceof TransactionMessage ? transaction.decoded : transaction;
2791
+ if (legacy.length || signed?.to !== addresses.nativeToken || signed?.method !== "transfer")
2792
+ throw new Error("transfer() requires a signed native-token transfer transaction");
2793
+ return this.call(signed);
3576
2794
  }
3577
2795
  balanceOf(address) {
3578
2796
  return this.staticCall(addresses.nativeToken, "balanceOf", [address]);
@@ -3599,7 +2817,7 @@ class Chain extends VersionControl {
3599
2817
  * @example chain.lookup('myCoolContractName') // qmqsfddfdgfg...
3600
2818
  */
3601
2819
  lookup(name) {
3602
- return this.call(addresses.nameService, "lookup", [name]);
2820
+ return this.staticCall(addresses.nameService, "lookup", [name]);
3603
2821
  }
3604
2822
  #monitorPeerConnections() {
3605
2823
  setInterval(() => {