@leofcoin/chain 1.10.8 → 1.10.10

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
@@ -2,8 +2,8 @@ import { createDebugger } from '@vandeurenglenn/debug';
2
2
  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
- import addresses, { contractFactory } from '@leofcoin/addresses';
6
- import { createTransactionHash, calculateFee, createContractMessage, signTransaction, contractFactoryMessage, nativeTokenMessage, validatorsMessage, nameServiceMessage } from '@leofcoin/lib';
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, 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';
@@ -78,19 +78,14 @@ class Transaction extends Protocol {
78
78
  return new Promise(async (resolve, reject) => {
79
79
  let size = 0;
80
80
  const _transactions = [];
81
- const MAX_BLOCK_TX_BYTES = 786432;
82
- await Promise.all(
83
- transactions.map(async (tx) => {
84
- tx = await new TransactionMessage(tx);
85
- const newSize = size + tx.encoded.length;
86
- if (newSize <= MAX_BLOCK_TX_BYTES) {
87
- size = newSize;
88
- _transactions.push({ ...tx.decoded, hash: await tx.hash() });
89
- } else {
90
- resolve(_transactions);
91
- }
92
- })
93
- );
81
+ for (const rawTransaction of transactions) {
82
+ const tx = await new TransactionMessage(rawTransaction);
83
+ if (tx.encoded.length > MAX_TRANSACTION_BYTES) continue;
84
+ const newSize = size + tx.encoded.length;
85
+ if (newSize > MAX_BLOCK_TRANSACTION_BYTES || _transactions.length >= MAX_BLOCK_TRANSACTIONS) break;
86
+ size = newSize;
87
+ _transactions.push({ ...tx.decoded, hash: await tx.hash() });
88
+ }
94
89
  return resolve(_transactions);
95
90
  });
96
91
  }
@@ -896,7 +891,7 @@ class Machine {
896
891
  * @param {Array} parameters
897
892
  * @returns Promise<message>
898
893
  */
899
- async execute(contract, method, parameters) {
894
+ async execute(contract, method, parameters, sender) {
900
895
  try {
901
896
  if (contract === contractFactory && method === "registerContract") {
902
897
  if (await this.has(parameters[0])) throw new Error(`duplicate contract @${parameters[0]}`);
@@ -937,11 +932,18 @@ ${error.message}`);
937
932
  to: contract,
938
933
  contract,
939
934
  method,
940
- params: parameters
935
+ params: parameters,
936
+ sender
941
937
  }
942
938
  });
943
939
  });
944
940
  }
941
+ collectFee(from, payments, burned) {
942
+ return this.#askWorker("collectFee", { from, payments, burned });
943
+ }
944
+ settleRewards(rewards) {
945
+ return this.#askWorker("settleRewards", { rewards });
946
+ }
945
947
  get(contract, method, parameters) {
946
948
  return new Promise((resolve, reject) => {
947
949
  const id = randombytes(20).toString();
@@ -1785,9 +1787,20 @@ class State extends Contract {
1785
1787
  #loadBlockTransactions;
1786
1788
  #getLastTransactions;
1787
1789
  // todo throw error
1788
- async #_executeTransaction(transaction) {
1790
+ async #_executeTransaction(transaction, validators, feesEnabled, burnBasisPoints) {
1789
1791
  try {
1790
- await this.#machine.execute(transaction.decoded.to, transaction.decoded.method, transaction.decoded.params);
1792
+ const hash = await transaction.hash();
1793
+ if (feesEnabled) {
1794
+ const fee = BigInt(await calculateFee(transaction.decoded));
1795
+ const { payments, burned } = distributeTransactionFee(fee, hash, validators, burnBasisPoints);
1796
+ await this.#machine.collectFee(transaction.decoded.from, payments, burned);
1797
+ }
1798
+ await this.#machine.execute(
1799
+ transaction.decoded.to,
1800
+ transaction.decoded.method,
1801
+ transaction.decoded.params,
1802
+ transaction.decoded.from
1803
+ );
1791
1804
  } catch (error) {
1792
1805
  console.log(error);
1793
1806
  await globalThis.transactionPoolStore.delete(await transaction.hash());
@@ -1816,23 +1829,34 @@ class State extends Contract {
1816
1829
  try {
1817
1830
  debug$1(`loading block: ${Number(block.index)} ${block.hash}`);
1818
1831
  let transactions = await this.#loadBlockTransactions(block.transactions || []);
1832
+ const validators = block.validators.map(({ address }) => address);
1819
1833
  debug$1(`loading transactions: ${transactions.length} for block ${block.index}`);
1820
- let priority = [];
1821
1834
  for (const transaction of transactions) {
1822
1835
  const hash = await transaction.hash();
1823
- if (transaction.decoded.priority) priority.push(transaction);
1824
1836
  if (poolTransactionKeys.has(hash)) await globalThis.transactionPoolStore.delete(hash);
1825
1837
  }
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);
1838
+ transactions = transactions.sort((a, b) => {
1839
+ if (a.decoded.priority !== b.decoded.priority) return a.decoded.priority ? -1 : 1;
1840
+ const left = BigInt(a.decoded.nonce);
1841
+ const right = BigInt(b.decoded.nonce);
1842
+ return left < right ? -1 : left > right ? 1 : 0;
1843
+ });
1834
1844
  debug$1(`executing ${transactions.length} transactions for block ${block.index}`);
1835
- await Promise.all(transactions.map((transaction) => this.#_executeTransaction(transaction)));
1845
+ const feesEnabled = supportsTransactionFees(block.protocolVersion);
1846
+ const monetaryPolicyEnabled = supportsMonetaryPolicy(block.protocolVersion);
1847
+ if (monetaryPolicyEnabled) await validateBlockResourceLimits(transactions);
1848
+ const policy = monetaryPolicyEnabled ? calculateMonetaryPolicy(
1849
+ BigInt(await this.#machine.get(nativeToken, "totalSupply")),
1850
+ BigInt(await this.#machine.get(nativeToken, "targetSupply"))
1851
+ ) : { subsidy: 0n, burnBasisPoints: 1000n };
1852
+ for (const transaction of transactions) {
1853
+ await this.#_executeTransaction(transaction, validators, feesEnabled, policy.burnBasisPoints);
1854
+ }
1855
+ if (monetaryPolicyEnabled && policy.subsidy > 0n) {
1856
+ await this.#machine.settleRewards([
1857
+ ...distributeAmount(policy.subsidy, validators, Number(block.index) % validators.length)
1858
+ ]);
1859
+ }
1836
1860
  this.#blocks[block.index].loaded = true;
1837
1861
  debug$1(`executed transactions for block ${block.index}`);
1838
1862
  if (Number(block.index) === 0) this.#loaded = true;
@@ -2276,7 +2300,6 @@ const validateChainLink = (localTip, incoming) => {
2276
2300
  return "append";
2277
2301
  };
2278
2302
 
2279
- const BLOCK_REWARD = 150n;
2280
2303
  const validateCanonicalValidatorSet = (blockIndex, expectedValidators, validators) => {
2281
2304
  const actualValidators = validators.map(({ address }) => address);
2282
2305
  const canonicalExpected = [...new Set(expectedValidators)].sort();
@@ -2286,19 +2309,18 @@ const validateCanonicalValidatorSet = (blockIndex, expectedValidators, validator
2286
2309
  );
2287
2310
  }
2288
2311
  };
2289
- const validateBlockEconomics = (block, calculatedFees) => {
2312
+ const validateBlockEconomics = (block, calculatedFees, validatorFees = /* @__PURE__ */ new Map(), subsidyRewards = /* @__PURE__ */ new Map(), expectedSubsidy = [...subsidyRewards.values()].reduce((sum, reward) => sum + reward, 0n)) => {
2290
2313
  if (block.validators.length === 0) {
2291
2314
  throw new Error(`Block ${block.index} cannot distribute rewards without validators`);
2292
2315
  }
2293
- if (block.reward !== BLOCK_REWARD) {
2294
- throw new Error(`Block ${block.index} has invalid base reward: expected ${BLOCK_REWARD}, got ${block.reward}`);
2316
+ if (block.reward !== expectedSubsidy) {
2317
+ throw new Error(`Block ${block.index} has invalid base reward: expected ${expectedSubsidy}, got ${block.reward}`);
2295
2318
  }
2296
2319
  if (block.fees !== calculatedFees) {
2297
2320
  throw new Error(`Block ${block.index} has invalid fees: expected ${calculatedFees}, got ${block.fees}`);
2298
2321
  }
2299
- const validatorCount = BigInt(block.validators.length);
2300
- const expectedReward = calculatedFees / validatorCount + BLOCK_REWARD / validatorCount;
2301
2322
  for (const validator of block.validators) {
2323
+ const expectedReward = (subsidyRewards.get(validator.address) || 0n) + (validatorFees.get(validator.address) || 0n);
2302
2324
  if (validator.reward !== expectedReward) {
2303
2325
  throw new Error(
2304
2326
  `Block ${block.index} has an invalid reward for validator ${validator.address}: expected ${expectedReward}, got ${validator.reward}`
@@ -2766,10 +2788,57 @@ class Chain extends VersionControl {
2766
2788
  return transaction;
2767
2789
  })
2768
2790
  );
2769
- const calculatedFees = (await Promise.all(transactions.map((transaction) => calculateFee(transaction.decoded)))).reduce((total, fee) => total + BigInt(fee), 0n);
2770
- validateBlockEconomics(blockMessage.decoded, calculatedFees);
2791
+ const feesEnabled = supportsTransactionFees(blockMessage.decoded.protocolVersion);
2792
+ if (feesEnabled) await this.#assertFeeBurnSupported(blockMessage.decoded.protocolVersion);
2793
+ const adaptivePolicy = supportsMonetaryPolicy(blockMessage.decoded.protocolVersion);
2794
+ if (adaptivePolicy) await validateBlockResourceLimits(transactions);
2795
+ const policy = await this.#monetaryPolicy(blockMessage.decoded.protocolVersion);
2796
+ const calculatedFees = feesEnabled ? (await Promise.all(transactions.map((transaction) => calculateFee(transaction.decoded)))).reduce((total, fee) => total + BigInt(fee), 0n) : 0n;
2797
+ const feeEntries = feesEnabled ? await Promise.all(
2798
+ transactions.map(async (transaction) => ({
2799
+ fee: BigInt(await calculateFee(transaction.decoded)),
2800
+ transactionHash: await transaction.hash()
2801
+ }))
2802
+ ) : [];
2803
+ const validatorAddresses = blockMessage.decoded.validators.map(({ address }) => address);
2804
+ const validatorFees = feesEnabled ? aggregateValidatorFees(feeEntries, validatorAddresses, policy.burnBasisPoints) : new Map(validatorAddresses.map((address) => [address, 0n]));
2805
+ const subsidyRewards = adaptivePolicy ? distributeAmount(policy.subsidy, validatorAddresses, Number(blockMessage.decoded.index) % validatorAddresses.length) : new Map(validatorAddresses.map((address) => [address, policy.subsidy / BigInt(validatorAddresses.length)]));
2806
+ validateBlockEconomics(blockMessage.decoded, calculatedFees, validatorFees, subsidyRewards, policy.subsidy);
2807
+ if (feesEnabled) {
2808
+ const feesBySender = /* @__PURE__ */ new Map();
2809
+ for (let index = 0; index < transactions.length; index += 1) {
2810
+ const sender = transactions[index].decoded.from;
2811
+ feesBySender.set(sender, (feesBySender.get(sender) || 0n) + feeEntries[index].fee);
2812
+ }
2813
+ await Promise.all(
2814
+ [...feesBySender].map(async ([sender, required]) => {
2815
+ const balance = BigInt(await this.balanceOf(sender) || 0n);
2816
+ if (balance < required) {
2817
+ throw new Error(`insufficient balance for transaction fees from ${sender}: need ${required}, got ${balance}`);
2818
+ }
2819
+ })
2820
+ );
2821
+ }
2771
2822
  return transactions;
2772
2823
  }
2824
+ async #assertFeeBurnSupported(protocolVersion) {
2825
+ const creator = await this.staticCall(addresses.nativeToken, "creator");
2826
+ const [canBurn, canMint] = await Promise.all([
2827
+ this.staticCall(addresses.nativeToken, "hasRole", [creator, "BURN"]),
2828
+ this.staticCall(addresses.nativeToken, "hasRole", [creator, "MINT"])
2829
+ ]);
2830
+ if (!canBurn || supportsMonetaryPolicy(protocolVersion) && !canMint) {
2831
+ throw new Error("native token genesis does not support protocol monetary policy");
2832
+ }
2833
+ }
2834
+ async #monetaryPolicy(protocolVersion) {
2835
+ if (!supportsMonetaryPolicy(protocolVersion)) return { subsidy: 150n, burnBasisPoints: 1000n, floorSupply: 0n };
2836
+ const [totalSupply, targetSupply] = await Promise.all([
2837
+ this.staticCall(addresses.nativeToken, "totalSupply"),
2838
+ this.staticCall(addresses.nativeToken, "targetSupply")
2839
+ ]);
2840
+ return calculateMonetaryPolicy(BigInt(totalSupply), BigInt(targetSupply));
2841
+ }
2773
2842
  /** Check if the next block will cross an epoch boundary (block-based timing) */
2774
2843
  #isEpochBoundary(blockHeight) {
2775
2844
  return (blockHeight + 1) % this.#epochLength === 0;
@@ -3218,9 +3287,12 @@ class Chain extends VersionControl {
3218
3287
  async #versionHandler() {
3219
3288
  return new globalThis.peernet.protos["peernet-response"]({ response: this.version });
3220
3289
  }
3221
- async #executeTransaction({ hash, from, to, method, params, nonce }) {
3290
+ async #executeTransaction({ hash, from, to, method, params, nonce, feePayments = { payments: [], burned: 0n } }) {
3222
3291
  try {
3223
- let result = await this.machine.execute(to, method, params);
3292
+ if (feePayments.payments.length > 0 || feePayments.burned > 0n) {
3293
+ await this.machine.collectFee(from, feePayments.payments, feePayments.burned);
3294
+ }
3295
+ let result = await this.machine.execute(to, method, params, from);
3224
3296
  globalThis.pubsub.publish(`transaction.completed.${hash}`, { status: "fulfilled", hash });
3225
3297
  return result || "no state change";
3226
3298
  } catch (error) {
@@ -3289,10 +3361,18 @@ class Chain extends VersionControl {
3289
3361
  if (nonceDiff !== 0) return nonceDiff;
3290
3362
  return 0;
3291
3363
  });
3364
+ const monetaryPolicy = await this.#monetaryPolicy(blockMessage.decoded.protocolVersion);
3292
3365
  for (const transaction of allTransactions) {
3293
3366
  if (!contracts.includes(transaction.decoded.to)) contracts.push(transaction.decoded.to);
3294
3367
  this.removePendingNonce(transaction.decoded.from, transaction.decoded.nonce);
3295
- await this.#handleTransaction(transaction, []);
3368
+ const transactionHash = await transaction.hash();
3369
+ const validatorAddresses = blockMessage.decoded.validators.map(({ address }) => address);
3370
+ const feeDistribution = supportsTransactionFees(blockMessage.decoded.protocolVersion) ? distributeTransactionFee(BigInt(await calculateFee(transaction.decoded)), transactionHash, validatorAddresses, monetaryPolicy.burnBasisPoints) : { payments: [], burned: 0n };
3371
+ await this.#handleTransaction(transaction, [], void 0, feeDistribution);
3372
+ }
3373
+ if (supportsMonetaryPolicy(blockMessage.decoded.protocolVersion) && monetaryPolicy.subsidy > 0n) {
3374
+ const validators = blockMessage.decoded.validators.map(({ address }) => address);
3375
+ await this.machine.settleRewards([...distributeAmount(monetaryPolicy.subsidy, validators, blockIndex % validators.length)]);
3296
3376
  }
3297
3377
  try {
3298
3378
  promises = await Promise.allSettled(promises);
@@ -3349,7 +3429,7 @@ class Chain extends VersionControl {
3349
3429
  if (await this.hasTransactionToHandle() && !this.#runningEpoch && this.#participating) await this.#runEpoch();
3350
3430
  return true;
3351
3431
  }
3352
- async #handleTransaction(transaction, latestTransactions, block) {
3432
+ async #handleTransaction(transaction, latestTransactions, block, feePayments = { payments: [], burned: 0n }) {
3353
3433
  await this.validateTransactionSignature(transaction);
3354
3434
  const hash = await transaction.hash();
3355
3435
  const doubleTransactions = [];
@@ -3362,7 +3442,7 @@ class Chain extends VersionControl {
3362
3442
  return;
3363
3443
  }
3364
3444
  try {
3365
- const result = await this.#executeTransaction({ ...transaction.decoded, hash });
3445
+ const result = await this.#executeTransaction({ ...transaction.decoded, hash, feePayments });
3366
3446
  if (block) {
3367
3447
  block.transactions.push(hash);
3368
3448
  block.fees = block.fees += await calculateFee(transaction.decoded);
@@ -3393,7 +3473,7 @@ class Chain extends VersionControl {
3393
3473
  fees: BigInt(0),
3394
3474
  timestamp,
3395
3475
  previousHash: "",
3396
- reward: BLOCK_REWARD,
3476
+ reward: 0n,
3397
3477
  index: 0,
3398
3478
  producer: "",
3399
3479
  producerProof: "",
@@ -3415,10 +3495,18 @@ class Chain extends VersionControl {
3415
3495
  if (nonceDiff !== 0) return nonceDiff;
3416
3496
  return 0;
3417
3497
  });
3498
+ let blockTransactionBytes = 0;
3418
3499
  for (const { transaction, hash } of allTransactions) {
3500
+ const transactionBytes = transaction.encoded.length;
3501
+ if (transactionBytes > MAX_TRANSACTION_BYTES) {
3502
+ await globalThis.transactionPoolStore.delete(hash);
3503
+ continue;
3504
+ }
3505
+ if (block.transactions.length >= MAX_BLOCK_TRANSACTIONS || blockTransactionBytes + transactionBytes > MAX_BLOCK_TRANSACTION_BYTES) break;
3419
3506
  await this.validateTransactionSignature(transaction);
3420
3507
  block.transactions.push(hash);
3421
- block.fees += BigInt(await calculateFee(transaction.decoded));
3508
+ blockTransactionBytes += transactionBytes;
3509
+ if (supportsTransactionFees(this.version)) block.fees += BigInt(await calculateFee(transaction.decoded));
3422
3510
  await globalThis.peernet.put(hash, transaction.encoded, "transaction");
3423
3511
  }
3424
3512
  if (block.transactions.length === 0) return;
@@ -3428,9 +3516,20 @@ class Chain extends VersionControl {
3428
3516
  const canonicalValidators = await this.staticCall(addresses.validators, "validators");
3429
3517
  const sortedValidators = [...canonicalValidators].sort();
3430
3518
  if (sortedValidators.length === 0) throw new Error("cannot produce a block without validators");
3519
+ if (supportsTransactionFees(this.version)) await this.#assertFeeBurnSupported(this.version);
3520
+ const monetaryPolicy = await this.#monetaryPolicy(this.version);
3521
+ block.reward = monetaryPolicy.subsidy;
3522
+ const feeEntries = supportsTransactionFees(this.version) ? await Promise.all(
3523
+ allTransactions.map(async ({ transaction, hash }) => ({
3524
+ fee: BigInt(await calculateFee(transaction.decoded)),
3525
+ transactionHash: hash
3526
+ }))
3527
+ ) : [];
3528
+ const validatorFees = supportsTransactionFees(this.version) ? aggregateValidatorFees(feeEntries, sortedValidators, monetaryPolicy.burnBasisPoints) : new Map(sortedValidators.map((address) => [address, 0n]));
3529
+ const subsidyRewards = supportsMonetaryPolicy(this.version) ? distributeAmount(monetaryPolicy.subsidy, sortedValidators, Number(block.index) % sortedValidators.length) : new Map(sortedValidators.map((address) => [address, monetaryPolicy.subsidy / BigInt(sortedValidators.length)]));
3431
3530
  block.validators = sortedValidators.map((validatorAddress) => ({
3432
3531
  address: validatorAddress,
3433
- reward: block.fees / BigInt(sortedValidators.length) + block.reward / BigInt(sortedValidators.length)
3532
+ reward: (validatorFees.get(validatorAddress) || 0n) + (subsidyRewards.get(validatorAddress) || 0n)
3434
3533
  }));
3435
3534
  try {
3436
3535
  block.producer = globalThis.peernet.selectedAccount || "";
@@ -3555,7 +3654,7 @@ class Chain extends VersionControl {
3555
3654
  * @returns
3556
3655
  */
3557
3656
  internalCall(sender, contract, method, parameters) {
3558
- return this.machine.execute(contract, method, parameters);
3657
+ return this.machine.execute(contract, method, parameters, sender);
3559
3658
  }
3560
3659
  /**
3561
3660
  *
@@ -3565,7 +3664,7 @@ class Chain extends VersionControl {
3565
3664
  * @returns
3566
3665
  */
3567
3666
  call(contract, method, parameters) {
3568
- return this.machine.execute(contract, method, parameters);
3667
+ return this.machine.execute(contract, method, parameters, globalThis.peernet.selectedAccount);
3569
3668
  }
3570
3669
  staticCall(contract, method, parameters) {
3571
3670
  return this.machine.get(contract, method, parameters);