@leofcoin/chain 1.10.6 → 1.10.8

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/chain.js CHANGED
@@ -1115,6 +1115,87 @@ class Jobber {
1115
1115
  }
1116
1116
  }
1117
1117
 
1118
+ const MAX_RESPONSE_DEPTH$1 = 3;
1119
+ const asLastBlockMessage = async (blockInput, claimedHash) => {
1120
+ const block = new BlockMessage(blockInput);
1121
+ const hash = await block.hash();
1122
+ if (claimedHash && hash !== claimedHash) {
1123
+ throw new Error(`lastBlock hash mismatch: expected ${claimedHash}, got ${hash}`);
1124
+ }
1125
+ return new LastBlockMessage({ hash, index: block.decoded.index });
1126
+ };
1127
+ const resolveLegacyHash = async (hash) => {
1128
+ const blockData = await globalThis.peernet?.get?.(hash, "block");
1129
+ if (!blockData) throw new Error(`unable to resolve lastBlock hash: ${hash}`);
1130
+ return asLastBlockMessage(blockData, hash);
1131
+ };
1132
+ const resolveLastBlockMessage = async (result, depth = 0) => {
1133
+ if (depth > MAX_RESPONSE_DEPTH$1) throw new Error("lastBlock response nesting exceeds limit");
1134
+ if (result instanceof Uint8Array) {
1135
+ let codec;
1136
+ try {
1137
+ codec = new Codec(result);
1138
+ } catch {
1139
+ }
1140
+ if (codec) {
1141
+ if (codec.name === "peernet-response") {
1142
+ const ResponseMessage = globalThis.peernet?.protos?.["peernet-response"];
1143
+ if (!ResponseMessage) throw new Error("peernet-response codec is not registered");
1144
+ const wrapped = new ResponseMessage(result);
1145
+ if (wrapped?.decoded?.response === void 0) throw new Error("peernet-response has no response payload");
1146
+ return resolveLastBlockMessage(wrapped.decoded.response, depth + 1);
1147
+ }
1148
+ if (codec.name === "last-block-message") return new LastBlockMessage(result);
1149
+ if (codec.name === "block-message") return asLastBlockMessage(result);
1150
+ }
1151
+ const candidate = new TextDecoder().decode(result);
1152
+ if (candidate) return resolveLegacyHash(candidate);
1153
+ }
1154
+ if (typeof result === "string") return resolveLegacyHash(result);
1155
+ if (result && typeof result === "object") {
1156
+ const response = result.decoded?.response ?? result.response;
1157
+ if (response !== void 0) return resolveLastBlockMessage(response, depth + 1);
1158
+ if ("hash" in result && "index" in result) return new LastBlockMessage(result);
1159
+ if ("previousHash" in result && "index" in result) return asLastBlockMessage(result);
1160
+ }
1161
+ throw new Error(`invalid lastBlock payload: ${typeof result}`);
1162
+ };
1163
+
1164
+ const MAX_RESPONSE_DEPTH = 3;
1165
+ const decodeResponsePayload = async (result, depth = 0) => {
1166
+ if (depth > MAX_RESPONSE_DEPTH) throw new Error("peernet response nesting exceeds limit");
1167
+ if (result instanceof Uint8Array) {
1168
+ let codec;
1169
+ try {
1170
+ codec = new Codec(result);
1171
+ } catch {
1172
+ }
1173
+ if (codec?.name === "peernet-response") {
1174
+ const ResponseMessage = globalThis.peernet?.protos?.["peernet-response"];
1175
+ if (!ResponseMessage) throw new Error("peernet-response codec is not registered");
1176
+ const wrapped = new ResponseMessage(result);
1177
+ return decodeResponsePayload(wrapped.decoded.response, depth + 1);
1178
+ }
1179
+ try {
1180
+ return JSON.parse(new TextDecoder().decode(result));
1181
+ } catch {
1182
+ return result;
1183
+ }
1184
+ }
1185
+ if (result && typeof result === "object") {
1186
+ const response = result.decoded?.response ?? result.response;
1187
+ if (response !== void 0) return decodeResponsePayload(response, depth + 1);
1188
+ }
1189
+ if (typeof result === "string") {
1190
+ try {
1191
+ return JSON.parse(result);
1192
+ } catch {
1193
+ return result;
1194
+ }
1195
+ }
1196
+ return result;
1197
+ };
1198
+
1118
1199
  const debug$1 = createDebugger("leofcoin/state");
1119
1200
  class State extends Contract {
1120
1201
  constructor(config) {
@@ -1194,42 +1275,6 @@ class State extends Contract {
1194
1275
  get resolving() {
1195
1276
  return this.#resolving;
1196
1277
  }
1197
- async #resolveLastBlockMessage(result) {
1198
- if (result instanceof Uint8Array) {
1199
- try {
1200
- let payload = result;
1201
- for (let i = 0; i < 3; i += 1) {
1202
- try {
1203
- const wrapped = await new globalThis.peernet.protos["peernet-response"](payload);
1204
- if (wrapped?.decoded?.response === void 0) break;
1205
- payload = wrapped.decoded.response;
1206
- } catch {
1207
- break;
1208
- }
1209
- }
1210
- if (payload !== result) return this.#resolveLastBlockMessage(payload);
1211
- return new LastBlockMessage(result);
1212
- } catch {
1213
- const candidate = new TextDecoder().decode(result);
1214
- if (candidate) {
1215
- const blockData = await globalThis.peernet.get(candidate, "block");
1216
- if (blockData) return new BlockMessage(blockData);
1217
- }
1218
- }
1219
- }
1220
- if (typeof result === "string") {
1221
- const blockData = await globalThis.peernet.get(result, "block");
1222
- if (blockData) return new BlockMessage(blockData);
1223
- }
1224
- if (result && typeof result === "object") {
1225
- const response = result.decoded?.response ?? result.response;
1226
- if (response !== void 0) return this.#resolveLastBlockMessage(response);
1227
- if ("hash" in result && "index" in result) {
1228
- return new LastBlockMessage(result);
1229
- }
1230
- }
1231
- throw new Error(`invalid lastBlock payload: ${typeof result}`);
1232
- }
1233
1278
  get contracts() {
1234
1279
  return this.#machine.contracts;
1235
1280
  }
@@ -1674,21 +1719,9 @@ class State extends Contract {
1674
1719
  compatiblePeerCount += 1;
1675
1720
  const task = async () => {
1676
1721
  try {
1677
- let result = await peer.request(node.encoded);
1722
+ const result = await peer.request(node.encoded);
1678
1723
  const resultType = result instanceof Uint8Array ? `bytes:${result.length}` : typeof result;
1679
1724
  debug$1(`lastBlock result type: ${resultType}`);
1680
- console.log({ result });
1681
- if (result instanceof Uint8Array) {
1682
- for (let i = 0; i < 3; i += 1) {
1683
- try {
1684
- const wrapped = await new globalThis.peernet.protos["peernet-response"](result);
1685
- if (wrapped?.decoded?.response === void 0) break;
1686
- result = wrapped.decoded.response;
1687
- } catch {
1688
- break;
1689
- }
1690
- }
1691
- }
1692
1725
  return { result, peer };
1693
1726
  } catch (error) {
1694
1727
  const peerId = peer?.peerId || peer?.id || peer?.address || "unknown";
@@ -1712,7 +1745,7 @@ class State extends Contract {
1712
1745
  console.log({ promises });
1713
1746
  promises = await Promise.all(
1714
1747
  promises.map(async (item) => ({
1715
- value: (await this.#resolveLastBlockMessage(item.value)).decoded,
1748
+ value: (await resolveLastBlockMessage(item.value)).decoded,
1716
1749
  peer: item.peer
1717
1750
  }))
1718
1751
  );
@@ -1733,10 +1766,11 @@ class State extends Contract {
1733
1766
  });
1734
1767
  let node2 = await globalThis.peernet.prepareMessage(data2);
1735
1768
  try {
1736
- let message2 = await peer.request(node2.encoded);
1737
- message2 = await new globalThis.peernet.protos["peernet-response"](message2);
1769
+ const message2 = await decodeResponsePayload(await peer.request(node2.encoded));
1770
+ const blocks = message2?.blocks;
1771
+ if (!Array.isArray(blocks)) throw new Error("knownBlocks response does not contain a blocks array");
1738
1772
  const MAX_WANTLIST_SIZE = 1e3;
1739
- const incoming = message2.decoded.response.blocks.filter((block) => !this.knownBlocks.includes(block));
1773
+ const incoming = blocks.filter((block) => !this.knownBlocks.includes(block));
1740
1774
  const remaining = MAX_WANTLIST_SIZE - this.wantList.length;
1741
1775
  if (remaining > 0) this.wantList.push(...incoming.slice(0, remaining));
1742
1776
  } catch (error) {
@@ -1816,28 +1850,20 @@ class State extends Contract {
1816
1850
  this.#chainState = "loaded";
1817
1851
  return true;
1818
1852
  }
1819
- promiseRequests(promises) {
1820
- return new Promise(async (resolve, reject) => {
1821
- const timeout = setTimeout(() => {
1822
- resolve([{ index: 0, hash: "0x0" }]);
1823
- debug$1("sync timed out");
1824
- }, this.requestTimeout);
1825
- console.log({ promises });
1826
- promises = await Promise.allSettled(promises);
1827
- console.log({ promises });
1828
- promises = promises.filter(({ status }) => status === "fulfilled");
1829
- clearTimeout(timeout);
1830
- if (promises.length > 0) {
1831
- promises = promises.map(async ({ value }) => {
1832
- const node = await new globalThis.peernet.protos["peernet-response"](value.result);
1833
- return { value: node.decoded.response, peer: value.peer };
1834
- });
1835
- promises = await Promise.all(promises);
1836
- resolve(promises);
1837
- } else {
1838
- resolve([]);
1839
- }
1840
- });
1853
+ async promiseRequests(promises) {
1854
+ let timeout;
1855
+ const settled = await Promise.race([
1856
+ Promise.allSettled(promises),
1857
+ new Promise((resolve) => {
1858
+ timeout = setTimeout(() => resolve(null), this.requestTimeout);
1859
+ })
1860
+ ]);
1861
+ clearTimeout(timeout);
1862
+ if (settled === null) {
1863
+ debug$1("sync timed out");
1864
+ return [];
1865
+ }
1866
+ return settled.filter(({ status }) => status === "fulfilled").map(({ value }) => ({ value: value.result, peer: value.peer }));
1841
1867
  }
1842
1868
  get canSync() {
1843
1869
  if (this.#chainSyncing) return false;
@@ -2250,6 +2276,46 @@ const validateChainLink = (localTip, incoming) => {
2250
2276
  return "append";
2251
2277
  };
2252
2278
 
2279
+ const BLOCK_REWARD = 150n;
2280
+ const validateCanonicalValidatorSet = (blockIndex, expectedValidators, validators) => {
2281
+ const actualValidators = validators.map(({ address }) => address);
2282
+ const canonicalExpected = [...new Set(expectedValidators)].sort();
2283
+ if (actualValidators.length !== canonicalExpected.length || actualValidators.some((address, index) => address !== canonicalExpected[index])) {
2284
+ throw new Error(
2285
+ `Block ${blockIndex} validator set mismatch: expected ${canonicalExpected.join(",")}, got ${actualValidators.join(",")}`
2286
+ );
2287
+ }
2288
+ };
2289
+ const validateBlockEconomics = (block, calculatedFees) => {
2290
+ if (block.validators.length === 0) {
2291
+ throw new Error(`Block ${block.index} cannot distribute rewards without validators`);
2292
+ }
2293
+ if (block.reward !== BLOCK_REWARD) {
2294
+ throw new Error(`Block ${block.index} has invalid base reward: expected ${BLOCK_REWARD}, got ${block.reward}`);
2295
+ }
2296
+ if (block.fees !== calculatedFees) {
2297
+ throw new Error(`Block ${block.index} has invalid fees: expected ${calculatedFees}, got ${block.fees}`);
2298
+ }
2299
+ const validatorCount = BigInt(block.validators.length);
2300
+ const expectedReward = calculatedFees / validatorCount + BLOCK_REWARD / validatorCount;
2301
+ for (const validator of block.validators) {
2302
+ if (validator.reward !== expectedReward) {
2303
+ throw new Error(
2304
+ `Block ${block.index} has an invalid reward for validator ${validator.address}: expected ${expectedReward}, got ${validator.reward}`
2305
+ );
2306
+ }
2307
+ }
2308
+ };
2309
+
2310
+ const resolveTransactionReference = async (expectedHash, transactionData) => {
2311
+ const transaction = transactionData instanceof TransactionMessage ? transactionData : new TransactionMessage(transactionData);
2312
+ const actualHash = await transaction.hash();
2313
+ if (actualHash !== expectedHash) {
2314
+ throw new Error(`Transaction hash mismatch: expected ${expectedHash}, got ${actualHash}`);
2315
+ }
2316
+ return transaction;
2317
+ };
2318
+
2253
2319
  const consensusSignableData = (validatorsAddress, type, message) => ({
2254
2320
  from: String(message.from),
2255
2321
  to: validatorsAddress,
@@ -2417,8 +2483,23 @@ class Chain extends VersionControl {
2417
2483
  debug(`[consensus] Block hash mismatch in proposal: expected ${blockHash}, got ${actualHash}`);
2418
2484
  return;
2419
2485
  }
2486
+ if (BigInt(blockMessage.decoded.index) !== index) {
2487
+ debug(`[consensus] Proposal height ${index} does not match block height ${blockMessage.decoded.index}`);
2488
+ return;
2489
+ }
2490
+ if (blockMessage.decoded.producer !== from) {
2491
+ debug(`[consensus] Proposal sender ${from} does not match block producer ${blockMessage.decoded.producer}`);
2492
+ return;
2493
+ }
2494
+ validateChainLink(localBlock, {
2495
+ index: Number(blockMessage.decoded.index),
2496
+ hash: actualHash,
2497
+ previousHash: String(blockMessage.decoded.previousHash)
2498
+ });
2499
+ await this.#validateBlockValidators(blockMessage);
2500
+ await this.#resolveBlockTransactions(blockMessage);
2420
2501
  } catch (e) {
2421
- debug(`[consensus] Cannot fetch proposed block ${blockHash}:`, e?.message);
2502
+ debug(`[consensus] Invalid proposed block ${blockHash}:`, e?.message);
2422
2503
  return;
2423
2504
  }
2424
2505
  this.#consensusRound = Number(round);
@@ -2673,15 +2754,21 @@ class Chain extends VersionControl {
2673
2754
  if (new Set(validatorAddresses).size !== validatorAddresses.length) {
2674
2755
  throw new Error(`Block ${blockMessage.decoded.index} validators contain duplicates`);
2675
2756
  }
2676
- const validatorCount = BigInt(validators.length);
2677
- const expectedReward = blockMessage.decoded.fees / validatorCount + blockMessage.decoded.reward / validatorCount;
2678
- for (const validator of validators) {
2679
- if (validator.reward !== expectedReward) {
2680
- throw new Error(
2681
- `Block ${blockMessage.decoded.index} has an invalid reward for validator ${validator.address}: expected ${expectedReward}, got ${validator.reward}`
2682
- );
2683
- }
2684
- }
2757
+ const expectedValidators = await this.staticCall(addresses.validators, "validators");
2758
+ validateCanonicalValidatorSet(blockMessage.decoded.index, expectedValidators, validators);
2759
+ }
2760
+ async #resolveBlockTransactions(blockMessage) {
2761
+ const transactions = await Promise.all(
2762
+ blockMessage.decoded.transactions.map(async (expectedHash) => {
2763
+ const data = await globalThis.peernet.get(expectedHash, "transaction");
2764
+ const transaction = await resolveTransactionReference(expectedHash, data);
2765
+ await this.validateTransactionSignature(transaction);
2766
+ return transaction;
2767
+ })
2768
+ );
2769
+ const calculatedFees = (await Promise.all(transactions.map((transaction) => calculateFee(transaction.decoded)))).reduce((total, fee) => total + BigInt(fee), 0n);
2770
+ validateBlockEconomics(blockMessage.decoded, calculatedFees);
2771
+ return transactions;
2685
2772
  }
2686
2773
  /** Check if the next block will cross an epoch boundary (block-based timing) */
2687
2774
  #isEpochBoundary(blockHeight) {
@@ -2905,42 +2992,6 @@ class Chain extends VersionControl {
2905
2992
  throw error;
2906
2993
  }
2907
2994
  }
2908
- async #resolveLastBlockMessage(result) {
2909
- if (result instanceof Uint8Array) {
2910
- try {
2911
- let payload = result;
2912
- for (let i = 0; i < 3; i += 1) {
2913
- try {
2914
- const wrapped = await new globalThis.peernet.protos["peernet-response"](payload);
2915
- if (wrapped?.decoded?.response === void 0) break;
2916
- payload = wrapped.decoded.response;
2917
- } catch {
2918
- break;
2919
- }
2920
- }
2921
- if (payload !== result) return this.#resolveLastBlockMessage(payload);
2922
- return new LastBlockMessage(result);
2923
- } catch {
2924
- const candidate = new TextDecoder().decode(result);
2925
- if (candidate) {
2926
- const blockData = await globalThis.peernet.get(candidate, "block");
2927
- if (blockData) return new BlockMessage(blockData);
2928
- }
2929
- }
2930
- }
2931
- if (typeof result === "string") {
2932
- const blockData = await globalThis.peernet.get(result, "block");
2933
- if (blockData) return new BlockMessage(blockData);
2934
- }
2935
- if (result && typeof result === "object") {
2936
- const response = result.decoded?.response ?? result.response;
2937
- if (response !== void 0) return this.#resolveLastBlockMessage(response);
2938
- if ("hash" in result && "index" in result) {
2939
- return new LastBlockMessage(result);
2940
- }
2941
- }
2942
- throw new Error(`invalid lastBlock payload: ${typeof result}`);
2943
- }
2944
2995
  async #decodeKnownBlocksResponse(response) {
2945
2996
  if (!response) return null;
2946
2997
  if (Array.isArray(response)) {
@@ -3073,7 +3124,7 @@ class Chain extends VersionControl {
3073
3124
  if (lastBlockRaw === void 0 || lastBlockRaw === null) {
3074
3125
  throw new Error(`invalid lastBlock payload: ${typeof lastBlockRaw}`);
3075
3126
  }
3076
- const lastBlockMessage = await this.#resolveLastBlockMessage(lastBlockRaw);
3127
+ const lastBlockMessage = await resolveLastBlockMessage(lastBlockRaw);
3077
3128
  console.log(lastBlockMessage);
3078
3129
  lastBlock = lastBlockMessage.decoded;
3079
3130
  } catch (error) {
@@ -3216,13 +3267,7 @@ class Chain extends VersionControl {
3216
3267
  }
3217
3268
  console.log(`[chain] \u2705 Block data integrity verified: ${hash}`);
3218
3269
  await this.#validateBlockValidators(blockMessage);
3219
- const transactions = await Promise.all(
3220
- blockMessage.decoded.transactions.map(async (hash2) => {
3221
- const data = await peernet.get(hash2, "transaction");
3222
- return new TransactionMessage(data);
3223
- })
3224
- );
3225
- await Promise.all(transactions.map((transaction) => this.validateTransactionSignature(transaction)));
3270
+ const transactions = await this.#resolveBlockTransactions(blockMessage);
3226
3271
  await Promise.all(
3227
3272
  blockMessage.decoded.transactions.map(async (transactionHash) => {
3228
3273
  if (await transactionPoolStore.has(transactionHash)) await transactionPoolStore.delete(transactionHash);
@@ -3348,7 +3393,7 @@ class Chain extends VersionControl {
3348
3393
  fees: BigInt(0),
3349
3394
  timestamp,
3350
3395
  previousHash: "",
3351
- reward: BigInt(150),
3396
+ reward: BLOCK_REWARD,
3352
3397
  index: 0,
3353
3398
  producer: "",
3354
3399
  producerProof: "",