Skip to content
Open
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
2 changes: 2 additions & 0 deletions cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ var (
utils.AllowUnprotectedTxs,
utils.BatchRequestLimit,
utils.BatchResponseMaxSize,
utils.RPCTxSyncDefaultTimeoutFlag,
utils.RPCTxSyncMaxTimeoutFlag,
}

metricsFlags = []cli.Flag{
Expand Down
18 changes: 18 additions & 0 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,18 @@ var (
Value: ethconfig.Defaults.LogQueryLimit,
Category: flags.APICategory,
}
RPCTxSyncDefaultTimeoutFlag = &cli.DurationFlag{
Name: "rpc.txsync.defaulttimeout",
Usage: "Default timeout for eth_sendRawTransactionSync (e.g. 2s, 500ms)",
Value: ethconfig.Defaults.TxSyncDefaultTimeout,
Category: flags.APICategory,
}
RPCTxSyncMaxTimeoutFlag = &cli.DurationFlag{
Name: "rpc.txsync.maxtimeout",
Usage: "Maximum allowed timeout for eth_sendRawTransactionSync (e.g. 5m)",
Value: ethconfig.Defaults.TxSyncMaxTimeout,
Category: flags.APICategory,
}
// Authenticated RPC HTTP settings
AuthListenFlag = &cli.StringFlag{
Name: "authrpc.addr",
Expand Down Expand Up @@ -1717,6 +1729,12 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
if ctx.IsSet(RPCGlobalLogQueryLimit.Name) {
cfg.LogQueryLimit = ctx.Int(RPCGlobalLogQueryLimit.Name)
}
if ctx.IsSet(RPCTxSyncDefaultTimeoutFlag.Name) {
cfg.TxSyncDefaultTimeout = ctx.Duration(RPCTxSyncDefaultTimeoutFlag.Name)
}
if ctx.IsSet(RPCTxSyncMaxTimeoutFlag.Name) {
cfg.TxSyncMaxTimeout = ctx.Duration(RPCTxSyncMaxTimeoutFlag.Name)
}
if !ctx.Bool(SnapshotFlag.Name) || cfg.SnapshotCache == 0 {
// If snap-sync is requested, this flag is also required
if cfg.SyncMode == ethconfig.SnapSync {
Expand Down
8 changes: 8 additions & 0 deletions eth/api_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -486,3 +486,11 @@ func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, re
func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*types.Transaction, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) {
return b.eth.stateAtTransaction(ctx, block, txIndex, reexec)
}

func (b *EthAPIBackend) RPCTxSyncDefaultTimeout() time.Duration {
return b.eth.config.TxSyncDefaultTimeout
}

func (b *EthAPIBackend) RPCTxSyncMaxTimeout() time.Duration {
return b.eth.config.TxSyncMaxTimeout
}
48 changes: 27 additions & 21 deletions eth/ethconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,27 +49,29 @@ var FullNodeGPO = gasprice.Config{

// Defaults contains default settings for use on the Ethereum main net.
var Defaults = Config{
HistoryMode: history.KeepAll,
SyncMode: SnapSync,
NetworkId: 0, // enable auto configuration of networkID == chainID
TxLookupLimit: 2350000,
TransactionHistory: 2350000,
LogHistory: 2350000,
StateHistory: params.FullImmutabilityThreshold,
DatabaseCache: 512,
TrieCleanCache: 154,
TrieDirtyCache: 256,
TrieTimeout: 60 * time.Minute,
SnapshotCache: 102,
FilterLogCacheSize: 32,
LogQueryLimit: 1000,
Miner: miner.DefaultConfig,
TxPool: legacypool.DefaultConfig,
BlobPool: blobpool.DefaultConfig,
RPCGasCap: 50000000,
RPCEVMTimeout: 5 * time.Second,
GPO: FullNodeGPO,
RPCTxFeeCap: 1, // 1 ether
HistoryMode: history.KeepAll,
SyncMode: SnapSync,
NetworkId: 0, // enable auto configuration of networkID == chainID
TxLookupLimit: 2350000,
TransactionHistory: 2350000,
LogHistory: 2350000,
StateHistory: params.FullImmutabilityThreshold,
DatabaseCache: 512,
TrieCleanCache: 154,
TrieDirtyCache: 256,
TrieTimeout: 60 * time.Minute,
SnapshotCache: 102,
FilterLogCacheSize: 32,
LogQueryLimit: 1000,
Miner: miner.DefaultConfig,
TxPool: legacypool.DefaultConfig,
BlobPool: blobpool.DefaultConfig,
RPCGasCap: 50000000,
RPCEVMTimeout: 5 * time.Second,
GPO: FullNodeGPO,
RPCTxFeeCap: 1, // 1 ether
TxSyncDefaultTimeout: 20 * time.Second,
TxSyncMaxTimeout: 1 * time.Minute,
}

//go:generate go run github.com/fjl/gencodec -type Config -formats toml -out gen_config.go
Expand Down Expand Up @@ -183,6 +185,10 @@ type Config struct {

// OverrideVerkle (TODO: remove after the fork)
OverrideVerkle *uint64 `toml:",omitempty"`

// EIP-7966: eth_sendRawTransactionSync timeouts
TxSyncDefaultTimeout time.Duration `toml:",omitempty"`
TxSyncMaxTimeout time.Duration `toml:",omitempty"`
}

// CreateConsensusEngine creates a consensus engine for the given chain config.
Expand Down
33 changes: 33 additions & 0 deletions ethclient/ethclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"errors"
"fmt"
"math/big"
"time"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -696,6 +697,38 @@ func (ec *Client) SendTransaction(ctx context.Context, tx *types.Transaction) er
return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", hexutil.Encode(data))
}

// SendTransactionSync submits a signed tx and waits for a receipt (or until
// the optional timeout elapses on the server side). If timeout == 0, the server
// uses its default.
func (ec *Client) SendTransactionSync(
ctx context.Context,
tx *types.Transaction,
timeout ...time.Duration,
) (*types.Receipt, error) {
raw, err := tx.MarshalBinary()
if err != nil {
return nil, err
}
return ec.SendRawTransactionSync(ctx, raw, timeout...)
}

func (ec *Client) SendRawTransactionSync(
ctx context.Context,
rawTx []byte,
timeout ...time.Duration,
) (*types.Receipt, error) {
var ms *hexutil.Uint64
if len(timeout) > 0 && timeout[0] > 0 {
v := hexutil.Uint64(timeout[0] / time.Millisecond)
ms = &v
}
var receipt types.Receipt
if err := ec.c.CallContext(ctx, &receipt, "eth_sendRawTransactionSync", hexutil.Bytes(rawTx), ms); err != nil {
return nil, err
}
return &receipt, nil
}

// RevertErrorData returns the 'revert reason' data of a contract call.
//
// This can be used with CallContract and EstimateGas, and only when the server is Geth.
Expand Down
92 changes: 92 additions & 0 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import (
const estimateGasErrorRatio = 0.015

var errBlobTxNotSupported = errors.New("signing blob transactions not supported")
var errSubClosed = errors.New("chain subscription closed")

// EthereumAPI provides an API to access Ethereum related information.
type EthereumAPI struct {
Expand Down Expand Up @@ -1652,6 +1653,97 @@ func (api *TransactionAPI) SendRawTransaction(ctx context.Context, input hexutil
return SubmitTransaction(ctx, api.b, tx)
}

// SendRawTransactionSync will add the signed transaction to the transaction pool
// and wait until the transaction has been included in a block and return the receipt, or the timeout.
func (api *TransactionAPI) SendRawTransactionSync(ctx context.Context, input hexutil.Bytes, timeoutMs *hexutil.Uint64) (map[string]interface{}, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah it's annoying that the spec specifies the timeout in ms. In other places we use string like 10m or 20s. But oh well.

tx := new(types.Transaction)
if err := tx.UnmarshalBinary(input); err != nil {
return nil, err
}

ch := make(chan core.ChainEvent, 128)
sub := api.b.SubscribeChainEvent(ch)
subErrCh := sub.Err()
defer sub.Unsubscribe()

hash, err := SubmitTransaction(ctx, api.b, tx)
if err != nil {
return nil, err
}

maxTimeout := api.b.RPCTxSyncMaxTimeout()
defaultTimeout := api.b.RPCTxSyncDefaultTimeout()

timeout := defaultTimeout
if timeoutMs != nil && *timeoutMs > 0 {
req := time.Duration(*timeoutMs) * time.Millisecond
if req > maxTimeout {
timeout = maxTimeout
} else {
timeout = req
}
}

receiptCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

// Fast path.
if r, err := api.GetTransactionReceipt(receiptCtx, hash); err == nil && r != nil {
return r, nil
}

for {
select {
case <-receiptCtx.Done():
// If server-side wait window elapsed, return the structured timeout.
if errors.Is(receiptCtx.Err(), context.DeadlineExceeded) {
return nil, &txSyncTimeoutError{
msg: fmt.Sprintf("The transaction was added to the transaction pool but wasn't processed in %v.", timeout),
hash: hash,
}
}
// Otherwise, bubble the caller's context error (canceled or deadline).
if err := ctx.Err(); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

If deadline has passed it will still be returned here as an error. You can check the error against DeadlineExceeded to catch that case and return the proper error code.

Copy link
Author

@aodhgan aodhgan Oct 13, 2025

Choose a reason for hiding this comment

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

changed in 3ab8ec8

return nil, err
}
// Fallback: return the derived context's error.
return nil, receiptCtx.Err()

case err, ok := <-subErrCh:
if !ok {
return nil, errSubClosed
}
return nil, err

case ev, ok := <-ch:
if !ok {
return nil, errSubClosed
}
rs := ev.Receipts
txs := ev.Transactions
if len(rs) == 0 || len(rs) != len(txs) {
continue
}
for i := range rs {
if rs[i].TxHash == hash {
if rs[i].BlockNumber != nil && rs[i].BlockHash != (common.Hash{}) {
signer := types.LatestSigner(api.b.ChainConfig())
return MarshalReceipt(
rs[i],
rs[i].BlockHash,
rs[i].BlockNumber.Uint64(),
signer,
txs[i],
int(rs[i].TransactionIndex),
), nil
}
return api.GetTransactionReceipt(receiptCtx, hash)
}
}
}
}
}

// Sign calculates an ECDSA signature for:
// keccak256("\x19Ethereum Signed Message:\n" + len(message) + message).
//
Expand Down
Loading