@leofcoin/chain 1.10.7 → 1.10.9

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
@@ -3,7 +3,7 @@ import { Codec } from '@leofcoin/codec-format-interface';
3
3
  import { jsonStringifyBigInt, jsonParseBigInt, formatBytes, parseUnits, formatUnits } from '@leofcoin/utils';
4
4
  import { TransactionMessage, BlockMessage, ContractMessage, LastBlockMessage, PrevoteMessage, PrecommitMessage, ProposalMessage, BWMessage, StateMessage } from '@leofcoin/messages';
5
5
  import addresses, { contractFactory } from '@leofcoin/addresses';
6
- import { createTransactionHash, calculateFee, createContractMessage, signTransaction, contractFactoryMessage, nativeTokenMessage, validatorsMessage, nameServiceMessage } from '@leofcoin/lib';
6
+ import { createTransactionHash, calculateFee, createContractMessage, signTransaction, distributeTransactionFee, supportsTransactionFees, aggregateValidatorFees, contractFactoryMessage, nativeTokenMessage, validatorsMessage, nameServiceMessage } from '@leofcoin/lib';
7
7
  import MultiWallet from '@leofcoin/multi-wallet';
8
8
  import { fromBase58 } from '@vandeurenglenn/typed-array-utils';
9
9
  import semver from 'semver';
@@ -896,7 +896,7 @@ class Machine {
896
896
  * @param {Array} parameters
897
897
  * @returns Promise<message>
898
898
  */
899
- async execute(contract, method, parameters) {
899
+ async execute(contract, method, parameters, sender) {
900
900
  try {
901
901
  if (contract === contractFactory && method === "registerContract") {
902
902
  if (await this.has(parameters[0])) throw new Error(`duplicate contract @${parameters[0]}`);
@@ -937,11 +937,15 @@ ${error.message}`);
937
937
  to: contract,
938
938
  contract,
939
939
  method,
940
- params: parameters
940
+ params: parameters,
941
+ sender
941
942
  }
942
943
  });
943
944
  });
944
945
  }
946
+ collectFee(from, payments, burned) {
947
+ return this.#askWorker("collectFee", { from, payments, burned });
948
+ }
945
949
  get(contract, method, parameters) {
946
950
  return new Promise((resolve, reject) => {
947
951
  const id = randombytes(20).toString();
@@ -1785,9 +1789,20 @@ class State extends Contract {
1785
1789
  #loadBlockTransactions;
1786
1790
  #getLastTransactions;
1787
1791
  // todo throw error
1788
- async #_executeTransaction(transaction) {
1792
+ async #_executeTransaction(transaction, validators, feesEnabled) {
1789
1793
  try {
1790
- await this.#machine.execute(transaction.decoded.to, transaction.decoded.method, transaction.decoded.params);
1794
+ const hash = await transaction.hash();
1795
+ if (feesEnabled) {
1796
+ const fee = BigInt(await calculateFee(transaction.decoded));
1797
+ const { payments, burned } = distributeTransactionFee(fee, hash, validators);
1798
+ await this.#machine.collectFee(transaction.decoded.from, payments, burned);
1799
+ }
1800
+ await this.#machine.execute(
1801
+ transaction.decoded.to,
1802
+ transaction.decoded.method,
1803
+ transaction.decoded.params,
1804
+ transaction.decoded.from
1805
+ );
1791
1806
  } catch (error) {
1792
1807
  console.log(error);
1793
1808
  await globalThis.transactionPoolStore.delete(await transaction.hash());
@@ -1816,23 +1831,21 @@ class State extends Contract {
1816
1831
  try {
1817
1832
  debug$1(`loading block: ${Number(block.index)} ${block.hash}`);
1818
1833
  let transactions = await this.#loadBlockTransactions(block.transactions || []);
1834
+ const validators = block.validators.map(({ address }) => address);
1819
1835
  debug$1(`loading transactions: ${transactions.length} for block ${block.index}`);
1820
- let priority = [];
1821
1836
  for (const transaction of transactions) {
1822
1837
  const hash = await transaction.hash();
1823
- if (transaction.decoded.priority) priority.push(transaction);
1824
1838
  if (poolTransactionKeys.has(hash)) await globalThis.transactionPoolStore.delete(hash);
1825
1839
  }
1826
- if (priority.length > 0) {
1827
- debug$1(`executing ${priority.length} priority transactions for block ${block.index}`);
1828
- priority = priority.sort((a, b) => a.nonce - b.nonce);
1829
- for (const transaction of priority) {
1830
- await this.#_executeTransaction(transaction);
1831
- }
1832
- }
1833
- transactions = transactions.filter((transaction) => !transaction.decoded.priority);
1840
+ transactions = transactions.sort((a, b) => {
1841
+ if (a.decoded.priority !== b.decoded.priority) return a.decoded.priority ? -1 : 1;
1842
+ const left = BigInt(a.decoded.nonce);
1843
+ const right = BigInt(b.decoded.nonce);
1844
+ return left < right ? -1 : left > right ? 1 : 0;
1845
+ });
1834
1846
  debug$1(`executing ${transactions.length} transactions for block ${block.index}`);
1835
- await Promise.all(transactions.map((transaction) => this.#_executeTransaction(transaction)));
1847
+ const feesEnabled = supportsTransactionFees(block.protocolVersion);
1848
+ for (const transaction of transactions) await this.#_executeTransaction(transaction, validators, feesEnabled);
1836
1849
  this.#blocks[block.index].loaded = true;
1837
1850
  debug$1(`executed transactions for block ${block.index}`);
1838
1851
  if (Number(block.index) === 0) this.#loaded = true;
@@ -2276,6 +2289,47 @@ const validateChainLink = (localTip, incoming) => {
2276
2289
  return "append";
2277
2290
  };
2278
2291
 
2292
+ const BLOCK_REWARD = 150n;
2293
+ const validateCanonicalValidatorSet = (blockIndex, expectedValidators, validators) => {
2294
+ const actualValidators = validators.map(({ address }) => address);
2295
+ const canonicalExpected = [...new Set(expectedValidators)].sort();
2296
+ if (actualValidators.length !== canonicalExpected.length || actualValidators.some((address, index) => address !== canonicalExpected[index])) {
2297
+ throw new Error(
2298
+ `Block ${blockIndex} validator set mismatch: expected ${canonicalExpected.join(",")}, got ${actualValidators.join(",")}`
2299
+ );
2300
+ }
2301
+ };
2302
+ const validateBlockEconomics = (block, calculatedFees, validatorFees = /* @__PURE__ */ new Map()) => {
2303
+ if (block.validators.length === 0) {
2304
+ throw new Error(`Block ${block.index} cannot distribute rewards without validators`);
2305
+ }
2306
+ if (block.reward !== BLOCK_REWARD) {
2307
+ throw new Error(`Block ${block.index} has invalid base reward: expected ${BLOCK_REWARD}, got ${block.reward}`);
2308
+ }
2309
+ if (block.fees !== calculatedFees) {
2310
+ throw new Error(`Block ${block.index} has invalid fees: expected ${calculatedFees}, got ${block.fees}`);
2311
+ }
2312
+ const validatorCount = BigInt(block.validators.length);
2313
+ const baseReward = BLOCK_REWARD / validatorCount;
2314
+ for (const validator of block.validators) {
2315
+ const expectedReward = baseReward + (validatorFees.get(validator.address) || 0n);
2316
+ if (validator.reward !== expectedReward) {
2317
+ throw new Error(
2318
+ `Block ${block.index} has an invalid reward for validator ${validator.address}: expected ${expectedReward}, got ${validator.reward}`
2319
+ );
2320
+ }
2321
+ }
2322
+ };
2323
+
2324
+ const resolveTransactionReference = async (expectedHash, transactionData) => {
2325
+ const transaction = transactionData instanceof TransactionMessage ? transactionData : new TransactionMessage(transactionData);
2326
+ const actualHash = await transaction.hash();
2327
+ if (actualHash !== expectedHash) {
2328
+ throw new Error(`Transaction hash mismatch: expected ${expectedHash}, got ${actualHash}`);
2329
+ }
2330
+ return transaction;
2331
+ };
2332
+
2279
2333
  const consensusSignableData = (validatorsAddress, type, message) => ({
2280
2334
  from: String(message.from),
2281
2335
  to: validatorsAddress,
@@ -2443,8 +2497,23 @@ class Chain extends VersionControl {
2443
2497
  debug(`[consensus] Block hash mismatch in proposal: expected ${blockHash}, got ${actualHash}`);
2444
2498
  return;
2445
2499
  }
2500
+ if (BigInt(blockMessage.decoded.index) !== index) {
2501
+ debug(`[consensus] Proposal height ${index} does not match block height ${blockMessage.decoded.index}`);
2502
+ return;
2503
+ }
2504
+ if (blockMessage.decoded.producer !== from) {
2505
+ debug(`[consensus] Proposal sender ${from} does not match block producer ${blockMessage.decoded.producer}`);
2506
+ return;
2507
+ }
2508
+ validateChainLink(localBlock, {
2509
+ index: Number(blockMessage.decoded.index),
2510
+ hash: actualHash,
2511
+ previousHash: String(blockMessage.decoded.previousHash)
2512
+ });
2513
+ await this.#validateBlockValidators(blockMessage);
2514
+ await this.#resolveBlockTransactions(blockMessage);
2446
2515
  } catch (e) {
2447
- debug(`[consensus] Cannot fetch proposed block ${blockHash}:`, e?.message);
2516
+ debug(`[consensus] Invalid proposed block ${blockHash}:`, e?.message);
2448
2517
  return;
2449
2518
  }
2450
2519
  this.#consensusRound = Number(round);
@@ -2699,14 +2768,52 @@ class Chain extends VersionControl {
2699
2768
  if (new Set(validatorAddresses).size !== validatorAddresses.length) {
2700
2769
  throw new Error(`Block ${blockMessage.decoded.index} validators contain duplicates`);
2701
2770
  }
2702
- const validatorCount = BigInt(validators.length);
2703
- const expectedReward = blockMessage.decoded.fees / validatorCount + blockMessage.decoded.reward / validatorCount;
2704
- for (const validator of validators) {
2705
- if (validator.reward !== expectedReward) {
2706
- throw new Error(
2707
- `Block ${blockMessage.decoded.index} has an invalid reward for validator ${validator.address}: expected ${expectedReward}, got ${validator.reward}`
2708
- );
2771
+ const expectedValidators = await this.staticCall(addresses.validators, "validators");
2772
+ validateCanonicalValidatorSet(blockMessage.decoded.index, expectedValidators, validators);
2773
+ }
2774
+ async #resolveBlockTransactions(blockMessage) {
2775
+ const transactions = await Promise.all(
2776
+ blockMessage.decoded.transactions.map(async (expectedHash) => {
2777
+ const data = await globalThis.peernet.get(expectedHash, "transaction");
2778
+ const transaction = await resolveTransactionReference(expectedHash, data);
2779
+ await this.validateTransactionSignature(transaction);
2780
+ return transaction;
2781
+ })
2782
+ );
2783
+ const feesEnabled = supportsTransactionFees(blockMessage.decoded.protocolVersion);
2784
+ if (feesEnabled) await this.#assertFeeBurnSupported();
2785
+ const calculatedFees = feesEnabled ? (await Promise.all(transactions.map((transaction) => calculateFee(transaction.decoded)))).reduce((total, fee) => total + BigInt(fee), 0n) : 0n;
2786
+ const feeEntries = feesEnabled ? await Promise.all(
2787
+ transactions.map(async (transaction) => ({
2788
+ fee: BigInt(await calculateFee(transaction.decoded)),
2789
+ transactionHash: await transaction.hash()
2790
+ }))
2791
+ ) : [];
2792
+ const validatorAddresses = blockMessage.decoded.validators.map(({ address }) => address);
2793
+ const validatorFees = feesEnabled ? aggregateValidatorFees(feeEntries, validatorAddresses) : new Map(validatorAddresses.map((address) => [address, 0n]));
2794
+ validateBlockEconomics(blockMessage.decoded, calculatedFees, validatorFees);
2795
+ if (feesEnabled) {
2796
+ const feesBySender = /* @__PURE__ */ new Map();
2797
+ for (let index = 0; index < transactions.length; index += 1) {
2798
+ const sender = transactions[index].decoded.from;
2799
+ feesBySender.set(sender, (feesBySender.get(sender) || 0n) + feeEntries[index].fee);
2709
2800
  }
2801
+ await Promise.all(
2802
+ [...feesBySender].map(async ([sender, required]) => {
2803
+ const balance = BigInt(await this.balanceOf(sender) || 0n);
2804
+ if (balance < required) {
2805
+ throw new Error(`insufficient balance for transaction fees from ${sender}: need ${required}, got ${balance}`);
2806
+ }
2807
+ })
2808
+ );
2809
+ }
2810
+ return transactions;
2811
+ }
2812
+ async #assertFeeBurnSupported() {
2813
+ const creator = await this.staticCall(addresses.nativeToken, "creator");
2814
+ const canBurn = await this.staticCall(addresses.nativeToken, "hasRole", [creator, "BURN"]);
2815
+ if (!canBurn) {
2816
+ throw new Error("native token genesis does not support protocol fee burning");
2710
2817
  }
2711
2818
  }
2712
2819
  /** Check if the next block will cross an epoch boundary (block-based timing) */
@@ -3157,9 +3264,12 @@ class Chain extends VersionControl {
3157
3264
  async #versionHandler() {
3158
3265
  return new globalThis.peernet.protos["peernet-response"]({ response: this.version });
3159
3266
  }
3160
- async #executeTransaction({ hash, from, to, method, params, nonce }) {
3267
+ async #executeTransaction({ hash, from, to, method, params, nonce, feePayments = { payments: [], burned: 0n } }) {
3161
3268
  try {
3162
- let result = await this.machine.execute(to, method, params);
3269
+ if (feePayments.payments.length > 0 || feePayments.burned > 0n) {
3270
+ await this.machine.collectFee(from, feePayments.payments, feePayments.burned);
3271
+ }
3272
+ let result = await this.machine.execute(to, method, params, from);
3163
3273
  globalThis.pubsub.publish(`transaction.completed.${hash}`, { status: "fulfilled", hash });
3164
3274
  return result || "no state change";
3165
3275
  } catch (error) {
@@ -3206,13 +3316,7 @@ class Chain extends VersionControl {
3206
3316
  }
3207
3317
  console.log(`[chain] \u2705 Block data integrity verified: ${hash}`);
3208
3318
  await this.#validateBlockValidators(blockMessage);
3209
- const transactions = await Promise.all(
3210
- blockMessage.decoded.transactions.map(async (hash2) => {
3211
- const data = await peernet.get(hash2, "transaction");
3212
- return new TransactionMessage(data);
3213
- })
3214
- );
3215
- await Promise.all(transactions.map((transaction) => this.validateTransactionSignature(transaction)));
3319
+ const transactions = await this.#resolveBlockTransactions(blockMessage);
3216
3320
  await Promise.all(
3217
3321
  blockMessage.decoded.transactions.map(async (transactionHash) => {
3218
3322
  if (await transactionPoolStore.has(transactionHash)) await transactionPoolStore.delete(transactionHash);
@@ -3237,7 +3341,10 @@ class Chain extends VersionControl {
3237
3341
  for (const transaction of allTransactions) {
3238
3342
  if (!contracts.includes(transaction.decoded.to)) contracts.push(transaction.decoded.to);
3239
3343
  this.removePendingNonce(transaction.decoded.from, transaction.decoded.nonce);
3240
- await this.#handleTransaction(transaction, []);
3344
+ const transactionHash = await transaction.hash();
3345
+ const validatorAddresses = blockMessage.decoded.validators.map(({ address }) => address);
3346
+ const feeDistribution = supportsTransactionFees(blockMessage.decoded.protocolVersion) ? distributeTransactionFee(BigInt(await calculateFee(transaction.decoded)), transactionHash, validatorAddresses) : { payments: [], burned: 0n };
3347
+ await this.#handleTransaction(transaction, [], void 0, feeDistribution);
3241
3348
  }
3242
3349
  try {
3243
3350
  promises = await Promise.allSettled(promises);
@@ -3294,7 +3401,7 @@ class Chain extends VersionControl {
3294
3401
  if (await this.hasTransactionToHandle() && !this.#runningEpoch && this.#participating) await this.#runEpoch();
3295
3402
  return true;
3296
3403
  }
3297
- async #handleTransaction(transaction, latestTransactions, block) {
3404
+ async #handleTransaction(transaction, latestTransactions, block, feePayments = { payments: [], burned: 0n }) {
3298
3405
  await this.validateTransactionSignature(transaction);
3299
3406
  const hash = await transaction.hash();
3300
3407
  const doubleTransactions = [];
@@ -3307,7 +3414,7 @@ class Chain extends VersionControl {
3307
3414
  return;
3308
3415
  }
3309
3416
  try {
3310
- const result = await this.#executeTransaction({ ...transaction.decoded, hash });
3417
+ const result = await this.#executeTransaction({ ...transaction.decoded, hash, feePayments });
3311
3418
  if (block) {
3312
3419
  block.transactions.push(hash);
3313
3420
  block.fees = block.fees += await calculateFee(transaction.decoded);
@@ -3338,7 +3445,7 @@ class Chain extends VersionControl {
3338
3445
  fees: BigInt(0),
3339
3446
  timestamp,
3340
3447
  previousHash: "",
3341
- reward: BigInt(150),
3448
+ reward: BLOCK_REWARD,
3342
3449
  index: 0,
3343
3450
  producer: "",
3344
3451
  producerProof: "",
@@ -3363,7 +3470,7 @@ class Chain extends VersionControl {
3363
3470
  for (const { transaction, hash } of allTransactions) {
3364
3471
  await this.validateTransactionSignature(transaction);
3365
3472
  block.transactions.push(hash);
3366
- block.fees += BigInt(await calculateFee(transaction.decoded));
3473
+ if (supportsTransactionFees(this.version)) block.fees += BigInt(await calculateFee(transaction.decoded));
3367
3474
  await globalThis.peernet.put(hash, transaction.encoded, "transaction");
3368
3475
  }
3369
3476
  if (block.transactions.length === 0) return;
@@ -3373,9 +3480,17 @@ class Chain extends VersionControl {
3373
3480
  const canonicalValidators = await this.staticCall(addresses.validators, "validators");
3374
3481
  const sortedValidators = [...canonicalValidators].sort();
3375
3482
  if (sortedValidators.length === 0) throw new Error("cannot produce a block without validators");
3483
+ if (supportsTransactionFees(this.version)) await this.#assertFeeBurnSupported();
3484
+ const feeEntries = supportsTransactionFees(this.version) ? await Promise.all(
3485
+ allTransactions.map(async ({ transaction, hash }) => ({
3486
+ fee: BigInt(await calculateFee(transaction.decoded)),
3487
+ transactionHash: hash
3488
+ }))
3489
+ ) : [];
3490
+ const validatorFees = supportsTransactionFees(this.version) ? aggregateValidatorFees(feeEntries, sortedValidators) : new Map(sortedValidators.map((address) => [address, 0n]));
3376
3491
  block.validators = sortedValidators.map((validatorAddress) => ({
3377
3492
  address: validatorAddress,
3378
- reward: block.fees / BigInt(sortedValidators.length) + block.reward / BigInt(sortedValidators.length)
3493
+ reward: (validatorFees.get(validatorAddress) || 0n) + block.reward / BigInt(sortedValidators.length)
3379
3494
  }));
3380
3495
  try {
3381
3496
  block.producer = globalThis.peernet.selectedAccount || "";
@@ -3500,7 +3615,7 @@ class Chain extends VersionControl {
3500
3615
  * @returns
3501
3616
  */
3502
3617
  internalCall(sender, contract, method, parameters) {
3503
- return this.machine.execute(contract, method, parameters);
3618
+ return this.machine.execute(contract, method, parameters, sender);
3504
3619
  }
3505
3620
  /**
3506
3621
  *
@@ -3510,7 +3625,7 @@ class Chain extends VersionControl {
3510
3625
  * @returns
3511
3626
  */
3512
3627
  call(contract, method, parameters) {
3513
- return this.machine.execute(contract, method, parameters);
3628
+ return this.machine.execute(contract, method, parameters, globalThis.peernet.selectedAccount);
3514
3629
  }
3515
3630
  staticCall(contract, method, parameters) {
3516
3631
  return this.machine.get(contract, method, parameters);