From 9dba15a6734bf5e561febd3d642db5a8656dddd7 Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Thu, 5 Feb 2026 17:15:11 +0800 Subject: [PATCH] feat(core): implement EIP-7623 increase calldata cost 30946 (#2031) Link to spec: https://eips.ethereum.org/EIPS/eip-7623 --------- Co-authored-by: Marius van der Wijden Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> Co-authored-by: lightclient --- core/error.go | 4 ++ core/state_processor_test.go | 4 +- core/state_transition.go | 81 ++++++++++++++++++++++++++---------- core/tracing/hooks.go | 3 ++ core/txpool/validation.go | 19 ++++++--- params/protocol_params.go | 2 + 6 files changed, 85 insertions(+), 28 deletions(-) diff --git a/core/error.go b/core/error.go index 1e4c9a015c..a0ccd05a7b 100644 --- a/core/error.go +++ b/core/error.go @@ -76,6 +76,10 @@ var ( // than required to start the invocation. ErrIntrinsicGas = errors.New("intrinsic gas too low") + // ErrFloorDataGas is returned if the transaction is specified to use less gas + // than required for the data floor cost. + ErrFloorDataGas = errors.New("insufficient gas for floor data gas cost") + // ErrTxTypeNotSupported is returned if a transaction is not supported in the // current network configuration. ErrTxTypeNotSupported = types.ErrTxTypeNotSupported diff --git a/core/state_processor_test.go b/core/state_processor_test.go index d8c48b5809..7d183d6e6a 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -231,9 +231,9 @@ func TestStateProcessorErrors(t *testing.T) { }, { // ErrMaxInitCodeSizeExceeded txs: []*types.Transaction{ - mkDynamicCreationTx(0, 500000, common.Big0, big.NewInt(params.InitialBaseFee), tooBigInitCode[:]), + mkDynamicCreationTx(0, 520000, common.Big0, big.NewInt(params.InitialBaseFee), tooBigInitCode[:]), }, - want: "could not apply tx 0 [0x42c498ac252e8943de9e8ab3267113b0a0cde54543df1ab0711b0d87ccca3e03]: max initcode size exceeded: code size 49153 limit 49152", + want: "could not apply tx 0 [0x41d48b664cf891e625a16696a90e892ba3857c0b5ea759c3f2bdb4158338cb85]: max initcode size exceeded: code size 49153 limit 49152", }, { // ErrIntrinsicGas: Not enough gas to cover init code txs: []*types.Transaction{ diff --git a/core/state_transition.go b/core/state_transition.go index b6925f258a..66282911ac 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -17,6 +17,7 @@ package core import ( + "bytes" "fmt" "math" "math/big" @@ -71,19 +72,15 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set // Bump the required gas by the amount of transactional data if dataLen > 0 { // Zero and non-zero bytes are priced differently - var nz uint64 - for _, byt := range data { - if byt != 0 { - nz++ - } - } + z := uint64(bytes.Count(data, []byte{0})) + nz := dataLen - z + // Make sure we don't exceed uint64 for all data combinations if (math.MaxUint64-gas)/params.TxDataNonZeroGas < nz { return 0, ErrGasUintOverflow } gas += nz * params.TxDataNonZeroGas - z := dataLen - nz if (math.MaxUint64-gas)/params.TxDataZeroGas < z { return 0, ErrGasUintOverflow } @@ -107,6 +104,21 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set return gas, nil } +// FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623). +func FloorDataGas(data []byte) (uint64, error) { + var ( + z = uint64(bytes.Count(data, []byte{0})) + nz = uint64(len(data)) - z + tokens = nz*params.TxTokenPerNonZeroByte + z + ) + // Check for overflow + if (math.MaxUint64-params.TxGas)/params.TxCostFloorPerToken < tokens { + return 0, ErrGasUintOverflow + } + // Minimum gas required for a transaction based on its data tokens (EIP-7623). + return params.TxGas + tokens*params.TxCostFloorPerToken, nil +} + // toWordSize returns the ceiled word size required for init code payment calculation. func toWordSize(size uint64) uint64 { if size > math.MaxUint64-31 { @@ -384,6 +396,7 @@ func (st *StateTransition) TransitionDb(owner common.Address) (*ExecutionResult, sender = vm.AccountRef(st.from()) rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber) contractCreation = msg.To == nil + floorDataGas uint64 ) // Check clauses 4-5, subtract intrinsic gas if everything is correct @@ -394,6 +407,16 @@ func (st *StateTransition) TransitionDb(owner common.Address) (*ExecutionResult, if st.gasRemaining < gas { return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining, gas) } + // Gas limit suffices for the floor data cost (EIP-7623) + if rules.IsPrague { + floorDataGas, err = FloorDataGas(msg.Data) + if err != nil { + return nil, err + } + if msg.GasLimit < floorDataGas { + return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, msg.GasLimit, floorDataGas) + } + } if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil { t.OnGasChange(st.gasRemaining, st.gasRemaining-gas, tracing.GasChangeTxIntrinsicGas) } @@ -449,13 +472,20 @@ func (st *StateTransition) TransitionDb(owner common.Address) (*ExecutionResult, ret, st.gasRemaining, vmerr = st.evm.Call(sender, st.to(), msg.Data, st.gasRemaining, value) } - if !rules.IsEIP1559 { - // Before EIP-3529: refunds were capped to gasUsed / 2 - st.refundGas(params.RefundQuotient) - } else { - // After EIP-3529: refunds are capped to gasUsed / 5 - st.refundGas(params.RefundQuotientEIP3529) + // Compute refund counter, capped to a refund quotient. + gasRefund := st.calcRefund() + st.gasRemaining += gasRefund + if rules.IsPrague { + // After EIP-7623: Data-heavy transactions pay the floor gas. + if st.gasUsed() < floorDataGas { + prev := st.gasRemaining + st.gasRemaining = st.initialGas - floorDataGas + if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil { + t.OnGasChange(prev, st.gasRemaining, tracing.GasChangeTxDataFloor) + } + } } + st.returnGas() // GasPrice of special tx is always 0, so we can skip AddBalance if !types.IsSpecialTx(msg.To) { @@ -540,22 +570,31 @@ func (st *StateTransition) applyAuthorization(msg *Message, auth *types.SetCodeA return nil } -func (st *StateTransition) refundGas(refundQuotient uint64) { - // Apply refund counter, capped to a refund quotient - refund := st.gasUsed() / refundQuotient +// calcRefund computes refund counter, capped to a refund quotient. +func (st *StateTransition) calcRefund() uint64 { + var refund uint64 + if !st.evm.ChainConfig().IsEIP1559(st.evm.Context.BlockNumber) { + // Before EIP-3529: refunds were capped to gasUsed / 2 + refund = st.gasUsed() / params.RefundQuotient + } else { + // After EIP-3529: refunds are capped to gasUsed / 5 + refund = st.gasUsed() / params.RefundQuotientEIP3529 + } if refund > st.state.GetRefund() { refund = st.state.GetRefund() } - if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && refund > 0 { st.evm.Config.Tracer.OnGasChange(st.gasRemaining, st.gasRemaining+refund, tracing.GasChangeTxRefunds) } + return refund +} - st.gasRemaining += refund - +// returnGas returns ETH for remaining gas, +// exchanged at the original rate. +func (st *StateTransition) returnGas() { if st.msg.BalanceTokenFee == nil { - // Return ETH for remaining gas, exchanged at the original rate. - remaining := new(big.Int).Mul(new(big.Int).SetUint64(st.gasRemaining), st.msg.GasPrice) + remaining := new(big.Int).SetUint64(st.gasRemaining) + remaining.Mul(remaining, st.msg.GasPrice) st.state.AddBalance(st.from(), remaining, tracing.BalanceIncreaseGasReturn) } diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go index f7281112fd..2bcea0696c 100644 --- a/core/tracing/hooks.go +++ b/core/tracing/hooks.go @@ -299,6 +299,9 @@ const ( GasChangeCallStorageColdAccess GasChangeReason = 13 // GasChangeCallFailedExecution is the burning of the remaining gas when the execution failed without a revert. GasChangeCallFailedExecution GasChangeReason = 14 + // GasChangeTxDataFloor is the amount of extra gas the transaction has to pay to reach the minimum gas requirement for the + // transaction data. This change will always be a negative change. + GasChangeTxDataFloor GasChangeReason = 19 // GasChangeIgnored is a special value that can be used to indicate that the gas change should be ignored as // it will be "manually" tracked by a direct emit of the gas change event. diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 8c1a8b43e5..bffb4bafb3 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -108,11 +108,6 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types if tx.GasPrice().Sign() == 0 { return ErrZeroGasPrice } - // Ensure the gas price is high enough to cover the requirement of the calling - // pool and/or block producer - if tx.GasTipCapIntCmp(opts.MinTip) < 0 { - return fmt.Errorf("%w: tip needed %v, tip permitted %v", ErrUnderpriced, opts.MinTip, tx.GasTipCap()) - } // Ensure the transaction has more gas than the bare minimum needed to // cover the transaction metadata intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsEIP1559) @@ -122,6 +117,20 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types if tx.Gas() < intrGas { return fmt.Errorf("%w: needed %v, allowed %v", core.ErrIntrinsicGas, intrGas, tx.Gas()) } + // Ensure the transaction can cover floor data gas. + if rules.IsPrague { + floorDataGas, err := core.FloorDataGas(tx.Data()) + if err != nil { + return err + } + if tx.Gas() < floorDataGas { + return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrFloorDataGas, tx.Gas(), floorDataGas) + } + } + // Ensure the gas price is high enough to cover the requirement of the calling pool + if tx.GasTipCapIntCmp(opts.MinTip) < 0 { + return fmt.Errorf("%w: tip needed %v, tip permitted %v", ErrUnderpriced, opts.MinTip, tx.GasTipCap()) + } if tx.Type() == types.SetCodeTxType { if len(tx.SetCodeAuthorizations()) == 0 { return fmt.Errorf("set code tx must have at least one authorization tuple") diff --git a/params/protocol_params.go b/params/protocol_params.go index 03b70dd3ab..39797d8e8e 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -65,6 +65,8 @@ const ( SuicideRefundGas uint64 = 24000 // Refunded following a suicide operation. MemoryGas uint64 = 3 // Times the address of the (highest referenced byte in memory + 1). NOTE: referencing happens on read, write and in instructions such as RETURN and CALL. + TxTokenPerNonZeroByte uint64 = 4 // Token cost per non-zero byte as specified by EIP-7623. + TxCostFloorPerToken uint64 = 10 // Cost floor per byte of data as specified by EIP-7623. TxAccessListAddressGas uint64 = 2400 // Per address specified in EIP 2930 access list TxAccessListStorageKeyGas uint64 = 1900 // Per storage key specified in EIP 2930 access list TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702