Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions runner/payload/simulator/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,18 +242,32 @@ func (t *simulatorPayloadWorker) Stop(ctx context.Context) error {
return nil
}

// mineAndConfirmBatchSize is the maximum number of transactions to submit per
// mineAndConfirm call. Sending tens of thousands of transactions in a single
// batch would require waiting for the very last one to be mined, which can
// easily exceed the waitForReceipt timeout. Batching ensures each group of
// transactions is confirmed before the next group is submitted.
const mineAndConfirmBatchSize = 50

func (t *simulatorPayloadWorker) mineAndConfirm(ctx context.Context, txs []*types.Transaction) error {
t.mempool.AddTransactions(txs)
for len(txs) > 0 {
batch := txs
if len(batch) > mineAndConfirmBatchSize {
batch = txs[:mineAndConfirmBatchSize]
}
txs = txs[len(batch):]

receipt, err := t.waitForReceipt(ctx, txs[len(txs)-1].Hash())
if err != nil {
return errors.Wrap(err, "failed to wait for receipt")
}
t.mempool.AddTransactions(batch)

if receipt.Status != types.ReceiptStatusSuccessful {
return fmt.Errorf("receipt status not successful: %d", receipt.Status)
}
receipt, err := t.waitForReceipt(ctx, batch[len(batch)-1].Hash())
if err != nil {
return errors.Wrap(err, "failed to wait for receipt")
}

if receipt.Status != types.ReceiptStatusSuccessful {
return fmt.Errorf("receipt status not successful: %d", receipt.Status)
}
}
return nil
}

Expand Down
101 changes: 101 additions & 0 deletions runner/payload/simulator/worker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package simulator

import (
"context"
"fmt"
"math/big"
"testing"

"github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/require"
)

// fakeMineAndConfirm is a test-local reimplementation of mineAndConfirm that
// records how many transactions were submitted per batch, without touching a
// real chain. It mirrors the exact batching logic from the production function
// so changes to mineAndConfirmBatchSize are automatically tested.
func fakeMineAndConfirm(txs []*types.Transaction) ([]int, error) {
var batchSizes []int
for len(txs) > 0 {
batch := txs
if len(batch) > mineAndConfirmBatchSize {
batch = txs[:mineAndConfirmBatchSize]
}
txs = txs[len(batch):]
batchSizes = append(batchSizes, len(batch))
}
return batchSizes, nil
}

func makeTxs(n int) []*types.Transaction {
txs := make([]*types.Transaction, n)
for i := range txs {
txs[i] = types.NewTx(&types.LegacyTx{Nonce: uint64(i), Gas: 21000, GasPrice: big.NewInt(1)})
}
return txs
}

func TestMineAndConfirmBatching(t *testing.T) {
tests := []struct {
numTxs int
wantMaxBatch int
wantBatchCount int
}{
{numTxs: 0, wantMaxBatch: 0, wantBatchCount: 0},
{numTxs: 1, wantMaxBatch: 1, wantBatchCount: 1},
{numTxs: mineAndConfirmBatchSize, wantMaxBatch: mineAndConfirmBatchSize, wantBatchCount: 1},
{numTxs: mineAndConfirmBatchSize + 1, wantMaxBatch: mineAndConfirmBatchSize, wantBatchCount: 2},
{numTxs: mineAndConfirmBatchSize * 3, wantMaxBatch: mineAndConfirmBatchSize, wantBatchCount: 3},
// Simulate the real problematic case: ~640k init txs (scaled down for test speed).
// Before the fix this was sent as a single batch, timing out waitForReceipt.
{numTxs: 10000, wantMaxBatch: mineAndConfirmBatchSize, wantBatchCount: 10000 / mineAndConfirmBatchSize},
}

for _, tt := range tests {
t.Run(fmt.Sprintf("numTxs=%d", tt.numTxs), func(t *testing.T) {
txs := makeTxs(tt.numTxs)
batches, err := fakeMineAndConfirm(txs)
require.NoError(t, err)
require.Len(t, batches, tt.wantBatchCount)
for _, size := range batches {
require.LessOrEqual(t, size, tt.wantMaxBatch)
}
})
}
}

// TestMineAndConfirmNoBatchingWouldTimeout demonstrates the scale of the problem:
// for storage-reads-full-block at 150M gas, ~640k init transactions were sent in
// one batch but waitForReceipt only retries for 240s.
func TestMineAndConfirmNoBatchingWouldTimeout(t *testing.T) {
const (
gasLimit = 150_000_000
gasPerStorageCall = 220_000
numBlocks = 900
storageSlotsPerTx = 100
waitForReceiptMaxRetries = 240
)
numCallsPerBlock := (gasLimit - 1_000_000) / gasPerStorageCall
totalStorageSlotsNeeded := storageSlotsPerTx * numCallsPerBlock * numBlocks
initChunksNeeded := (totalStorageSlotsNeeded + 99) / 100

// Without batching: all init txs in one mineAndConfirm → wait for receipt of the last one.
// Each receipt poll is 1 second, and there are only 240 retries.
require.Greater(t, initChunksNeeded, waitForReceiptMaxRetries,
"init txs (%d) must exceed timeout window (%d retries) to demonstrate the bug",
initChunksNeeded, waitForReceiptMaxRetries)

// With batching: each batch of mineAndConfirmBatchSize is confirmed before the next.
// The last tx in each batch is confirmed within a few seconds.
require.LessOrEqual(t, mineAndConfirmBatchSize, waitForReceiptMaxRetries,
"batch size must fit within the receipt timeout window")

t.Logf("storage-reads-full-block at 150M gas: ~%d init txs needed, batch size %d",
initChunksNeeded, mineAndConfirmBatchSize)
}

// Verify the worker satisfies the interface (compilation check).
var _ interface {
Setup(ctx context.Context) error
SendTxs(ctx context.Context, pendingTxs int) (int, error)
} = (*simulatorPayloadWorker)(nil)
Loading