Skip to content
2 changes: 1 addition & 1 deletion cmd/mcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ func runDatadirMode(ctx context.Context, logger log.Logger, dataDir, privAPI, lo
}

// Create the typed JSON-RPC APIs — same path as rpcdaemon.
apiList := jsonrpc.APIList(db, backend, txPool, mining, ff, stateCache, blockReader, cfg, engine, logger, bridgeReader, heimdallReader)
apiList := jsonrpc.APIList(db, backend, txPool, mining, ff, stateCache, blockReader, cfg, engine, logger, bridgeReader, heimdallReader, nil)

// Extract the EthAPI, ErigonAPI, OtterscanAPI from the API list.
var (
Expand Down
2 changes: 1 addition & 1 deletion cmd/rpcdaemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func main() {
defer heimdallReader.Close()
}

apiList := jsonrpc.APIList(db, backend, txPool, mining, ff, stateCache, blockReader, cfg, engine, logger, bridgeReader, heimdallReader)
apiList := jsonrpc.APIList(db, backend, txPool, mining, ff, stateCache, blockReader, cfg, engine, logger, bridgeReader, heimdallReader, nil)
rpc.PreAllocateRPCMetricLabels(apiList)
if err := cli.StartRpcServer(ctx, cfg, apiList, logger); err != nil {
logger.Error(err.Error())
Expand Down
6 changes: 5 additions & 1 deletion execution/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@ func (b *Builder) Build(param *Parameters, interrupt *atomic.Bool) (result *type
return nil, err
}
createCfg := StageBuilderCreateBlockCfg(state, b.chainConfig, b.engine, param, b.blockReader)
execCfg := StageBuilderExecCfg(state, b.notifier, b.chainConfig, b.engine, b.vmConfig, b.tmpdir, interrupt, param.PayloadId, b.txnProvider, b.blockReader)
txnProvider := b.txnProvider
if param.CustomTxnProvider != nil {
txnProvider = param.CustomTxnProvider
}
execCfg := StageBuilderExecCfg(state, b.notifier, b.chainConfig, b.engine, b.vmConfig, b.tmpdir, interrupt, param.PayloadId, txnProvider, b.blockReader)
finishCfg := StageBuilderFinishCfg(b.chainConfig, b.engine, state, b.sealCancel, b.blockReader, b.latestBlockBuiltStore)

if err := createBlock(b.ctx, sd, compositeTx, executionAt, createCfg, b.logger); err != nil {
Expand Down
4 changes: 4 additions & 0 deletions execution/builder/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package builder
import (
"github.com/erigontech/erigon/common"
"github.com/erigontech/erigon/execution/types"
"github.com/erigontech/erigon/txnprovider"
)

// Parameters for PoS block building
Expand All @@ -32,4 +33,7 @@ type Parameters struct {
Withdrawals []*types.Withdrawal // added in Shapella (EIP-4895)
ParentBeaconBlockRoot *common.Hash // added in Dencun (EIP-4788)
SlotNumber *uint64 // added in Amsterdam (EIP-7843)
// CustomTxnProvider overrides the block's transaction source when non-nil.
// nil → use the injected TxnProvider (normal mempool path)
CustomTxnProvider txnprovider.TxnProvider
}
4 changes: 2 additions & 2 deletions execution/engineapi/engine_types/jsonrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ type ExecutionPayload struct {
BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"`
ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"`
SlotNumber *hexutil.Uint64 `json:"slotNumber"`
BlockAccessList hexutil.Bytes `json:"blockAccessList"`
BlockAccessList hexutil.Bytes `json:"blockAccessList,omitempty"`
}

// PayloadAttributes represent the attributes required to start assembling a payload
Expand Down Expand Up @@ -141,7 +141,7 @@ type ExecutionPayloadBody struct {
type ExecutionPayloadBodyV2 struct {
Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"`
Withdrawals []*types.Withdrawal `json:"withdrawals" gencodec:"required"`
BlockAccessList hexutil.Bytes `json:"blockAccessList"`
BlockAccessList hexutil.Bytes `json:"blockAccessList,omitempty"`
}

type PayloadStatus struct {
Expand Down
126 changes: 106 additions & 20 deletions execution/engineapi/testing_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,24 @@ package engineapi
import (
"context"
"errors"
"fmt"
"sync/atomic"
"time"

"github.com/erigontech/erigon/cl/clparams"
"github.com/erigontech/erigon/common"
"github.com/erigontech/erigon/common/hexutil"
"github.com/erigontech/erigon/common/log/v3"
"github.com/erigontech/erigon/db/kv"
execctx "github.com/erigontech/erigon/db/state/execctx"
"github.com/erigontech/erigon/execution/builder"
"github.com/erigontech/erigon/execution/engineapi/engine_types"
"github.com/erigontech/erigon/execution/execmodule"
"github.com/erigontech/erigon/execution/state"
"github.com/erigontech/erigon/execution/types"
"github.com/erigontech/erigon/execution/types/accounts"
"github.com/erigontech/erigon/rpc"
"github.com/erigontech/erigon/txnprovider"
)

// TestingAPI is the interface for the testing_ RPC namespace.
Expand All @@ -43,30 +52,91 @@ type TestingAPI interface {
//
// transactions: nil → draw from mempool (normal builder behaviour)
// [] → build an empty block (mempool bypassed, no txs)
// [...] → TODO: explicit tx list not yet supported; returns error
//
// NOTE: overriding extraData post-assembly means the BlockHash in the returned payload
// will NOT match a block header that includes that extraData. Callers should treat the
// result as a template when extraData is overridden.
// [...] → build a block containing exactly these transactions (strict nonce check)
BuildBlockV1(ctx context.Context, parentHash common.Hash, payloadAttributes *engine_types.PayloadAttributes, transactions *[]hexutil.Bytes, extraData *hexutil.Bytes) (*engine_types.GetPayloadResponse, error)
}

// testingImpl is the concrete implementation of TestingAPI.
type testingImpl struct {
server *EngineServer
logger log.Logger
db kv.TemporalRoDB
}

// NewTestingImpl returns a new TestingAPI implementation wrapping the given EngineServer.
func NewTestingImpl(server *EngineServer) TestingAPI {
return &testingImpl{server: server}
func NewTestingImpl(server *EngineServer, logger log.Logger, db kv.TemporalRoDB) TestingAPI {
return &testingImpl{server: server, logger: logger, db: db}
}

// decodeTxnProvider decodes raw transactions into a TxnProvider.
// Returns nil if transactions is nil (mempool path).
// Opens a single temporal DB transaction for all nonce lookups to avoid per-sender overhead.
func (t *testingImpl) decodeTxnProvider(ctx context.Context, transactions *[]hexutil.Bytes, blockNumber, timestamp uint64) (txnprovider.TxnProvider, error) {
if transactions == nil {
return nil, nil
}

var reader *state.ReaderV3
if t.db != nil {
dbTx, err := t.db.BeginTemporalRo(ctx)
if err != nil {
return nil, fmt.Errorf("testing_buildBlockV1: could not begin temporal transaction: %w", err)
}
defer dbTx.Rollback()
sd, err := execctx.NewSharedDomains(ctx, dbTx, t.logger)
if err != nil {
return nil, fmt.Errorf("testing_buildBlockV1: NewSharedDomains error: %w", err)
}
defer sd.Close()
reader = state.NewReaderV3(sd.AsGetter(dbTx))
}

decoded := make([]types.Transaction, 0, len(*transactions))
signer := types.MakeSigner(t.server.config, blockNumber, timestamp)
expectedNonce := make(map[accounts.Address]uint64, len(*transactions))
for i, rawTx := range *transactions {
tx, err := types.DecodeTransaction(rawTx)
if err != nil {
return nil, &rpc.InvalidParamsError{Message: fmt.Sprintf("transaction %d: decode error: %v", i, err)}
}
sender, err := signer.Sender(tx)
if err != nil {
return nil, &rpc.InvalidParamsError{Message: fmt.Sprintf("transaction %d: cannot recover sender: %v", i, err)}
}
tx.SetSender(sender)
if _, seen := expectedNonce[sender]; !seen {
var stateNonce uint64
if reader != nil {
acc, err := reader.ReadAccountData(accounts.InternAddress(sender.Value()))
if err != nil {
return nil, fmt.Errorf("testing_buildBlockV1: ReadAccountData error: %w", err)
}
if acc != nil {
stateNonce = acc.Nonce
}
}
expectedNonce[sender] = stateNonce
}
Comment on lines +107 to +119
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decodeTxnProvider calls getAccountNonce the first time it sees each sender, and getAccountNonce opens a new temporal DB transaction + SharedDomains every time. For large explicit tx lists with many senders this adds significant overhead. Consider opening the temporal tx/SharedDomains/state.Reader once per BuildBlockV1 call (or once per decodeTxnProvider call) and reading all needed nonces from that single reader, caching them in expectedNonce.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

want := expectedNonce[sender]
got := tx.GetNonce()
if got > want {
return nil, &rpc.InvalidParamsError{Message: fmt.Sprintf("nonce too high: address %v, tx: %d state: %d", sender.Value(), got, want)}
}
if got < want {
return nil, &rpc.InvalidParamsError{Message: fmt.Sprintf("nonce too low: address %v, tx: %d state: %d", sender.Value(), got, want)}
}
expectedNonce[sender]++
decoded = append(decoded, tx)
}
return &staticTxnProvider{txns: decoded}, nil
}

// NewTestingRPCEntry returns the rpc.API descriptor for the testing_ namespace.
func NewTestingRPCEntry(server *EngineServer) rpc.API {
func NewTestingRPCEntry(server *EngineServer, logger log.Logger, db kv.TemporalRoDB) rpc.API {
return rpc.API{
Namespace: "testing",
Public: false,
Service: TestingAPI(NewTestingImpl(server)),
Service: TestingAPI(NewTestingImpl(server, logger, db)),
Version: "1.0",
}
}
Expand All @@ -83,12 +153,6 @@ func (t *testingImpl) BuildBlockV1(
return nil, &rpc.InvalidParamsError{Message: "payloadAttributes must not be null"}
}

// Explicit transaction list is not yet supported (requires proto extension).
// TODO: implement forced_transactions via AssembleBlockRequest proto extension.
if transactions != nil && len(*transactions) > 0 {
return nil, &rpc.InvalidParamsError{Message: "explicit transaction list not yet supported in testing_buildBlockV1; use null for mempool or [] for empty block"}
}

// Validate parent block exists.
parentHeader := t.server.chainRW.GetHeaderByHash(ctx, parentHash)
if parentHeader == nil {
Expand Down Expand Up @@ -130,13 +194,19 @@ func (t *testingImpl) BuildBlockV1(
return nil, &rpc.InvalidParamsError{Message: "parentBeaconBlockRoot not supported before Cancun"}
}

customProvider, err := t.decodeTxnProvider(ctx, transactions, parentHeader.Number.Uint64()+1, timestamp)
if err != nil {
return nil, err
}

// Build the AssembleBlock parameters (mirrors forkchoiceUpdated logic).
assembleParams := &builder.Parameters{
ParentHash: parentHash,
Timestamp: timestamp,
PrevRandao: payloadAttributes.PrevRandao,
SuggestedFeeRecipient: payloadAttributes.SuggestedFeeRecipient,
SlotNumber: (*uint64)(payloadAttributes.SlotNumber),
CustomTxnProvider: customProvider,
}
if version >= clparams.CapellaVersion {
assembleParams.Withdrawals = payloadAttributes.Withdrawals
Expand All @@ -155,7 +225,6 @@ func (t *testingImpl) BuildBlockV1(
deadline = ctxDeadline
}

// Step 1: AssembleBlock (locked scope).
var payloadID uint64
execBusy, err := func() (bool, error) {
t.server.lock.Lock()
Expand All @@ -180,7 +249,6 @@ func (t *testingImpl) BuildBlockV1(
return nil, errors.New("execution service is busy, cannot build block")
}

// Step 2: GetAssembledBlock (separate locked scope).
var assembled execmodule.AssembledBlockResult
execBusy, err = func() (bool, error) {
t.server.lock.Lock()
Expand Down Expand Up @@ -211,12 +279,30 @@ func (t *testingImpl) BuildBlockV1(
return nil, err
}
response.ShouldOverrideBuilder = false

// Override extra data if provided. Note: the BlockHash in ExecutionPayload reflects the
// originally built block; overriding ExtraData here means BlockHash will NOT match.
// Return blockValue=0, matching Geth's BuildTestingPayload behaviour.
response.BlockValue = new(hexutil.Big)
if extraData != nil {
h := types.CopyHeader(assembled.Block.Block.Header())
h.Extra = *extraData
response.ExecutionPayload.ExtraData = *extraData
response.ExecutionPayload.BlockHash = h.Hash()
}

return response, nil
}

// staticTxnProvider is a TxnProvider that yields a fixed transaction list exactly once,
// then returns nil on every subsequent call. Used only by the testing_ namespace.
type staticTxnProvider struct {
txns []types.Transaction
done atomic.Bool
}

func (s *staticTxnProvider) ProvideTxns(_ context.Context, _ ...txnprovider.ProvideOption) ([]types.Transaction, error) {
if !s.done.CompareAndSwap(false, true) {
return nil, nil
}
txns := s.txns
s.txns = nil // release for GC after handing off
return txns, nil
}
Loading
Loading