mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-04-22 01:24:41 +00:00
internal/ethapi: add eth_SendRawTransactionSync (#32830)
New RPC method eth_sendRawTransactionSync(rawTx, timeoutMs?) that submits a signed tx and blocks until a receipt is available or a timeout elapses. Two CLI flags to tune server-side limits: --rpc.txsync.defaulttimeout (default wait window) --rpc.txsync.maxtimeout (upper bound; requests are clamped) closes https://github.com/ethereum/go-ethereum/issues/32094 --------- Co-authored-by: aodhgan <gawnieg@gmail.com> Co-authored-by: Sina Mahmoodi <itz.s1na@gmail.com>
This commit is contained in:
parent
c37bd67019
commit
ff54ca02de
9 changed files with 359 additions and 26 deletions
|
|
@ -188,6 +188,8 @@ var (
|
||||||
utils.AllowUnprotectedTxs,
|
utils.AllowUnprotectedTxs,
|
||||||
utils.BatchRequestLimit,
|
utils.BatchRequestLimit,
|
||||||
utils.BatchResponseMaxSize,
|
utils.BatchResponseMaxSize,
|
||||||
|
utils.RPCTxSyncDefaultTimeoutFlag,
|
||||||
|
utils.RPCTxSyncMaxTimeoutFlag,
|
||||||
}
|
}
|
||||||
|
|
||||||
metricsFlags = []cli.Flag{
|
metricsFlags = []cli.Flag{
|
||||||
|
|
|
||||||
|
|
@ -615,6 +615,18 @@ var (
|
||||||
Value: ethconfig.Defaults.LogQueryLimit,
|
Value: ethconfig.Defaults.LogQueryLimit,
|
||||||
Category: flags.APICategory,
|
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
|
// Authenticated RPC HTTP settings
|
||||||
AuthListenFlag = &cli.StringFlag{
|
AuthListenFlag = &cli.StringFlag{
|
||||||
Name: "authrpc.addr",
|
Name: "authrpc.addr",
|
||||||
|
|
@ -1717,6 +1729,12 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
|
||||||
if ctx.IsSet(RPCGlobalLogQueryLimit.Name) {
|
if ctx.IsSet(RPCGlobalLogQueryLimit.Name) {
|
||||||
cfg.LogQueryLimit = ctx.Int(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 !ctx.Bool(SnapshotFlag.Name) || cfg.SnapshotCache == 0 {
|
||||||
// If snap-sync is requested, this flag is also required
|
// If snap-sync is requested, this flag is also required
|
||||||
if cfg.SyncMode == ethconfig.SnapSync {
|
if cfg.SyncMode == ethconfig.SnapSync {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
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)
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,27 +49,29 @@ var FullNodeGPO = gasprice.Config{
|
||||||
|
|
||||||
// Defaults contains default settings for use on the Ethereum main net.
|
// Defaults contains default settings for use on the Ethereum main net.
|
||||||
var Defaults = Config{
|
var Defaults = Config{
|
||||||
HistoryMode: history.KeepAll,
|
HistoryMode: history.KeepAll,
|
||||||
SyncMode: SnapSync,
|
SyncMode: SnapSync,
|
||||||
NetworkId: 0, // enable auto configuration of networkID == chainID
|
NetworkId: 0, // enable auto configuration of networkID == chainID
|
||||||
TxLookupLimit: 2350000,
|
TxLookupLimit: 2350000,
|
||||||
TransactionHistory: 2350000,
|
TransactionHistory: 2350000,
|
||||||
LogHistory: 2350000,
|
LogHistory: 2350000,
|
||||||
StateHistory: params.FullImmutabilityThreshold,
|
StateHistory: params.FullImmutabilityThreshold,
|
||||||
DatabaseCache: 512,
|
DatabaseCache: 512,
|
||||||
TrieCleanCache: 154,
|
TrieCleanCache: 154,
|
||||||
TrieDirtyCache: 256,
|
TrieDirtyCache: 256,
|
||||||
TrieTimeout: 60 * time.Minute,
|
TrieTimeout: 60 * time.Minute,
|
||||||
SnapshotCache: 102,
|
SnapshotCache: 102,
|
||||||
FilterLogCacheSize: 32,
|
FilterLogCacheSize: 32,
|
||||||
LogQueryLimit: 1000,
|
LogQueryLimit: 1000,
|
||||||
Miner: miner.DefaultConfig,
|
Miner: miner.DefaultConfig,
|
||||||
TxPool: legacypool.DefaultConfig,
|
TxPool: legacypool.DefaultConfig,
|
||||||
BlobPool: blobpool.DefaultConfig,
|
BlobPool: blobpool.DefaultConfig,
|
||||||
RPCGasCap: 50000000,
|
RPCGasCap: 50000000,
|
||||||
RPCEVMTimeout: 5 * time.Second,
|
RPCEVMTimeout: 5 * time.Second,
|
||||||
GPO: FullNodeGPO,
|
GPO: FullNodeGPO,
|
||||||
RPCTxFeeCap: 1, // 1 ether
|
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
|
//go:generate go run github.com/fjl/gencodec -type Config -formats toml -out gen_config.go
|
||||||
|
|
@ -183,6 +185,10 @@ type Config struct {
|
||||||
|
|
||||||
// OverrideVerkle (TODO: remove after the fork)
|
// OverrideVerkle (TODO: remove after the fork)
|
||||||
OverrideVerkle *uint64 `toml:",omitempty"`
|
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.
|
// CreateConsensusEngine creates a consensus engine for the given chain config.
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum"
|
"github.com/ethereum/go-ethereum"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
|
@ -705,6 +706,39 @@ func (ec *Client) SendTransaction(ctx context.Context, tx *types.Transaction) er
|
||||||
return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", hexutil.Encode(data))
|
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 timeout != nil {
|
||||||
|
if d := hexutil.Uint64(timeout.Milliseconds()); d > 0 {
|
||||||
|
ms = &d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.
|
// 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.
|
// This can be used with CallContract and EstimateGas, and only when the server is Geth.
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ import (
|
||||||
const estimateGasErrorRatio = 0.015
|
const estimateGasErrorRatio = 0.015
|
||||||
|
|
||||||
var errBlobTxNotSupported = errors.New("signing blob transactions not supported")
|
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.
|
// EthereumAPI provides an API to access Ethereum related information.
|
||||||
type EthereumAPI struct {
|
type EthereumAPI struct {
|
||||||
|
|
@ -1666,6 +1667,92 @@ func (api *TransactionAPI) SendRawTransaction(ctx context.Context, input hexutil
|
||||||
return SubmitTransaction(ctx, api.b, tx)
|
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) {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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:
|
// Sign calculates an ECDSA signature for:
|
||||||
// keccak256("\x19Ethereum Signed Message:\n" + len(message) + message).
|
// keccak256("\x19Ethereum Signed Message:\n" + len(message) + message).
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -440,6 +440,19 @@ type testBackend struct {
|
||||||
|
|
||||||
pending *types.Block
|
pending *types.Block
|
||||||
pendingReceipts types.Receipts
|
pendingReceipts types.Receipts
|
||||||
|
|
||||||
|
chainFeed *event.Feed
|
||||||
|
autoMine bool
|
||||||
|
|
||||||
|
sentTx *types.Transaction
|
||||||
|
sentTxHash common.Hash
|
||||||
|
|
||||||
|
syncDefaultTimeout time.Duration
|
||||||
|
syncMaxTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func fakeBlockHash(txh common.Hash) common.Hash {
|
||||||
|
return crypto.Keccak256Hash([]byte("testblock"), txh.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestBackend(t *testing.T, n int, gspec *core.Genesis, engine consensus.Engine, generator func(i int, b *core.BlockGen)) *testBackend {
|
func newTestBackend(t *testing.T, n int, gspec *core.Genesis, engine consensus.Engine, generator func(i int, b *core.BlockGen)) *testBackend {
|
||||||
|
|
@ -466,6 +479,7 @@ func newTestBackend(t *testing.T, n int, gspec *core.Genesis, engine consensus.E
|
||||||
acc: acc,
|
acc: acc,
|
||||||
pending: blocks[n],
|
pending: blocks[n],
|
||||||
pendingReceipts: receipts[n],
|
pendingReceipts: receipts[n],
|
||||||
|
chainFeed: new(event.Feed),
|
||||||
}
|
}
|
||||||
return backend
|
return backend
|
||||||
}
|
}
|
||||||
|
|
@ -587,19 +601,64 @@ func (b testBackend) GetEVM(ctx context.Context, state *state.StateDB, header *t
|
||||||
return vm.NewEVM(context, state, b.chain.Config(), *vmConfig)
|
return vm.NewEVM(context, state, b.chain.Config(), *vmConfig)
|
||||||
}
|
}
|
||||||
func (b testBackend) SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription {
|
func (b testBackend) SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription {
|
||||||
panic("implement me")
|
return b.chainFeed.Subscribe(ch)
|
||||||
}
|
}
|
||||||
func (b testBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription {
|
func (b testBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription {
|
||||||
panic("implement me")
|
panic("implement me")
|
||||||
}
|
}
|
||||||
func (b testBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error {
|
func (b *testBackend) SendTx(ctx context.Context, tx *types.Transaction) error {
|
||||||
panic("implement me")
|
b.sentTx = tx
|
||||||
|
b.sentTxHash = tx.Hash()
|
||||||
|
|
||||||
|
if b.autoMine {
|
||||||
|
// Synthesize a "mined" receipt at head+1
|
||||||
|
num := b.chain.CurrentHeader().Number.Uint64() + 1
|
||||||
|
receipt := &types.Receipt{
|
||||||
|
TxHash: tx.Hash(),
|
||||||
|
Status: types.ReceiptStatusSuccessful,
|
||||||
|
BlockHash: fakeBlockHash(tx.Hash()),
|
||||||
|
BlockNumber: new(big.Int).SetUint64(num),
|
||||||
|
TransactionIndex: 0,
|
||||||
|
CumulativeGasUsed: 21000,
|
||||||
|
GasUsed: 21000,
|
||||||
|
}
|
||||||
|
// Broadcast a ChainEvent that includes the receipts and txs
|
||||||
|
b.chainFeed.Send(core.ChainEvent{
|
||||||
|
Header: &types.Header{
|
||||||
|
Number: new(big.Int).SetUint64(num),
|
||||||
|
},
|
||||||
|
Receipts: types.Receipts{receipt},
|
||||||
|
Transactions: types.Transactions{tx},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
func (b testBackend) GetCanonicalTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) {
|
func (b *testBackend) GetCanonicalTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) {
|
||||||
|
// Treat the auto-mined tx as canonically placed at head+1.
|
||||||
|
if b.autoMine && txHash == b.sentTxHash {
|
||||||
|
num := b.chain.CurrentHeader().Number.Uint64() + 1
|
||||||
|
return true, b.sentTx, fakeBlockHash(txHash), num, 0
|
||||||
|
}
|
||||||
tx, blockHash, blockNumber, index := rawdb.ReadCanonicalTransaction(b.db, txHash)
|
tx, blockHash, blockNumber, index := rawdb.ReadCanonicalTransaction(b.db, txHash)
|
||||||
return tx != nil, tx, blockHash, blockNumber, index
|
return tx != nil, tx, blockHash, blockNumber, index
|
||||||
}
|
}
|
||||||
func (b testBackend) GetCanonicalReceipt(tx *types.Transaction, blockHash common.Hash, blockNumber, blockIndex uint64) (*types.Receipt, error) {
|
func (b *testBackend) GetCanonicalReceipt(tx *types.Transaction, blockHash common.Hash, blockNumber, blockIndex uint64) (*types.Receipt, error) {
|
||||||
|
if b.autoMine && tx != nil && tx.Hash() == b.sentTxHash &&
|
||||||
|
blockHash == fakeBlockHash(tx.Hash()) &&
|
||||||
|
blockIndex == 0 &&
|
||||||
|
blockNumber == b.chain.CurrentHeader().Number.Uint64()+1 {
|
||||||
|
return &types.Receipt{
|
||||||
|
Type: tx.Type(),
|
||||||
|
Status: types.ReceiptStatusSuccessful,
|
||||||
|
CumulativeGasUsed: 21000,
|
||||||
|
GasUsed: 21000,
|
||||||
|
EffectiveGasPrice: big.NewInt(1),
|
||||||
|
BlockHash: blockHash,
|
||||||
|
BlockNumber: new(big.Int).SetUint64(blockNumber),
|
||||||
|
TransactionIndex: 0,
|
||||||
|
TxHash: tx.Hash(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
return b.chain.GetCanonicalReceipt(tx, blockHash, blockNumber, blockIndex)
|
return b.chain.GetCanonicalReceipt(tx, blockHash, blockNumber, blockIndex)
|
||||||
}
|
}
|
||||||
func (b testBackend) TxIndexDone() bool {
|
func (b testBackend) TxIndexDone() bool {
|
||||||
|
|
@ -3889,3 +3948,109 @@ func (b configTimeBackend) HeaderByNumber(_ context.Context, n rpc.BlockNumber)
|
||||||
func (b configTimeBackend) CurrentHeader() *types.Header {
|
func (b configTimeBackend) CurrentHeader() *types.Header {
|
||||||
return &types.Header{Time: b.time}
|
return &types.Header{Time: b.time}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *testBackend) RPCTxSyncDefaultTimeout() time.Duration {
|
||||||
|
if b.syncDefaultTimeout != 0 {
|
||||||
|
return b.syncDefaultTimeout
|
||||||
|
}
|
||||||
|
return 2 * time.Second
|
||||||
|
}
|
||||||
|
func (b *testBackend) RPCTxSyncMaxTimeout() time.Duration {
|
||||||
|
if b.syncMaxTimeout != 0 {
|
||||||
|
return b.syncMaxTimeout
|
||||||
|
}
|
||||||
|
return 5 * time.Minute
|
||||||
|
}
|
||||||
|
func (b *backendMock) RPCTxSyncDefaultTimeout() time.Duration { return 2 * time.Second }
|
||||||
|
func (b *backendMock) RPCTxSyncMaxTimeout() time.Duration { return 5 * time.Minute }
|
||||||
|
|
||||||
|
func makeSignedRaw(t *testing.T, api *TransactionAPI, from, to common.Address, value *big.Int) (hexutil.Bytes, *types.Transaction) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
fillRes, err := api.FillTransaction(context.Background(), TransactionArgs{
|
||||||
|
From: &from,
|
||||||
|
To: &to,
|
||||||
|
Value: (*hexutil.Big)(value),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FillTransaction failed: %v", err)
|
||||||
|
}
|
||||||
|
signRes, err := api.SignTransaction(context.Background(), argsFromTransaction(fillRes.Tx, from))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SignTransaction failed: %v", err)
|
||||||
|
}
|
||||||
|
return signRes.Raw, signRes.Tx
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeSelfSignedRaw is a convenience for a 0-ETH self-transfer.
|
||||||
|
func makeSelfSignedRaw(t *testing.T, api *TransactionAPI, addr common.Address) (hexutil.Bytes, *types.Transaction) {
|
||||||
|
return makeSignedRaw(t, api, addr, addr, big.NewInt(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendRawTransactionSync_Success(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
genesis := &core.Genesis{
|
||||||
|
Config: params.TestChainConfig,
|
||||||
|
Alloc: types.GenesisAlloc{},
|
||||||
|
}
|
||||||
|
b := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
|
||||||
|
b.autoMine = true // immediately “mines” the tx in-memory
|
||||||
|
|
||||||
|
api := NewTransactionAPI(b, new(AddrLocker))
|
||||||
|
|
||||||
|
raw, _ := makeSelfSignedRaw(t, api, b.acc.Address)
|
||||||
|
|
||||||
|
receipt, err := api.SendRawTransactionSync(context.Background(), raw, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if receipt == nil {
|
||||||
|
t.Fatalf("expected non-nil receipt")
|
||||||
|
}
|
||||||
|
if _, ok := receipt["blockNumber"]; !ok {
|
||||||
|
t.Fatalf("expected blockNumber in receipt, got %#v", receipt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendRawTransactionSync_Timeout(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
genesis := &core.Genesis{
|
||||||
|
Config: params.TestChainConfig,
|
||||||
|
Alloc: types.GenesisAlloc{},
|
||||||
|
}
|
||||||
|
b := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
|
||||||
|
b.autoMine = false // don't mine, should time out
|
||||||
|
|
||||||
|
api := NewTransactionAPI(b, new(AddrLocker))
|
||||||
|
|
||||||
|
raw, _ := makeSelfSignedRaw(t, api, b.acc.Address)
|
||||||
|
|
||||||
|
timeout := hexutil.Uint64(200) // 200ms
|
||||||
|
receipt, err := api.SendRawTransactionSync(context.Background(), raw, &timeout)
|
||||||
|
|
||||||
|
if receipt != nil {
|
||||||
|
t.Fatalf("expected nil receipt, got %#v", receipt)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected timeout error, got nil")
|
||||||
|
}
|
||||||
|
// assert error shape & data (hash)
|
||||||
|
var de interface {
|
||||||
|
ErrorCode() int
|
||||||
|
ErrorData() interface{}
|
||||||
|
}
|
||||||
|
if !errors.As(err, &de) {
|
||||||
|
t.Fatalf("expected data error with code/data, got %T %v", err, err)
|
||||||
|
}
|
||||||
|
if de.ErrorCode() != errCodeTxSyncTimeout {
|
||||||
|
t.Fatalf("expected code %d, got %d", errCodeTxSyncTimeout, de.ErrorCode())
|
||||||
|
}
|
||||||
|
tx := new(types.Transaction)
|
||||||
|
if e := tx.UnmarshalBinary(raw); e != nil {
|
||||||
|
t.Fatal(e)
|
||||||
|
}
|
||||||
|
if got, want := de.ErrorData(), tx.Hash().Hex(); got != want {
|
||||||
|
t.Fatalf("expected ErrorData=%s, got %v", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,8 @@ type Backend interface {
|
||||||
RPCEVMTimeout() time.Duration // global timeout for eth_call over rpc: DoS protection
|
RPCEVMTimeout() time.Duration // global timeout for eth_call over rpc: DoS protection
|
||||||
RPCTxFeeCap() float64 // global tx fee cap for all transaction related APIs
|
RPCTxFeeCap() float64 // global tx fee cap for all transaction related APIs
|
||||||
UnprotectedAllowed() bool // allows only for EIP155 transactions.
|
UnprotectedAllowed() bool // allows only for EIP155 transactions.
|
||||||
|
RPCTxSyncDefaultTimeout() time.Duration
|
||||||
|
RPCTxSyncMaxTimeout() time.Duration
|
||||||
|
|
||||||
// Blockchain API
|
// Blockchain API
|
||||||
SetHead(number uint64)
|
SetHead(number uint64)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
"github.com/ethereum/go-ethereum/core"
|
"github.com/ethereum/go-ethereum/core"
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
"github.com/ethereum/go-ethereum/core/vm"
|
||||||
|
|
@ -33,6 +34,11 @@ type revertError struct {
|
||||||
reason string // revert reason hex encoded
|
reason string // revert reason hex encoded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type txSyncTimeoutError struct {
|
||||||
|
msg string
|
||||||
|
hash common.Hash
|
||||||
|
}
|
||||||
|
|
||||||
// ErrorCode returns the JSON error code for a revert.
|
// ErrorCode returns the JSON error code for a revert.
|
||||||
// See: https://ethereum.org/en/developers/docs/apis/json-rpc/#error-codes
|
// See: https://ethereum.org/en/developers/docs/apis/json-rpc/#error-codes
|
||||||
func (e *revertError) ErrorCode() int {
|
func (e *revertError) ErrorCode() int {
|
||||||
|
|
@ -108,6 +114,7 @@ const (
|
||||||
errCodeInvalidParams = -32602
|
errCodeInvalidParams = -32602
|
||||||
errCodeReverted = -32000
|
errCodeReverted = -32000
|
||||||
errCodeVMError = -32015
|
errCodeVMError = -32015
|
||||||
|
errCodeTxSyncTimeout = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
func txValidationError(err error) *invalidTxError {
|
func txValidationError(err error) *invalidTxError {
|
||||||
|
|
@ -168,3 +175,7 @@ type blockGasLimitReachedError struct{ message string }
|
||||||
|
|
||||||
func (e *blockGasLimitReachedError) Error() string { return e.message }
|
func (e *blockGasLimitReachedError) Error() string { return e.message }
|
||||||
func (e *blockGasLimitReachedError) ErrorCode() int { return errCodeBlockGasLimitReached }
|
func (e *blockGasLimitReachedError) ErrorCode() int { return errCodeBlockGasLimitReached }
|
||||||
|
|
||||||
|
func (e *txSyncTimeoutError) Error() string { return e.msg }
|
||||||
|
func (e *txSyncTimeoutError) ErrorCode() int { return errCodeTxSyncTimeout }
|
||||||
|
func (e *txSyncTimeoutError) ErrorData() interface{} { return e.hash.Hex() }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue