@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.
@@ -1056,6 +1056,61 @@ const contractFactoryMessage = bytecodes.contractFactory;
1056
1056
  const nativeTokenMessage = bytecodes.nativeToken;
1057
1057
  const nameServiceMessage = bytecodes.nameService;
1058
1058
  const validatorsMessage = bytecodes.validators;
1059
+ const TRANSACTION_FEE_BYTES = 1024n;
1060
+ const TRANSACTION_FEE_UNIT = 10n;
1061
+ const FEE_BURN_BASIS_POINTS = 1000n;
1062
+ const FEE_BASIS_POINTS = 10000n;
1063
+ const FEE_PROTOCOL_VERSION = "1.10.9";
1064
+ const parseProtocolVersion = (version) => {
1065
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
1066
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : void 0;
1067
+ };
1068
+ const supportsTransactionFees = (version) => {
1069
+ const actual = parseProtocolVersion(version);
1070
+ const required = parseProtocolVersion(FEE_PROTOCOL_VERSION);
1071
+ if (!actual)
1072
+ return false;
1073
+ for (let index = 0; index < required.length; index += 1) {
1074
+ if (actual[index] !== required[index])
1075
+ return actual[index] > required[index];
1076
+ }
1077
+ return true;
1078
+ };
1079
+ const feeRotationIndex = (transactionHash, validatorCount) => {
1080
+ let value = 0n;
1081
+ for (const character of transactionHash)
1082
+ value = (value * 31n + BigInt(character.charCodeAt(0))) % 4294967291n;
1083
+ return Number(value % BigInt(validatorCount));
1084
+ };
1085
+ const distributeTransactionFee = (fee, transactionHash, validatorAddresses) => {
1086
+ const canonicalValidators = [...new Set(validatorAddresses)].sort();
1087
+ if (canonicalValidators.length === 0)
1088
+ throw new Error("cannot distribute transaction fee without validators");
1089
+ if (fee < 0n)
1090
+ throw new Error("transaction fee cannot be negative");
1091
+ const burned = fee * FEE_BURN_BASIS_POINTS / FEE_BASIS_POINTS;
1092
+ const validatorPool = fee - burned;
1093
+ const count = BigInt(canonicalValidators.length);
1094
+ const base = validatorPool / count;
1095
+ const remainder = Number(validatorPool % count);
1096
+ const start = feeRotationIndex(transactionHash, canonicalValidators.length);
1097
+ const validatorFees = new Map(canonicalValidators.map((validator) => [validator, base]));
1098
+ for (let index = 0; index < remainder; index += 1) {
1099
+ const validator = canonicalValidators[(start + index) % canonicalValidators.length];
1100
+ validatorFees.set(validator, validatorFees.get(validator) + 1n);
1101
+ }
1102
+ const payments = [...validatorFees.entries()].filter(([, amount]) => amount > 0n).map(([to, amount]) => ({ to, amount }));
1103
+ return { burned, validatorFees, payments };
1104
+ };
1105
+ const aggregateValidatorFees = (fees, validatorAddresses) => {
1106
+ const totals = new Map([...new Set(validatorAddresses)].sort().map((validator) => [validator, 0n]));
1107
+ for (const entry of fees) {
1108
+ const { validatorFees } = distributeTransactionFee(entry.fee, entry.transactionHash, validatorAddresses);
1109
+ for (const [validator, amount] of validatorFees)
1110
+ totals.set(validator, totals.get(validator) + amount);
1111
+ }
1112
+ return totals;
1113
+ };
1059
1114
  const createContractMessage = async (creator, contract, constructorParameters = []) => {
1060
1115
  return new ContractMessage({
1061
1116
  creator,
@@ -1064,11 +1119,10 @@ const createContractMessage = async (creator, contract, constructorParameters =
1064
1119
  });
1065
1120
  };
1066
1121
  const calculateFee = async (transaction, format = false) => {
1067
- if (transaction.to === validators$2)
1068
- return 0;
1069
1122
  transaction = await new TransactionMessage(transaction);
1070
- let fee = toBigInt(String(transaction.encoded.length));
1071
- fee /= 1073741824n;
1123
+ const encodedBytes = BigInt(transaction.encoded.length);
1124
+ const units = (encodedBytes + TRANSACTION_FEE_BYTES - 1n) / TRANSACTION_FEE_BYTES;
1125
+ const fee = units * TRANSACTION_FEE_UNIT;
1072
1126
  return format ? formatUnits(fee.toString()) : fee;
1073
1127
  };
1074
1128
  const createTransactionHash = async (transaction) => {
@@ -8801,7 +8855,7 @@ class Machine {
8801
8855
  * @param {Array} parameters
8802
8856
  * @returns Promise<message>
8803
8857
  */
8804
- async execute(contract, method, parameters) {
8858
+ async execute(contract, method, parameters, sender) {
8805
8859
  try {
8806
8860
  if (contract === contractFactory$2 && method === "registerContract") {
8807
8861
  if (await this.has(parameters[0])) throw new Error(`duplicate contract @${parameters[0]}`);
@@ -8842,11 +8896,15 @@ ${error.message}`);
8842
8896
  to: contract,
8843
8897
  contract,
8844
8898
  method,
8845
- params: parameters
8899
+ params: parameters,
8900
+ sender
8846
8901
  }
8847
8902
  });
8848
8903
  });
8849
8904
  }
8905
+ collectFee(from, payments, burned) {
8906
+ return this.#askWorker("collectFee", { from, payments, burned });
8907
+ }
8850
8908
  get(contract, method, parameters) {
8851
8909
  return new Promise((resolve, reject) => {
8852
8910
  const id = randombytes(20).toString();
@@ -9690,9 +9748,20 @@ class State extends Contract {
9690
9748
  #loadBlockTransactions;
9691
9749
  #getLastTransactions;
9692
9750
  // todo throw error
9693
- async #_executeTransaction(transaction) {
9751
+ async #_executeTransaction(transaction, validators, feesEnabled) {
9694
9752
  try {
9695
- await this.#machine.execute(transaction.decoded.to, transaction.decoded.method, transaction.decoded.params);
9753
+ const hash = await transaction.hash();
9754
+ if (feesEnabled) {
9755
+ const fee = BigInt(await calculateFee(transaction.decoded));
9756
+ const { payments, burned } = distributeTransactionFee(fee, hash, validators);
9757
+ await this.#machine.collectFee(transaction.decoded.from, payments, burned);
9758
+ }
9759
+ await this.#machine.execute(
9760
+ transaction.decoded.to,
9761
+ transaction.decoded.method,
9762
+ transaction.decoded.params,
9763
+ transaction.decoded.from
9764
+ );
9696
9765
  } catch (error) {
9697
9766
  console.log(error);
9698
9767
  await globalThis.transactionPoolStore.delete(await transaction.hash());
@@ -9721,23 +9790,21 @@ class State extends Contract {
9721
9790
  try {
9722
9791
  debug$1(`loading block: ${Number(block.index)} ${block.hash}`);
9723
9792
  let transactions = await this.#loadBlockTransactions(block.transactions || []);
9793
+ const validators = block.validators.map(({ address }) => address);
9724
9794
  debug$1(`loading transactions: ${transactions.length} for block ${block.index}`);
9725
- let priority = [];
9726
9795
  for (const transaction of transactions) {
9727
9796
  const hash = await transaction.hash();
9728
- if (transaction.decoded.priority) priority.push(transaction);
9729
9797
  if (poolTransactionKeys.has(hash)) await globalThis.transactionPoolStore.delete(hash);
9730
9798
  }
9731
- if (priority.length > 0) {
9732
- debug$1(`executing ${priority.length} priority transactions for block ${block.index}`);
9733
- priority = priority.sort((a, b) => a.nonce - b.nonce);
9734
- for (const transaction of priority) {
9735
- await this.#_executeTransaction(transaction);
9736
- }
9737
- }
9738
- transactions = transactions.filter((transaction) => !transaction.decoded.priority);
9799
+ transactions = transactions.sort((a, b) => {
9800
+ if (a.decoded.priority !== b.decoded.priority) return a.decoded.priority ? -1 : 1;
9801
+ const left = BigInt(a.decoded.nonce);
9802
+ const right = BigInt(b.decoded.nonce);
9803
+ return left < right ? -1 : left > right ? 1 : 0;
9804
+ });
9739
9805
  debug$1(`executing ${transactions.length} transactions for block ${block.index}`);
9740
- await Promise.all(transactions.map((transaction) => this.#_executeTransaction(transaction)));
9806
+ const feesEnabled = supportsTransactionFees(block.protocolVersion);
9807
+ for (const transaction of transactions) await this.#_executeTransaction(transaction, validators, feesEnabled);
9741
9808
  this.#blocks[block.index].loaded = true;
9742
9809
  debug$1(`executed transactions for block ${block.index}`);
9743
9810
  if (Number(block.index) === 0) this.#loaded = true;
@@ -10181,6 +10248,47 @@ const validateChainLink = (localTip, incoming) => {
10181
10248
  return "append";
10182
10249
  };
10183
10250
 
10251
+ const BLOCK_REWARD = 150n;
10252
+ const validateCanonicalValidatorSet = (blockIndex, expectedValidators, validators) => {
10253
+ const actualValidators = validators.map(({ address }) => address);
10254
+ const canonicalExpected = [...new Set(expectedValidators)].sort();
10255
+ if (actualValidators.length !== canonicalExpected.length || actualValidators.some((address, index) => address !== canonicalExpected[index])) {
10256
+ throw new Error(
10257
+ `Block ${blockIndex} validator set mismatch: expected ${canonicalExpected.join(",")}, got ${actualValidators.join(",")}`
10258
+ );
10259
+ }
10260
+ };
10261
+ const validateBlockEconomics = (block, calculatedFees, validatorFees = /* @__PURE__ */ new Map()) => {
10262
+ if (block.validators.length === 0) {
10263
+ throw new Error(`Block ${block.index} cannot distribute rewards without validators`);
10264
+ }
10265
+ if (block.reward !== BLOCK_REWARD) {
10266
+ throw new Error(`Block ${block.index} has invalid base reward: expected ${BLOCK_REWARD}, got ${block.reward}`);
10267
+ }
10268
+ if (block.fees !== calculatedFees) {
10269
+ throw new Error(`Block ${block.index} has invalid fees: expected ${calculatedFees}, got ${block.fees}`);
10270
+ }
10271
+ const validatorCount = BigInt(block.validators.length);
10272
+ const baseReward = BLOCK_REWARD / validatorCount;
10273
+ for (const validator of block.validators) {
10274
+ const expectedReward = baseReward + (validatorFees.get(validator.address) || 0n);
10275
+ if (validator.reward !== expectedReward) {
10276
+ throw new Error(
10277
+ `Block ${block.index} has an invalid reward for validator ${validator.address}: expected ${expectedReward}, got ${validator.reward}`
10278
+ );
10279
+ }
10280
+ }
10281
+ };
10282
+
10283
+ const resolveTransactionReference = async (expectedHash, transactionData) => {
10284
+ const transaction = transactionData instanceof TransactionMessage ? transactionData : new TransactionMessage(transactionData);
10285
+ const actualHash = await transaction.hash();
10286
+ if (actualHash !== expectedHash) {
10287
+ throw new Error(`Transaction hash mismatch: expected ${expectedHash}, got ${actualHash}`);
10288
+ }
10289
+ return transaction;
10290
+ };
10291
+
10184
10292
  const consensusSignableData = (validatorsAddress, type, message) => ({
10185
10293
  from: String(message.from),
10186
10294
  to: validatorsAddress,
@@ -10348,8 +10456,23 @@ class Chain extends VersionControl {
10348
10456
  debug(`[consensus] Block hash mismatch in proposal: expected ${blockHash}, got ${actualHash}`);
10349
10457
  return;
10350
10458
  }
10459
+ if (BigInt(blockMessage.decoded.index) !== index) {
10460
+ debug(`[consensus] Proposal height ${index} does not match block height ${blockMessage.decoded.index}`);
10461
+ return;
10462
+ }
10463
+ if (blockMessage.decoded.producer !== from) {
10464
+ debug(`[consensus] Proposal sender ${from} does not match block producer ${blockMessage.decoded.producer}`);
10465
+ return;
10466
+ }
10467
+ validateChainLink(localBlock, {
10468
+ index: Number(blockMessage.decoded.index),
10469
+ hash: actualHash,
10470
+ previousHash: String(blockMessage.decoded.previousHash)
10471
+ });
10472
+ await this.#validateBlockValidators(blockMessage);
10473
+ await this.#resolveBlockTransactions(blockMessage);
10351
10474
  } catch (e) {
10352
- debug(`[consensus] Cannot fetch proposed block ${blockHash}:`, e?.message);
10475
+ debug(`[consensus] Invalid proposed block ${blockHash}:`, e?.message);
10353
10476
  return;
10354
10477
  }
10355
10478
  this.#consensusRound = Number(round);
@@ -10604,14 +10727,52 @@ class Chain extends VersionControl {
10604
10727
  if (new Set(validatorAddresses).size !== validatorAddresses.length) {
10605
10728
  throw new Error(`Block ${blockMessage.decoded.index} validators contain duplicates`);
10606
10729
  }
10607
- const validatorCount = BigInt(validators.length);
10608
- const expectedReward = blockMessage.decoded.fees / validatorCount + blockMessage.decoded.reward / validatorCount;
10609
- for (const validator of validators) {
10610
- if (validator.reward !== expectedReward) {
10611
- throw new Error(
10612
- `Block ${blockMessage.decoded.index} has an invalid reward for validator ${validator.address}: expected ${expectedReward}, got ${validator.reward}`
10613
- );
10730
+ const expectedValidators = await this.staticCall(addresses.validators, "validators");
10731
+ validateCanonicalValidatorSet(blockMessage.decoded.index, expectedValidators, validators);
10732
+ }
10733
+ async #resolveBlockTransactions(blockMessage) {
10734
+ const transactions = await Promise.all(
10735
+ blockMessage.decoded.transactions.map(async (expectedHash) => {
10736
+ const data = await globalThis.peernet.get(expectedHash, "transaction");
10737
+ const transaction = await resolveTransactionReference(expectedHash, data);
10738
+ await this.validateTransactionSignature(transaction);
10739
+ return transaction;
10740
+ })
10741
+ );
10742
+ const feesEnabled = supportsTransactionFees(blockMessage.decoded.protocolVersion);
10743
+ if (feesEnabled) await this.#assertFeeBurnSupported();
10744
+ const calculatedFees = feesEnabled ? (await Promise.all(transactions.map((transaction) => calculateFee(transaction.decoded)))).reduce((total, fee) => total + BigInt(fee), 0n) : 0n;
10745
+ const feeEntries = feesEnabled ? await Promise.all(
10746
+ transactions.map(async (transaction) => ({
10747
+ fee: BigInt(await calculateFee(transaction.decoded)),
10748
+ transactionHash: await transaction.hash()
10749
+ }))
10750
+ ) : [];
10751
+ const validatorAddresses = blockMessage.decoded.validators.map(({ address }) => address);
10752
+ const validatorFees = feesEnabled ? aggregateValidatorFees(feeEntries, validatorAddresses) : new Map(validatorAddresses.map((address) => [address, 0n]));
10753
+ validateBlockEconomics(blockMessage.decoded, calculatedFees, validatorFees);
10754
+ if (feesEnabled) {
10755
+ const feesBySender = /* @__PURE__ */ new Map();
10756
+ for (let index = 0; index < transactions.length; index += 1) {
10757
+ const sender = transactions[index].decoded.from;
10758
+ feesBySender.set(sender, (feesBySender.get(sender) || 0n) + feeEntries[index].fee);
10614
10759
  }
10760
+ await Promise.all(
10761
+ [...feesBySender].map(async ([sender, required]) => {
10762
+ const balance = BigInt(await this.balanceOf(sender) || 0n);
10763
+ if (balance < required) {
10764
+ throw new Error(`insufficient balance for transaction fees from ${sender}: need ${required}, got ${balance}`);
10765
+ }
10766
+ })
10767
+ );
10768
+ }
10769
+ return transactions;
10770
+ }
10771
+ async #assertFeeBurnSupported() {
10772
+ const creator = await this.staticCall(addresses.nativeToken, "creator");
10773
+ const canBurn = await this.staticCall(addresses.nativeToken, "hasRole", [creator, "BURN"]);
10774
+ if (!canBurn) {
10775
+ throw new Error("native token genesis does not support protocol fee burning");
10615
10776
  }
10616
10777
  }
10617
10778
  /** Check if the next block will cross an epoch boundary (block-based timing) */
@@ -11062,9 +11223,12 @@ class Chain extends VersionControl {
11062
11223
  async #versionHandler() {
11063
11224
  return new globalThis.peernet.protos["peernet-response"]({ response: this.version });
11064
11225
  }
11065
- async #executeTransaction({ hash, from, to, method, params, nonce }) {
11226
+ async #executeTransaction({ hash, from, to, method, params, nonce, feePayments = { payments: [], burned: 0n } }) {
11066
11227
  try {
11067
- let result = await this.machine.execute(to, method, params);
11228
+ if (feePayments.payments.length > 0 || feePayments.burned > 0n) {
11229
+ await this.machine.collectFee(from, feePayments.payments, feePayments.burned);
11230
+ }
11231
+ let result = await this.machine.execute(to, method, params, from);
11068
11232
  globalThis.pubsub.publish(`transaction.completed.${hash}`, { status: "fulfilled", hash });
11069
11233
  return result || "no state change";
11070
11234
  } catch (error) {
@@ -11111,13 +11275,7 @@ class Chain extends VersionControl {
11111
11275
  }
11112
11276
  console.log(`[chain] \u2705 Block data integrity verified: ${hash}`);
11113
11277
  await this.#validateBlockValidators(blockMessage);
11114
- const transactions = await Promise.all(
11115
- blockMessage.decoded.transactions.map(async (hash2) => {
11116
- const data = await peernet.get(hash2, "transaction");
11117
- return new TransactionMessage(data);
11118
- })
11119
- );
11120
- await Promise.all(transactions.map((transaction) => this.validateTransactionSignature(transaction)));
11278
+ const transactions = await this.#resolveBlockTransactions(blockMessage);
11121
11279
  await Promise.all(
11122
11280
  blockMessage.decoded.transactions.map(async (transactionHash) => {
11123
11281
  if (await transactionPoolStore.has(transactionHash)) await transactionPoolStore.delete(transactionHash);
@@ -11142,7 +11300,10 @@ class Chain extends VersionControl {
11142
11300
  for (const transaction of allTransactions) {
11143
11301
  if (!contracts.includes(transaction.decoded.to)) contracts.push(transaction.decoded.to);
11144
11302
  this.removePendingNonce(transaction.decoded.from, transaction.decoded.nonce);
11145
- await this.#handleTransaction(transaction, []);
11303
+ const transactionHash = await transaction.hash();
11304
+ const validatorAddresses = blockMessage.decoded.validators.map(({ address }) => address);
11305
+ const feeDistribution = supportsTransactionFees(blockMessage.decoded.protocolVersion) ? distributeTransactionFee(BigInt(await calculateFee(transaction.decoded)), transactionHash, validatorAddresses) : { payments: [], burned: 0n };
11306
+ await this.#handleTransaction(transaction, [], void 0, feeDistribution);
11146
11307
  }
11147
11308
  try {
11148
11309
  promises = await Promise.allSettled(promises);
@@ -11199,7 +11360,7 @@ class Chain extends VersionControl {
11199
11360
  if (await this.hasTransactionToHandle() && !this.#runningEpoch && this.#participating) await this.#runEpoch();
11200
11361
  return true;
11201
11362
  }
11202
- async #handleTransaction(transaction, latestTransactions, block) {
11363
+ async #handleTransaction(transaction, latestTransactions, block, feePayments = { payments: [], burned: 0n }) {
11203
11364
  await this.validateTransactionSignature(transaction);
11204
11365
  const hash = await transaction.hash();
11205
11366
  const doubleTransactions = [];
@@ -11212,7 +11373,7 @@ class Chain extends VersionControl {
11212
11373
  return;
11213
11374
  }
11214
11375
  try {
11215
- const result = await this.#executeTransaction({ ...transaction.decoded, hash });
11376
+ const result = await this.#executeTransaction({ ...transaction.decoded, hash, feePayments });
11216
11377
  if (block) {
11217
11378
  block.transactions.push(hash);
11218
11379
  block.fees = block.fees += await calculateFee(transaction.decoded);
@@ -11243,7 +11404,7 @@ class Chain extends VersionControl {
11243
11404
  fees: BigInt(0),
11244
11405
  timestamp,
11245
11406
  previousHash: "",
11246
- reward: BigInt(150),
11407
+ reward: BLOCK_REWARD,
11247
11408
  index: 0,
11248
11409
  producer: "",
11249
11410
  producerProof: "",
@@ -11268,7 +11429,7 @@ class Chain extends VersionControl {
11268
11429
  for (const { transaction, hash } of allTransactions) {
11269
11430
  await this.validateTransactionSignature(transaction);
11270
11431
  block.transactions.push(hash);
11271
- block.fees += BigInt(await calculateFee(transaction.decoded));
11432
+ if (supportsTransactionFees(this.version)) block.fees += BigInt(await calculateFee(transaction.decoded));
11272
11433
  await globalThis.peernet.put(hash, transaction.encoded, "transaction");
11273
11434
  }
11274
11435
  if (block.transactions.length === 0) return;
@@ -11278,9 +11439,17 @@ class Chain extends VersionControl {
11278
11439
  const canonicalValidators = await this.staticCall(addresses.validators, "validators");
11279
11440
  const sortedValidators = [...canonicalValidators].sort();
11280
11441
  if (sortedValidators.length === 0) throw new Error("cannot produce a block without validators");
11442
+ if (supportsTransactionFees(this.version)) await this.#assertFeeBurnSupported();
11443
+ const feeEntries = supportsTransactionFees(this.version) ? await Promise.all(
11444
+ allTransactions.map(async ({ transaction, hash }) => ({
11445
+ fee: BigInt(await calculateFee(transaction.decoded)),
11446
+ transactionHash: hash
11447
+ }))
11448
+ ) : [];
11449
+ const validatorFees = supportsTransactionFees(this.version) ? aggregateValidatorFees(feeEntries, sortedValidators) : new Map(sortedValidators.map((address) => [address, 0n]));
11281
11450
  block.validators = sortedValidators.map((validatorAddress) => ({
11282
11451
  address: validatorAddress,
11283
- reward: block.fees / BigInt(sortedValidators.length) + block.reward / BigInt(sortedValidators.length)
11452
+ reward: (validatorFees.get(validatorAddress) || 0n) + block.reward / BigInt(sortedValidators.length)
11284
11453
  }));
11285
11454
  try {
11286
11455
  block.producer = globalThis.peernet.selectedAccount || "";
@@ -11405,7 +11574,7 @@ class Chain extends VersionControl {
11405
11574
  * @returns
11406
11575
  */
11407
11576
  internalCall(sender, contract, method, parameters) {
11408
- return this.machine.execute(contract, method, parameters);
11577
+ return this.machine.execute(contract, method, parameters, sender);
11409
11578
  }
11410
11579
  /**
11411
11580
  *
@@ -11415,7 +11584,7 @@ class Chain extends VersionControl {
11415
11584
  * @returns
11416
11585
  */
11417
11586
  call(contract, method, parameters) {
11418
- return this.machine.execute(contract, method, parameters);
11587
+ return this.machine.execute(contract, method, parameters, globalThis.peernet.selectedAccount);
11419
11588
  }
11420
11589
  staticCall(contract, method, parameters) {
11421
11590
  return this.machine.get(contract, method, parameters);