@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/browser/chain.js +213 -44
- package/exports/browser/workers/machine-worker.js +998 -12
- package/exports/chain.js +156 -41
- package/exports/workers/machine-worker.js +998 -12
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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);
|