Skip to content

Commit f77dcc0

Browse files
committed
EIP-8141: blob-pool routing and production blob-budget metering for blob-carrying frame transactions
Complete the non-consensus blob layer for EIP-8141 frame transactions on top of the consensus-correct accounting/validation/settlement in PR #12701. - Blob-pool routing: introduce an instance-level Transaction.CarriesBlobs gate (BlobVersionedHashes present) and route on it at the pool-membership sites so a blob-carrying frame tx (type 6) lands in the blob pool like a type-3 tx, without touching the type-level SupportsBlobs() that networking/decode paths key on. Type-3 routing is unchanged. TxTypeTxFilter, snapshot invalidation, eviction cascade, metrics, and ContainsTx follow the same gate. - Production blob-budget metering: routing feeds frame-blob txs through the blob selection path in TxPoolTxSource, so they are metered against the block blob budget (MaxBlobGasPerBlock / MaxProductionBlobCount) and a produced block never exceeds it. The normal-pool picker guard is kept as defense in depth for any frame-blob tx that reaches production without a resolvable sidecar. - Networking: withhold blob-carrying frame txs from gossip until the EIP-7594 sidecar wire format exists, rather than leaking them in bare consensus form. Deferred (documented markers): the EIP-7594 sidecar / network-wrapper wire format for type 6 (decode/encode + mempool acceptance of the wrapper form). Production of frame-blob txs is gated on it, since the blobs bundle cannot be built without the sidecar. Tests: frame-blob tx routes to the blob pool while a plain frame tx and a type-3 tx route unchanged; a frame-blob tx with a sidecar is metered/selected within the blob budget and excluded when it would overflow it.
1 parent 22e6164 commit f77dcc0

9 files changed

Lines changed: 223 additions & 15 deletions

File tree

src/Nethermind/Nethermind.Blockchain.Test/TransactionsExecutorTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,8 +415,10 @@ public void BlockProductionTransactionsExecutor_tx_picker_uses_state_changes_fro
415415
[Test]
416416
public void CanAddTransaction_skips_blob_carrying_frame_transaction()
417417
{
418-
// EIP8141: block production does not yet meter blob-carrying frame txs against the block
419-
// blob budget, so the picker excludes them conservatively to avoid self-invalidating a block.
418+
// EIP8141: blob-carrying frame txs are routed to the blob pool and metered against the block
419+
// blob budget by the blob-selection path, so they do not reach this normal-pool picker in the
420+
// standard flow. The picker still excludes any that arrive here (defense in depth): without a
421+
// resolvable EIP-7594 sidecar they cannot be produced with a complete blobs bundle.
420422
IWorldState stateProvider = TestWorldStateFactory.CreateForTest();
421423
using IDisposable scope = stateProvider.BeginScope(IWorldState.PreGenesis);
422424
stateProvider.CreateAccount(TestItem.AddressA, 1.Ether);

src/Nethermind/Nethermind.Blockchain.Test/TxPoolSourceTests.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Nethermind.Evm;
2020
using Nethermind.Core.Crypto;
2121
using Nethermind.Core.Extensions;
22+
using Nethermind.Crypto;
2223
using Nethermind.Int256;
2324
using Nethermind.TxPool.Comparison;
2425

@@ -150,4 +151,79 @@ public void GetTransactions_should_order_blob_txs_before_regular_txs_when_blob_h
150151
// Assert: High priority blob tx should come BEFORE lower priority regular tx
151152
Assert.That(result, Is.EqualTo(new[] { highPriorityBlobTx, lowerPriorityRegularTx }).UsingTransactionComparer());
152153
}
154+
155+
// EIP-8141: a blob-carrying frame tx (type 6) routed to the blob pool is produced through the same
156+
// blob-budget selection path as a type-3 tx. It is metered against the block blob budget: selected
157+
// when within budget, excluded when it would overflow it, so a produced block never exceeds the budget.
158+
[TestCase(3, 6, true)]
159+
[TestCase(3, 2, false)]
160+
public void GetTransactions_meters_blob_carrying_frame_tx_against_blob_budget(int blobCount, int blobLimit, bool expectSelected)
161+
{
162+
TestSingleReleaseSpecProvider specProvider = new(Cancun.Instance);
163+
TransactionComparerProvider transactionComparerProvider = new(specProvider, Build.A.BlockTree().TestObject);
164+
165+
Transaction frameBlobTx = BuildFrameBlobTxWithSidecar(senderByte: 1, blobCount: blobCount);
166+
167+
ITxPool txPool = Substitute.For<ITxPool>();
168+
txPool.GetPendingTransactions().Returns([]);
169+
txPool.GetPendingLightBlobTransactionsBySender()
170+
.Returns(new Dictionary<AddressAsKey, Transaction[]> { { frameBlobTx.SenderAddress!, [frameBlobTx] } });
171+
txPool.SupportsBlobs.Returns(true);
172+
173+
ITxFilterPipeline txFilterPipeline = Substitute.For<ITxFilterPipeline>();
174+
txFilterPipeline.Execute(Arg.Any<Transaction>(), Arg.Any<BlockHeader>(), Arg.Any<IReleaseSpec>()).Returns(true);
175+
176+
TxPoolTxSource txSource = new(txPool, specProvider, transactionComparerProvider, LimboLogs.Instance,
177+
txFilterPipeline, new BlocksConfig { SecondsPerSlot = 12, BlockProductionBlobLimit = blobLimit });
178+
179+
BlockHeader parent = Build.A.BlockHeader.WithNumber(0).WithExcessBlobGas(0).TestObject;
180+
Transaction[] result = txSource.GetTransactions(parent, long.MaxValue).ToArray();
181+
182+
ulong selectedBlobs = result.Aggregate(0UL, (sum, tx) => sum + (ulong)tx.GetBlobCount());
183+
using (Assert.EnterMultipleScope())
184+
{
185+
Assert.That(result.Contains(frameBlobTx), Is.EqualTo(expectSelected));
186+
Assert.That(selectedBlobs, Is.EqualTo(expectSelected ? (ulong)blobCount : 0UL));
187+
Assert.That(selectedBlobs, Is.LessThanOrEqualTo((ulong)Cancun.Instance.MaxProductionBlobCount(blobLimit)));
188+
}
189+
}
190+
191+
private static Transaction BuildFrameBlobTxWithSidecar(byte senderByte, int blobCount)
192+
{
193+
byte[][] versionedHashes = new byte[blobCount][];
194+
byte[][] blobs = new byte[blobCount][];
195+
byte[][] commitments = new byte[blobCount][];
196+
byte[][] proofs = new byte[blobCount][];
197+
for (int i = 0; i < blobCount; i++)
198+
{
199+
byte[] hash = new byte[Eip4844Constants.BytesPerBlobVersionedHash];
200+
hash[0] = KzgPolynomialCommitments.KzgBlobHashVersionV1;
201+
hash[1] = (byte)i;
202+
versionedHashes[i] = hash;
203+
blobs[i] = [];
204+
commitments[i] = [];
205+
proofs[i] = [];
206+
}
207+
208+
Transaction tx = new()
209+
{
210+
Type = TxType.FrameTx,
211+
ChainId = TestBlockchainIds.ChainId,
212+
SenderAddress = new Address(new byte[19].Concat(new[] { senderByte }).ToArray()),
213+
Nonce = 0,
214+
GasLimit = 1_000_000,
215+
GasPrice = 1,
216+
DecodedMaxFeePerGas = 100.GWei,
217+
MaxFeePerBlobGas = 1000,
218+
Frames =
219+
[
220+
new TxFrame(TxFrame.ModeVerify, TxFrame.ApproveExecutionAndPayment, target: null, gasLimit: 100_000, UInt256.Zero, default),
221+
],
222+
FrameSignatures = [],
223+
BlobVersionedHashes = versionedHashes,
224+
NetworkWrapper = new ShardBlobNetworkWrapper(blobs, commitments, proofs, ProofVersion.V0),
225+
};
226+
tx.Hash = tx.CalculateHash();
227+
return tx;
228+
}
153229
}

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,12 @@ public virtual AddingTxEventArgs CanAddTransaction(Block block, Transaction curr
6868
return args.Set(TxAction.Skip, "Transaction already in block");
6969
}
7070

71-
// EIP8141: blob-carrying frame transactions sit in the normal pool and are not yet
72-
// metered against the block blob budget during production (deferred: blob-pool routing
73-
// and blob-budget selection). Now that they count towards header.BlobGasUsed, an
74-
// unguarded producer could exceed MaxBlobGasPerBlock and self-invalidate the block.
75-
// Exclude them conservatively until block production tracks their blob gas.
71+
// EIP8141: blob-carrying frame transactions are routed to the blob pool and metered against
72+
// the block blob budget by the blob-selection path in TxPoolTxSource (like type-3), so they
73+
// never reach this normal-pool picker in the standard flow. This guard stays as defense in
74+
// depth: a frame tx arriving here still carries no resolvable EIP-7594 sidecar, so producing
75+
// it would count towards header.BlobGasUsed yet leave the block's blobs bundle incomplete.
76+
// Exclude it until the sidecar wire format lands and its blob data can be published.
7677
if (currentTx.Type == TxType.FrameTx && currentTx.BlobVersionedHashes is { Length: > 0 })
7778
{
7879
return args.Set(TxAction.Skip, "Blob-carrying frame transaction not yet supported in block production");

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ public TxValidator(ulong chainId)
8787
// Frame transactions have no envelope ECDSA signature (explicit sender, protocol-validated
8888
// signature list) — signature/intrinsic-gas validators do not apply; per-frame gas and
8989
// signature validation happen during processing.
90+
// EIP8141: a blob-carrying frame tx currently has no EIP-7594 sidecar network wrapper, so no
91+
// sidecar/proof validator is registered here (a wrapper on a frame tx is rejected as malformed
92+
// by NonBlobFieldsTxValidator's absence + the network-form gates). When the type-6 sidecar wire
93+
// format lands, add a MempoolBlobTxValidator-equivalent (and proof-version validator) to this chain.
9094
RegisterValidator(TxType.FrameTx, new CompositeTxValidator([
9195
new ReleaseSpecTxValidator(static spec => spec.IsEip8141Enabled),
9296
NonceCapTxValidator.Instance,

src/Nethermind/Nethermind.Core/Transaction.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ public class Transaction
5555
public bool SupportsBlobs => Type.SupportsBlobs();
5656
public bool SupportsAuthorizationList => Type.SupportsAuthorizationList();
5757
public bool SupportsFrames => Type.SupportsFrames();
58+
59+
/// <summary>
60+
/// EIP-8141: whether this transaction is blob-carrying for pool routing and blob-gas accounting —
61+
/// a type-3 (EIP-4844) blob tx or a blob-carrying frame tx (type 6 with versioned hashes).
62+
/// </summary>
63+
/// <remarks>
64+
/// Instance-level gate distinct from the type-level <see cref="SupportsBlobs"/>, which networking
65+
/// and decode paths key on. Mirrors the accounting predicate used in block validation and the blob
66+
/// gas calculator so a blob-carrying frame tx counts towards the block blob budget like a type-3 tx.
67+
/// </remarks>
68+
public bool CarriesBlobs => BlobVersionedHashes is { Length: > 0 };
5869
public ulong GasLimit { get; set; }
5970
private ulong _spentGas;
6071
private ulong _blockGasUsed;

src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.Blobs.cs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,5 +1410,108 @@ public void should_batch_return_blobs_from_cache_and_db()
14101410
Assert.That(proofs[1].Length, Is.EqualTo(Ckzg.CellsPerExtBlob));
14111411
}
14121412
}
1413+
1414+
// EIP-8141: a blob-carrying frame tx (type 6 with versioned hashes) is routed to the blob pool,
1415+
// mirroring type-3 routing, so it is subject to blob-pool rules. A plain frame tx and a type-3
1416+
// blob tx must route unchanged.
1417+
[Test]
1418+
public void blob_carrying_frame_tx_is_routed_to_blob_pool()
1419+
{
1420+
TxPoolConfig txPoolConfig = new() { BlobsSupport = BlobsSupportMode.InMemory };
1421+
_txPool = CreatePool(txPoolConfig, GetBogotaSpecProvider());
1422+
EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue);
1423+
1424+
Transaction frameBlobTx = BuildBlobFrameTx(nonce: 0, blobCount: 1);
1425+
1426+
AcceptTxResult result = _txPool.SubmitTx(frameBlobTx, TxHandlingOptions.None);
1427+
1428+
using (Assert.EnterMultipleScope())
1429+
{
1430+
Assert.That(result, Is.EqualTo(AcceptTxResult.Accepted));
1431+
Assert.That(_txPool.GetPendingBlobTransactionsCount(), Is.EqualTo(1));
1432+
Assert.That(_txPool.GetPendingTransactionsCount(), Is.EqualTo(0));
1433+
}
1434+
}
1435+
1436+
[Test]
1437+
public void non_blob_frame_tx_is_routed_to_normal_pool()
1438+
{
1439+
TxPoolConfig txPoolConfig = new() { BlobsSupport = BlobsSupportMode.InMemory };
1440+
_txPool = CreatePool(txPoolConfig, GetBogotaSpecProvider());
1441+
EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue);
1442+
1443+
Transaction frameTx = BuildBlobFrameTx(nonce: 0, blobCount: 0);
1444+
1445+
AcceptTxResult result = _txPool.SubmitTx(frameTx, TxHandlingOptions.None);
1446+
1447+
using (Assert.EnterMultipleScope())
1448+
{
1449+
Assert.That(result, Is.EqualTo(AcceptTxResult.Accepted));
1450+
Assert.That(_txPool.GetPendingTransactionsCount(), Is.EqualTo(1));
1451+
Assert.That(_txPool.GetPendingBlobTransactionsCount(), Is.EqualTo(0));
1452+
}
1453+
}
1454+
1455+
[Test]
1456+
public void type3_blob_tx_routing_is_unchanged_alongside_frame_txs()
1457+
{
1458+
TxPoolConfig txPoolConfig = new() { BlobsSupport = BlobsSupportMode.InMemory };
1459+
_txPool = CreatePool(txPoolConfig, GetBogotaSpecProvider());
1460+
EnsureSenderBalance(TestItem.AddressB, UInt256.MaxValue);
1461+
1462+
Transaction type3Tx = Build.A.Transaction
1463+
.WithShardBlobTxTypeAndFields(1, spec: new ReleaseSpec() { IsEip7594Enabled = true })
1464+
.WithMaxFeePerGas(1.GWei)
1465+
.WithMaxPriorityFeePerGas(1.GWei)
1466+
.WithNonce(0)
1467+
.SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyB).TestObject;
1468+
1469+
AcceptTxResult result = _txPool.SubmitTx(type3Tx, TxHandlingOptions.None);
1470+
1471+
using (Assert.EnterMultipleScope())
1472+
{
1473+
Assert.That(result, Is.EqualTo(AcceptTxResult.Accepted));
1474+
Assert.That(_txPool.GetPendingBlobTransactionsCount(), Is.EqualTo(1));
1475+
Assert.That(_txPool.GetPendingTransactionsCount(), Is.EqualTo(0));
1476+
}
1477+
}
1478+
1479+
private static ISpecProvider GetBogotaSpecProvider() => new TestSpecProvider(Bogota.Instance);
1480+
1481+
private Transaction BuildBlobFrameTx(ulong nonce, int blobCount)
1482+
{
1483+
byte[][] versionedHashes = null;
1484+
if (blobCount > 0)
1485+
{
1486+
versionedHashes = new byte[blobCount][];
1487+
for (int i = 0; i < blobCount; i++)
1488+
{
1489+
byte[] hash = new byte[Eip4844Constants.BytesPerBlobVersionedHash];
1490+
hash[0] = KzgPolynomialCommitments.KzgBlobHashVersionV1;
1491+
hash[1] = (byte)i;
1492+
versionedHashes[i] = hash;
1493+
}
1494+
}
1495+
1496+
Transaction tx = new()
1497+
{
1498+
Type = TxType.FrameTx,
1499+
ChainId = _specProvider.ChainId,
1500+
SenderAddress = TestItem.AddressA,
1501+
Nonce = nonce,
1502+
GasLimit = 1_000_000,
1503+
GasPrice = 1,
1504+
DecodedMaxFeePerGas = 1.GWei,
1505+
MaxFeePerBlobGas = blobCount > 0 ? 1.GWei : UInt256.Zero,
1506+
Frames =
1507+
[
1508+
new TxFrame(TxFrame.ModeVerify, TxFrame.ApproveExecutionAndPayment, target: null, gasLimit: 100_000, UInt256.Zero, default),
1509+
],
1510+
FrameSignatures = [],
1511+
BlobVersionedHashes = versionedHashes,
1512+
};
1513+
tx.Hash = tx.CalculateHash();
1514+
return tx;
1515+
}
14131516
}
14141517
}

src/Nethermind/Nethermind.TxPool/Filters/TxTypeTxFilter.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ public class TxTypeTxFilter(TxDistinctSortedPool txs, TxDistinctSortedPool blobT
1313
{
1414
public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandlingOptions txHandlingOptions)
1515
{
16-
TxDistinctSortedPool otherTxTypePool = tx.SupportsBlobs ? txs : blobTxs;
16+
// EIP-8141: route on the instance-level blob gate so a blob-carrying frame tx is treated as a
17+
// blob-pool member (conflicts with the sender's normal-pool txs), mirroring type-3 routing.
18+
TxDistinctSortedPool otherTxTypePool = tx.CarriesBlobs ? txs : blobTxs;
1719
if (otherTxTypePool.ContainsBucket(tx.SenderAddress!)) // as unknownSenderFilter will run before this one
1820
{
1921
Metrics.PendingTransactionsConflictingTxType++;

src/Nethermind/Nethermind.TxPool/SpecDrivenTxGossipPolicy.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,10 @@ public class SpecDrivenTxGossipPolicy(IChainHeadInfoProvider chainHeadInfoProvid
1010
private IChainHeadInfoProvider ChainHeadInfoProvider { get; } = chainHeadInfoProvider;
1111

1212
public bool ShouldGossipTransaction(Transaction tx) =>
13-
!tx.SupportsBlobs || tx.GetProofVersion() == ChainHeadInfoProvider.CurrentProofVersion;
13+
// EIP8141: a blob-carrying frame tx (type 6) has no EIP-7594 sidecar network wrapper yet, so it
14+
// cannot participate in the announce-by-hash / serve-with-sidecar blob gossip protocol. Withhold
15+
// it from gossip until that wire format lands, rather than leaking it in bare consensus form.
16+
tx.CarriesBlobs && !tx.SupportsBlobs
17+
? false
18+
: !tx.SupportsBlobs || tx.GetProofVersion() == ChainHeadInfoProvider.CurrentProofVersion;
1419
}

src/Nethermind/Nethermind.TxPool/TxPool.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ public AcceptTxResult SubmitTx(Transaction tx, TxHandlingOptions handlingOptions
647647
// null-assignment (which would be missing the just-added tx).
648648
if (accepted)
649649
{
650-
if (tx.SupportsBlobs)
650+
if (tx.CarriesBlobs)
651651
_blobTransactionSnapshot = null;
652652
else
653653
_transactionSnapshot = null;
@@ -704,7 +704,7 @@ private AcceptTxResult AddCore(Transaction tx, ref TxFilteringState state, bool
704704
{
705705
bool eip1559Enabled = _specProvider.GetCurrentHeadSpec().IsEip1559Enabled;
706706
UInt256 effectiveGasPrice = tx.CalculateEffectiveGasPrice(eip1559Enabled, _headInfo.CurrentBaseFee);
707-
TxDistinctSortedPool relevantPool = (tx.SupportsBlobs ? _blobTransactions : _transactions);
707+
TxDistinctSortedPool relevantPool = (tx.CarriesBlobs ? _blobTransactions : _transactions);
708708

709709
relevantPool.TryGetBucketsWorstValue(tx.SenderAddress!, out Transaction? worstTx);
710710
tx.GasBottleneck = (worstTx is null || effectiveGasPrice <= worstTx.GasBottleneck)
@@ -724,7 +724,7 @@ private AcceptTxResult AddCore(Transaction tx, ref TxFilteringState state, bool
724724
if (tx.Hash == removed?.Hash)
725725
{
726726
// it means it was added and immediately evicted - pool was full of better txs
727-
if (!isPersistentBroadcast || tx.SupportsBlobs || !_broadcaster.Broadcast(tx, true))
727+
if (!isPersistentBroadcast || tx.CarriesBlobs || !_broadcaster.Broadcast(tx, true))
728728
{
729729
// we are adding only to persistent broadcast - not good enough for standard pool,
730730
// but can be good enough for TxBroadcaster pool - for local txs only
@@ -741,7 +741,7 @@ private AcceptTxResult AddCore(Transaction tx, ref TxFilteringState state, bool
741741
Interlocked.Increment(ref Metrics.PendingTransactionsAdded);
742742
Interlocked.Increment(ref _pendingTransactionsAdded);
743743
if (tx.Supports1559) { Metrics.Pending1559TransactionsAdded++; }
744-
if (tx.SupportsBlobs) { Metrics.PendingBlobTransactionsAdded++; }
744+
if (tx.CarriesBlobs) { Metrics.PendingBlobTransactionsAdded++; }
745745

746746
if (removed is not null)
747747
{
@@ -879,7 +879,7 @@ void MarkForEviction(Transaction tx, bool allowLaterPoolReentrance)
879879
if (allowLaterPoolReentrance) _hashCache.DeleteFromLongTerm(tx.Hash!);
880880
updateTx(transactions, tx, null, lastElement);
881881
// evict all following txs to prevent nonce gaps between blob tx
882-
evictNextTxs |= tx.SupportsBlobs;
882+
evictNextTxs |= tx.CarriesBlobs;
883883
}
884884
}
885885

@@ -966,7 +966,11 @@ public bool RemoveTransaction(Hash256? hash)
966966

967967
public bool ContainsTx(Hash256 hash, TxType txType) => txType == TxType.Blob
968968
? _blobTransactions.ContainsKey(hash)
969-
: _transactions.ContainsKey(hash) || _broadcaster.ContainsTx(hash);
969+
// EIP-8141: a blob-carrying frame tx lives in the blob pool; a plain frame tx in the normal
970+
// pool. Without the instance we cannot tell them apart by type alone, so check both for type 6.
971+
: _transactions.ContainsKey(hash)
972+
|| (txType == TxType.FrameTx && _blobTransactions.ContainsKey(hash))
973+
|| _broadcaster.ContainsTx(hash);
970974

971975
public bool TryGetPendingTransaction(Hash256 hash, [NotNullWhen(true)] out Transaction? transaction) =>
972976
_transactions.TryGetValue(hash, out transaction)

0 commit comments

Comments
 (0)