@leofcoin/chain 1.10.9 → 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/browser/chain.js +149 -40
- package/exports/browser/workers/machine-worker.js +103 -7
- package/exports/chain.js +76 -37
- package/exports/workers/machine-worker.js +103 -7
- package/package.json +1 -1
package/exports/browser/chain.js
CHANGED
|
@@ -1058,9 +1058,17 @@ const nameServiceMessage = bytecodes.nameService;
|
|
|
1058
1058
|
const validatorsMessage = bytecodes.validators;
|
|
1059
1059
|
const TRANSACTION_FEE_BYTES = 1024n;
|
|
1060
1060
|
const TRANSACTION_FEE_UNIT = 10n;
|
|
1061
|
-
const
|
|
1061
|
+
const MAX_TRANSACTION_BYTES = 32 * 1024;
|
|
1062
|
+
const MAX_BLOCK_TRANSACTION_BYTES = 128 * 1024;
|
|
1063
|
+
const MAX_BLOCK_TRANSACTIONS = 256;
|
|
1064
|
+
BigInt(MAX_TRANSACTION_BYTES) / TRANSACTION_FEE_BYTES * TRANSACTION_FEE_UNIT;
|
|
1062
1065
|
const FEE_BASIS_POINTS = 10000n;
|
|
1063
1066
|
const FEE_PROTOCOL_VERSION = "1.10.9";
|
|
1067
|
+
const MONETARY_POLICY_PROTOCOL_VERSION = "1.10.10";
|
|
1068
|
+
const BLOCKS_PER_YEAR = 5256000n;
|
|
1069
|
+
const ANNUAL_ISSUANCE_BASIS_POINTS = 200n;
|
|
1070
|
+
const SUPPLY_FLOOR_BASIS_POINTS = 9500n;
|
|
1071
|
+
const MONETARY_FEE_BURN_BASIS_POINTS = 1000n;
|
|
1064
1072
|
const parseProtocolVersion = (version) => {
|
|
1065
1073
|
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
1066
1074
|
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : void 0;
|
|
@@ -1076,19 +1084,61 @@ const supportsTransactionFees = (version) => {
|
|
|
1076
1084
|
}
|
|
1077
1085
|
return true;
|
|
1078
1086
|
};
|
|
1087
|
+
const supportsMonetaryPolicy = (version) => {
|
|
1088
|
+
const actual = parseProtocolVersion(version);
|
|
1089
|
+
const required = parseProtocolVersion(MONETARY_POLICY_PROTOCOL_VERSION);
|
|
1090
|
+
if (!actual)
|
|
1091
|
+
return false;
|
|
1092
|
+
for (let index = 0; index < required.length; index += 1) {
|
|
1093
|
+
if (actual[index] !== required[index])
|
|
1094
|
+
return actual[index] > required[index];
|
|
1095
|
+
}
|
|
1096
|
+
return true;
|
|
1097
|
+
};
|
|
1098
|
+
const calculateMonetaryPolicy = (totalSupply, targetSupply) => {
|
|
1099
|
+
if (totalSupply < 0n || targetSupply <= 0n)
|
|
1100
|
+
throw new Error("invalid monetary policy supply");
|
|
1101
|
+
const floorSupply = targetSupply * SUPPLY_FLOOR_BASIS_POINTS / FEE_BASIS_POINTS;
|
|
1102
|
+
if (totalSupply < floorSupply) {
|
|
1103
|
+
const annualIssuance = targetSupply * ANNUAL_ISSUANCE_BASIS_POINTS / FEE_BASIS_POINTS;
|
|
1104
|
+
const scheduled = annualIssuance / BLOCKS_PER_YEAR || 1n;
|
|
1105
|
+
return { subsidy: scheduled < floorSupply - totalSupply ? scheduled : floorSupply - totalSupply, burnBasisPoints: 0n, floorSupply };
|
|
1106
|
+
}
|
|
1107
|
+
return {
|
|
1108
|
+
subsidy: 0n,
|
|
1109
|
+
burnBasisPoints: totalSupply >= targetSupply ? MONETARY_FEE_BURN_BASIS_POINTS : 0n,
|
|
1110
|
+
floorSupply
|
|
1111
|
+
};
|
|
1112
|
+
};
|
|
1113
|
+
const distributeAmount = (amount, addresses, rotation = 0) => {
|
|
1114
|
+
const canonical = [...new Set(addresses)].sort();
|
|
1115
|
+
if (canonical.length === 0)
|
|
1116
|
+
throw new Error("cannot distribute without validators");
|
|
1117
|
+
const count = BigInt(canonical.length);
|
|
1118
|
+
const base = amount / count;
|
|
1119
|
+
const remainder = Number(amount % count);
|
|
1120
|
+
const result = new Map(canonical.map((address) => [address, base]));
|
|
1121
|
+
for (let index = 0; index < remainder; index += 1) {
|
|
1122
|
+
const address = canonical[(rotation + index) % canonical.length];
|
|
1123
|
+
result.set(address, result.get(address) + 1n);
|
|
1124
|
+
}
|
|
1125
|
+
return result;
|
|
1126
|
+
};
|
|
1079
1127
|
const feeRotationIndex = (transactionHash, validatorCount) => {
|
|
1080
1128
|
let value = 0n;
|
|
1081
1129
|
for (const character of transactionHash)
|
|
1082
1130
|
value = (value * 31n + BigInt(character.charCodeAt(0))) % 4294967291n;
|
|
1083
1131
|
return Number(value % BigInt(validatorCount));
|
|
1084
1132
|
};
|
|
1085
|
-
const distributeTransactionFee = (fee, transactionHash, validatorAddresses) => {
|
|
1133
|
+
const distributeTransactionFee = (fee, transactionHash, validatorAddresses, burnBasisPoints = 1000n) => {
|
|
1086
1134
|
const canonicalValidators = [...new Set(validatorAddresses)].sort();
|
|
1087
1135
|
if (canonicalValidators.length === 0)
|
|
1088
1136
|
throw new Error("cannot distribute transaction fee without validators");
|
|
1089
1137
|
if (fee < 0n)
|
|
1090
1138
|
throw new Error("transaction fee cannot be negative");
|
|
1091
|
-
|
|
1139
|
+
if (burnBasisPoints < 0n || burnBasisPoints > FEE_BASIS_POINTS)
|
|
1140
|
+
throw new Error("invalid fee burn rate");
|
|
1141
|
+
const burned = fee * burnBasisPoints / FEE_BASIS_POINTS;
|
|
1092
1142
|
const validatorPool = fee - burned;
|
|
1093
1143
|
const count = BigInt(canonicalValidators.length);
|
|
1094
1144
|
const base = validatorPool / count;
|
|
@@ -1102,10 +1152,10 @@ const distributeTransactionFee = (fee, transactionHash, validatorAddresses) => {
|
|
|
1102
1152
|
const payments = [...validatorFees.entries()].filter(([, amount]) => amount > 0n).map(([to, amount]) => ({ to, amount }));
|
|
1103
1153
|
return { burned, validatorFees, payments };
|
|
1104
1154
|
};
|
|
1105
|
-
const aggregateValidatorFees = (fees, validatorAddresses) => {
|
|
1155
|
+
const aggregateValidatorFees = (fees, validatorAddresses, burnBasisPoints = 1000n) => {
|
|
1106
1156
|
const totals = new Map([...new Set(validatorAddresses)].sort().map((validator) => [validator, 0n]));
|
|
1107
1157
|
for (const entry of fees) {
|
|
1108
|
-
const { validatorFees } = distributeTransactionFee(entry.fee, entry.transactionHash, validatorAddresses);
|
|
1158
|
+
const { validatorFees } = distributeTransactionFee(entry.fee, entry.transactionHash, validatorAddresses, burnBasisPoints);
|
|
1109
1159
|
for (const [validator, amount] of validatorFees)
|
|
1110
1160
|
totals.set(validator, totals.get(validator) + amount);
|
|
1111
1161
|
}
|
|
@@ -1125,6 +1175,26 @@ const calculateFee = async (transaction, format = false) => {
|
|
|
1125
1175
|
const fee = units * TRANSACTION_FEE_UNIT;
|
|
1126
1176
|
return format ? formatUnits(fee.toString()) : fee;
|
|
1127
1177
|
};
|
|
1178
|
+
const validateTransactionResourceLimits = async (transaction) => {
|
|
1179
|
+
const message = await new TransactionMessage(transaction);
|
|
1180
|
+
const size = message.encoded.length;
|
|
1181
|
+
if (size > MAX_TRANSACTION_BYTES) {
|
|
1182
|
+
throw new Error(`transaction exceeds ${MAX_TRANSACTION_BYTES} byte protocol limit: ${size}`);
|
|
1183
|
+
}
|
|
1184
|
+
return size;
|
|
1185
|
+
};
|
|
1186
|
+
const validateBlockResourceLimits = async (transactions) => {
|
|
1187
|
+
if (transactions.length > MAX_BLOCK_TRANSACTIONS) {
|
|
1188
|
+
throw new Error(`block exceeds ${MAX_BLOCK_TRANSACTIONS} transaction protocol limit`);
|
|
1189
|
+
}
|
|
1190
|
+
let size = 0;
|
|
1191
|
+
for (const transaction of transactions)
|
|
1192
|
+
size += await validateTransactionResourceLimits(transaction);
|
|
1193
|
+
if (size > MAX_BLOCK_TRANSACTION_BYTES) {
|
|
1194
|
+
throw new Error(`block transactions exceed ${MAX_BLOCK_TRANSACTION_BYTES} byte protocol limit: ${size}`);
|
|
1195
|
+
}
|
|
1196
|
+
return size;
|
|
1197
|
+
};
|
|
1128
1198
|
const createTransactionHash = async (transaction) => {
|
|
1129
1199
|
const isRawTransactionMessage = transaction instanceof RawTransactionMessage;
|
|
1130
1200
|
let message;
|
|
@@ -7899,19 +7969,14 @@ class Transaction extends Protocol {
|
|
|
7899
7969
|
return new Promise(async (resolve, reject) => {
|
|
7900
7970
|
let size = 0;
|
|
7901
7971
|
const _transactions = [];
|
|
7902
|
-
const
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
} else {
|
|
7911
|
-
resolve(_transactions);
|
|
7912
|
-
}
|
|
7913
|
-
})
|
|
7914
|
-
);
|
|
7972
|
+
for (const rawTransaction of transactions) {
|
|
7973
|
+
const tx = await new TransactionMessage(rawTransaction);
|
|
7974
|
+
if (tx.encoded.length > MAX_TRANSACTION_BYTES) continue;
|
|
7975
|
+
const newSize = size + tx.encoded.length;
|
|
7976
|
+
if (newSize > MAX_BLOCK_TRANSACTION_BYTES || _transactions.length >= MAX_BLOCK_TRANSACTIONS) break;
|
|
7977
|
+
size = newSize;
|
|
7978
|
+
_transactions.push({ ...tx.decoded, hash: await tx.hash() });
|
|
7979
|
+
}
|
|
7915
7980
|
return resolve(_transactions);
|
|
7916
7981
|
});
|
|
7917
7982
|
}
|
|
@@ -8905,6 +8970,9 @@ ${error.message}`);
|
|
|
8905
8970
|
collectFee(from, payments, burned) {
|
|
8906
8971
|
return this.#askWorker("collectFee", { from, payments, burned });
|
|
8907
8972
|
}
|
|
8973
|
+
settleRewards(rewards) {
|
|
8974
|
+
return this.#askWorker("settleRewards", { rewards });
|
|
8975
|
+
}
|
|
8908
8976
|
get(contract, method, parameters) {
|
|
8909
8977
|
return new Promise((resolve, reject) => {
|
|
8910
8978
|
const id = randombytes(20).toString();
|
|
@@ -9748,12 +9816,12 @@ class State extends Contract {
|
|
|
9748
9816
|
#loadBlockTransactions;
|
|
9749
9817
|
#getLastTransactions;
|
|
9750
9818
|
// todo throw error
|
|
9751
|
-
async #_executeTransaction(transaction, validators, feesEnabled) {
|
|
9819
|
+
async #_executeTransaction(transaction, validators, feesEnabled, burnBasisPoints) {
|
|
9752
9820
|
try {
|
|
9753
9821
|
const hash = await transaction.hash();
|
|
9754
9822
|
if (feesEnabled) {
|
|
9755
9823
|
const fee = BigInt(await calculateFee(transaction.decoded));
|
|
9756
|
-
const { payments, burned } = distributeTransactionFee(fee, hash, validators);
|
|
9824
|
+
const { payments, burned } = distributeTransactionFee(fee, hash, validators, burnBasisPoints);
|
|
9757
9825
|
await this.#machine.collectFee(transaction.decoded.from, payments, burned);
|
|
9758
9826
|
}
|
|
9759
9827
|
await this.#machine.execute(
|
|
@@ -9804,7 +9872,20 @@ class State extends Contract {
|
|
|
9804
9872
|
});
|
|
9805
9873
|
debug$1(`executing ${transactions.length} transactions for block ${block.index}`);
|
|
9806
9874
|
const feesEnabled = supportsTransactionFees(block.protocolVersion);
|
|
9807
|
-
|
|
9875
|
+
const monetaryPolicyEnabled = supportsMonetaryPolicy(block.protocolVersion);
|
|
9876
|
+
if (monetaryPolicyEnabled) await validateBlockResourceLimits(transactions);
|
|
9877
|
+
const policy = monetaryPolicyEnabled ? calculateMonetaryPolicy(
|
|
9878
|
+
BigInt(await this.#machine.get(nativeToken$2, "totalSupply")),
|
|
9879
|
+
BigInt(await this.#machine.get(nativeToken$2, "targetSupply"))
|
|
9880
|
+
) : { subsidy: 0n, burnBasisPoints: 1000n };
|
|
9881
|
+
for (const transaction of transactions) {
|
|
9882
|
+
await this.#_executeTransaction(transaction, validators, feesEnabled, policy.burnBasisPoints);
|
|
9883
|
+
}
|
|
9884
|
+
if (monetaryPolicyEnabled && policy.subsidy > 0n) {
|
|
9885
|
+
await this.#machine.settleRewards([
|
|
9886
|
+
...distributeAmount(policy.subsidy, validators, Number(block.index) % validators.length)
|
|
9887
|
+
]);
|
|
9888
|
+
}
|
|
9808
9889
|
this.#blocks[block.index].loaded = true;
|
|
9809
9890
|
debug$1(`executed transactions for block ${block.index}`);
|
|
9810
9891
|
if (Number(block.index) === 0) this.#loaded = true;
|
|
@@ -10248,7 +10329,6 @@ const validateChainLink = (localTip, incoming) => {
|
|
|
10248
10329
|
return "append";
|
|
10249
10330
|
};
|
|
10250
10331
|
|
|
10251
|
-
const BLOCK_REWARD = 150n;
|
|
10252
10332
|
const validateCanonicalValidatorSet = (blockIndex, expectedValidators, validators) => {
|
|
10253
10333
|
const actualValidators = validators.map(({ address }) => address);
|
|
10254
10334
|
const canonicalExpected = [...new Set(expectedValidators)].sort();
|
|
@@ -10258,20 +10338,18 @@ const validateCanonicalValidatorSet = (blockIndex, expectedValidators, validator
|
|
|
10258
10338
|
);
|
|
10259
10339
|
}
|
|
10260
10340
|
};
|
|
10261
|
-
const validateBlockEconomics = (block, calculatedFees, validatorFees = /* @__PURE__ */ new Map()) => {
|
|
10341
|
+
const validateBlockEconomics = (block, calculatedFees, validatorFees = /* @__PURE__ */ new Map(), subsidyRewards = /* @__PURE__ */ new Map(), expectedSubsidy = [...subsidyRewards.values()].reduce((sum, reward) => sum + reward, 0n)) => {
|
|
10262
10342
|
if (block.validators.length === 0) {
|
|
10263
10343
|
throw new Error(`Block ${block.index} cannot distribute rewards without validators`);
|
|
10264
10344
|
}
|
|
10265
|
-
if (block.reward !==
|
|
10266
|
-
throw new Error(`Block ${block.index} has invalid base reward: expected ${
|
|
10345
|
+
if (block.reward !== expectedSubsidy) {
|
|
10346
|
+
throw new Error(`Block ${block.index} has invalid base reward: expected ${expectedSubsidy}, got ${block.reward}`);
|
|
10267
10347
|
}
|
|
10268
10348
|
if (block.fees !== calculatedFees) {
|
|
10269
10349
|
throw new Error(`Block ${block.index} has invalid fees: expected ${calculatedFees}, got ${block.fees}`);
|
|
10270
10350
|
}
|
|
10271
|
-
const validatorCount = BigInt(block.validators.length);
|
|
10272
|
-
const baseReward = BLOCK_REWARD / validatorCount;
|
|
10273
10351
|
for (const validator of block.validators) {
|
|
10274
|
-
const expectedReward =
|
|
10352
|
+
const expectedReward = (subsidyRewards.get(validator.address) || 0n) + (validatorFees.get(validator.address) || 0n);
|
|
10275
10353
|
if (validator.reward !== expectedReward) {
|
|
10276
10354
|
throw new Error(
|
|
10277
10355
|
`Block ${block.index} has an invalid reward for validator ${validator.address}: expected ${expectedReward}, got ${validator.reward}`
|
|
@@ -10740,7 +10818,10 @@ class Chain extends VersionControl {
|
|
|
10740
10818
|
})
|
|
10741
10819
|
);
|
|
10742
10820
|
const feesEnabled = supportsTransactionFees(blockMessage.decoded.protocolVersion);
|
|
10743
|
-
if (feesEnabled) await this.#assertFeeBurnSupported();
|
|
10821
|
+
if (feesEnabled) await this.#assertFeeBurnSupported(blockMessage.decoded.protocolVersion);
|
|
10822
|
+
const adaptivePolicy = supportsMonetaryPolicy(blockMessage.decoded.protocolVersion);
|
|
10823
|
+
if (adaptivePolicy) await validateBlockResourceLimits(transactions);
|
|
10824
|
+
const policy = await this.#monetaryPolicy(blockMessage.decoded.protocolVersion);
|
|
10744
10825
|
const calculatedFees = feesEnabled ? (await Promise.all(transactions.map((transaction) => calculateFee(transaction.decoded)))).reduce((total, fee) => total + BigInt(fee), 0n) : 0n;
|
|
10745
10826
|
const feeEntries = feesEnabled ? await Promise.all(
|
|
10746
10827
|
transactions.map(async (transaction) => ({
|
|
@@ -10749,8 +10830,9 @@ class Chain extends VersionControl {
|
|
|
10749
10830
|
}))
|
|
10750
10831
|
) : [];
|
|
10751
10832
|
const validatorAddresses = blockMessage.decoded.validators.map(({ address }) => address);
|
|
10752
|
-
const validatorFees = feesEnabled ? aggregateValidatorFees(feeEntries, validatorAddresses) : new Map(validatorAddresses.map((address) => [address, 0n]));
|
|
10753
|
-
|
|
10833
|
+
const validatorFees = feesEnabled ? aggregateValidatorFees(feeEntries, validatorAddresses, policy.burnBasisPoints) : new Map(validatorAddresses.map((address) => [address, 0n]));
|
|
10834
|
+
const subsidyRewards = adaptivePolicy ? distributeAmount(policy.subsidy, validatorAddresses, Number(blockMessage.decoded.index) % validatorAddresses.length) : new Map(validatorAddresses.map((address) => [address, policy.subsidy / BigInt(validatorAddresses.length)]));
|
|
10835
|
+
validateBlockEconomics(blockMessage.decoded, calculatedFees, validatorFees, subsidyRewards, policy.subsidy);
|
|
10754
10836
|
if (feesEnabled) {
|
|
10755
10837
|
const feesBySender = /* @__PURE__ */ new Map();
|
|
10756
10838
|
for (let index = 0; index < transactions.length; index += 1) {
|
|
@@ -10768,13 +10850,24 @@ class Chain extends VersionControl {
|
|
|
10768
10850
|
}
|
|
10769
10851
|
return transactions;
|
|
10770
10852
|
}
|
|
10771
|
-
async #assertFeeBurnSupported() {
|
|
10853
|
+
async #assertFeeBurnSupported(protocolVersion) {
|
|
10772
10854
|
const creator = await this.staticCall(addresses.nativeToken, "creator");
|
|
10773
|
-
const canBurn = await
|
|
10774
|
-
|
|
10775
|
-
|
|
10855
|
+
const [canBurn, canMint] = await Promise.all([
|
|
10856
|
+
this.staticCall(addresses.nativeToken, "hasRole", [creator, "BURN"]),
|
|
10857
|
+
this.staticCall(addresses.nativeToken, "hasRole", [creator, "MINT"])
|
|
10858
|
+
]);
|
|
10859
|
+
if (!canBurn || supportsMonetaryPolicy(protocolVersion) && !canMint) {
|
|
10860
|
+
throw new Error("native token genesis does not support protocol monetary policy");
|
|
10776
10861
|
}
|
|
10777
10862
|
}
|
|
10863
|
+
async #monetaryPolicy(protocolVersion) {
|
|
10864
|
+
if (!supportsMonetaryPolicy(protocolVersion)) return { subsidy: 150n, burnBasisPoints: 1000n, floorSupply: 0n };
|
|
10865
|
+
const [totalSupply, targetSupply] = await Promise.all([
|
|
10866
|
+
this.staticCall(addresses.nativeToken, "totalSupply"),
|
|
10867
|
+
this.staticCall(addresses.nativeToken, "targetSupply")
|
|
10868
|
+
]);
|
|
10869
|
+
return calculateMonetaryPolicy(BigInt(totalSupply), BigInt(targetSupply));
|
|
10870
|
+
}
|
|
10778
10871
|
/** Check if the next block will cross an epoch boundary (block-based timing) */
|
|
10779
10872
|
#isEpochBoundary(blockHeight) {
|
|
10780
10873
|
return (blockHeight + 1) % this.#epochLength === 0;
|
|
@@ -11297,14 +11390,19 @@ class Chain extends VersionControl {
|
|
|
11297
11390
|
if (nonceDiff !== 0) return nonceDiff;
|
|
11298
11391
|
return 0;
|
|
11299
11392
|
});
|
|
11393
|
+
const monetaryPolicy = await this.#monetaryPolicy(blockMessage.decoded.protocolVersion);
|
|
11300
11394
|
for (const transaction of allTransactions) {
|
|
11301
11395
|
if (!contracts.includes(transaction.decoded.to)) contracts.push(transaction.decoded.to);
|
|
11302
11396
|
this.removePendingNonce(transaction.decoded.from, transaction.decoded.nonce);
|
|
11303
11397
|
const transactionHash = await transaction.hash();
|
|
11304
11398
|
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 };
|
|
11399
|
+
const feeDistribution = supportsTransactionFees(blockMessage.decoded.protocolVersion) ? distributeTransactionFee(BigInt(await calculateFee(transaction.decoded)), transactionHash, validatorAddresses, monetaryPolicy.burnBasisPoints) : { payments: [], burned: 0n };
|
|
11306
11400
|
await this.#handleTransaction(transaction, [], void 0, feeDistribution);
|
|
11307
11401
|
}
|
|
11402
|
+
if (supportsMonetaryPolicy(blockMessage.decoded.protocolVersion) && monetaryPolicy.subsidy > 0n) {
|
|
11403
|
+
const validators = blockMessage.decoded.validators.map(({ address }) => address);
|
|
11404
|
+
await this.machine.settleRewards([...distributeAmount(monetaryPolicy.subsidy, validators, blockIndex % validators.length)]);
|
|
11405
|
+
}
|
|
11308
11406
|
try {
|
|
11309
11407
|
promises = await Promise.allSettled(promises);
|
|
11310
11408
|
const noncesByAddress = {};
|
|
@@ -11404,7 +11502,7 @@ class Chain extends VersionControl {
|
|
|
11404
11502
|
fees: BigInt(0),
|
|
11405
11503
|
timestamp,
|
|
11406
11504
|
previousHash: "",
|
|
11407
|
-
reward:
|
|
11505
|
+
reward: 0n,
|
|
11408
11506
|
index: 0,
|
|
11409
11507
|
producer: "",
|
|
11410
11508
|
producerProof: "",
|
|
@@ -11426,9 +11524,17 @@ class Chain extends VersionControl {
|
|
|
11426
11524
|
if (nonceDiff !== 0) return nonceDiff;
|
|
11427
11525
|
return 0;
|
|
11428
11526
|
});
|
|
11527
|
+
let blockTransactionBytes = 0;
|
|
11429
11528
|
for (const { transaction, hash } of allTransactions) {
|
|
11529
|
+
const transactionBytes = transaction.encoded.length;
|
|
11530
|
+
if (transactionBytes > MAX_TRANSACTION_BYTES) {
|
|
11531
|
+
await globalThis.transactionPoolStore.delete(hash);
|
|
11532
|
+
continue;
|
|
11533
|
+
}
|
|
11534
|
+
if (block.transactions.length >= MAX_BLOCK_TRANSACTIONS || blockTransactionBytes + transactionBytes > MAX_BLOCK_TRANSACTION_BYTES) break;
|
|
11430
11535
|
await this.validateTransactionSignature(transaction);
|
|
11431
11536
|
block.transactions.push(hash);
|
|
11537
|
+
blockTransactionBytes += transactionBytes;
|
|
11432
11538
|
if (supportsTransactionFees(this.version)) block.fees += BigInt(await calculateFee(transaction.decoded));
|
|
11433
11539
|
await globalThis.peernet.put(hash, transaction.encoded, "transaction");
|
|
11434
11540
|
}
|
|
@@ -11439,17 +11545,20 @@ class Chain extends VersionControl {
|
|
|
11439
11545
|
const canonicalValidators = await this.staticCall(addresses.validators, "validators");
|
|
11440
11546
|
const sortedValidators = [...canonicalValidators].sort();
|
|
11441
11547
|
if (sortedValidators.length === 0) throw new Error("cannot produce a block without validators");
|
|
11442
|
-
if (supportsTransactionFees(this.version)) await this.#assertFeeBurnSupported();
|
|
11548
|
+
if (supportsTransactionFees(this.version)) await this.#assertFeeBurnSupported(this.version);
|
|
11549
|
+
const monetaryPolicy = await this.#monetaryPolicy(this.version);
|
|
11550
|
+
block.reward = monetaryPolicy.subsidy;
|
|
11443
11551
|
const feeEntries = supportsTransactionFees(this.version) ? await Promise.all(
|
|
11444
11552
|
allTransactions.map(async ({ transaction, hash }) => ({
|
|
11445
11553
|
fee: BigInt(await calculateFee(transaction.decoded)),
|
|
11446
11554
|
transactionHash: hash
|
|
11447
11555
|
}))
|
|
11448
11556
|
) : [];
|
|
11449
|
-
const validatorFees = supportsTransactionFees(this.version) ? aggregateValidatorFees(feeEntries, sortedValidators) : new Map(sortedValidators.map((address) => [address, 0n]));
|
|
11557
|
+
const validatorFees = supportsTransactionFees(this.version) ? aggregateValidatorFees(feeEntries, sortedValidators, monetaryPolicy.burnBasisPoints) : new Map(sortedValidators.map((address) => [address, 0n]));
|
|
11558
|
+
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)]));
|
|
11450
11559
|
block.validators = sortedValidators.map((validatorAddress) => ({
|
|
11451
11560
|
address: validatorAddress,
|
|
11452
|
-
reward: (validatorFees.get(validatorAddress) || 0n) +
|
|
11561
|
+
reward: (validatorFees.get(validatorAddress) || 0n) + (subsidyRewards.get(validatorAddress) || 0n)
|
|
11453
11562
|
}));
|
|
11454
11563
|
try {
|
|
11455
11564
|
block.producer = globalThis.peernet.selectedAccount || "";
|
|
@@ -977,9 +977,17 @@ var bytecodes = {
|
|
|
977
977
|
|
|
978
978
|
const TRANSACTION_FEE_BYTES = 1024n;
|
|
979
979
|
const TRANSACTION_FEE_UNIT = 10n;
|
|
980
|
-
const
|
|
980
|
+
const MAX_TRANSACTION_BYTES = 32 * 1024;
|
|
981
|
+
const MAX_BLOCK_TRANSACTION_BYTES = 128 * 1024;
|
|
982
|
+
const MAX_BLOCK_TRANSACTIONS = 256;
|
|
983
|
+
(BigInt(MAX_TRANSACTION_BYTES) / TRANSACTION_FEE_BYTES) * TRANSACTION_FEE_UNIT;
|
|
981
984
|
const FEE_BASIS_POINTS = 10000n;
|
|
982
985
|
const FEE_PROTOCOL_VERSION = '1.10.9';
|
|
986
|
+
const MONETARY_POLICY_PROTOCOL_VERSION = '1.10.10';
|
|
987
|
+
const BLOCKS_PER_YEAR = 5256000n;
|
|
988
|
+
const ANNUAL_ISSUANCE_BASIS_POINTS = 200n;
|
|
989
|
+
const SUPPLY_FLOOR_BASIS_POINTS = 9500n;
|
|
990
|
+
const MONETARY_FEE_BURN_BASIS_POINTS = 1000n;
|
|
983
991
|
const parseProtocolVersion = (version) => {
|
|
984
992
|
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
985
993
|
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : undefined;
|
|
@@ -995,19 +1003,61 @@ const supportsTransactionFees = (version) => {
|
|
|
995
1003
|
}
|
|
996
1004
|
return true;
|
|
997
1005
|
};
|
|
1006
|
+
const supportsMonetaryPolicy = (version) => {
|
|
1007
|
+
const actual = parseProtocolVersion(version);
|
|
1008
|
+
const required = parseProtocolVersion(MONETARY_POLICY_PROTOCOL_VERSION);
|
|
1009
|
+
if (!actual)
|
|
1010
|
+
return false;
|
|
1011
|
+
for (let index = 0; index < required.length; index += 1) {
|
|
1012
|
+
if (actual[index] !== required[index])
|
|
1013
|
+
return actual[index] > required[index];
|
|
1014
|
+
}
|
|
1015
|
+
return true;
|
|
1016
|
+
};
|
|
1017
|
+
const calculateMonetaryPolicy = (totalSupply, targetSupply) => {
|
|
1018
|
+
if (totalSupply < 0n || targetSupply <= 0n)
|
|
1019
|
+
throw new Error('invalid monetary policy supply');
|
|
1020
|
+
const floorSupply = (targetSupply * SUPPLY_FLOOR_BASIS_POINTS) / FEE_BASIS_POINTS;
|
|
1021
|
+
if (totalSupply < floorSupply) {
|
|
1022
|
+
const annualIssuance = (targetSupply * ANNUAL_ISSUANCE_BASIS_POINTS) / FEE_BASIS_POINTS;
|
|
1023
|
+
const scheduled = annualIssuance / BLOCKS_PER_YEAR || 1n;
|
|
1024
|
+
return { subsidy: scheduled < floorSupply - totalSupply ? scheduled : floorSupply - totalSupply, burnBasisPoints: 0n, floorSupply };
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
subsidy: 0n,
|
|
1028
|
+
burnBasisPoints: totalSupply >= targetSupply ? MONETARY_FEE_BURN_BASIS_POINTS : 0n,
|
|
1029
|
+
floorSupply
|
|
1030
|
+
};
|
|
1031
|
+
};
|
|
1032
|
+
const distributeAmount = (amount, addresses, rotation = 0) => {
|
|
1033
|
+
const canonical = [...new Set(addresses)].sort();
|
|
1034
|
+
if (canonical.length === 0)
|
|
1035
|
+
throw new Error('cannot distribute without validators');
|
|
1036
|
+
const count = BigInt(canonical.length);
|
|
1037
|
+
const base = amount / count;
|
|
1038
|
+
const remainder = Number(amount % count);
|
|
1039
|
+
const result = new Map(canonical.map((address) => [address, base]));
|
|
1040
|
+
for (let index = 0; index < remainder; index += 1) {
|
|
1041
|
+
const address = canonical[(rotation + index) % canonical.length];
|
|
1042
|
+
result.set(address, result.get(address) + 1n);
|
|
1043
|
+
}
|
|
1044
|
+
return result;
|
|
1045
|
+
};
|
|
998
1046
|
const feeRotationIndex = (transactionHash, validatorCount) => {
|
|
999
1047
|
let value = 0n;
|
|
1000
1048
|
for (const character of transactionHash)
|
|
1001
1049
|
value = (value * 31n + BigInt(character.charCodeAt(0))) % 4294967291n;
|
|
1002
1050
|
return Number(value % BigInt(validatorCount));
|
|
1003
1051
|
};
|
|
1004
|
-
const distributeTransactionFee = (fee, transactionHash, validatorAddresses) => {
|
|
1052
|
+
const distributeTransactionFee = (fee, transactionHash, validatorAddresses, burnBasisPoints = 1000n) => {
|
|
1005
1053
|
const canonicalValidators = [...new Set(validatorAddresses)].sort();
|
|
1006
1054
|
if (canonicalValidators.length === 0)
|
|
1007
1055
|
throw new Error('cannot distribute transaction fee without validators');
|
|
1008
1056
|
if (fee < 0n)
|
|
1009
1057
|
throw new Error('transaction fee cannot be negative');
|
|
1010
|
-
|
|
1058
|
+
if (burnBasisPoints < 0n || burnBasisPoints > FEE_BASIS_POINTS)
|
|
1059
|
+
throw new Error('invalid fee burn rate');
|
|
1060
|
+
const burned = (fee * burnBasisPoints) / FEE_BASIS_POINTS;
|
|
1011
1061
|
const validatorPool = fee - burned;
|
|
1012
1062
|
const count = BigInt(canonicalValidators.length);
|
|
1013
1063
|
const base = validatorPool / count;
|
|
@@ -1030,6 +1080,26 @@ const calculateFee = async (transaction, format = false) => {
|
|
|
1030
1080
|
const fee = units * TRANSACTION_FEE_UNIT;
|
|
1031
1081
|
return format ? formatUnits(fee.toString()) : fee;
|
|
1032
1082
|
};
|
|
1083
|
+
const validateTransactionResourceLimits = async (transaction) => {
|
|
1084
|
+
const message = await new TransactionMessage(transaction);
|
|
1085
|
+
const size = message.encoded.length;
|
|
1086
|
+
if (size > MAX_TRANSACTION_BYTES) {
|
|
1087
|
+
throw new Error(`transaction exceeds ${MAX_TRANSACTION_BYTES} byte protocol limit: ${size}`);
|
|
1088
|
+
}
|
|
1089
|
+
return size;
|
|
1090
|
+
};
|
|
1091
|
+
const validateBlockResourceLimits = async (transactions) => {
|
|
1092
|
+
if (transactions.length > MAX_BLOCK_TRANSACTIONS) {
|
|
1093
|
+
throw new Error(`block exceeds ${MAX_BLOCK_TRANSACTIONS} transaction protocol limit`);
|
|
1094
|
+
}
|
|
1095
|
+
let size = 0;
|
|
1096
|
+
for (const transaction of transactions)
|
|
1097
|
+
size += await validateTransactionResourceLimits(transaction);
|
|
1098
|
+
if (size > MAX_BLOCK_TRANSACTION_BYTES) {
|
|
1099
|
+
throw new Error(`block transactions exceed ${MAX_BLOCK_TRANSACTION_BYTES} byte protocol limit: ${size}`);
|
|
1100
|
+
}
|
|
1101
|
+
return size;
|
|
1102
|
+
};
|
|
1033
1103
|
|
|
1034
1104
|
class LittlePubSub {
|
|
1035
1105
|
subscribers = new Map();
|
|
@@ -1211,7 +1281,7 @@ const runTask = async (id, taskName, input) => {
|
|
|
1211
1281
|
});
|
|
1212
1282
|
}
|
|
1213
1283
|
};
|
|
1214
|
-
const _executeTransaction = async (transaction, validators, feesEnabled) => {
|
|
1284
|
+
const _executeTransaction = async (transaction, validators, feesEnabled, burnBasisPoints) => {
|
|
1215
1285
|
const hash = await new TransactionMessage(transaction).hash();
|
|
1216
1286
|
if (latestTransactions.includes(hash)) {
|
|
1217
1287
|
throw new Error(`double transaction found: ${hash}`);
|
|
@@ -1223,7 +1293,7 @@ const _executeTransaction = async (transaction, validators, feesEnabled) => {
|
|
|
1223
1293
|
globalThis.state = await createState();
|
|
1224
1294
|
if (feesEnabled) {
|
|
1225
1295
|
const fee = BigInt(await calculateFee(transaction));
|
|
1226
|
-
const { payments, burned } = distributeTransactionFee(fee, hash, validators);
|
|
1296
|
+
const { payments, burned } = distributeTransactionFee(fee, hash, validators, burnBasisPoints);
|
|
1227
1297
|
await _.collectFee({ from, payments, burned });
|
|
1228
1298
|
}
|
|
1229
1299
|
await _.execute({ contract: to, method, params });
|
|
@@ -1327,6 +1397,17 @@ const _ = {
|
|
|
1327
1397
|
}
|
|
1328
1398
|
return total;
|
|
1329
1399
|
},
|
|
1400
|
+
settleRewards: ({ rewards }) => {
|
|
1401
|
+
globalThis.msg = createMessage(contracts[nativeToken$2].creator, nativeToken$2);
|
|
1402
|
+
let minted = 0n;
|
|
1403
|
+
for (const [validator, rawAmount] of rewards) {
|
|
1404
|
+
const amount = BigInt(rawAmount);
|
|
1405
|
+
if (amount > 0n)
|
|
1406
|
+
contracts[nativeToken$2].mint(validator, amount);
|
|
1407
|
+
minted += amount;
|
|
1408
|
+
}
|
|
1409
|
+
return minted;
|
|
1410
|
+
},
|
|
1330
1411
|
init: async (message) => {
|
|
1331
1412
|
let { peerid, fromState, state, info } = message;
|
|
1332
1413
|
if (info)
|
|
@@ -1438,8 +1519,20 @@ const _ = {
|
|
|
1438
1519
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
1439
1520
|
});
|
|
1440
1521
|
const feesEnabled = supportsTransactionFees(block.protocolVersion);
|
|
1441
|
-
|
|
1442
|
-
|
|
1522
|
+
const monetaryPolicyEnabled = supportsMonetaryPolicy(block.protocolVersion);
|
|
1523
|
+
if (monetaryPolicyEnabled)
|
|
1524
|
+
await validateBlockResourceLimits(transactions);
|
|
1525
|
+
const policy = monetaryPolicyEnabled
|
|
1526
|
+
? calculateMonetaryPolicy(BigInt(contracts[nativeToken$2].totalSupply), BigInt(contracts[nativeToken$2].targetSupply))
|
|
1527
|
+
: { subsidy: 0n, burnBasisPoints: 1000n };
|
|
1528
|
+
for (const transaction of transactions) {
|
|
1529
|
+
await _executeTransaction(transaction, validators, feesEnabled, policy.burnBasisPoints);
|
|
1530
|
+
}
|
|
1531
|
+
if (monetaryPolicyEnabled && policy.subsidy > 0n) {
|
|
1532
|
+
_.settleRewards({
|
|
1533
|
+
rewards: [...distributeAmount(policy.subsidy, validators, Number(block.index) % validators.length)]
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1443
1536
|
block.loaded = true;
|
|
1444
1537
|
worker.postMessage({
|
|
1445
1538
|
type: 'debug',
|
|
@@ -1507,6 +1600,9 @@ worker.onmessage(({ id, type, input }) => {
|
|
|
1507
1600
|
case 'collectFee':
|
|
1508
1601
|
runTask(id, 'collectFee', input);
|
|
1509
1602
|
break;
|
|
1603
|
+
case 'settleRewards':
|
|
1604
|
+
runTask(id, 'settleRewards', input);
|
|
1605
|
+
break;
|
|
1510
1606
|
case 'contracts':
|
|
1511
1607
|
respond(id, contracts);
|
|
1512
1608
|
break;
|
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, distributeTransactionFee, supportsTransactionFees, aggregateValidatorFees, 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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
}
|
|
@@ -946,6 +941,9 @@ ${error.message}`);
|
|
|
946
941
|
collectFee(from, payments, burned) {
|
|
947
942
|
return this.#askWorker("collectFee", { from, payments, burned });
|
|
948
943
|
}
|
|
944
|
+
settleRewards(rewards) {
|
|
945
|
+
return this.#askWorker("settleRewards", { rewards });
|
|
946
|
+
}
|
|
949
947
|
get(contract, method, parameters) {
|
|
950
948
|
return new Promise((resolve, reject) => {
|
|
951
949
|
const id = randombytes(20).toString();
|
|
@@ -1789,12 +1787,12 @@ class State extends Contract {
|
|
|
1789
1787
|
#loadBlockTransactions;
|
|
1790
1788
|
#getLastTransactions;
|
|
1791
1789
|
// todo throw error
|
|
1792
|
-
async #_executeTransaction(transaction, validators, feesEnabled) {
|
|
1790
|
+
async #_executeTransaction(transaction, validators, feesEnabled, burnBasisPoints) {
|
|
1793
1791
|
try {
|
|
1794
1792
|
const hash = await transaction.hash();
|
|
1795
1793
|
if (feesEnabled) {
|
|
1796
1794
|
const fee = BigInt(await calculateFee(transaction.decoded));
|
|
1797
|
-
const { payments, burned } = distributeTransactionFee(fee, hash, validators);
|
|
1795
|
+
const { payments, burned } = distributeTransactionFee(fee, hash, validators, burnBasisPoints);
|
|
1798
1796
|
await this.#machine.collectFee(transaction.decoded.from, payments, burned);
|
|
1799
1797
|
}
|
|
1800
1798
|
await this.#machine.execute(
|
|
@@ -1845,7 +1843,20 @@ class State extends Contract {
|
|
|
1845
1843
|
});
|
|
1846
1844
|
debug$1(`executing ${transactions.length} transactions for block ${block.index}`);
|
|
1847
1845
|
const feesEnabled = supportsTransactionFees(block.protocolVersion);
|
|
1848
|
-
|
|
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
|
+
}
|
|
1849
1860
|
this.#blocks[block.index].loaded = true;
|
|
1850
1861
|
debug$1(`executed transactions for block ${block.index}`);
|
|
1851
1862
|
if (Number(block.index) === 0) this.#loaded = true;
|
|
@@ -2289,7 +2300,6 @@ const validateChainLink = (localTip, incoming) => {
|
|
|
2289
2300
|
return "append";
|
|
2290
2301
|
};
|
|
2291
2302
|
|
|
2292
|
-
const BLOCK_REWARD = 150n;
|
|
2293
2303
|
const validateCanonicalValidatorSet = (blockIndex, expectedValidators, validators) => {
|
|
2294
2304
|
const actualValidators = validators.map(({ address }) => address);
|
|
2295
2305
|
const canonicalExpected = [...new Set(expectedValidators)].sort();
|
|
@@ -2299,20 +2309,18 @@ const validateCanonicalValidatorSet = (blockIndex, expectedValidators, validator
|
|
|
2299
2309
|
);
|
|
2300
2310
|
}
|
|
2301
2311
|
};
|
|
2302
|
-
const validateBlockEconomics = (block, calculatedFees, validatorFees = /* @__PURE__ */ new Map()) => {
|
|
2312
|
+
const validateBlockEconomics = (block, calculatedFees, validatorFees = /* @__PURE__ */ new Map(), subsidyRewards = /* @__PURE__ */ new Map(), expectedSubsidy = [...subsidyRewards.values()].reduce((sum, reward) => sum + reward, 0n)) => {
|
|
2303
2313
|
if (block.validators.length === 0) {
|
|
2304
2314
|
throw new Error(`Block ${block.index} cannot distribute rewards without validators`);
|
|
2305
2315
|
}
|
|
2306
|
-
if (block.reward !==
|
|
2307
|
-
throw new Error(`Block ${block.index} has invalid base reward: expected ${
|
|
2316
|
+
if (block.reward !== expectedSubsidy) {
|
|
2317
|
+
throw new Error(`Block ${block.index} has invalid base reward: expected ${expectedSubsidy}, got ${block.reward}`);
|
|
2308
2318
|
}
|
|
2309
2319
|
if (block.fees !== calculatedFees) {
|
|
2310
2320
|
throw new Error(`Block ${block.index} has invalid fees: expected ${calculatedFees}, got ${block.fees}`);
|
|
2311
2321
|
}
|
|
2312
|
-
const validatorCount = BigInt(block.validators.length);
|
|
2313
|
-
const baseReward = BLOCK_REWARD / validatorCount;
|
|
2314
2322
|
for (const validator of block.validators) {
|
|
2315
|
-
const expectedReward =
|
|
2323
|
+
const expectedReward = (subsidyRewards.get(validator.address) || 0n) + (validatorFees.get(validator.address) || 0n);
|
|
2316
2324
|
if (validator.reward !== expectedReward) {
|
|
2317
2325
|
throw new Error(
|
|
2318
2326
|
`Block ${block.index} has an invalid reward for validator ${validator.address}: expected ${expectedReward}, got ${validator.reward}`
|
|
@@ -2781,7 +2789,10 @@ class Chain extends VersionControl {
|
|
|
2781
2789
|
})
|
|
2782
2790
|
);
|
|
2783
2791
|
const feesEnabled = supportsTransactionFees(blockMessage.decoded.protocolVersion);
|
|
2784
|
-
if (feesEnabled) await this.#assertFeeBurnSupported();
|
|
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);
|
|
2785
2796
|
const calculatedFees = feesEnabled ? (await Promise.all(transactions.map((transaction) => calculateFee(transaction.decoded)))).reduce((total, fee) => total + BigInt(fee), 0n) : 0n;
|
|
2786
2797
|
const feeEntries = feesEnabled ? await Promise.all(
|
|
2787
2798
|
transactions.map(async (transaction) => ({
|
|
@@ -2790,8 +2801,9 @@ class Chain extends VersionControl {
|
|
|
2790
2801
|
}))
|
|
2791
2802
|
) : [];
|
|
2792
2803
|
const validatorAddresses = blockMessage.decoded.validators.map(({ address }) => address);
|
|
2793
|
-
const validatorFees = feesEnabled ? aggregateValidatorFees(feeEntries, validatorAddresses) : new Map(validatorAddresses.map((address) => [address, 0n]));
|
|
2794
|
-
|
|
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);
|
|
2795
2807
|
if (feesEnabled) {
|
|
2796
2808
|
const feesBySender = /* @__PURE__ */ new Map();
|
|
2797
2809
|
for (let index = 0; index < transactions.length; index += 1) {
|
|
@@ -2809,13 +2821,24 @@ class Chain extends VersionControl {
|
|
|
2809
2821
|
}
|
|
2810
2822
|
return transactions;
|
|
2811
2823
|
}
|
|
2812
|
-
async #assertFeeBurnSupported() {
|
|
2824
|
+
async #assertFeeBurnSupported(protocolVersion) {
|
|
2813
2825
|
const creator = await this.staticCall(addresses.nativeToken, "creator");
|
|
2814
|
-
const canBurn = await
|
|
2815
|
-
|
|
2816
|
-
|
|
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");
|
|
2817
2832
|
}
|
|
2818
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
|
+
}
|
|
2819
2842
|
/** Check if the next block will cross an epoch boundary (block-based timing) */
|
|
2820
2843
|
#isEpochBoundary(blockHeight) {
|
|
2821
2844
|
return (blockHeight + 1) % this.#epochLength === 0;
|
|
@@ -3338,14 +3361,19 @@ class Chain extends VersionControl {
|
|
|
3338
3361
|
if (nonceDiff !== 0) return nonceDiff;
|
|
3339
3362
|
return 0;
|
|
3340
3363
|
});
|
|
3364
|
+
const monetaryPolicy = await this.#monetaryPolicy(blockMessage.decoded.protocolVersion);
|
|
3341
3365
|
for (const transaction of allTransactions) {
|
|
3342
3366
|
if (!contracts.includes(transaction.decoded.to)) contracts.push(transaction.decoded.to);
|
|
3343
3367
|
this.removePendingNonce(transaction.decoded.from, transaction.decoded.nonce);
|
|
3344
3368
|
const transactionHash = await transaction.hash();
|
|
3345
3369
|
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 };
|
|
3370
|
+
const feeDistribution = supportsTransactionFees(blockMessage.decoded.protocolVersion) ? distributeTransactionFee(BigInt(await calculateFee(transaction.decoded)), transactionHash, validatorAddresses, monetaryPolicy.burnBasisPoints) : { payments: [], burned: 0n };
|
|
3347
3371
|
await this.#handleTransaction(transaction, [], void 0, feeDistribution);
|
|
3348
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)]);
|
|
3376
|
+
}
|
|
3349
3377
|
try {
|
|
3350
3378
|
promises = await Promise.allSettled(promises);
|
|
3351
3379
|
const noncesByAddress = {};
|
|
@@ -3445,7 +3473,7 @@ class Chain extends VersionControl {
|
|
|
3445
3473
|
fees: BigInt(0),
|
|
3446
3474
|
timestamp,
|
|
3447
3475
|
previousHash: "",
|
|
3448
|
-
reward:
|
|
3476
|
+
reward: 0n,
|
|
3449
3477
|
index: 0,
|
|
3450
3478
|
producer: "",
|
|
3451
3479
|
producerProof: "",
|
|
@@ -3467,9 +3495,17 @@ class Chain extends VersionControl {
|
|
|
3467
3495
|
if (nonceDiff !== 0) return nonceDiff;
|
|
3468
3496
|
return 0;
|
|
3469
3497
|
});
|
|
3498
|
+
let blockTransactionBytes = 0;
|
|
3470
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;
|
|
3471
3506
|
await this.validateTransactionSignature(transaction);
|
|
3472
3507
|
block.transactions.push(hash);
|
|
3508
|
+
blockTransactionBytes += transactionBytes;
|
|
3473
3509
|
if (supportsTransactionFees(this.version)) block.fees += BigInt(await calculateFee(transaction.decoded));
|
|
3474
3510
|
await globalThis.peernet.put(hash, transaction.encoded, "transaction");
|
|
3475
3511
|
}
|
|
@@ -3480,17 +3516,20 @@ class Chain extends VersionControl {
|
|
|
3480
3516
|
const canonicalValidators = await this.staticCall(addresses.validators, "validators");
|
|
3481
3517
|
const sortedValidators = [...canonicalValidators].sort();
|
|
3482
3518
|
if (sortedValidators.length === 0) throw new Error("cannot produce a block without validators");
|
|
3483
|
-
if (supportsTransactionFees(this.version)) await this.#assertFeeBurnSupported();
|
|
3519
|
+
if (supportsTransactionFees(this.version)) await this.#assertFeeBurnSupported(this.version);
|
|
3520
|
+
const monetaryPolicy = await this.#monetaryPolicy(this.version);
|
|
3521
|
+
block.reward = monetaryPolicy.subsidy;
|
|
3484
3522
|
const feeEntries = supportsTransactionFees(this.version) ? await Promise.all(
|
|
3485
3523
|
allTransactions.map(async ({ transaction, hash }) => ({
|
|
3486
3524
|
fee: BigInt(await calculateFee(transaction.decoded)),
|
|
3487
3525
|
transactionHash: hash
|
|
3488
3526
|
}))
|
|
3489
3527
|
) : [];
|
|
3490
|
-
const validatorFees = supportsTransactionFees(this.version) ? aggregateValidatorFees(feeEntries, sortedValidators) : new Map(sortedValidators.map((address) => [address, 0n]));
|
|
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)]));
|
|
3491
3530
|
block.validators = sortedValidators.map((validatorAddress) => ({
|
|
3492
3531
|
address: validatorAddress,
|
|
3493
|
-
reward: (validatorFees.get(validatorAddress) || 0n) +
|
|
3532
|
+
reward: (validatorFees.get(validatorAddress) || 0n) + (subsidyRewards.get(validatorAddress) || 0n)
|
|
3494
3533
|
}));
|
|
3495
3534
|
try {
|
|
3496
3535
|
block.producer = globalThis.peernet.selectedAccount || "";
|
|
@@ -977,9 +977,17 @@ var bytecodes = {
|
|
|
977
977
|
|
|
978
978
|
const TRANSACTION_FEE_BYTES = 1024n;
|
|
979
979
|
const TRANSACTION_FEE_UNIT = 10n;
|
|
980
|
-
const
|
|
980
|
+
const MAX_TRANSACTION_BYTES = 32 * 1024;
|
|
981
|
+
const MAX_BLOCK_TRANSACTION_BYTES = 128 * 1024;
|
|
982
|
+
const MAX_BLOCK_TRANSACTIONS = 256;
|
|
983
|
+
(BigInt(MAX_TRANSACTION_BYTES) / TRANSACTION_FEE_BYTES) * TRANSACTION_FEE_UNIT;
|
|
981
984
|
const FEE_BASIS_POINTS = 10000n;
|
|
982
985
|
const FEE_PROTOCOL_VERSION = '1.10.9';
|
|
986
|
+
const MONETARY_POLICY_PROTOCOL_VERSION = '1.10.10';
|
|
987
|
+
const BLOCKS_PER_YEAR = 5256000n;
|
|
988
|
+
const ANNUAL_ISSUANCE_BASIS_POINTS = 200n;
|
|
989
|
+
const SUPPLY_FLOOR_BASIS_POINTS = 9500n;
|
|
990
|
+
const MONETARY_FEE_BURN_BASIS_POINTS = 1000n;
|
|
983
991
|
const parseProtocolVersion = (version) => {
|
|
984
992
|
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
985
993
|
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : undefined;
|
|
@@ -995,19 +1003,61 @@ const supportsTransactionFees = (version) => {
|
|
|
995
1003
|
}
|
|
996
1004
|
return true;
|
|
997
1005
|
};
|
|
1006
|
+
const supportsMonetaryPolicy = (version) => {
|
|
1007
|
+
const actual = parseProtocolVersion(version);
|
|
1008
|
+
const required = parseProtocolVersion(MONETARY_POLICY_PROTOCOL_VERSION);
|
|
1009
|
+
if (!actual)
|
|
1010
|
+
return false;
|
|
1011
|
+
for (let index = 0; index < required.length; index += 1) {
|
|
1012
|
+
if (actual[index] !== required[index])
|
|
1013
|
+
return actual[index] > required[index];
|
|
1014
|
+
}
|
|
1015
|
+
return true;
|
|
1016
|
+
};
|
|
1017
|
+
const calculateMonetaryPolicy = (totalSupply, targetSupply) => {
|
|
1018
|
+
if (totalSupply < 0n || targetSupply <= 0n)
|
|
1019
|
+
throw new Error('invalid monetary policy supply');
|
|
1020
|
+
const floorSupply = (targetSupply * SUPPLY_FLOOR_BASIS_POINTS) / FEE_BASIS_POINTS;
|
|
1021
|
+
if (totalSupply < floorSupply) {
|
|
1022
|
+
const annualIssuance = (targetSupply * ANNUAL_ISSUANCE_BASIS_POINTS) / FEE_BASIS_POINTS;
|
|
1023
|
+
const scheduled = annualIssuance / BLOCKS_PER_YEAR || 1n;
|
|
1024
|
+
return { subsidy: scheduled < floorSupply - totalSupply ? scheduled : floorSupply - totalSupply, burnBasisPoints: 0n, floorSupply };
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
subsidy: 0n,
|
|
1028
|
+
burnBasisPoints: totalSupply >= targetSupply ? MONETARY_FEE_BURN_BASIS_POINTS : 0n,
|
|
1029
|
+
floorSupply
|
|
1030
|
+
};
|
|
1031
|
+
};
|
|
1032
|
+
const distributeAmount = (amount, addresses, rotation = 0) => {
|
|
1033
|
+
const canonical = [...new Set(addresses)].sort();
|
|
1034
|
+
if (canonical.length === 0)
|
|
1035
|
+
throw new Error('cannot distribute without validators');
|
|
1036
|
+
const count = BigInt(canonical.length);
|
|
1037
|
+
const base = amount / count;
|
|
1038
|
+
const remainder = Number(amount % count);
|
|
1039
|
+
const result = new Map(canonical.map((address) => [address, base]));
|
|
1040
|
+
for (let index = 0; index < remainder; index += 1) {
|
|
1041
|
+
const address = canonical[(rotation + index) % canonical.length];
|
|
1042
|
+
result.set(address, result.get(address) + 1n);
|
|
1043
|
+
}
|
|
1044
|
+
return result;
|
|
1045
|
+
};
|
|
998
1046
|
const feeRotationIndex = (transactionHash, validatorCount) => {
|
|
999
1047
|
let value = 0n;
|
|
1000
1048
|
for (const character of transactionHash)
|
|
1001
1049
|
value = (value * 31n + BigInt(character.charCodeAt(0))) % 4294967291n;
|
|
1002
1050
|
return Number(value % BigInt(validatorCount));
|
|
1003
1051
|
};
|
|
1004
|
-
const distributeTransactionFee = (fee, transactionHash, validatorAddresses) => {
|
|
1052
|
+
const distributeTransactionFee = (fee, transactionHash, validatorAddresses, burnBasisPoints = 1000n) => {
|
|
1005
1053
|
const canonicalValidators = [...new Set(validatorAddresses)].sort();
|
|
1006
1054
|
if (canonicalValidators.length === 0)
|
|
1007
1055
|
throw new Error('cannot distribute transaction fee without validators');
|
|
1008
1056
|
if (fee < 0n)
|
|
1009
1057
|
throw new Error('transaction fee cannot be negative');
|
|
1010
|
-
|
|
1058
|
+
if (burnBasisPoints < 0n || burnBasisPoints > FEE_BASIS_POINTS)
|
|
1059
|
+
throw new Error('invalid fee burn rate');
|
|
1060
|
+
const burned = (fee * burnBasisPoints) / FEE_BASIS_POINTS;
|
|
1011
1061
|
const validatorPool = fee - burned;
|
|
1012
1062
|
const count = BigInt(canonicalValidators.length);
|
|
1013
1063
|
const base = validatorPool / count;
|
|
@@ -1030,6 +1080,26 @@ const calculateFee = async (transaction, format = false) => {
|
|
|
1030
1080
|
const fee = units * TRANSACTION_FEE_UNIT;
|
|
1031
1081
|
return format ? formatUnits(fee.toString()) : fee;
|
|
1032
1082
|
};
|
|
1083
|
+
const validateTransactionResourceLimits = async (transaction) => {
|
|
1084
|
+
const message = await new TransactionMessage(transaction);
|
|
1085
|
+
const size = message.encoded.length;
|
|
1086
|
+
if (size > MAX_TRANSACTION_BYTES) {
|
|
1087
|
+
throw new Error(`transaction exceeds ${MAX_TRANSACTION_BYTES} byte protocol limit: ${size}`);
|
|
1088
|
+
}
|
|
1089
|
+
return size;
|
|
1090
|
+
};
|
|
1091
|
+
const validateBlockResourceLimits = async (transactions) => {
|
|
1092
|
+
if (transactions.length > MAX_BLOCK_TRANSACTIONS) {
|
|
1093
|
+
throw new Error(`block exceeds ${MAX_BLOCK_TRANSACTIONS} transaction protocol limit`);
|
|
1094
|
+
}
|
|
1095
|
+
let size = 0;
|
|
1096
|
+
for (const transaction of transactions)
|
|
1097
|
+
size += await validateTransactionResourceLimits(transaction);
|
|
1098
|
+
if (size > MAX_BLOCK_TRANSACTION_BYTES) {
|
|
1099
|
+
throw new Error(`block transactions exceed ${MAX_BLOCK_TRANSACTION_BYTES} byte protocol limit: ${size}`);
|
|
1100
|
+
}
|
|
1101
|
+
return size;
|
|
1102
|
+
};
|
|
1033
1103
|
|
|
1034
1104
|
class LittlePubSub {
|
|
1035
1105
|
subscribers = new Map();
|
|
@@ -1211,7 +1281,7 @@ const runTask = async (id, taskName, input) => {
|
|
|
1211
1281
|
});
|
|
1212
1282
|
}
|
|
1213
1283
|
};
|
|
1214
|
-
const _executeTransaction = async (transaction, validators, feesEnabled) => {
|
|
1284
|
+
const _executeTransaction = async (transaction, validators, feesEnabled, burnBasisPoints) => {
|
|
1215
1285
|
const hash = await new TransactionMessage(transaction).hash();
|
|
1216
1286
|
if (latestTransactions.includes(hash)) {
|
|
1217
1287
|
throw new Error(`double transaction found: ${hash}`);
|
|
@@ -1223,7 +1293,7 @@ const _executeTransaction = async (transaction, validators, feesEnabled) => {
|
|
|
1223
1293
|
globalThis.state = await createState();
|
|
1224
1294
|
if (feesEnabled) {
|
|
1225
1295
|
const fee = BigInt(await calculateFee(transaction));
|
|
1226
|
-
const { payments, burned } = distributeTransactionFee(fee, hash, validators);
|
|
1296
|
+
const { payments, burned } = distributeTransactionFee(fee, hash, validators, burnBasisPoints);
|
|
1227
1297
|
await _.collectFee({ from, payments, burned });
|
|
1228
1298
|
}
|
|
1229
1299
|
await _.execute({ contract: to, method, params });
|
|
@@ -1327,6 +1397,17 @@ const _ = {
|
|
|
1327
1397
|
}
|
|
1328
1398
|
return total;
|
|
1329
1399
|
},
|
|
1400
|
+
settleRewards: ({ rewards }) => {
|
|
1401
|
+
globalThis.msg = createMessage(contracts[nativeToken$2].creator, nativeToken$2);
|
|
1402
|
+
let minted = 0n;
|
|
1403
|
+
for (const [validator, rawAmount] of rewards) {
|
|
1404
|
+
const amount = BigInt(rawAmount);
|
|
1405
|
+
if (amount > 0n)
|
|
1406
|
+
contracts[nativeToken$2].mint(validator, amount);
|
|
1407
|
+
minted += amount;
|
|
1408
|
+
}
|
|
1409
|
+
return minted;
|
|
1410
|
+
},
|
|
1330
1411
|
init: async (message) => {
|
|
1331
1412
|
let { peerid, fromState, state, info } = message;
|
|
1332
1413
|
if (info)
|
|
@@ -1438,8 +1519,20 @@ const _ = {
|
|
|
1438
1519
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
1439
1520
|
});
|
|
1440
1521
|
const feesEnabled = supportsTransactionFees(block.protocolVersion);
|
|
1441
|
-
|
|
1442
|
-
|
|
1522
|
+
const monetaryPolicyEnabled = supportsMonetaryPolicy(block.protocolVersion);
|
|
1523
|
+
if (monetaryPolicyEnabled)
|
|
1524
|
+
await validateBlockResourceLimits(transactions);
|
|
1525
|
+
const policy = monetaryPolicyEnabled
|
|
1526
|
+
? calculateMonetaryPolicy(BigInt(contracts[nativeToken$2].totalSupply), BigInt(contracts[nativeToken$2].targetSupply))
|
|
1527
|
+
: { subsidy: 0n, burnBasisPoints: 1000n };
|
|
1528
|
+
for (const transaction of transactions) {
|
|
1529
|
+
await _executeTransaction(transaction, validators, feesEnabled, policy.burnBasisPoints);
|
|
1530
|
+
}
|
|
1531
|
+
if (monetaryPolicyEnabled && policy.subsidy > 0n) {
|
|
1532
|
+
_.settleRewards({
|
|
1533
|
+
rewards: [...distributeAmount(policy.subsidy, validators, Number(block.index) % validators.length)]
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1443
1536
|
block.loaded = true;
|
|
1444
1537
|
worker.postMessage({
|
|
1445
1538
|
type: 'debug',
|
|
@@ -1507,6 +1600,9 @@ worker.onmessage(({ id, type, input }) => {
|
|
|
1507
1600
|
case 'collectFee':
|
|
1508
1601
|
runTask(id, 'collectFee', input);
|
|
1509
1602
|
break;
|
|
1603
|
+
case 'settleRewards':
|
|
1604
|
+
runTask(id, 'settleRewards', input);
|
|
1605
|
+
break;
|
|
1510
1606
|
case 'contracts':
|
|
1511
1607
|
respond(id, contracts);
|
|
1512
1608
|
break;
|