diff --git a/mempool/txpool/legacypool/legacypool.go b/mempool/txpool/legacypool/legacypool.go index 7139a3b9d..4faccebdf 100644 --- a/mempool/txpool/legacypool/legacypool.go +++ b/mempool/txpool/legacypool/legacypool.go @@ -18,7 +18,6 @@ package legacypool import ( - "errors" "maps" "math/big" "slices" @@ -31,6 +30,7 @@ import ( "github.com/ethereum/go-ethereum/common/prque" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/txpool/legacypool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto/kzg4844" @@ -58,25 +58,6 @@ const ( txMaxSize = 4 * txSlotSize // 128KB ) -var ( - // ErrTxPoolOverflow is returned if the transaction pool is full and can't accept - // another remote transaction. - ErrTxPoolOverflow = errors.New("txpool is full") - - // ErrOutOfOrderTxFromDelegated is returned when the transaction with gapped - // nonce received from the accounts with delegation or pending delegation. - ErrOutOfOrderTxFromDelegated = errors.New("gapped-nonce tx from delegated accounts") - - // ErrAuthorityReserved is returned if a transaction has an authorization - // signed by an address which already has in-flight transactions known to the - // pool. - ErrAuthorityReserved = errors.New("authority already reserved") - - // ErrFutureReplacePending is returned if a future transaction replaces a pending - // one. Future transactions should only be able to replace other future transactions. - ErrFutureReplacePending = errors.New("future transaction tries to replace pending") -) - var ( evictionInterval = time.Minute // Time interval to check for evictable transactions statsReportInterval = 8 * time.Second // Time interval to report transaction pool stats @@ -623,7 +604,7 @@ func (pool *LegacyPool) checkDelegationLimit(tx *types.Transaction) error { if pending == nil { // Transaction with gapped nonce is not supported for delegated accounts if pool.pendingNonces.get(from) != tx.Nonce() { - return ErrOutOfOrderTxFromDelegated + return legacypool.ErrOutOfOrderTxFromDelegated } return nil } @@ -654,7 +635,7 @@ func (pool *LegacyPool) validateAuth(tx *types.Transaction) error { count += queue.Len() } if count > 1 { - return ErrAuthorityReserved + return legacypool.ErrAuthorityReserved } // Because there is no exclusive lock held between different subpools // when processing transactions, the SetCode transaction may be accepted @@ -665,7 +646,7 @@ func (pool *LegacyPool) validateAuth(tx *types.Transaction) error { // that attackers cannot easily stack a SetCode transaction when the sender // is reserved by other pools. if pool.reserver.Has(auth) { - return ErrAuthorityReserved + return legacypool.ErrAuthorityReserved } } } @@ -730,7 +711,7 @@ func (pool *LegacyPool) add(tx *types.Transaction) (replaced bool, err error) { // replacements to 25% of the slots if pool.changesSinceReorg > int(pool.config.GlobalSlots/4) { throttleTxMeter.Mark(1) - return false, ErrTxPoolOverflow + return false, legacypool.ErrTxPoolOverflow } // New transaction is better than our worse ones, make room for it. @@ -741,7 +722,7 @@ func (pool *LegacyPool) add(tx *types.Transaction) (replaced bool, err error) { if !success { log.Trace("Discarding overflown transaction", "hash", hash) overflowedTxMeter.Mark(1) - return false, ErrTxPoolOverflow + return false, legacypool.ErrTxPoolOverflow } // If the new transaction is a future transaction it should never churn pending transactions @@ -760,7 +741,7 @@ func (pool *LegacyPool) add(tx *types.Transaction) (replaced bool, err error) { pool.priced.Put(dropTx) } log.Trace("Discarding future transaction replacing pending tx", "hash", hash) - return false, ErrFutureReplacePending + return false, legacypool.ErrFutureReplacePending } } diff --git a/mempool/txpool/legacypool/legacypool_test.go b/mempool/txpool/legacypool/legacypool_test.go index 7e93fa5fb..41d298bae 100644 --- a/mempool/txpool/legacypool/legacypool_test.go +++ b/mempool/txpool/legacypool/legacypool_test.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/txpool/legacypool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" @@ -1696,8 +1697,8 @@ func TestUnderpricing(t *testing.T) { t.Fatalf("failed to add well priced transaction: %v", err) } // Ensure that replacing a pending transaction with a future transaction fails - if err := pool.addRemoteSync(pricedTransaction(5, 100000, big.NewInt(6), keys[1])); !errors.Is(err, ErrFutureReplacePending) { - t.Fatalf("adding future replace transaction error mismatch: have %v, want %v", err, ErrFutureReplacePending) + if err := pool.addRemoteSync(pricedTransaction(5, 100000, big.NewInt(6), keys[1])); !errors.Is(err, legacypool.ErrFutureReplacePending) { + t.Fatalf("adding future replace transaction error mismatch: have %v, want %v", err, legacypool.ErrFutureReplacePending) } pending, queued = pool.Stats() if pending != 4 { @@ -2297,8 +2298,8 @@ func TestSetCodeTransactions(t *testing.T) { statedb.SetCode(aa, []byte{byte(vm.ADDRESS), byte(vm.PUSH0), byte(vm.SSTORE)}) // Send gapped transaction, it should be rejected. - if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1), keyA)); !errors.Is(err, ErrOutOfOrderTxFromDelegated) { - t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrOutOfOrderTxFromDelegated, err) + if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1), keyA)); !errors.Is(err, legacypool.ErrOutOfOrderTxFromDelegated) { + t.Fatalf("%s: error mismatch: want %v, have %v", name, legacypool.ErrOutOfOrderTxFromDelegated, err) } // Send transactions. First is accepted, second is rejected. if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), keyA)); err != nil { @@ -2377,8 +2378,8 @@ func TestSetCodeTransactions(t *testing.T) { t.Fatalf("%s: failed to add with pending delegation: %v", name, err) } // Delegation rejected since two txs are already in-flight. - if err := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{0, keyB}})); !errors.Is(err, ErrAuthorityReserved) { - t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrAuthorityReserved, err) + if err := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{0, keyB}})); !errors.Is(err, legacypool.ErrAuthorityReserved) { + t.Fatalf("%s: error mismatch: want %v, have %v", name, legacypool.ErrAuthorityReserved, err) } }, }, diff --git a/mempool/txpool/locals/errors.go b/mempool/txpool/locals/errors.go index 2eca36b8e..4e7d16209 100644 --- a/mempool/txpool/locals/errors.go +++ b/mempool/txpool/locals/errors.go @@ -2,29 +2,36 @@ package locals import ( "errors" + "strings" "github.com/cosmos/evm/mempool/txpool" - "github.com/cosmos/evm/mempool/txpool/legacypool" + "github.com/ethereum/go-ethereum/core/txpool/legacypool" ) -// IsTemporaryReject determines whether the given error indicates a temporary -// reason to reject a transaction from being included in the txpool. The result -// may change if the txpool's state changes later. +var ( + // ErrNonceGap is returned if the tx nonce is higher than the account nonce. + // This is a duplicate of mempool.ErrNonceGap to avoid import cycle. + ErrNonceGap = errors.New("tx nonce is higher than account nonce") +) + +// IsTemporaryReject determines whether the given error indicates a temporary reason to reject a +// transaction from being included in the txpool. The result may change if the txpool's state changes later. +// We use strings.Contains instead of errors.Is because we are passing in rawLog errors. func IsTemporaryReject(err error) bool { + if err == nil { + return false + } + switch { - case errors.Is(err, legacypool.ErrOutOfOrderTxFromDelegated): - return true - case errors.Is(err, txpool.ErrInflightTxLimitReached): + case strings.Contains(err.Error(), legacypool.ErrOutOfOrderTxFromDelegated.Error()), + strings.Contains(err.Error(), txpool.ErrInflightTxLimitReached.Error()), + strings.Contains(err.Error(), legacypool.ErrAuthorityReserved.Error()), + strings.Contains(err.Error(), txpool.ErrUnderpriced.Error()), + strings.Contains(err.Error(), legacypool.ErrTxPoolOverflow.Error()), + strings.Contains(err.Error(), legacypool.ErrFutureReplacePending.Error()), + strings.Contains(err.Error(), ErrNonceGap.Error()): return true - case errors.Is(err, legacypool.ErrAuthorityReserved): - return true - case errors.Is(err, txpool.ErrUnderpriced): - return true - case errors.Is(err, legacypool.ErrTxPoolOverflow): - return true - case errors.Is(err, legacypool.ErrFutureReplacePending): - return true - default: - return false } + + return false } diff --git a/mempool/txpool/locals/errors_test.go b/mempool/txpool/locals/errors_test.go index a163131b7..902f703a1 100644 --- a/mempool/txpool/locals/errors_test.go +++ b/mempool/txpool/locals/errors_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/cosmos/evm/mempool/txpool" - "github.com/cosmos/evm/mempool/txpool/legacypool" + "github.com/ethereum/go-ethereum/core/txpool/legacypool" ) func TestIsTemporaryReject_PositiveCases(t *testing.T) { @@ -19,6 +19,7 @@ func TestIsTemporaryReject_PositiveCases(t *testing.T) { {name: "underpriced", err: txpool.ErrUnderpriced}, {name: "txpool overflow", err: legacypool.ErrTxPoolOverflow}, {name: "future replace pending", err: legacypool.ErrFutureReplacePending}, + {name: "tx nonce is higher than account nonce", err: ErrNonceGap}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { diff --git a/rpc/backend/call_tx.go b/rpc/backend/call_tx.go index 5c65bce3b..1c2d24275 100644 --- a/rpc/backend/call_tx.go +++ b/rpc/backend/call_tx.go @@ -8,6 +8,7 @@ import ( "math/big" "strings" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" @@ -27,77 +28,117 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// Resend accepts an existing transaction and a new gas price and limit. It will remove -// the given transaction from the pool and reinsert it with the new gas price and limit. -func (b *Backend) Resend(args evmtypes.TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) { - if args.Nonce == nil { - return common.Hash{}, fmt.Errorf("missing transaction nonce in transaction spec") +// SendTransaction sends transaction based on received args using Node's key to sign it +func (b *Backend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) { + // Look up the wallet containing the requested signer + if !b.Cfg.JSONRPC.AllowInsecureUnlock { + b.Logger.Debug("account unlock with HTTP access is forbidden") + return common.Hash{}, fmt.Errorf("account unlock with HTTP access is forbidden") } - args, err := b.SetTxDefaults(args) + _, err := b.ClientCtx.Keyring.KeyByAddress(sdk.AccAddress(args.GetFrom().Bytes())) if err != nil { - return common.Hash{}, err + b.Logger.Error("failed to find key in keyring", "address", args.GetFrom(), "error", err.Error()) + return common.Hash{}, fmt.Errorf("failed to find key in the node's keyring; %s; %s", keystore.ErrNoMatch, err.Error()) } - // The signer used should always be the 'latest' known one because we expect - // signers to be backwards-compatible with old transactions. - cfg := b.ChainConfig() - if cfg == nil { - cfg = evmtypes.DefaultChainConfig(b.EvmChainID.Uint64()).EthereumConfig(nil) + if args.ChainID != nil && (b.EvmChainID).Cmp((*big.Int)(args.ChainID)) != 0 { + return common.Hash{}, fmt.Errorf("chainId does not match node's (have=%v, want=%v)", args.ChainID, (*hexutil.Big)(b.EvmChainID)) } - signer := ethtypes.LatestSigner(cfg) + args, err = b.SetTxDefaults(args) + if err != nil { + return common.Hash{}, err + } - matchTx := args.ToTransaction(ethtypes.LegacyTxType) + bn, err := b.BlockNumber() + if err != nil { + b.Logger.Debug("failed to fetch latest block number", "error", err.Error()) + return common.Hash{}, err + } - // Before replacing the old transaction, ensure the _new_ transaction fee is reasonable. - price := matchTx.GasPrice() - if gasPrice != nil { - price = gasPrice.ToInt() + header, err := b.CurrentHeader() + if err != nil { + return common.Hash{}, err } - gas := matchTx.Gas() - if gasLimit != nil { - gas = uint64(*gasLimit) + + signer := ethtypes.MakeSigner(b.ChainConfig(), new(big.Int).SetUint64(uint64(bn)), header.Time) + + // LegacyTx derives EvmChainID from the signature. To make sure the msg.ValidateBasic makes + // the corresponding EvmChainID validation, we need to sign the transaction before calling it + + // Sign transaction + msg := evmtypes.NewTxFromArgs(&args) + if err := msg.Sign(signer, b.ClientCtx.Keyring); err != nil { + b.Logger.Debug("failed to sign tx", "error", err.Error()) + return common.Hash{}, err } - if err := rpctypes.CheckTxFee(price, gas, b.RPCTxFeeCap()); err != nil { + + if err := msg.ValidateBasic(); err != nil { + b.Logger.Debug("tx failed basic validation", "error", err.Error()) return common.Hash{}, err } - pending, err := b.PendingTransactions() + baseDenom := evmtypes.GetEVMCoinDenom() + + // Assemble transaction from fields + tx, err := msg.BuildTx(b.ClientCtx.TxConfig.NewTxBuilder(), baseDenom) if err != nil { + b.Logger.Error("build cosmos tx failed", "error", err.Error()) return common.Hash{}, err } - for _, tx := range pending { - // FIXME does Resend api possible at all? https://github.com/evmos/ethermint/issues/905 - p, err := evmtypes.UnwrapEthereumMsg(tx, common.Hash{}) - if err != nil { - // not valid ethereum tx - continue - } + // Encode transaction by default Tx encoder + txEncoder := b.ClientCtx.TxConfig.TxEncoder() + txBytes, err := txEncoder(tx) + if err != nil { + b.Logger.Error("failed to encode eth tx using default encoder", "error", err.Error()) + return common.Hash{}, err + } - pTx := p.AsTransaction() + ethTx := msg.AsTransaction() - wantSigHash := signer.Hash(matchTx) - pFrom, err := ethtypes.Sender(signer, pTx) - if err != nil { - continue - } + // check the local node config in case unprotected txs are disabled + if !b.UnprotectedAllowed() && !ethTx.Protected() { + // Ensure only eip155 signed transactions are submitted if EIP155Required is set. + return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") + } - if pFrom == *args.From && signer.Hash(pTx) == wantSigHash { - // Match. Re-sign and send the transaction. - if gasPrice != nil && (*big.Int)(gasPrice).Sign() != 0 { - args.GasPrice = gasPrice - } - if gasLimit != nil && *gasLimit != 0 { - args.Gas = gasLimit - } + txHash := ethTx.Hash() - return b.SendTransaction(args) // TODO: this calls SetTxDefaults again, refactor to avoid calling it twice + // Broadcast transaction in sync mode (default) + // NOTE: If error is encountered on the node, the broadcast will not return an error + syncCtx := b.ClientCtx.WithBroadcastMode(flags.BroadcastSync) + rsp, err := syncCtx.BroadcastTx(txBytes) + if rsp != nil && rsp.Code != 0 { + err = errorsmod.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) + } + // Check for temporary rejection in response raw log + if b.Mempool != nil && rsp != nil && rsp.Code != 0 { + if txlocals.IsTemporaryReject(errors.New(rsp.RawLog)) { + b.Logger.Debug("temporary rejection in response raw log, tracking locally", "hash", txHash.Hex(), "err", rsp.RawLog) + b.Mempool.TrackLocalTxs([]*ethtypes.Transaction{ethTx}) + return txHash, nil + } + err = errorsmod.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) + } + if err != nil { + // Check for temporary rejection in error + if b.Mempool != nil && txlocals.IsTemporaryReject(err) { + b.Logger.Debug("temporary rejection in error, tracking locally", "hash", txHash.Hex(), "err", err.Error()) + b.Mempool.TrackLocalTxs([]*ethtypes.Transaction{ethTx}) + return txHash, nil } + b.Logger.Error("failed to broadcast tx", "error", err.Error()) + return txHash, err } - return common.Hash{}, fmt.Errorf("transaction %#x not found", matchTx.Hash()) + // Return transaction hash + // On success, track as local too to persist across restarts until mined + if b.Mempool != nil { + b.Mempool.TrackLocalTxs([]*ethtypes.Transaction{ethTx}) + } + return txHash, nil } // SendRawTransaction send a raw Ethereum transaction. @@ -151,21 +192,19 @@ func (b *Backend) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { syncCtx := b.ClientCtx.WithBroadcastMode(flags.BroadcastSync) rsp, err := syncCtx.BroadcastTx(txBytes) - if rsp != nil && rsp.Code != 0 { - err = errorsmod.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) - } - if err != nil { - // Check if this is a nonce gap error that was successfully queued - if b.Mempool != nil && strings.Contains(err.Error(), mempool.ErrNonceGap.Error()) { - // Transaction was successfully queued due to nonce gap, return success to client - b.Logger.Debug("transaction queued due to nonce gap", "hash", txHash.Hex()) - // Track as local for priority and persistence + // Check for temporary rejection in response raw log + if b.Mempool != nil && rsp != nil && rsp.Code != 0 { + if txlocals.IsTemporaryReject(errors.New(rsp.RawLog)) { + b.Logger.Debug("temporary rejection in response raw log, tracking locally", "hash", txHash.Hex(), "err", rsp.RawLog) b.Mempool.TrackLocalTxs([]*ethtypes.Transaction{tx}) return txHash, nil } - // Temporary txpool rejections should be locally tracked for resubmission + err = errorsmod.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) + } + if err != nil { + // Check for temporary rejection in response raw log if b.Mempool != nil && txlocals.IsTemporaryReject(err) { - b.Logger.Debug("temporary rejection, tracking locally", "hash", txHash.Hex(), "err", err.Error()) + b.Logger.Debug("temporary rejection in error, tracking locally", "hash", txHash.Hex(), "err", err.Error()) b.Mempool.TrackLocalTxs([]*ethtypes.Transaction{tx}) return txHash, nil } @@ -185,7 +224,7 @@ func (b *Backend) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { } // SendRawTransaction does not return error when committed nonce <= tx.Nonce < pending nonce - // Track as local for persistence until mined + // Track as local for persistence until pending b.Mempool.TrackLocalTxs([]*ethtypes.Transaction{tx}) return txHash, nil } @@ -201,6 +240,79 @@ func (b *Backend) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { return txHash, nil } +// Resend accepts an existing transaction and a new gas price and limit. It will remove +// the given transaction from the pool and reinsert it with the new gas price and limit. +func (b *Backend) Resend(args evmtypes.TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) { + if args.Nonce == nil { + return common.Hash{}, fmt.Errorf("missing transaction nonce in transaction spec") + } + + args, err := b.SetTxDefaults(args) + if err != nil { + return common.Hash{}, err + } + + // The signer used should always be the 'latest' known one because we expect + // signers to be backwards-compatible with old transactions. + cfg := b.ChainConfig() + if cfg == nil { + cfg = evmtypes.DefaultChainConfig(b.EvmChainID.Uint64()).EthereumConfig(nil) + } + + signer := ethtypes.LatestSigner(cfg) + + matchTx := args.ToTransaction(ethtypes.LegacyTxType) + + // Before replacing the old transaction, ensure the _new_ transaction fee is reasonable. + price := matchTx.GasPrice() + if gasPrice != nil { + price = gasPrice.ToInt() + } + gas := matchTx.Gas() + if gasLimit != nil { + gas = uint64(*gasLimit) + } + if err := rpctypes.CheckTxFee(price, gas, b.RPCTxFeeCap()); err != nil { + return common.Hash{}, err + } + + pending, err := b.PendingTransactions() + if err != nil { + return common.Hash{}, err + } + + for _, tx := range pending { + // FIXME does Resend api possible at all? https://github.com/evmos/ethermint/issues/905 + p, err := evmtypes.UnwrapEthereumMsg(tx, common.Hash{}) + if err != nil { + // not valid ethereum tx + continue + } + + pTx := p.AsTransaction() + + wantSigHash := signer.Hash(matchTx) + pFrom, err := ethtypes.Sender(signer, pTx) + if err != nil { + continue + } + + if pFrom == *args.From && signer.Hash(pTx) == wantSigHash { + // Match. Re-sign and send the transaction. + if gasPrice != nil && (*big.Int)(gasPrice).Sign() != 0 { + args.GasPrice = gasPrice + } + if gasLimit != nil && *gasLimit != 0 { + args.Gas = gasLimit + } + + return b.SendTransaction(args) // TODO: this calls SetTxDefaults again, refactor to avoid calling it twice + } + } + + return common.Hash{}, fmt.Errorf("transaction %#x not found", matchTx.Hash()) +} + // SetTxDefaults populates tx message with default values in case they are not // provided on the args func (b *Backend) SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.TransactionArgs, error) { diff --git a/rpc/backend/sign_tx.go b/rpc/backend/sign_tx.go index a8bd67437..19d444507 100644 --- a/rpc/backend/sign_tx.go +++ b/rpc/backend/sign_tx.go @@ -1,141 +1,18 @@ package backend import ( - "errors" "fmt" - "math/big" - "strings" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core/apitypes" - "github.com/cosmos/evm/mempool" - txlocals "github.com/cosmos/evm/mempool/txpool/locals" - evmtypes "github.com/cosmos/evm/x/vm/types" - - errorsmod "cosmossdk.io/errors" - - "github.com/cosmos/cosmos-sdk/client/flags" sdk "github.com/cosmos/cosmos-sdk/types" signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" ) -// SendTransaction sends transaction based on received args using Node's key to sign it -func (b *Backend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) { - // Look up the wallet containing the requested signer - if !b.Cfg.JSONRPC.AllowInsecureUnlock { - b.Logger.Debug("account unlock with HTTP access is forbidden") - return common.Hash{}, fmt.Errorf("account unlock with HTTP access is forbidden") - } - - _, err := b.ClientCtx.Keyring.KeyByAddress(sdk.AccAddress(args.GetFrom().Bytes())) - if err != nil { - b.Logger.Error("failed to find key in keyring", "address", args.GetFrom(), "error", err.Error()) - return common.Hash{}, fmt.Errorf("failed to find key in the node's keyring; %s; %s", keystore.ErrNoMatch, err.Error()) - } - - if args.ChainID != nil && (b.EvmChainID).Cmp((*big.Int)(args.ChainID)) != 0 { - return common.Hash{}, fmt.Errorf("chainId does not match node's (have=%v, want=%v)", args.ChainID, (*hexutil.Big)(b.EvmChainID)) - } - - args, err = b.SetTxDefaults(args) - if err != nil { - return common.Hash{}, err - } - - bn, err := b.BlockNumber() - if err != nil { - b.Logger.Debug("failed to fetch latest block number", "error", err.Error()) - return common.Hash{}, err - } - - header, err := b.CurrentHeader() - if err != nil { - return common.Hash{}, err - } - - signer := ethtypes.MakeSigner(b.ChainConfig(), new(big.Int).SetUint64(uint64(bn)), header.Time) - - // LegacyTx derives EvmChainID from the signature. To make sure the msg.ValidateBasic makes - // the corresponding EvmChainID validation, we need to sign the transaction before calling it - - // Sign transaction - msg := evmtypes.NewTxFromArgs(&args) - if err := msg.Sign(signer, b.ClientCtx.Keyring); err != nil { - b.Logger.Debug("failed to sign tx", "error", err.Error()) - return common.Hash{}, err - } - - if err := msg.ValidateBasic(); err != nil { - b.Logger.Debug("tx failed basic validation", "error", err.Error()) - return common.Hash{}, err - } - - baseDenom := evmtypes.GetEVMCoinDenom() - - // Assemble transaction from fields - tx, err := msg.BuildTx(b.ClientCtx.TxConfig.NewTxBuilder(), baseDenom) - if err != nil { - b.Logger.Error("build cosmos tx failed", "error", err.Error()) - return common.Hash{}, err - } - - // Encode transaction by default Tx encoder - txEncoder := b.ClientCtx.TxConfig.TxEncoder() - txBytes, err := txEncoder(tx) - if err != nil { - b.Logger.Error("failed to encode eth tx using default encoder", "error", err.Error()) - return common.Hash{}, err - } - - ethTx := msg.AsTransaction() - - // check the local node config in case unprotected txs are disabled - if !b.UnprotectedAllowed() && !ethTx.Protected() { - // Ensure only eip155 signed transactions are submitted if EIP155Required is set. - return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") - } - - txHash := ethTx.Hash() - - // Broadcast transaction in sync mode (default) - // NOTE: If error is encountered on the node, the broadcast will not return an error - syncCtx := b.ClientCtx.WithBroadcastMode(flags.BroadcastSync) - rsp, err := syncCtx.BroadcastTx(txBytes) - if rsp != nil && rsp.Code != 0 { - err = errorsmod.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) - } - if err != nil { - // Check if this is a nonce gap error that was successfully queued - if b.Mempool != nil && strings.Contains(err.Error(), mempool.ErrNonceGap.Error()) { - // Transaction was successfully queued due to nonce gap, return success to client - b.Logger.Debug("transaction queued due to nonce gap", "hash", txHash.Hex()) - // Track as local for priority and persistence - b.Mempool.TrackLocalTxs([]*ethtypes.Transaction{ethTx}) - return txHash, nil - } - // Temporary txpool rejections should be locally tracked for resubmission - if b.Mempool != nil && txlocals.IsTemporaryReject(err) { - b.Logger.Debug("temporary rejection, tracking locally", "hash", txHash.Hex(), "err", err.Error()) - b.Mempool.TrackLocalTxs([]*ethtypes.Transaction{ethTx}) - return txHash, nil - } - b.Logger.Error("failed to broadcast tx", "error", err.Error()) - return txHash, err - } - - // Return transaction hash - // On success, track as local too to persist across restarts until mined - if b.Mempool != nil { - b.Mempool.TrackLocalTxs([]*ethtypes.Transaction{ethTx}) - } - return txHash, nil -} - // Sign signs the provided data using the private key of address via Geth's signature standard. func (b *Backend) Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { from := sdk.AccAddress(address.Bytes())