diff --git a/src/Nethermind/Nethermind.Core.Test/Encoding/FrameTxDecoderTests.cs b/src/Nethermind/Nethermind.Core.Test/Encoding/FrameTxDecoderTests.cs index 8122f44b18a9..0df825e38d34 100644 --- a/src/Nethermind/Nethermind.Core.Test/Encoding/FrameTxDecoderTests.cs +++ b/src/Nethermind/Nethermind.Core.Test/Encoding/FrameTxDecoderTests.cs @@ -374,6 +374,13 @@ private static IEnumerable RoundtripCases() Frame(), ])).SetName("Roundtrip_AllModesFlagsTargetsAndData"); + yield return new TestCaseData(CreateFrameTx(frames: + [ + Frame(gasLimit: 500_000, stateGasLimit: 183_600), + Frame(mode: TxFrame.ModeVerify, gasLimit: 90_000, stateGasLimit: 0), + Frame(mode: TxFrame.ModeSender, gasLimit: ulong.MaxValue - 1, stateGasLimit: 1), + ])).SetName("Roundtrip_TwoDimensionalGasLimits"); + yield return new TestCaseData(CreateFrameTx(signatures: [ new TxFrameSignature(TxFrameSignature.SchemeArbitrary, null, default, FilledBytes(11, 0x77)), @@ -479,7 +486,8 @@ private static void AssertFramesEqual(TxFrame[] actual, TxFrame[] expected) Assert.That(actual[i].Mode, Is.EqualTo(expected[i].Mode), $"frame {i} mode"); Assert.That(actual[i].Flags, Is.EqualTo(expected[i].Flags), $"frame {i} flags"); Assert.That(actual[i].Target, Is.EqualTo(expected[i].Target), $"frame {i} target"); - Assert.That(actual[i].GasLimit, Is.EqualTo(expected[i].GasLimit), $"frame {i} gas limit"); + Assert.That(actual[i].ExecutionGasLimit, Is.EqualTo(expected[i].ExecutionGasLimit), $"frame {i} execution gas limit"); + Assert.That(actual[i].StateGasLimit, Is.EqualTo(expected[i].StateGasLimit), $"frame {i} state gas limit"); Assert.That(actual[i].Value, Is.EqualTo(expected[i].Value), $"frame {i} value"); Assert.That(actual[i].Data.ToArray(), Is.EqualTo(expected[i].Data.ToArray()), $"frame {i} data"); } @@ -510,8 +518,8 @@ private static Transaction CreateFrameTx(TxFrame[]? frames = null, TxFrameSignat DecodedMaxFeePerGas = 30.GWei, }; - private static TxFrame Frame(byte mode = TxFrame.ModeDefault, byte flags = 0, Address? target = null, ulong gasLimit = 100_000, UInt256 value = default, byte[]? data = null) => - new(mode, flags, target, gasLimit, value, data ?? Array.Empty()); + private static TxFrame Frame(byte mode = TxFrame.ModeDefault, byte flags = 0, Address? target = null, ulong gasLimit = 100_000, ulong stateGasLimit = 0, UInt256 value = default, byte[]? data = null) => + new(mode, flags, target, gasLimit, stateGasLimit, value, data ?? Array.Empty()); private static byte[] FilledBytes(int length, byte fill) { diff --git a/src/Nethermind/Nethermind.Core.Test/FrameTxValidationTests.cs b/src/Nethermind/Nethermind.Core.Test/FrameTxValidationTests.cs index baf551ae4519..b6e6cc7589d4 100644 --- a/src/Nethermind/Nethermind.Core.Test/FrameTxValidationTests.cs +++ b/src/Nethermind/Nethermind.Core.Test/FrameTxValidationTests.cs @@ -174,6 +174,9 @@ private static IEnumerable ConstraintCases() yield return Case("ExpiryFrameWithShortData_InvalidExpiryFrame", static tx => tx.Frames = [Frame(mode: TxFrame.ModeVerify, target: Eip8141Constants.ExpiryVerifierAddress, data: new byte[Eip8141Constants.ExpiryDataLength - 1])], FrameTxValidation.InvalidExpiryFrame); + yield return Case("ExpiryFrameWithStateGas_InvalidExpiryFrame", + static tx => tx.Frames = [new TxFrame(TxFrame.ModeVerify, flags: 0, Eip8141Constants.ExpiryVerifierAddress, 30_000, 1, UInt256.Zero, new byte[Eip8141Constants.ExpiryDataLength])], + FrameTxValidation.InvalidExpiryFrame); yield return Case("TwoExpiryFrames_MultipleExpiryFrames", static tx => tx.Frames = [SelfVerifyFrame(), ExpiryFrame(), ExpiryFrame()], FrameTxValidation.MultipleExpiryFrames); diff --git a/src/Nethermind/Nethermind.Core/FrameTxValidation.cs b/src/Nethermind/Nethermind.Core/FrameTxValidation.cs index 79ea4885a01d..f5d83b5175d3 100644 --- a/src/Nethermind/Nethermind.Core/FrameTxValidation.cs +++ b/src/Nethermind/Nethermind.Core/FrameTxValidation.cs @@ -155,7 +155,7 @@ public static bool IsWellFormed(Transaction transaction, bool postTxEnabled, out if (frame.Mode == TxFrame.ModeVerify && frame.Target == Eip8141Constants.ExpiryVerifierAddress) { - if (frame.Flags != 0 || !frame.Value.IsZero || frame.Data.Length != Eip8141Constants.ExpiryDataLength) + if (frame.Flags != 0 || !frame.Value.IsZero || frame.StateGasLimit != 0 || frame.Data.Length != Eip8141Constants.ExpiryDataLength) { error = InvalidExpiryFrame; return false; @@ -170,8 +170,9 @@ public static bool IsWellFormed(Transaction transaction, bool postTxEnabled, out hasExpiryFrame = true; } - ulong accumulated = totalFrameGas + frame.GasLimit; - if (accumulated < totalFrameGas) + ulong frameGas = frame.ExecutionGasLimit + frame.StateGasLimit; + ulong accumulated = totalFrameGas + frameGas; + if (frameGas < frame.ExecutionGasLimit || accumulated < totalFrameGas) { error = FrameGasOverflow; return false; @@ -250,8 +251,10 @@ static bool BelongsToAtomicBatch(TxFrame[] frames, int i) => }; /// - /// An upper bound on the public-mempool validation work of : the gas limits - /// of its validation prefix plus the cost of verifying its signatures, saturating at . + /// An upper bound on the public-mempool validation work of : the execution-gas + /// limits (EIP-8141 MAX_VERIFY_GAS) of its validation prefix plus the cost of verifying its signatures, + /// saturating at . The prefix's limits.state is bounded separately by + /// MAX_VERIFY_STATE_GAS and does not enter this budget. /// /// /// Derived from the frame layout alone, so no state is read. Each layout of EIP-8141 "Public @@ -269,7 +272,7 @@ public static ulong ValidationWorkGas(Transaction transaction) ulong total = 0; for (int i = 0; i < counted; i++) { - total = Saturating(total, frames[i].GasLimit); + total = Saturating(total, frames[i].ExecutionGasLimit); } foreach (TxFrameSignature signature in transaction.FrameSignatures ?? []) @@ -280,6 +283,27 @@ public static ulong ValidationWorkGas(Transaction transaction) return total; } + /// + /// An upper bound on the state growth EIP-8141 admits through the public mempool for + /// : the sum of its validation prefix's limits.state, saturating at + /// . Bounded separately by MAX_VERIFY_STATE_GAS; signature verification + /// uses no state gas, so it does not enter this budget. + /// + /// The frame transaction to price. + public static ulong ValidationWorkStateGas(Transaction transaction) + { + TxFrame[] frames = transaction.Frames ?? []; + int counted = RecognizedPrefixLength(frames, transaction.SenderAddress) ?? frames.Length; + + ulong total = 0; + for (int i = 0; i < counted; i++) + { + total = Saturating(total, frames[i].StateGasLimit); + } + + return total; + } + /// /// The number of leading frames forming a validation prefix EIP-8141 recognizes for the public /// mempool, or null when the layout matches none of them. @@ -401,8 +425,9 @@ private static bool CalculateGasBudget(Transaction transaction, IReleaseSpec spe tokens += CountCalldataTokens(frame.Data.Span, spec); dataLength += (ulong)frame.Data.Length; - ulong accumulated = totalFrameGas + frame.GasLimit; - if (accumulated < totalFrameGas) + ulong frameGas = frame.ExecutionGasLimit + frame.StateGasLimit; + ulong accumulated = totalFrameGas + frameGas; + if (frameGas < frame.ExecutionGasLimit || accumulated < totalFrameGas) { return false; } diff --git a/src/Nethermind/Nethermind.Core/TxFrame.cs b/src/Nethermind/Nethermind.Core/TxFrame.cs index 26ad038063fa..6d79ae54041f 100644 --- a/src/Nethermind/Nethermind.Core/TxFrame.cs +++ b/src/Nethermind/Nethermind.Core/TxFrame.cs @@ -7,10 +7,11 @@ namespace Nethermind.Core; /// -/// A single frame of an EIP-8141 frame transaction: [mode, flags, target, gas_limit, value, data]. +/// A single frame of an EIP-8141 frame transaction: [mode, flags, target, limits, value, data], +/// where limits = [execution, state]. /// https://eips.ethereum.org/EIPS/eip-8141 /// -public class TxFrame(byte mode, byte flags, Address? target, ulong gasLimit, UInt256 value, ReadOnlyMemory data) +public class TxFrame(byte mode, byte flags, Address? target, ulong executionGasLimit, ulong stateGasLimit, UInt256 value, ReadOnlyMemory data) { public const byte ModeDefault = 0; public const byte ModeVerify = 1; @@ -26,13 +27,29 @@ public class TxFrame(byte mode, byte flags, Address? target, ulong gasLimit, UIn public const byte ApproveScopeMask = ApproveExecutionAndPayment; public const byte AtomicBatchFlag = 0x4; + /// Constructs a frame whose entire budget is execution gas, with limits.state == 0. + public TxFrame(byte mode, byte flags, Address? target, ulong gasLimit, UInt256 value, ReadOnlyMemory data) + : this(mode, flags, target, gasLimit, 0, value, data) + { + } + public byte Mode { get; } = mode; public byte Flags { get; } = flags; /// Null resolves to the transaction sender during execution. public Address? Target { get; } = target; - public ulong GasLimit { get; } = gasLimit; + /// EIP-8141 limits.execution: the frame's execution-gas budget. + public ulong ExecutionGasLimit { get; } = executionGasLimit; + + /// EIP-8141 limits.state: the frame's state-gas budget (EIP-8037). + public ulong StateGasLimit { get; } = stateGasLimit; + + /// The combined gas the frame reserves against the payer, limits.execution + limits.state. + /// Static validation rejects a transaction whose frame reservations overflow, so this sum never + /// wraps for a frame reaching execution. + public ulong GasLimit => ExecutionGasLimit + StateGasLimit; + public UInt256 Value { get; } = value; public ReadOnlyMemory Data { get; } = data; diff --git a/src/Nethermind/Nethermind.Evm.Test/FrameTxBlockGasTests.cs b/src/Nethermind/Nethermind.Evm.Test/FrameTxBlockGasTests.cs index d8837f178f5b..31b5a7f678d2 100644 --- a/src/Nethermind/Nethermind.Evm.Test/FrameTxBlockGasTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/FrameTxBlockGasTests.cs @@ -90,6 +90,63 @@ public void Execute_PayloadFrameReverts_OwesNoStateGas() "a reverted frame commits no state, so it grows none"); } + /// + /// A frame's state gas is drawn from its own limits.state reservoir, independent of + /// limits.execution: a fresh-slot write whose execution budget cannot absorb the state charge + /// still succeeds when the state budget covers it, and the same charge lands in the state dimension. + /// + [Test] + public void Execute_PayloadFrameStateBudgetCoversTheWrite_SucceedsFromTheStateReservoir() + { + Deploy(Sender, ApproveCode(TxFrame.ApproveExecutionAndPayment), UInt256.Parse("100000000000000000000")); + Deploy(Writer, Prepare.EvmCode.PushData(1).PushData(0).Op(Instruction.SSTORE).Op(Instruction.STOP).Done); + + const ulong executionBudget = 30_000; + TestAllTracerWithOutput tracer = new(); + Transaction tx = FrameTx(nonce: 0, + new TxFrame(TxFrame.ModeVerify, TxFrame.ApproveExecutionAndPayment, target: null, gasLimit: 200_000, UInt256.Zero, default), + new TxFrame(TxFrame.ModeSender, 0, Writer, executionBudget, 150_000, UInt256.Zero, default)); + + Assert.That(Process(tx, tracer).TransactionExecuted, Is.True); + + const ulong stateCharge = (ulong)GasCostOf.SSetState; + using (Assert.EnterMultipleScope()) + { + Assert.That(_state.Get(new StorageCell(Writer, (UInt256)0)).ToArray(), Is.Not.All.EqualTo((byte)0), + "the write committed, so its state gas came from the reservoir rather than out-of-gassing"); + Assert.That(tracer.GasConsumedResult.BlockStateGas, Is.EqualTo(stateCharge), + "the reservoir-funded write still bills the state dimension"); + Assert.That(tracer.GasConsumedResult.EffectiveBlockGas, + Is.EqualTo(tracer.GasConsumedResult.SpentGas - stateCharge)); + } + } + + /// + /// With no state budget the same write's state charge spills into limits.execution, which cannot + /// cover it, so the frame halts and commits nothing — proving the two budgets are independent pools. + /// + [Test] + public void Execute_PayloadFrameStateSpillsIntoTooSmallExecutionBudget_HaltsAndOwesNoStateGas() + { + Deploy(Sender, ApproveCode(TxFrame.ApproveExecutionAndPayment), UInt256.Parse("100000000000000000000")); + Deploy(Writer, Prepare.EvmCode.PushData(1).PushData(0).Op(Instruction.SSTORE).Op(Instruction.STOP).Done); + + const ulong executionBudget = 30_000; + TestAllTracerWithOutput tracer = new(); + Transaction tx = FrameTx(nonce: 0, + new TxFrame(TxFrame.ModeVerify, TxFrame.ApproveExecutionAndPayment, target: null, gasLimit: 200_000, UInt256.Zero, default), + new TxFrame(TxFrame.ModeSender, 0, Writer, executionBudget, 0, UInt256.Zero, default)); + + Assert.That(Process(tx, tracer).TransactionExecuted, Is.True); + + using (Assert.EnterMultipleScope()) + { + Assert.That(_state.Get(new StorageCell(Writer, (UInt256)0)).ToArray(), Is.All.EqualTo((byte)0), + "the write halted out of gas, so no slot was committed"); + Assert.That(tracer.GasConsumedResult.BlockStateGas, Is.Zero); + } + } + /// An atomic batch whose later frame fails gives back the state gas its earlier frame owed. /// /// The unroll restores the pre-batch state, so the fresh slot the first frame wrote never reaches the diff --git a/src/Nethermind/Nethermind.Evm.Test/FrameTxProcessorTests.cs b/src/Nethermind/Nethermind.Evm.Test/FrameTxProcessorTests.cs index d6a260dfc5ac..9956e1fbb749 100644 --- a/src/Nethermind/Nethermind.Evm.Test/FrameTxProcessorTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/FrameTxProcessorTests.cs @@ -496,6 +496,7 @@ public void Execute_TxParamSigHash_ExposesCanonicalHash() [TestCase((byte)0x06, 3UL, TestName = "Execute_FrameParam_AllowedScope")] [TestCase((byte)0x07, 0UL, TestName = "Execute_FrameParam_AtomicBatch")] [TestCase((byte)0x08, 0UL, TestName = "Execute_FrameParam_Value")] + [TestCase((byte)0x09, 0UL, TestName = "Execute_FrameParam_StateGasLimit")] public void Execute_FrameParamIntrospection_ReadsCompletedFrame(byte param, ulong expected) { DeploySmartSender(ApproveCode(TxFrame.ApproveExecutionAndPayment)); @@ -511,6 +512,22 @@ public void Execute_FrameParamIntrospection_ReadsCompletedFrame(byte param, ulon AssertStorage(Observer, 0, (UInt256)expected); } + [Test] + public void Execute_FrameParam_StateGasLimit_ReadsDeclaredStateBudget() + { + DeploySmartSender(ApproveCode(TxFrame.ApproveExecutionAndPayment)); + DeployContract(Observer, Prepare.EvmCode + .PushData(0x09).PushData(1).Op(Instruction.FRAMEPARAM).PushData(0).Op(Instruction.SSTORE) + .Op(Instruction.STOP).Done); + Transaction tx = FrameTx(nonce: 0, SelfVerifyFrame(), + new TxFrame(TxFrame.ModeDefault, 0, Observer, 200_000, 50_000, UInt256.Zero, default)); + + TransactionResult result = Process(tx); + + Assert.That(result.TransactionExecuted, Is.True); + AssertStorage(Observer, 0, (UInt256)50_000); + } + [Test] public void Execute_FrameParamStatusOfCurrentFrame_ExceptionallyHalts() { diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 122b17e6950d..e37753ab6f81 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -39,6 +39,10 @@ public struct EthereumGasPolicy : IGasPolicy [MethodImpl(MethodImplOptions.AggressiveInlining)] public static EthereumGasPolicy FromULong(ulong value) => new() { Value = value }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static EthereumGasPolicy FromFrameLimits(ulong executionGasLimit, ulong stateGasLimit) => + new() { Value = executionGasLimit, StateReservoir = (long)stateGasLimit }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static EthereumGasPolicy CreateSystemTransactionIntrinsicGas(ulong blockGasLimit) => new() diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs index 0af6e9c80562..27086bc05c40 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs @@ -15,6 +15,15 @@ public interface IGasPolicy where TSelf : struct, IGasPolicy { static abstract TSelf FromULong(ulong value); + /// + /// Seeds an EIP-8141 frame budget from its two-dimensional limits = [execution, state]: the execution + /// dimension funds gas_left and the state dimension funds the state reservoir. Pre-EIP-8037 policies, + /// having no state dimension, fall back to a single combined budget. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static virtual TSelf FromFrameLimits(ulong executionGasLimit, ulong stateGasLimit) => + TSelf.FromULong(executionGasLimit > ulong.MaxValue - stateGasLimit ? ulong.MaxValue : executionGasLimit + stateGasLimit); + [MethodImpl(MethodImplOptions.AggressiveInlining)] static virtual TSelf CreateSystemTransactionIntrinsicGas(ulong blockGasLimit) => TSelf.FromULong(0); diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.FrameTx.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.FrameTx.cs index 02a4be280522..2d593de644b9 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.FrameTx.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.FrameTx.cs @@ -217,14 +217,14 @@ public static EvmExceptionType InstructionFrameParam(V // Spec stack order: frameIndex on top, param second. if (!stack.PopUInt256(out UInt256 frameIndex, out UInt256 param)) return EvmExceptionType.StackUnderflow; if (frameIndex >= (UInt256)ctx.Frames.Length) return EvmExceptionType.BadInstruction; - if (param > 0x08) return EvmExceptionType.BadInstruction; + if (param > 0x09) return EvmExceptionType.BadInstruction; int index = (int)frameIndex.u0; TxFrame frame = ctx.Frames[index]; return param.u0 switch { 0x00 => stack.PushAddress(ctx.ResolvedTarget(index)), - 0x01 => stack.PushUInt256((UInt256)frame.GasLimit), + 0x01 => stack.PushUInt256((UInt256)frame.ExecutionGasLimit), 0x02 => stack.PushUInt32(frame.Mode), 0x03 => stack.PushUInt32(frame.Flags), 0x04 => stack.PushUInt256((UInt256)frame.Data.Length), @@ -232,6 +232,7 @@ public static EvmExceptionType InstructionFrameParam(V 0x06 => stack.PushUInt32(frame.AllowedApproveScope), 0x07 => stack.PushUInt32((uint)(frame.IsAtomicBatch ? 1 : 0)), 0x08 => stack.PushUInt256(frame.Value), + 0x09 => stack.PushUInt256((UInt256)frame.StateGasLimit), _ => EvmExceptionType.BadInstruction, }; } diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessorBase.FrameTx.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessorBase.FrameTx.cs index daf70f492f7b..0d574c476c89 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessorBase.FrameTx.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessorBase.FrameTx.cs @@ -428,9 +428,10 @@ private TransactionResult ExecuteFrameTx(Transaction tx, ITxTracer tracer, Execu ulong grossGas = intrinsicGas + totalFrameGasUsed; ulong spentGas = Math.Max(grossGas - RefundHelper.CalculateClaimableRefund(grossGas, (ulong)refundCounter, spec), floorGas); ulong blockStateGas = (ulong)totalFrameStateGasUsed; - // blockStateGas <= grossGas by the reservoir-0 invariant: each frame rents an empty state-gas - // reservoir, so every state charge is already counted inside totalFrameGasUsed (hence grossGas), - // which is what makes the SaturatingSub in CalculateBlockExecutionGas sound. + // blockStateGas <= grossGas: each frame's charge folded into totalFrameGasUsed already includes + // the state gas it used, whether drawn from its limits.state reservoir or spilled into + // limits.execution, so the state total is a subset of grossGas, which is what makes the + // SaturatingSub in CalculateBlockExecutionGas sound. ulong blockRegularGas = Eip8037BlockGasInclusionCheck.CalculateBlockExecutionGas(grossGas, blockStateGas, floorGas); // Block-level gas accounting reads Transaction.BlockGasUsed, whose getter otherwise falls back // to tx.GasLimit (the frame-gas sum, not the gas actually spent). Set it explicitly like the @@ -591,7 +592,7 @@ private TransactionSubstate ExecuteFrame(TxFrame frame, Address resolvedTarget, } using VmState state = VmState.RentTopLevel( - TGasPolicy.FromULong(frame.GasLimit), + TGasPolicy.FromFrameLimits(frame.ExecutionGasLimit, frame.StateGasLimit), isStatic ? ExecutionType.STATICCALL : ExecutionType.TRANSACTION, env, in frameTracker, @@ -604,8 +605,10 @@ private TransactionSubstate ExecuteFrame(TxFrame frame, Address resolvedTarget, ? VirtualMachine.ExecuteTransaction(state, WorldState, tracer) : VirtualMachine.ExecuteTransaction(state, WorldState, tracer); - ulong remainingGas = substate.IsError ? 0 : TGasPolicy.GetRemainingGas(in state.Gas); - gasUsed = frame.GasLimit - remainingGas; + ulong combinedLimit = frame.ExecutionGasLimit + frame.StateGasLimit; + gasUsed = substate.IsError + ? combinedLimit - (ulong)Math.Max(0, TGasPolicy.GetStateReservoir(in state.Gas)) + : TGasPolicy.GetPreRefundGas(in state.Gas, combinedLimit); // Clamp rather than assert: unlike the standard path, this also runs for reverted or errored // frames, where the state-gas value carries no non-negativity guarantee. stateGasUsed = Math.Max(0, TGasPolicy.GetStateGasUsed(in state.Gas)); diff --git a/src/Nethermind/Nethermind.Facade/Eth/RpcTransaction/FrameForRpc.cs b/src/Nethermind/Nethermind.Facade/Eth/RpcTransaction/FrameForRpc.cs index e02ac190dee9..b25068f8717a 100644 --- a/src/Nethermind/Nethermind.Facade/Eth/RpcTransaction/FrameForRpc.cs +++ b/src/Nethermind/Nethermind.Facade/Eth/RpcTransaction/FrameForRpc.cs @@ -8,13 +8,15 @@ namespace Nethermind.Facade.Eth.RpcTransaction; -/// JSON-RPC view of an EIP-8141 frame: [mode, flags, target, gas_limit, value, data]. +/// JSON-RPC view of an EIP-8141 frame: [mode, flags, target, limits, value, data], +/// where limits = [execution, state]. public class FrameForRpc { public byte Mode { get; set; } public byte Flags { get; set; } public Address? Target { get; set; } - public ulong GasLimit { get; set; } + public ulong ExecutionGasLimit { get; set; } + public ulong StateGasLimit { get; set; } public UInt256 Value { get; set; } public byte[] Data { get; set; } = []; @@ -26,12 +28,13 @@ public FrameForRpc(TxFrame frame) Mode = frame.Mode; Flags = frame.Flags; Target = frame.Target; - GasLimit = frame.GasLimit; + ExecutionGasLimit = frame.ExecutionGasLimit; + StateGasLimit = frame.StateGasLimit; Value = frame.Value; Data = frame.Data.ToArray(); } - public TxFrame ToFrame() => new(Mode, Flags, Target, GasLimit, Value, Data); + public TxFrame ToFrame() => new(Mode, Flags, Target, ExecutionGasLimit, StateGasLimit, Value, Data); public static FrameForRpc[]? FromFrames(TxFrame[]? frames) => frames?.Select(static f => new FrameForRpc(f)).ToArray(); diff --git a/src/Nethermind/Nethermind.JsonRpc.Test/Modules/RpcTransaction/FrameTransactionForRpcTests.cs b/src/Nethermind/Nethermind.JsonRpc.Test/Modules/RpcTransaction/FrameTransactionForRpcTests.cs index d1b7d7d84b45..8c632ed0230a 100644 --- a/src/Nethermind/Nethermind.JsonRpc.Test/Modules/RpcTransaction/FrameTransactionForRpcTests.cs +++ b/src/Nethermind/Nethermind.JsonRpc.Test/Modules/RpcTransaction/FrameTransactionForRpcTests.cs @@ -86,7 +86,8 @@ public void FrameTransactionForRpc_SerializesFrames() Assert.That(frames[0].GetProperty("mode").GetInt32(), Is.EqualTo(TxFrame.ModeVerify)); Assert.That(frames[0].GetProperty("flags").GetInt32(), Is.EqualTo(TxFrame.ApproveExecutionAndPayment)); Assert.That(frames[0].GetProperty("target").GetString(), Is.EqualTo(TestItem.AddressB.ToString())); - Assert.That(frames[0].GetProperty("gasLimit").GetString(), Does.Match("^0x[0-9a-f]+$")); + Assert.That(frames[0].GetProperty("executionGasLimit").GetString(), Does.Match("^0x[0-9a-f]+$")); + Assert.That(frames[0].GetProperty("stateGasLimit").GetString(), Does.Match("^0x[0-9a-f]+$")); } } diff --git a/src/Nethermind/Nethermind.Serialization.Rlp/TxFrameDecoder.cs b/src/Nethermind/Nethermind.Serialization.Rlp/TxFrameDecoder.cs index 0f6379a77404..2b663d2728a7 100644 --- a/src/Nethermind/Nethermind.Serialization.Rlp/TxFrameDecoder.cs +++ b/src/Nethermind/Nethermind.Serialization.Rlp/TxFrameDecoder.cs @@ -9,7 +9,8 @@ namespace Nethermind.Serialization.Rlp; /// -/// Decodes the EIP-8141 frame tuple [mode, flags, target, gas_limit, value, data]. +/// Decodes the EIP-8141 frame tuple [mode, flags, target, limits, value, data], where +/// limits = [execution, state]. /// An empty target byte string decodes to null (resolves to the transaction sender). /// public sealed class TxFrameDecoder : RlpDecoder @@ -27,7 +28,16 @@ protected override TxFrame DecodeInternal(ref RlpReader decoderContext, RlpBehav byte mode = decoderContext.DecodeByte(); byte flags = decoderContext.DecodeByte(); Address? target = decoderContext.DecodeAddressOrNull(); - ulong gasLimit = decoderContext.DecodeULong(); + + int limitsLength = decoderContext.ReadSequenceLength(); + int limitsCheck = limitsLength + decoderContext.Position; + ulong executionGasLimit = decoderContext.DecodeULong(); + ulong stateGasLimit = decoderContext.DecodeULong(); + if (!rlpBehaviors.HasFlag(RlpBehaviors.AllowExtraBytes)) + { + decoderContext.Check(limitsCheck); + } + UInt256 value = decoderContext.DecodeUInt256(); ReadOnlyMemory data = decoderContext.DecodeByteArrayMemory(_dataRlpLimit); @@ -36,7 +46,7 @@ protected override TxFrame DecodeInternal(ref RlpReader decoderContext, RlpBehav decoderContext.Check(check); } - return new TxFrame(mode, flags, target, gasLimit, value, data); + return new TxFrame(mode, flags, target, executionGasLimit, stateGasLimit, value, data); } public override void Encode(ref TWriter writer, TxFrame item, RlpBehaviors rlpBehaviors = RlpBehaviors.None) @@ -45,7 +55,9 @@ public override void Encode(ref TWriter writer, TxFrame item, RlpBehavi writer.Encode((ulong)item.Mode); writer.Encode((ulong)item.Flags); writer.Encode(item.Target); - writer.Encode(item.GasLimit); + writer.StartSequence(GetLimitsContentLength(item)); + writer.Encode(item.ExecutionGasLimit); + writer.Encode(item.StateGasLimit); writer.Encode(item.Value); writer.Encode(item.Data); } @@ -85,7 +97,10 @@ private static int GetContentLength(TxFrame item) => Rlp.LengthOf((ulong)item.Mode) + Rlp.LengthOf((ulong)item.Flags) + Rlp.LengthOf(item.Target) - + Rlp.LengthOf(item.GasLimit) + + Rlp.LengthOfSequence(GetLimitsContentLength(item)) + Rlp.LengthOf(item.Value) + Rlp.LengthOf(item.Data); + + private static int GetLimitsContentLength(TxFrame item) => + Rlp.LengthOf(item.ExecutionGasLimit) + Rlp.LengthOf(item.StateGasLimit); } diff --git a/src/Nethermind/Nethermind.TxPool.Test/FrameTxVerifyGasFilterTest.cs b/src/Nethermind/Nethermind.TxPool.Test/FrameTxVerifyGasFilterTest.cs index 0e554a2a8737..481ea5288985 100644 --- a/src/Nethermind/Nethermind.TxPool.Test/FrameTxVerifyGasFilterTest.cs +++ b/src/Nethermind/Nethermind.TxPool.Test/FrameTxVerifyGasFilterTest.cs @@ -57,6 +57,32 @@ public void Accept_ChargesEveryFrameThatMayRunBeforePayment(TxFrame[] frames, Ac Assert.That(filter.Accept(tx, ref state, TxHandlingOptions.None), Is.EqualTo(expected)); } + private static TxFrame SelfVerifyWithState(ulong executionGasLimit, ulong stateGasLimit) => + new(TxFrame.ModeVerify, TxFrame.ApproveExecutionAndPayment, target: null, executionGasLimit, stateGasLimit, UInt256.Zero, default); + + private static TxFrame ExecutionWithState(ulong executionGasLimit, ulong stateGasLimit) => + new(TxFrame.ModeSender, TxFrame.ApproveScopeNone, TestItem.AddressB, executionGasLimit, stateGasLimit, UInt256.Zero, default); + + private static IEnumerable StatePrefixCases() + { + yield return new TestCaseData(new[] { SelfVerifyWithState(1_000, 500_000) }, AcceptTxResult.Accepted) + .SetName("prefix state exactly at MAX_VERIFY_STATE_GAS is accepted"); + yield return new TestCaseData(new[] { SelfVerifyWithState(1_000, 500_001) }, AcceptTxResult.FrameTxVerifyGasTooHigh) + .SetName("prefix state one gas over MAX_VERIFY_STATE_GAS is rejected"); + yield return new TestCaseData(new[] { SelfVerify(1_000), ExecutionWithState(1_000, 3_000_000) }, AcceptTxResult.Accepted) + .SetName("state behind a recognized prefix is outside the ceiling"); + } + + [TestCaseSource(nameof(StatePrefixCases))] + public void Accept_BoundsThePrefixStateGas(TxFrame[] frames, AcceptTxResult expected) + { + Transaction tx = FrameTx(frames); + FrameTxVerifyGasFilter filter = new(new TxPoolConfig { FrameTxMaxVerifyGas = 0, FrameTxMaxVerifyStateGas = 500_000 }, LimboLogs.Instance.GetClassLogger()); + TxFilteringState state = new(tx, Substitute.For()); + + Assert.That(filter.Accept(tx, ref state, TxHandlingOptions.None), Is.EqualTo(expected)); + } + // The pool's account cache stores the empty account on a miss while the reader beneath it may // leave the out-value zeroed, so a filter reading the first probe and a filter reading the // second one must not see a different sender. diff --git a/src/Nethermind/Nethermind.TxPool/Filters/FrameTxVerifyGasFilter.cs b/src/Nethermind/Nethermind.TxPool/Filters/FrameTxVerifyGasFilter.cs index 4a5ea1358d6b..ffca8adc642c 100644 --- a/src/Nethermind/Nethermind.TxPool/Filters/FrameTxVerifyGasFilter.cs +++ b/src/Nethermind/Nethermind.TxPool/Filters/FrameTxVerifyGasFilter.cs @@ -8,20 +8,26 @@ namespace Nethermind.TxPool.Filters; /// /// Rejects an EIP-8141 frame transaction whose validation prefix and signature verification would cost more than -/// MAX_VERIFY_GAS to check. +/// MAX_VERIFY_GAS to check, or whose validation prefix budgets more than MAX_VERIFY_STATE_GAS of state gas. /// /// /// A public-mempool DoS bound, not a validity rule: a block carrying such a transaction stays valid. Must run after /// , which guarantees the frame list is well-formed. A configured limit of 0 lifts the -/// bound, matching the other per-sender pool limits. +/// respective bound, matching the other per-sender pool limits. /// internal sealed class FrameTxVerifyGasFilter(ITxPoolConfig txPoolConfig, ILogger logger) : IIncomingTxFilter { private readonly ulong _maxVerifyGas = txPoolConfig.FrameTxMaxVerifyGas; + private readonly ulong _maxVerifyStateGas = txPoolConfig.FrameTxMaxVerifyStateGas; public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandlingOptions txHandlingOptions) { - if (tx.SupportsFrames && _maxVerifyGas != 0) + if (!tx.SupportsFrames) + { + return AcceptTxResult.Accepted; + } + + if (_maxVerifyGas != 0) { ulong verifyGas = FrameTxValidation.ValidationWorkGas(tx); if (verifyGas > _maxVerifyGas) @@ -32,6 +38,17 @@ public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandl } } + if (_maxVerifyStateGas != 0) + { + ulong verifyStateGas = FrameTxValidation.ValidationWorkStateGas(tx); + if (verifyStateGas > _maxVerifyStateGas) + { + Metrics.PendingTransactionsFrameTxVerifyGasTooHigh++; + if (logger.IsTrace) logger.Trace($"Skipped adding transaction {tx.ToString(" ")}, validation prefix budgets {verifyStateGas} state gas (max {_maxVerifyStateGas})."); + return AcceptTxResult.FrameTxVerifyGasTooHigh; + } + } + return AcceptTxResult.Accepted; } } diff --git a/src/Nethermind/Nethermind.TxPool/ITxPoolConfig.cs b/src/Nethermind/Nethermind.TxPool/ITxPoolConfig.cs index da8878d12320..6d2878cd5cf3 100644 --- a/src/Nethermind/Nethermind.TxPool/ITxPoolConfig.cs +++ b/src/Nethermind/Nethermind.TxPool/ITxPoolConfig.cs @@ -35,6 +35,9 @@ public interface ITxPoolConfig : IConfig [ConfigItem(DefaultValue = "100000", Description = "EIP-8141 `MAX_VERIFY_GAS`: the max gas a frame transaction's validation prefix and signature verification may cost for the transaction to be accepted into the public mempool. `0` to lift the limit. Raise it only on a test network.")] ulong FrameTxMaxVerifyGas { get; set; } + [ConfigItem(DefaultValue = "500000", Description = "EIP-8141 `MAX_VERIFY_STATE_GAS`: the max state gas a frame transaction's validation prefix may budget across its `limits.state` for the transaction to be accepted into the public mempool. `0` to lift the limit. Raise it only on a test network.")] + ulong FrameTxMaxVerifyStateGas { get; set; } + [ConfigItem(DefaultValue = "16", Description = "The max number of pending blob transactions per single sender. `0` to lift the limit.")] int MaxPendingBlobTxsPerSender { get; set; } diff --git a/src/Nethermind/Nethermind.TxPool/TxPoolConfig.cs b/src/Nethermind/Nethermind.TxPool/TxPoolConfig.cs index cb2dc1aadde0..d6693fd4f62d 100644 --- a/src/Nethermind/Nethermind.TxPool/TxPoolConfig.cs +++ b/src/Nethermind/Nethermind.TxPool/TxPoolConfig.cs @@ -19,6 +19,7 @@ public class TxPoolConfig : ITxPoolConfig public int InMemoryBlobPoolSize { get; set; } = 512; // it is used when persistent pool is disabled public int MaxPendingTxsPerSender { get; set; } = 0; public ulong FrameTxMaxVerifyGas { get; set; } = 100_000; + public ulong FrameTxMaxVerifyStateGas { get; set; } = 500_000; public int MaxPendingBlobTxsPerSender { get; set; } = 16; public int HashCacheSize { get; set; } = 512 * 1024; public ulong? GasLimit { get; set; } = null;