diff --git a/packages/common/src/eips.ts b/packages/common/src/eips.ts index 6c43bfa341..1c6e31aab0 100644 --- a/packages/common/src/eips.ts +++ b/packages/common/src/eips.ts @@ -405,6 +405,15 @@ export const eipsDict: EIPsDict = { */ requiredEIPs: [3540, 3541, 3670], }, + /** + * Description : Increase calldata cost to reduce maximum block size + * URL : https://github.com/ethereum/EIPs/blob/da2a86bf15044416e8eb0301c9bdb8d561feeb32/EIPS/eip-7623.md + * Status : Review + */ + 7623: { + minimumHardfork: Hardfork.Chainstart, + requiredEIPs: [], + }, /** * Description : General purpose execution layer requests * URL : https://eips.ethereum.org/EIPS/eip-7685 diff --git a/packages/common/src/hardforks.ts b/packages/common/src/hardforks.ts index b6f045d37c..f83f077111 100644 --- a/packages/common/src/hardforks.ts +++ b/packages/common/src/hardforks.ts @@ -160,7 +160,7 @@ export const hardforksDict: HardforksDict = { prague: { // TODO update this accordingly to the right devnet setup //eips: [663, 3540, 3670, 4200, 4750, 5450, 6206, 7069, 7480, 7620, 7692, 7698], // This is EOF-only - eips: [2537, 2935, 6110, 7002, 7251, 7685, 7691, 7702], // This is current prague without EOF + eips: [2537, 2935, 6110, 7002, 7251, 7623, 7685, 7691, 7702], // This is current prague without EOF }, /** * Description: Next feature hardfork after prague, internally used for verkle testing/implementation (incomplete/experimental) diff --git a/packages/tx/src/capabilities/legacy.ts b/packages/tx/src/capabilities/legacy.ts index be56064168..39813721e9 100644 --- a/packages/tx/src/capabilities/legacy.ts +++ b/packages/tx/src/capabilities/legacy.ts @@ -2,6 +2,7 @@ import { Address, BIGINT_0, SECP256K1_ORDER_DIV_2, + bigIntMax, bigIntToUnpaddedBytes, bytesToHex, ecrecover, @@ -167,8 +168,20 @@ export function getValidationErrors(tx: LegacyTxInterface): string[] { errors.push('Invalid Signature') } - if (tx.getIntrinsicGas() > tx.gasLimit) { - errors.push(`gasLimit is too low. given ${tx.gasLimit}, need at least ${tx.getIntrinsicGas()}`) + let intrinsicGas = tx.getIntrinsicGas() + if (tx.common.isActivatedEIP(7623)) { + let tokens = 0 + for (let i = 0; i < tx.data.length; i++) { + tokens += tx.data[i] === 0 ? 1 : 4 + } + const floorCost = + tx.common.param('txGas') + tx.common.param('totalCostFloorPerToken') * BigInt(tokens) + intrinsicGas = bigIntMax(intrinsicGas, floorCost) + } + if (intrinsicGas > tx.gasLimit) { + errors.push( + `gasLimit is too low. The gasLimit is lower than the minimum gas limit of ${tx.getIntrinsicGas()}, the gas limit is: ${tx.gasLimit}`, + ) } return errors diff --git a/packages/tx/src/params.ts b/packages/tx/src/params.ts index 66ce2302bb..d5de087267 100644 --- a/packages/tx/src/params.ts +++ b/packages/tx/src/params.ts @@ -44,6 +44,12 @@ export const paramsTx: ParamsDict = { blobCommitmentVersionKzg: 1, // The number indicated a versioned hash is a KZG commitment }, /** + * Increase calldata cost to reduce maximum block size + */ + 7623: { + totalCostFloorPerToken: 10, + }, + /** . * Set EOA account code for one transaction . */ 7702: { diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 3697f810ad..7624dc34dc 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -12,6 +12,7 @@ import { KECCAK256_NULL, MAX_UINT64, SECP256K1_ORDER_DIV_2, + bigIntMax, bytesToBigInt, bytesToHex, bytesToUnprefixedHex, @@ -249,11 +250,24 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { // Validate gas limit against tx base fee (DataFee + TxFee + Creation Fee) const intrinsicGas = tx.getIntrinsicGas() + let floorCost = BIGINT_0 + + if (vm.common.isActivatedEIP(7623)) { + // Tx should at least cover the floor price for tx data + let tokens = 0 + for (let i = 0; i < tx.data.length; i++) { + tokens += tx.data[i] === 0 ? 1 : 4 + } + floorCost = + tx.common.param('txGas') + tx.common.param('totalCostFloorPerToken') * BigInt(tokens) + } + let gasLimit = tx.gasLimit - if (gasLimit < intrinsicGas) { + const minGasLimit = bigIntMax(intrinsicGas, floorCost) + if (gasLimit < minGasLimit) { const msg = _errorMsg( `tx gas limit ${Number(gasLimit)} is lower than the minimum gas limit of ${Number( - intrinsicGas, + minGasLimit, )}`, vm, block, @@ -614,6 +628,15 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { debugGas(`tx add baseFee ${intrinsicGas} to totalGasSpent (-> ${results.totalGasSpent})`) } + if (vm.common.isActivatedEIP(7623)) { + if (results.totalGasSpent < floorCost) { + results.totalGasSpent = floorCost + if (vm.DEBUG) { + debugGas(`tx apply floorCost ${floorCost} to totalGasSpent (-> ${results.totalGasSpent})`) + } + } + } + // Add blob gas used to result if (isBlob4844Tx(tx)) { results.blobGasUsed = totalblobGas @@ -635,6 +658,8 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { debug(`No tx gasRefund`) } } + + results.amountSpent = results.totalGasSpent * gasPrice // Update sender's balance diff --git a/packages/vm/test/api/EIPs/eip-7623.spec.ts b/packages/vm/test/api/EIPs/eip-7623.spec.ts new file mode 100644 index 0000000000..f362cb8b66 --- /dev/null +++ b/packages/vm/test/api/EIPs/eip-7623.spec.ts @@ -0,0 +1,125 @@ +import { createBlock } from '@ethereumjs/block' +import { Common, Hardfork, Mainnet } from '@ethereumjs/common' +import { createLegacyTx } from '@ethereumjs/tx' +import { Account, Address, createZeroAddress, hexToBytes, privateToAddress } from '@ethereumjs/util' +import { assert, describe, it } from 'vitest' + +import { createVM, runTx } from '../../../src/index.js' + +const common = new Common({ chain: Mainnet, hardfork: Hardfork.Prague }) + +const pkey = hexToBytes(`0x${'20'.repeat(32)}`) +const GWEI = BigInt(1000000000) +const sender = new Address(privateToAddress(pkey)) + +const coinbase = new Address(hexToBytes(`0x${'ff'.repeat(20)}`)) + +const block = createBlock( + { + header: { + baseFeePerGas: 7, + coinbase, + }, + }, + { common }, +) + +const code = hexToBytes('0x60008080806001415AF100') +const contractAddress = new Address(hexToBytes(`0x${'ee'.repeat(20)}`)) + +async function getVM(common: Common) { + const vm = await createVM({ common }) + await vm.stateManager.putAccount(sender, new Account()) + const account = await vm.stateManager.getAccount(sender) + const balance = GWEI * BigInt(21000) * BigInt(10000000) + account!.balance = balance + await vm.stateManager.putAccount(sender, account!) + + await vm.stateManager.putCode(contractAddress, code) + return vm +} + +describe('EIP 7623 calldata cost increase tests', () => { + it('charges floor gas', async () => { + const vm = await getVM(common) + + const tx = createLegacyTx( + { + to: createZeroAddress(), + data: new Uint8Array(100).fill(1), + gasLimit: 1000000, + gasPrice: 10, + }, + { common }, + ).sign(pkey) + + const result = await runTx(vm, { + block, + tx, + skipHardForkValidation: true, + }) + + const baseCost = tx.common.param('txGas') + const floorCost = tx.common.param('totalCostFloorPerToken') + + const expected = baseCost + BigInt(tx.data.length) * BigInt(4) * floorCost + + assert.equal(result.totalGasSpent, expected) + }) + it('rejects transactions having a gas limit below the floor gas limit', async () => { + const vm = await getVM(common) + + const tx = createLegacyTx( + { + to: createZeroAddress(), + data: new Uint8Array(100).fill(1), + gasLimit: 21000 + 100 * 4, + gasPrice: 10, + }, + { common }, + ).sign(pkey) + try { + await runTx(vm, { + block, + tx, + skipHardForkValidation: true, + }) + assert.fail('runTx should throw') + } catch (e) { + assert.ok('Successfully failed') + } + }) + it('correctly charges execution gas instead of floor gas when execution gas exceeds the floor gas', async () => { + const vm = await getVM(common) + const to = createZeroAddress() + + // Store 1 in slot 1 + await vm.stateManager.putCode(to, hexToBytes('0x6001600155')) + + const tx = createLegacyTx( + { + to: createZeroAddress(), + data: new Uint8Array(100).fill(1), + gasLimit: 1000000, + gasPrice: 10, + }, + { common }, + ).sign(pkey) + + const result = await runTx(vm, { + block, + tx, + skipHardForkValidation: true, + }) + + const baseCost = tx.common.param('txGas') + + const expected = + baseCost + + BigInt(tx.data.length) * tx.common.param('txDataNonZeroGas') + + BigInt(2 * 3) + + BigInt(22_100) + + assert.equal(result.totalGasSpent, expected) + }) +}) diff --git a/packages/vm/test/t8n/t8ntool.ts b/packages/vm/test/t8n/t8ntool.ts index fe322ffac7..bfb93d3b79 100644 --- a/packages/vm/test/t8n/t8ntool.ts +++ b/packages/vm/test/t8n/t8ntool.ts @@ -171,6 +171,9 @@ export class TransitionTool { for (const txData of this.txsData) { try { const tx = createTx(txData, { common: this.common }) + if (!tx.isValid()) { + throw new Error(tx.getValidationErrors().join(', ')) + } // Set `allowNoBlobs` to `true`, since the test might not have the blob // The 4844-tx at this should still be valid, since it has the `blobHashes` field await builder.addTransaction(tx, { allowNoBlobs: true }) @@ -306,7 +309,7 @@ export class TransitionTool { output.requests = [] for (const request of requests) { if (request.bytes.length > 1) { - output.requests.push(bytesToHex(request.bytes)) + output.requests.push(bytesToHex(request.bytes.slice(1))) } } }