Skip to content

Commit 22e6164

Browse files
committed
fix(eip8141): tighten blob-carrying frame tx validation and accounting
Address review follow-ups on the blob-support PR: - require MaxFeePerBlobGas for a blob-carrying frame tx and narrow the versioned-hash validator to internal - reserve the blob fee in block production via the instance blob predicate - return InsufficientSenderBalance for insufficient max fee per blob gas, matching the regular path's error classification - collapse the redundant blob-gas count guard and mark the still-unrouted EIP-1559 fee-collector leg on the frame path - pin the max_cost blob-leg pricing (TXPARAM 0x06) and parameterize the blob-count validation tests
1 parent f11e1db commit 22e6164

6 files changed

Lines changed: 53 additions & 22 deletions

File tree

src/Nethermind/Nethermind.Blockchain.Test/Validators/TxValidatorTests.cs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -850,31 +850,33 @@ static TransactionBuilder<Transaction> MakeTestObject(int blobCount = 1) => Buil
850850
// EIP-7594 (ethereum/EIPs#11985): a blob-carrying frame tx (EIP-8141) is bound by the same per-tx
851851
// blob-count limit (BLOB_COUNT_LIMIT = 6) and versioned-hash version byte (0x01) as a type-3 blob tx,
852852
// matching EELS validate_frame_transaction.
853-
[Test]
854-
public void IsWellFormed_FrameTxWithSingleValidBlob_ReturnTrue()
853+
private static IEnumerable<int> FrameTxWithinBlobCountLimitCases()
855854
{
856-
Transaction tx = BuildBlobFrameTx(blobCount: 1);
857-
TxValidator txValidator = new(TestBlockchainIds.ChainId);
858-
859-
Assert.That(txValidator.IsWellFormed(tx, Bogota.Instance).AsBool(), Is.True);
855+
yield return 1;
856+
yield return (int)Bogota.Instance.MaxBlobsPerTx; // BLOB_COUNT_LIMIT
860857
}
861858

862-
[Test]
863-
public void IsWellFormed_FrameTxAtBlobCountLimit_ReturnTrue()
859+
[TestCaseSource(nameof(FrameTxWithinBlobCountLimitCases))]
860+
public void IsWellFormed_FrameTxWithinBlobCountLimit_ReturnTrue(int blobCount)
864861
{
865-
Transaction tx = BuildBlobFrameTx(blobCount: (int)Bogota.Instance.MaxBlobsPerTx);
862+
Transaction tx = BuildBlobFrameTx(blobCount);
866863
TxValidator txValidator = new(TestBlockchainIds.ChainId);
867864

868865
Assert.That(txValidator.IsWellFormed(tx, Bogota.Instance).AsBool(), Is.True);
869866
}
870867

871868
[Test]
872-
public void IsWellFormed_FrameTxExceedsBlobCountLimit_ReturnFalse()
869+
public void IsWellFormed_FrameTxExceedsBlobCountLimit_ReturnBlobGasLimitExceeded()
873870
{
874-
Transaction tx = BuildBlobFrameTx(blobCount: (int)Bogota.Instance.MaxBlobsPerTx + 1);
871+
int blobCount = (int)Bogota.Instance.MaxBlobsPerTx + 1;
872+
Transaction tx = BuildBlobFrameTx(blobCount);
875873
TxValidator txValidator = new(TestBlockchainIds.ChainId);
876874

877-
Assert.That(txValidator.IsWellFormed(tx, Bogota.Instance).AsBool(), Is.False);
875+
ValidationResult result = txValidator.IsWellFormed(tx, Bogota.Instance);
876+
877+
Assert.That(result.AsBool(), Is.False);
878+
Assert.That(result.Error, Is.EqualTo(TxErrorMessages.BlobTxGasLimitExceeded(
879+
(ulong)blobCount * Eip4844Constants.GasPerBlob, Bogota.Instance.GasCosts.MaxBlobGasPerTx)));
878880
}
879881

880882
[Test]

src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.BlockProductionTransactionPicker.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ private static bool HasEnoughFunds(Transaction transaction, in UInt256 senderBal
126126
return false;
127127
}
128128

129-
if (transaction.SupportsBlobs && (
129+
if (transaction.BlobVersionedHashes is { Length: > 0 } && (
130130
!BlobGasCalculator.TryCalculateBlobBaseFee(block.Header, transaction, releaseSpec.BlobBaseFeeUpdateFraction, out UInt256 blobBaseFee) ||
131131
senderBalance < (maxFee += blobBaseFee)))
132132
{

src/Nethermind/Nethermind.Consensus/Validators/TxValidator.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ public ValidationResult IsWellFormed(Transaction transaction, IReleaseSpec relea
202202
byte[]?[]? blobVersionedHashes = transaction.BlobVersionedHashes;
203203
if (blobVersionedHashes is { Length: > 0 })
204204
{
205+
if (transaction.MaxFeePerBlobGas is null)
206+
{
207+
return TxErrorMessages.BlobTxMissingMaxFeePerBlobGas;
208+
}
209+
205210
ValidationResult blobGasLimitResult = BlobFieldsTxValidator.ValidateBlobGasLimits(blobVersionedHashes.Length, releaseSpec);
206211
return !blobGasLimitResult ? blobGasLimitResult : BlobFieldsTxValidator.ValidateBlobVersionedHashes(blobVersionedHashes);
207212
}
@@ -282,7 +287,7 @@ private static ValidationResult ValidateBlobFields(Transaction transaction, IRel
282287
/// Validates that every blob versioned hash is present, 32 bytes, and carries the KZG version byte
283288
/// (EIP-4844 <c>VERSIONED_HASH_VERSION_KZG = 0x01</c>).
284289
/// </summary>
285-
public static ValidationResult ValidateBlobVersionedHashes(byte[]?[] blobVersionedHashes)
290+
internal static ValidationResult ValidateBlobVersionedHashes(byte[]?[] blobVersionedHashes)
286291
{
287292
foreach (byte[]? versionedHash in blobVersionedHashes)
288293
{

src/Nethermind/Nethermind.Evm.Test/FrameTxProcessorTests.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,37 @@ public void Execute_BlobFrameTx_MaxFeePerBlobGasBelowBlobBaseFee_Invalid()
173173
using (Assert.EnterMultipleScope())
174174
{
175175
Assert.That(result.TransactionExecuted, Is.False);
176-
Assert.That(result.Error, Is.EqualTo(TransactionResult.ErrorType.InsufficientMaxFeePerGasForSenderBalance));
176+
Assert.That(result.Error, Is.EqualTo(TransactionResult.ErrorType.InsufficientSenderBalance));
177177
Assert.That(_stateProvider.GetBalance(Beneficiary), Is.EqualTo(UInt256.Zero), "beneficiary not credited");
178178
Assert.That(_stateProvider.GetNonce(Sender), Is.EqualTo(0UL), "nonce not consumed");
179179
}
180180
}
181181

182+
[Test]
183+
public void Execute_TxParamMaxCost_BlobCarryingFrameTx_ReservesBlobLegAtBlobBaseFeeNotMaxFee()
184+
{
185+
// TXPARAM 0x06 (max_cost) is consensus-visible mid-transaction and also gates payer solvency.
186+
// For a blob-carrying frame tx the blob leg of max_cost is reserved at the actual blob_base_fee,
187+
// not max_fee_per_blob_gas, so it must equal the blobless gas leg plus blob_gas × blob_base_fee.
188+
DeploySmartSender(ApproveCode(TxFrame.ApproveExecutionAndPayment));
189+
DeployContract(Observer, Prepare.EvmCode
190+
.PushData(0x06).Op(Instruction.TXPARAM).PushData(0).Op(Instruction.SSTORE)
191+
.Op(Instruction.STOP).Done);
192+
Transaction tx = FrameTx(nonce: 0, SelfVerifyFrame(), Frame(TxFrame.ModeDefault, target: Observer));
193+
tx.BlobVersionedHashes = [new byte[32]];
194+
tx.MaxFeePerBlobGas = 1000;
195+
196+
TransactionResult result = ProcessWithBlobHeader(tx, excessBlobGas: 0);
197+
198+
Assert.That(result.TransactionExecuted, Is.True);
199+
// Gas leg is the same 415_950 the blobless Execute_TxParam_MaxCost case observes (max fee 1).
200+
UInt256 expectedMaxCost = 415_950 + ExpectedBlobFee(excessBlobGas: 0, blobCount: 1);
201+
AssertStorage(Observer, 0, expectedMaxCost);
202+
UInt256 maxFeePricedMaxCost = 415_950 + tx.MaxFeePerBlobGas.Value * BlobGasCalculator.CalculateBlobGas(1);
203+
Assert.That(expectedMaxCost, Is.LessThan(maxFeePricedMaxCost),
204+
"max_cost priced below the max_fee_per_blob_gas reservation, i.e. at the blob base fee");
205+
}
206+
182207
[TestCase(7ul, 2ul, 10ul, 2ul, TestName = "Execute_NonZeroBaseFee_PremiumIsTheRequestedPriorityFee")]
183208
[TestCase(7ul, 5ul, 8ul, 1ul, TestName = "Execute_NonZeroBaseFee_PremiumCappedByMaxFeeMinusBaseFee")]
184209
public void Execute_NonZeroBaseFee_PaysBeneficiaryThePremiumAndBurnsTheBaseFee(

src/Nethermind/Nethermind.Evm/BlobGasCalculator.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,9 @@ public static ulong CalculateBlobGas(Transaction[] transactions)
4646
ulong blobCount = 0UL;
4747
foreach (Transaction tx in transactions)
4848
{
49-
// EIP-8141: include blob-carrying frame transactions (type 6), not just type-3, in the
50-
// block's blob gas — gate on blob hashes instead of the type-level SupportsBlobs.
51-
if (tx.BlobVersionedHashes is { Length: > 0 })
52-
{
53-
blobCount += (ulong)tx.GetBlobCount();
54-
}
49+
// EIP-8141: blob-carrying frame transactions (type 6) count towards the block's blob gas too,
50+
// so the count comes from the blob hashes rather than the type-level SupportsBlobs.
51+
blobCount += (ulong)tx.GetBlobCount();
5552
}
5653

5754
return CalculateBlobGas(blobCount);

src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessorBase.FrameTx.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ private TransactionResult ExecuteFrameTx(Transaction tx, ITxTracer tracer, Execu
104104
if (tx.MaxFeePerBlobGas.GetValueOrDefault() < feePerBlobGas)
105105
{
106106
TraceLogInvalidTx(tx, "INSUFFICIENT_MAX_FEE_PER_BLOB_GAS");
107-
return TransactionResult.ErrorType.InsufficientMaxFeePerGasForSenderBalance.WithDetail(
107+
return TransactionResult.ErrorType.InsufficientSenderBalance.WithDetail(
108108
BlockErrorMessages.InsufficientMaxFeePerBlobGas(tx.SenderAddress, tx.MaxFeePerBlobGas, feePerBlobGas));
109109
}
110110
}
@@ -338,6 +338,8 @@ private TransactionResult ExecuteFrameTx(Transaction tx, ITxTracer tracer, Execu
338338

339339
// EIP-4844 fee-collector chains (e.g. Gnosis) collect the burned blob fee instead of losing
340340
// it, consistent with the regular path's PayFees; elsewhere the fee is simply burned.
341+
// The EIP-1559 base-fee share of a frame tx is not routed to the collector (only burned); no
342+
// chain enables both frame txs and the fee collector, so this stays deferred.
341343
if (!blobFee.IsZero && spec.IsEip4844FeeCollectorEnabled && spec.FeeCollector is not null)
342344
{
343345
WorldState.AddToBalanceAndCreateIfNotExists(spec.FeeCollector, blobFee, spec);

0 commit comments

Comments
 (0)