diff --git a/cmd/bee/cmd/cmd.go b/cmd/bee/cmd/cmd.go index 98a6323be31..6c64e46cc03 100644 --- a/cmd/bee/cmd/cmd.go +++ b/cmd/bee/cmd/cmd.go @@ -82,6 +82,7 @@ const ( optionReserveCapacityDoubling = "reserve-capacity-doubling" optionSkipPostageSnapshot = "skip-postage-snapshot" optionNameMinimumGasTipCap = "minimum-gas-tip-cap" + optionNameGasLimitFallback = "gas-limit-fallback" optionNameP2PWSSEnable = "p2p-wss-enable" optionP2PWSSAddr = "p2p-wss-addr" optionNATWSSAddr = "nat-wss-addr" @@ -297,6 +298,7 @@ func (c *command) setAllFlags(cmd *cobra.Command) { cmd.Flags().Int(optionReserveCapacityDoubling, 0, "reserve capacity doubling") cmd.Flags().Bool(optionSkipPostageSnapshot, false, "skip postage snapshot") cmd.Flags().Uint64(optionNameMinimumGasTipCap, 0, "minimum gas tip cap in wei for transactions, 0 means use suggested gas tip cap") + cmd.Flags().Uint64(optionNameGasLimitFallback, 500_000, "gas limit fallback when estimation fails for contract transactions") cmd.Flags().Bool(optionNameP2PWSSEnable, false, "Enable Secure WebSocket P2P connections") cmd.Flags().String(optionP2PWSSAddr, ":1635", "p2p wss address") cmd.Flags().String(optionNATWSSAddr, "", "WSS NAT exposed address") diff --git a/cmd/bee/cmd/deploy.go b/cmd/bee/cmd/deploy.go index c0e20cb63e7..f8dca1665f9 100644 --- a/cmd/bee/cmd/deploy.go +++ b/cmd/bee/cmd/deploy.go @@ -60,6 +60,7 @@ func (c *command) initDeployCmd() error { blocktime, true, c.config.GetUint64(optionNameMinimumGasTipCap), + c.config.GetUint64(optionNameGasLimitFallback), ) if err != nil { return err diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index 2268e8549c1..4816ef4d410 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -303,6 +303,7 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo FullNodeMode: fullNode, Logger: logger, MinimumGasTipCap: c.config.GetUint64(optionNameMinimumGasTipCap), + GasLimitFallback: c.config.GetUint64(optionNameGasLimitFallback), MinimumStorageRadius: c.config.GetUint(optionMinimumStorageRadius), MutexProfile: c.config.GetBool(optionNamePProfMutex), NATAddr: c.config.GetString(optionNameNATAddr), diff --git a/packaging/bee.yaml b/packaging/bee.yaml index f5354f8e4d4..cfc57346a61 100644 --- a/packaging/bee.yaml +++ b/packaging/bee.yaml @@ -108,6 +108,8 @@ password-file: "/var/lib/bee/password" # tracing-port: "" ## service name identifier for tracing # tracing-service-name: bee +## gas limit fallback when estimation fails for contract transactions (default 500000) +# gas-limit-fallback: 500000 ## skips the gas estimate step for contract transactions # transaction-debug-mode: false ## log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace diff --git a/packaging/docker/docker-compose.yml b/packaging/docker/docker-compose.yml index 56b6581d574..427b2268aac 100644 --- a/packaging/docker/docker-compose.yml +++ b/packaging/docker/docker-compose.yml @@ -39,6 +39,7 @@ services: - BEE_TRACING_ENABLE - BEE_TRACING_ENDPOINT - BEE_TRACING_SERVICE_NAME + - BEE_GAS_LIMIT_FALLBACK - BEE_TRANSACTION - BEE_VERBOSITY - BEE_WELCOME_MESSAGE diff --git a/packaging/docker/env b/packaging/docker/env index f719c9d2601..98a300cf669 100644 --- a/packaging/docker/env +++ b/packaging/docker/env @@ -119,6 +119,8 @@ # BEE_TRACING_PORT= ## service name identifier for tracing # BEE_TRACING_SERVICE_NAME= +## gas limit fallback when estimation fails for contract transactions (default 500000) +# BEE_GAS_LIMIT_FALLBACK=500000 ## skips the gas estimate step for contract transactions # BEE_TRANSACTION_DEBUG_MODE=false ## bootstrap node using postage snapshot from the network diff --git a/packaging/homebrew-amd64/bee.yaml b/packaging/homebrew-amd64/bee.yaml index 811ffe62d60..aacc658fa17 100644 --- a/packaging/homebrew-amd64/bee.yaml +++ b/packaging/homebrew-amd64/bee.yaml @@ -108,6 +108,8 @@ password-file: "/usr/local/var/lib/swarm-bee/password" # tracing-port: "" ## service name identifier for tracing # tracing-service-name: bee +## gas limit fallback when estimation fails for contract transactions (default 500000) +# gas-limit-fallback: 500000 ## skips the gas estimate step for contract transactions # transaction-debug-mode: false ## log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace diff --git a/packaging/homebrew-arm64/bee.yaml b/packaging/homebrew-arm64/bee.yaml index 382405cb149..f460c73f85c 100644 --- a/packaging/homebrew-arm64/bee.yaml +++ b/packaging/homebrew-arm64/bee.yaml @@ -108,6 +108,8 @@ password-file: "/opt/homebrew/var/lib/swarm-bee/password" # tracing-port: "" ## service name identifier for tracing # tracing-service-name: bee +## gas limit fallback when estimation fails for contract transactions (default 500000) +# gas-limit-fallback: 500000 ## skips the gas estimate step for contract transactions # transaction-debug-mode: false ## log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace diff --git a/packaging/scoop/bee.yaml b/packaging/scoop/bee.yaml index 58b48f70b52..247e4dc7c3c 100644 --- a/packaging/scoop/bee.yaml +++ b/packaging/scoop/bee.yaml @@ -108,6 +108,8 @@ password-file: "./password" # tracing-port: "" ## service name identifier for tracing # tracing-service-name: bee +## gas limit fallback when estimation fails for contract transactions (default 500000) +# gas-limit-fallback: 500000 ## skips the gas estimate step for contract transactions # transaction-debug-mode: false ## log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace diff --git a/pkg/node/chain.go b/pkg/node/chain.go index 78fc0a5448d..4c6ee45f6b4 100644 --- a/pkg/node/chain.go +++ b/pkg/node/chain.go @@ -51,6 +51,7 @@ func InitChain( pollingInterval time.Duration, chainEnabled bool, minimumGasTipCap uint64, + fallbackGasLimit uint64, ) (transaction.Backend, common.Address, int64, transaction.Monitor, transaction.Service, error) { backend := backendnoop.New(chainID) @@ -91,7 +92,7 @@ func InitChain( transactionMonitor := transaction.NewMonitor(logger, backend, overlayEthAddress, pollingInterval, cancellationDepth) - transactionService, err := transaction.NewService(logger, overlayEthAddress, backend, signer, stateStore, backendChainID, transactionMonitor) + transactionService, err := transaction.NewService(logger, overlayEthAddress, backend, signer, stateStore, backendChainID, transactionMonitor, fallbackGasLimit) if err != nil { return nil, common.Address{}, 0, nil, nil, fmt.Errorf("transaction service: %w", err) } diff --git a/pkg/node/node.go b/pkg/node/node.go index ee89dc2fa1a..0100406f537 100644 --- a/pkg/node/node.go +++ b/pkg/node/node.go @@ -154,6 +154,7 @@ type Options struct { AutoTLSDomain string AutoTLSRegistrationEndpoint string FullNodeMode bool + GasLimitFallback uint64 Logger log.Logger MinimumGasTipCap uint64 MinimumStorageRadius uint @@ -412,6 +413,7 @@ func NewBee( o.BlockTime, chainEnabled, o.MinimumGasTipCap, + o.GasLimitFallback, ) if err != nil { return nil, fmt.Errorf("init chain: %w", err) @@ -698,6 +700,13 @@ func NewBee( return nil, fmt.Errorf("lookup erc20 postage address: %w", err) } + // Compute gas limit for contract transactions: when TrxDebugMode is enabled, + // gas estimation is skipped and DefaultGasLimit is used for all contract calls. + var contractGasLimit uint64 + if o.TrxDebugMode { + contractGasLimit = transaction.DefaultGasLimit + } + postageStampContractService = postagecontract.New( overlayEthAddress, postageStampContractAddress, @@ -707,7 +716,7 @@ func NewBee( post, batchStore, chainEnabled, - o.TrxDebugMode, + contractGasLimit, ) eventListener = listener.New(b.syncingStopped, logger, chainBackend, postageStampContractAddress, postageStampContractABI, o.BlockTime, postageSyncingStallingTimeout, postageSyncingBackoffTimeout) @@ -1088,7 +1097,7 @@ func NewBee( stakingContractAddress = common.HexToAddress(o.StakingContractAddress) } - stakingContract := staking.New(overlayEthAddress, stakingContractAddress, abiutil.MustParseABI(chainCfg.StakingABI), bzzTokenAddress, transactionService, common.BytesToHash(nonce), o.TrxDebugMode, uint8(o.ReserveCapacityDoubling)) + stakingContract := staking.New(overlayEthAddress, stakingContractAddress, abiutil.MustParseABI(chainCfg.StakingABI), bzzTokenAddress, transactionService, common.BytesToHash(nonce), contractGasLimit, uint8(o.ReserveCapacityDoubling)) if chainEnabled { @@ -1179,7 +1188,7 @@ func NewBee( redistributionContractAddress = common.HexToAddress(o.RedistributionContractAddress) } - redistributionContract := redistribution.New(swarmAddress, overlayEthAddress, logger, transactionService, redistributionContractAddress, abiutil.MustParseABI(chainCfg.RedistributionABI), o.TrxDebugMode) + redistributionContract := redistribution.New(swarmAddress, overlayEthAddress, logger, transactionService, redistributionContractAddress, abiutil.MustParseABI(chainCfg.RedistributionABI), contractGasLimit) isFullySynced := func() bool { reserveThreshold := reserveCapacity * 5 / 10 diff --git a/pkg/postage/postagecontract/contract.go b/pkg/postage/postagecontract/contract.go index e8570b64e9b..4d0d38b3505 100644 --- a/pkg/postage/postagecontract/contract.go +++ b/pkg/postage/postagecontract/contract.go @@ -79,17 +79,12 @@ func New( postageService postage.Service, postageStorer postage.Storer, chainEnabled bool, - setGasLimit bool, + gasLimit uint64, ) Interface { if !chainEnabled { return new(noOpPostageContract) } - var gasLimit uint64 - if setGasLimit { - gasLimit = transaction.DefaultGasLimit - } - return &postageContract{ owner: owner, postageStampContractAddress: postageStampContractAddress, diff --git a/pkg/postage/postagecontract/contract_test.go b/pkg/postage/postagecontract/contract_test.go index 80da7a8ab0d..680eb7d773f 100644 --- a/pkg/postage/postagecontract/contract_test.go +++ b/pkg/postage/postagecontract/contract_test.go @@ -141,7 +141,7 @@ func TestCreateBatch(t *testing.T) { postageMock, postagestoreMock.New(), true, - false, + 0, ) _, returnedID, err := contract.CreateBatch(ctx, initialBalance, depth, false, label) @@ -175,7 +175,7 @@ func TestCreateBatch(t *testing.T) { postageMock.New(), postagestoreMock.New(), true, - false, + 0, ) _, _, err := contract.CreateBatch(ctx, initialBalance, depth, false, label) @@ -204,7 +204,7 @@ func TestCreateBatch(t *testing.T) { postageMock.New(), postagestoreMock.New(), true, - false, + 0, ) _, _, err := contract.CreateBatch(ctx, initialBalance, depth, false, label) @@ -249,7 +249,7 @@ func TestCreateBatch(t *testing.T) { postageMock.New(), postagestoreMock.New(), true, - false, + 0, ) _, _, err = contract.CreateBatch(ctx, initialBalance, depth, false, label) @@ -360,7 +360,7 @@ func TestTopUpBatch(t *testing.T) { postageMock, batchStoreMock, true, - false, + 0, ) _, err = contract.TopUpBatch(ctx, batch.ID, topupBalance) @@ -389,7 +389,7 @@ func TestTopUpBatch(t *testing.T) { postageMock.New(), postagestoreMock.New(postagestoreMock.WithGetErr(errNotFound, 0)), true, - false, + 0, ) _, err := contract.TopUpBatch(ctx, postagetesting.MustNewID(), topupBalance) @@ -419,7 +419,7 @@ func TestTopUpBatch(t *testing.T) { postageMock.New(), batchStoreMock, true, - false, + 0, ) _, err := contract.TopUpBatch(ctx, batch.ID, topupBalance) @@ -549,7 +549,7 @@ func TestDiluteBatch(t *testing.T) { postageMock, batchStoreMock, true, - false, + 0, ) _, err = contract.DiluteBatch(ctx, batch.ID, newDepth) @@ -578,7 +578,7 @@ func TestDiluteBatch(t *testing.T) { postageMock.New(), postagestoreMock.New(postagestoreMock.WithGetErr(errNotFound, 0)), true, - false, + 0, ) _, err := contract.DiluteBatch(ctx, postagetesting.MustNewID(), uint8(17)) @@ -601,7 +601,7 @@ func TestDiluteBatch(t *testing.T) { postageMock.New(), batchStoreMock, true, - false, + 0, ) _, err := contract.DiluteBatch(ctx, batch.ID, batch.Depth-1) @@ -678,7 +678,7 @@ func TestBatchExpirer(t *testing.T) { postageMock, postagestoreMock.New(), true, - false, + 0, ) err = contract.ExpireBatches(ctx) @@ -712,7 +712,7 @@ func TestBatchExpirer(t *testing.T) { postageMock, postagestoreMock.New(), true, - false, + 0, ) err = contract.ExpireBatches(ctx) @@ -746,7 +746,7 @@ func TestBatchExpirer(t *testing.T) { postageMock, postagestoreMock.New(), true, - false, + 0, ) err = contract.ExpireBatches(ctx) @@ -823,7 +823,7 @@ func TestBatchExpirer(t *testing.T) { postageMock, postagestoreMock.New(), true, - false, + 0, ) err = contract.ExpireBatches(ctx) @@ -858,7 +858,7 @@ func TestBatchExpirer(t *testing.T) { postageMock, postagestoreMock.New(), true, - false, + 0, ) err = contract.ExpireBatches(ctx) @@ -895,7 +895,7 @@ func TestBatchExpirer(t *testing.T) { postageMock, postagestoreMock.New(), true, - false, + 0, ) err = contract.ExpireBatches(ctx) @@ -945,7 +945,7 @@ func TestBatchExpirer(t *testing.T) { postageMock, postagestoreMock.New(), true, - false, + 0, ) err = contract.ExpireBatches(ctx) diff --git a/pkg/storageincentives/redistribution/redistribution.go b/pkg/storageincentives/redistribution/redistribution.go index 52b2245db75..77013a8a990 100644 --- a/pkg/storageincentives/redistribution/redistribution.go +++ b/pkg/storageincentives/redistribution/redistribution.go @@ -48,13 +48,8 @@ func New( txService transaction.Service, incentivesContractAddress common.Address, incentivesContractABI abi.ABI, - setGasLimit bool, + gasLimit uint64, ) Contract { - var gasLimit uint64 - if setGasLimit { - gasLimit = transaction.DefaultGasLimit - } - return &contract{ overlay: overlay, owner: owner, diff --git a/pkg/storageincentives/redistribution/redistribution_test.go b/pkg/storageincentives/redistribution/redistribution_test.go index 60272afb8c0..6f4f429b705 100644 --- a/pkg/storageincentives/redistribution/redistribution_test.go +++ b/pkg/storageincentives/redistribution/redistribution_test.go @@ -88,7 +88,7 @@ func TestRedistribution(t *testing.T) { ), redistributionContractAddress, redistributionContractABI, - false, + 0, ) isPlaying, err := contract.IsPlaying(ctx, depth) @@ -120,7 +120,7 @@ func TestRedistribution(t *testing.T) { ), redistributionContractAddress, redistributionContractABI, - false, + 0, ) isPlaying, err := contract.IsPlaying(ctx, depth) @@ -150,7 +150,7 @@ func TestRedistribution(t *testing.T) { ), redistributionContractAddress, redistributionContractABI, - false, + 0, ) isWinner, err := contract.IsWinner(ctx) @@ -177,7 +177,7 @@ func TestRedistribution(t *testing.T) { ), redistributionContractAddress, redistributionContractABI, - false, + 0, ) isWinner, err := contract.IsWinner(ctx) @@ -223,7 +223,7 @@ func TestRedistribution(t *testing.T) { ), redistributionContractAddress, redistributionContractABI, - false, + 0, ) _, err = contract.Claim(ctx, proofs) @@ -265,7 +265,7 @@ func TestRedistribution(t *testing.T) { ), redistributionContractAddress, redistributionContractABI, - false, + 0, ) _, err = contract.Claim(ctx, proofs) @@ -308,7 +308,7 @@ func TestRedistribution(t *testing.T) { ), redistributionContractAddress, redistributionContractABI, - false, + 0, ) _, err = contract.Commit(ctx, testobfus, 0) @@ -353,7 +353,7 @@ func TestRedistribution(t *testing.T) { ), redistributionContractAddress, redistributionContractABI, - false, + 0, ) _, err = contract.Reveal(ctx, depth, common.Hex2Bytes("hash"), common.Hex2Bytes("nonce")) @@ -380,7 +380,7 @@ func TestRedistribution(t *testing.T) { ), redistributionContractAddress, redistributionContractABI, - false, + 0, ) salt, err := contract.ReserveSalt(ctx) @@ -410,7 +410,7 @@ func TestRedistribution(t *testing.T) { ), redistributionContractAddress, redistributionContractABI, - false, + 0, ) _, err := contract.IsPlaying(ctx, depth) @@ -443,7 +443,7 @@ func TestRedistribution(t *testing.T) { ), redistributionContractAddress, redistributionContractABI, - false, + 0, ) _, err = contract.Commit(ctx, common.Hex2Bytes("hash"), 0) diff --git a/pkg/storageincentives/staking/contract.go b/pkg/storageincentives/staking/contract.go index 7a91fa12266..b9f08801d5d 100644 --- a/pkg/storageincentives/staking/contract.go +++ b/pkg/storageincentives/staking/contract.go @@ -70,14 +70,9 @@ func New( bzzTokenAddress common.Address, transactionService transaction.Service, nonce common.Hash, - setGasLimit bool, + gasLimit uint64, height uint8, ) Contract { - var gasLimit uint64 - if setGasLimit { - gasLimit = transaction.DefaultGasLimit - } - return &contract{ owner: owner, stakingContractAddress: stakingContractAddress, diff --git a/pkg/storageincentives/staking/contract_test.go b/pkg/storageincentives/staking/contract_test.go index 23cb9ac548d..e032c4f2339 100644 --- a/pkg/storageincentives/staking/contract_test.go +++ b/pkg/storageincentives/staking/contract_test.go @@ -56,7 +56,7 @@ func TestIsOverlayFrozen(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -145,7 +145,7 @@ func TestDepositStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -207,7 +207,7 @@ func TestDepositStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -247,7 +247,7 @@ func TestDepositStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -280,7 +280,7 @@ func TestDepositStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -343,7 +343,7 @@ func TestDepositStake(t *testing.T) { }), ), nonce, - false, + 0, 1, ) @@ -376,7 +376,7 @@ func TestDepositStake(t *testing.T) { }), ), nonce, - false, + 0, 1, ) @@ -415,7 +415,7 @@ func TestDepositStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -465,7 +465,7 @@ func TestDepositStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -528,7 +528,7 @@ func TestDepositStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -589,7 +589,7 @@ func TestDepositStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -616,7 +616,7 @@ func TestDepositStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -685,7 +685,7 @@ func TestChangeHeight(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -749,7 +749,7 @@ func TestChangeHeight(t *testing.T) { }), ), nonce, - false, + 0, newHeight, ) @@ -813,7 +813,7 @@ func TestChangeHeight(t *testing.T) { }), ), nonce, - false, + 0, newHeight, ) @@ -848,7 +848,7 @@ func TestChangeHeight(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -875,7 +875,7 @@ func TestChangeHeight(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -931,7 +931,7 @@ func TestChangeStakeOverlay(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -958,7 +958,7 @@ func TestChangeStakeOverlay(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -993,7 +993,7 @@ func TestChangeStakeOverlay(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1039,7 +1039,7 @@ func TestChangeStakeOverlay(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1080,7 +1080,7 @@ func TestChangeStakeOverlay(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1127,7 +1127,7 @@ func TestGetCommittedStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1165,7 +1165,7 @@ func TestGetCommittedStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1203,7 +1203,7 @@ func TestGetCommittedStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1227,7 +1227,7 @@ func TestGetCommittedStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1274,7 +1274,7 @@ func TestGetWithdrawableStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1312,7 +1312,7 @@ func TestGetWithdrawableStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1336,7 +1336,7 @@ func TestGetWithdrawableStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1403,7 +1403,7 @@ func TestWithdrawStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1439,7 +1439,7 @@ func TestWithdrawStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1497,7 +1497,7 @@ func TestWithdrawStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1553,7 +1553,7 @@ func TestWithdrawStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1587,7 +1587,7 @@ func TestWithdrawStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1663,7 +1663,7 @@ func TestMigrateStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1698,7 +1698,7 @@ func TestMigrateStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1779,7 +1779,7 @@ func TestMigrateStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1844,7 +1844,7 @@ func TestMigrateStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1879,7 +1879,7 @@ func TestMigrateStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) @@ -1922,7 +1922,7 @@ func TestMigrateStake(t *testing.T) { }), ), nonce, - false, + 0, stakingHeight, ) diff --git a/pkg/transaction/backend/backend.go b/pkg/transaction/backend/backend.go index 827e7f8176a..67323d14e45 100644 --- a/pkg/transaction/backend/backend.go +++ b/pkg/transaction/backend/backend.go @@ -20,7 +20,7 @@ type Geth interface { CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) ChainID(ctx context.Context) (*big.Int, error) Close() - EstimateGasAtBlock(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) (uint64, error) + EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) diff --git a/pkg/transaction/backendmock/backend.go b/pkg/transaction/backendmock/backend.go index e07095fd352..364493b0341 100644 --- a/pkg/transaction/backendmock/backend.go +++ b/pkg/transaction/backendmock/backend.go @@ -15,12 +15,14 @@ import ( "github.com/ethersphere/bee/v2/pkg/transaction" ) +var ErrNotImplemented = errors.New("not implemented") + type backendMock struct { callContract func(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) sendTransaction func(ctx context.Context, tx *types.Transaction) error suggestedFeeAndTip func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) suggestGasTipCap func(ctx context.Context) (*big.Int, error) - estimateGasAtBlock func(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) (gas uint64, err error) + estimateGas func(ctx context.Context, msg ethereum.CallMsg) (gas uint64, err error) transactionReceipt func(ctx context.Context, txHash common.Hash) (*types.Receipt, error) pendingNonceAt func(ctx context.Context, account common.Address) (uint64, error) transactionByHash func(ctx context.Context, hash common.Hash) (tx *types.Transaction, isPending bool, err error) @@ -34,92 +36,92 @@ func (m *backendMock) CallContract(ctx context.Context, call ethereum.CallMsg, b if m.callContract != nil { return m.callContract(ctx, call, blockNumber) } - return nil, errors.New("not implemented") + return nil, ErrNotImplemented } func (m *backendMock) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { if m.pendingNonceAt != nil { return m.pendingNonceAt(ctx, account) } - return 0, errors.New("not implemented") + return 0, ErrNotImplemented } func (m *backendMock) SuggestedFeeAndTip(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { if m.suggestedFeeAndTip != nil { return m.suggestedFeeAndTip(ctx, gasPrice, boostPercent) } - return nil, nil, errors.New("not implemented") + return nil, nil, ErrNotImplemented } -func (m *backendMock) EstimateGasAtBlock(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) (uint64, error) { - if m.estimateGasAtBlock != nil { - return m.estimateGasAtBlock(ctx, msg, blockNumber) +func (m *backendMock) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) { + if m.estimateGas != nil { + return m.estimateGas(ctx, msg) } - return 0, errors.New("not implemented") + return 0, ErrNotImplemented } func (m *backendMock) SendTransaction(ctx context.Context, tx *types.Transaction) error { if m.sendTransaction != nil { return m.sendTransaction(ctx, tx) } - return errors.New("not implemented") + return ErrNotImplemented } func (*backendMock) FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]types.Log, error) { - return nil, errors.New("not implemented") + return nil, ErrNotImplemented } func (m *backendMock) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { if m.transactionReceipt != nil { return m.transactionReceipt(ctx, txHash) } - return nil, errors.New("not implemented") + return nil, ErrNotImplemented } func (m *backendMock) TransactionByHash(ctx context.Context, hash common.Hash) (tx *types.Transaction, isPending bool, err error) { if m.transactionByHash != nil { return m.transactionByHash(ctx, hash) } - return nil, false, errors.New("not implemented") + return nil, false, ErrNotImplemented } func (m *backendMock) BlockNumber(ctx context.Context) (uint64, error) { if m.blockNumber != nil { return m.blockNumber(ctx) } - return 0, errors.New("not implemented") + return 0, ErrNotImplemented } func (m *backendMock) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { if m.headerByNumber != nil { return m.headerByNumber(ctx, number) } - return nil, errors.New("not implemented") + return nil, ErrNotImplemented } func (m *backendMock) BalanceAt(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) { if m.balanceAt != nil { return m.balanceAt(ctx, address, block) } - return nil, errors.New("not implemented") + return nil, ErrNotImplemented } func (m *backendMock) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { if m.nonceAt != nil { return m.nonceAt(ctx, account, blockNumber) } - return 0, errors.New("not implemented") + return 0, ErrNotImplemented } func (m *backendMock) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { if m.suggestGasTipCap != nil { return m.suggestGasTipCap(ctx) } - return nil, errors.New("not implemented") + return nil, ErrNotImplemented } func (m *backendMock) ChainID(ctx context.Context) (*big.Int, error) { - return nil, errors.New("not implemented") + return nil, ErrNotImplemented } func (m *backendMock) Close() {} @@ -171,9 +173,9 @@ func WithSuggestGasTipCapFunc(f func(ctx context.Context) (*big.Int, error)) Opt }) } -func WithEstimateGasAtBlockFunc(f func(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) (gas uint64, err error)) Option { +func WithEstimateGasFunc(f func(ctx context.Context, msg ethereum.CallMsg) (gas uint64, err error)) Option { return optionFunc(func(s *backendMock) { - s.estimateGasAtBlock = f + s.estimateGas = f }) } diff --git a/pkg/transaction/backendnoop/backend.go b/pkg/transaction/backendnoop/backend.go index cc09b459bd2..4b149369981 100644 --- a/pkg/transaction/backendnoop/backend.go +++ b/pkg/transaction/backendnoop/backend.go @@ -55,7 +55,7 @@ func (b *Backend) SuggestGasTipCap(context.Context) (*big.Int, error) { return nil, postagecontract.ErrChainDisabled } -func (b *Backend) EstimateGasAtBlock(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) (uint64, error) { +func (b *Backend) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) { return 0, postagecontract.ErrChainDisabled } diff --git a/pkg/transaction/backendsimulation/backend.go b/pkg/transaction/backendsimulation/backend.go index eacddc44f0f..9c647b6404a 100644 --- a/pkg/transaction/backendsimulation/backend.go +++ b/pkg/transaction/backendsimulation/backend.go @@ -16,6 +16,8 @@ import ( "github.com/ethersphere/bee/v2/pkg/transaction" ) +var ErrNotImplemented = errors.New("not implemented") + type AccountAtKey struct { BlockNumber uint64 Account common.Address @@ -84,27 +86,27 @@ func (m *simulatedBackend) advanceBlock() { } func (*simulatedBackend) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { - return nil, errors.New("not implemented") + return nil, ErrNotImplemented } func (m *simulatedBackend) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { - return 0, errors.New("not implemented") + return 0, ErrNotImplemented } func (m *simulatedBackend) SuggestedFeeAndTip(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { - return nil, nil, errors.New("not implemented") + return nil, nil, ErrNotImplemented } -func (m *simulatedBackend) EstimateGasAtBlock(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) (uint64, error) { - return 0, errors.New("not implemented") +func (m *simulatedBackend) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) { + return 0, ErrNotImplemented } func (m *simulatedBackend) SendTransaction(ctx context.Context, tx *types.Transaction) error { - return errors.New("not implemented") + return ErrNotImplemented } func (*simulatedBackend) FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]types.Log, error) { - return nil, errors.New("not implemented") + return nil, ErrNotImplemented } func (m *simulatedBackend) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { @@ -117,7 +119,7 @@ func (m *simulatedBackend) TransactionReceipt(ctx context.Context, txHash common } func (m *simulatedBackend) TransactionByHash(ctx context.Context, hash common.Hash) (tx *types.Transaction, isPending bool, err error) { - return nil, false, errors.New("not implemented") + return nil, false, ErrNotImplemented } func (m *simulatedBackend) BlockNumber(ctx context.Context) (uint64, error) { @@ -126,11 +128,11 @@ func (m *simulatedBackend) BlockNumber(ctx context.Context) (uint64, error) { } func (m *simulatedBackend) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { - return nil, errors.New("not implemented") + return nil, ErrNotImplemented } func (m *simulatedBackend) BalanceAt(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) { - return nil, errors.New("not implemented") + return nil, ErrNotImplemented } func (m *simulatedBackend) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { @@ -143,11 +145,11 @@ func (m *simulatedBackend) NonceAt(ctx context.Context, account common.Address, } func (m *simulatedBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { - return nil, errors.New("not implemented") + return nil, ErrNotImplemented } func (m *simulatedBackend) ChainID(ctx context.Context) (*big.Int, error) { - return nil, errors.New("not implemented") + return nil, ErrNotImplemented } func (m *simulatedBackend) Close() {} diff --git a/pkg/transaction/transaction.go b/pkg/transaction/transaction.go index cc88e7a3ab4..e03dc003ce9 100644 --- a/pkg/transaction/transaction.go +++ b/pkg/transaction/transaction.go @@ -44,8 +44,12 @@ var ( ) const ( - DefaultGasLimit = 1_000_000 + DefaultGasLimit = 1_000_000 // Used for contract operations when setGasLimit flag is enabled DefaultTipBoostPercent = 25 + MaxGasLimit = 10_000_000 // Maximum allowed gas limit to prevent excessive values + MinGasLimit = 21_000 // Minimum gas for any transaction + GasBufferPercent = 33 // Add 33% buffer to estimated gas + FallbackGasLimit = 500_000 // Fallback when estimation fails and no minimum is set ) // TxRequest describes a request for a transaction that can be executed. @@ -111,34 +115,40 @@ type transactionService struct { ctx context.Context cancel context.CancelFunc - logger log.Logger - backend Backend - signer crypto.Signer - sender common.Address - store storage.StateStorer - chainID *big.Int - monitor Monitor + logger log.Logger + backend Backend + signer crypto.Signer + sender common.Address + store storage.StateStorer + chainID *big.Int + monitor Monitor + fallbackGasLimit uint64 } // NewService creates a new transaction service. -func NewService(logger log.Logger, overlayEthAddress common.Address, backend Backend, signer crypto.Signer, store storage.StateStorer, chainID *big.Int, monitor Monitor) (Service, error) { +func NewService(logger log.Logger, overlayEthAddress common.Address, backend Backend, signer crypto.Signer, store storage.StateStorer, chainID *big.Int, monitor Monitor, fallbackGasLimit uint64) (Service, error) { senderAddress, err := signer.EthereumAddress() if err != nil { return nil, err } + if fallbackGasLimit == 0 { + fallbackGasLimit = FallbackGasLimit + } + ctx, cancel := context.WithCancel(context.Background()) t := &transactionService{ - ctx: ctx, - cancel: cancel, - logger: logger.WithName(loggerName).WithValues("sender_address", overlayEthAddress).Register(), - backend: backend, - signer: signer, - sender: senderAddress, - store: store, - chainID: chainID, - monitor: monitor, + ctx: ctx, + cancel: cancel, + logger: logger.WithName(loggerName).WithValues("sender_address", overlayEthAddress).Register(), + backend: backend, + signer: signer, + sender: senderAddress, + store: store, + chainID: chainID, + monitor: monitor, + fallbackGasLimit: fallbackGasLimit, } if err = t.waitForAllPendingTx(); err != nil { @@ -273,22 +283,55 @@ func (t *transactionService) StoredTransaction(txHash common.Hash) (*StoredTrans func (t *transactionService) prepareTransaction(ctx context.Context, request *TxRequest, nonce uint64, boostPercent int) (tx *types.Transaction, err error) { var gasLimit uint64 if request.GasLimit == 0 { - gasLimit, err = t.backend.EstimateGasAtBlock(ctx, ethereum.CallMsg{ - From: t.sender, - To: request.To, - Data: request.Data, - }, nil) // nil for latest block + // Estimate gas using pending state for consistency with PendingNonceAt + gasLimit, err = t.backend.EstimateGas(ctx, ethereum.CallMsg{ + From: t.sender, + To: request.To, + Data: request.Data, + Value: request.Value, + }) + if err != nil { - t.logger.Debug("estimate gas failed", "error", err) - gasLimit = request.MinEstimatedGasLimit + t.logger.Warning("gas estimation failed, using fallback", + "error", err, + "description", request.Description, + ) + + if request.MinEstimatedGasLimit > 0 { + gasLimit = request.MinEstimatedGasLimit + } else if len(request.Data) > 0 { + // Contract call - use configured fallback + gasLimit = t.fallbackGasLimit + } else { + // Simple transfer - use minimum + gasLimit = MinGasLimit + } + } else { + // Estimation succeeded - add buffer for state changes + gasLimit += gasLimit * GasBufferPercent / 100 + + // Apply minimum if specified + if gasLimit < request.MinEstimatedGasLimit { + gasLimit = request.MinEstimatedGasLimit + } + + // Cap at maximum + if gasLimit > MaxGasLimit { + gasLimit = MaxGasLimit + } } - gasLimit += gasLimit / 2 // add 50% buffer to the estimated gas limit - if gasLimit < request.MinEstimatedGasLimit { - gasLimit = request.MinEstimatedGasLimit + // Ensure absolute minimum + if gasLimit < MinGasLimit { + gasLimit = MinGasLimit } } else { - gasLimit = request.GasLimit + // Use provided gas limit with bounds validation + gasLimit = min(max(request.GasLimit, MinGasLimit), MaxGasLimit) + } + + if gasLimit == 0 { + return nil, errors.New("gas limit cannot be zero") } /* @@ -308,6 +351,16 @@ func (t *transactionService) prepareTransaction(ctx context.Context, request *Tx return nil, err } + t.logger.Debug("prepared transaction", + "to", request.To, + "value", request.Value, + "gas_limit", gasLimit, + "gas_fee_cap", gasFeeCap, + "gas_tip_cap", gasTipCap, + "nonce", nonce, + "description", request.Description, + ) + return types.NewTx(&types.DynamicFeeTx{ Nonce: nonce, ChainID: t.chainID, diff --git a/pkg/transaction/transaction_test.go b/pkg/transaction/transaction_test.go index e9f4e385c32..0a43b055d98 100644 --- a/pkg/transaction/transaction_test.go +++ b/pkg/transaction/transaction_test.go @@ -117,8 +117,8 @@ func TestTransactionSend(t *testing.T) { txData := common.Hex2Bytes("0xabcdee") value := big.NewInt(1) suggestedGasTip := minimumTip - estimatedGasLimit := uint64(3) - gasLimit := estimatedGasLimit + estimatedGasLimit/2 // added 50% buffer + estimatedGasLimit := uint64(30000) + gasLimit := estimatedGasLimit + estimatedGasLimit*transaction.GasBufferPercent/100 // added 33% buffer nonce := uint64(2) chainID := big.NewInt(5) gasFeeCap := new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(2)), suggestedGasTip) @@ -151,7 +151,7 @@ func TestTransactionSend(t *testing.T) { } return nil }), - backendmock.WithEstimateGasAtBlockFunc(func(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) (gas uint64, err error) { + backendmock.WithEstimateGasFunc(func(ctx context.Context, msg ethereum.CallMsg) (gas uint64, err error) { if !bytes.Equal(msg.To.Bytes(), recipient.Bytes()) { t.Fatalf("estimating with wrong recipient. wanted %x, got %x", recipient, msg.To) } @@ -175,6 +175,7 @@ func TestTransactionSend(t *testing.T) { return nil, nil, nil }), ), + 0, ) if err != nil { t.Fatal(err) @@ -208,12 +209,15 @@ func TestTransactionSend(t *testing.T) { t.Run("send with estimate error", func(t *testing.T) { t.Parallel() + // When estimation fails, use MinEstimatedGasLimit without buffer + gasLimitFallback := estimatedGasLimit + signedTx := types.NewTx(&types.DynamicFeeTx{ ChainID: chainID, Nonce: nonce, To: &recipient, Value: value, - Gas: gasLimit, + Gas: gasLimitFallback, GasFeeCap: gasFeeCap, GasTipCap: suggestedGasTip, Data: txData, @@ -234,7 +238,7 @@ func TestTransactionSend(t *testing.T) { } return nil }), - backendmock.WithEstimateGasAtBlockFunc(func(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) (gas uint64, err error) { + backendmock.WithEstimateGasFunc(func(ctx context.Context, msg ethereum.CallMsg) (gas uint64, err error) { return 0, errors.New("estimate failure") }), backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) { @@ -252,6 +256,7 @@ func TestTransactionSend(t *testing.T) { return nil, nil, nil }), ), + 0, ) if err != nil { t.Fatal(err) @@ -267,7 +272,7 @@ func TestTransactionSend(t *testing.T) { t.Fatal("returning wrong transaction hash") } - checkStoredTransaction(t, transactionService, txHash, request, recipient, gasLimit, gasFeeCap, nonce) + checkStoredTransaction(t, transactionService, txHash, request, recipient, gasLimitFallback, gasFeeCap, nonce) pending, err := transactionService.PendingTransactions() if err != nil { @@ -314,7 +319,7 @@ func TestTransactionSend(t *testing.T) { } return nil }), - backendmock.WithEstimateGasAtBlockFunc(func(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) (gas uint64, err error) { + backendmock.WithEstimateGasFunc(func(ctx context.Context, msg ethereum.CallMsg) (gas uint64, err error) { if !bytes.Equal(msg.To.Bytes(), recipient.Bytes()) { t.Fatalf("estimating with wrong recipient. wanted %x, got %x", recipient, msg.To) } @@ -338,6 +343,7 @@ func TestTransactionSend(t *testing.T) { return nil, nil, nil }), ), + 0, ) if err != nil { t.Fatal(err) @@ -396,7 +402,7 @@ func TestTransactionSend(t *testing.T) { } return nil }), - backendmock.WithEstimateGasAtBlockFunc(func(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) (gas uint64, err error) { + backendmock.WithEstimateGasFunc(func(ctx context.Context, msg ethereum.CallMsg) (gas uint64, err error) { if !bytes.Equal(msg.To.Bytes(), recipient.Bytes()) { t.Fatalf("estimating with wrong recipient. wanted %x, got %x", recipient, msg.To) } @@ -416,6 +422,7 @@ func TestTransactionSend(t *testing.T) { store, chainID, monitormock.New(), + 0, ) if err != nil { t.Fatal(err) @@ -461,7 +468,7 @@ func TestTransactionSend(t *testing.T) { } return nil }), - backendmock.WithEstimateGasAtBlockFunc(func(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) (gas uint64, err error) { + backendmock.WithEstimateGasFunc(func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) { if !bytes.Equal(call.To.Bytes(), recipient.Bytes()) { t.Fatalf("estimating with wrong recipient. wanted %x, got %x", recipient, call.To) } @@ -481,6 +488,7 @@ func TestTransactionSend(t *testing.T) { store, chainID, monitormock.New(), + 0, ) if err != nil { t.Fatal(err) @@ -527,7 +535,7 @@ func TestTransactionSend(t *testing.T) { } return nil }), - backendmock.WithEstimateGasAtBlockFunc(func(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) (gas uint64, err error) { + backendmock.WithEstimateGasFunc(func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) { if !bytes.Equal(call.To.Bytes(), recipient.Bytes()) { t.Fatalf("estimating with wrong recipient. wanted %x, got %x", recipient, call.To) } @@ -547,6 +555,7 @@ func TestTransactionSend(t *testing.T) { store, chainID, monitormock.New(), + 0, ) if err != nil { t.Fatal(err) @@ -570,6 +579,503 @@ func TestTransactionSend(t *testing.T) { t.Fatalf("got wrong gas tip in stored transaction. wanted %d, got %d", customGasFeeCap, storedTransaction.GasTipCap) } }) + + t.Run("send with contract fallback", func(t *testing.T) { + t.Parallel() + + // When estimation fails for contract call (has data), use FallbackGasLimit (500k) + contractData := []byte{0xab, 0xcd, 0xef} // Explicit non-empty data for contract call + signedTx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + To: &recipient, + Value: value, + Gas: transaction.FallbackGasLimit, + GasFeeCap: gasFeeCap, + GasTipCap: suggestedGasTip, + Data: contractData, + }) + request := &transaction.TxRequest{ + To: &recipient, + Data: contractData, + Value: value, + } + store := storemock.NewStateStore() + + transactionService, err := transaction.NewService(logger, sender, + backendmock.New( + backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error { + if tx != signedTx { + t.Fatal("not sending signed transaction") + } + return nil + }), + backendmock.WithEstimateGasFunc(func(ctx context.Context, msg ethereum.CallMsg) (gas uint64, err error) { + return 0, errors.New("estimation failed") + }), + backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) { + return nonce, nil + }), + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return gasFeeCap, suggestedGasTip, nil + }), + ), + signerMockForTransaction(t, signedTx, sender, chainID), + store, + chainID, + monitormock.New(), + 0, + ) + if err != nil { + t.Fatal(err) + } + testutil.CleanupCloser(t, transactionService) + + txHash, err := transactionService.Send(context.Background(), request, 0) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) { + t.Fatal("returning wrong transaction hash") + } + + storedTransaction, err := transactionService.StoredTransaction(txHash) + if err != nil { + t.Fatal(err) + } + + if storedTransaction.GasLimit != transaction.FallbackGasLimit { + t.Fatalf("expected fallback gas limit %d, got %d", transaction.FallbackGasLimit, storedTransaction.GasLimit) + } + }) + + t.Run("send with simple transfer fallback", func(t *testing.T) { + t.Parallel() + + // When estimation fails for simple transfer (no data), use MinGasLimit (21k) + signedTx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + To: &recipient, + Value: value, + Gas: transaction.MinGasLimit, + GasFeeCap: gasFeeCap, + GasTipCap: suggestedGasTip, + Data: nil, + }) + request := &transaction.TxRequest{ + To: &recipient, + Data: nil, + Value: value, + } + store := storemock.NewStateStore() + + transactionService, err := transaction.NewService(logger, sender, + backendmock.New( + backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error { + if tx != signedTx { + t.Fatal("not sending signed transaction") + } + return nil + }), + backendmock.WithEstimateGasFunc(func(ctx context.Context, msg ethereum.CallMsg) (gas uint64, err error) { + return 0, errors.New("estimation failed") + }), + backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) { + return nonce, nil + }), + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return gasFeeCap, suggestedGasTip, nil + }), + ), + signerMockForTransaction(t, signedTx, sender, chainID), + store, + chainID, + monitormock.New(), + 0, + ) + if err != nil { + t.Fatal(err) + } + testutil.CleanupCloser(t, transactionService) + + txHash, err := transactionService.Send(context.Background(), request, 0) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) { + t.Fatal("returning wrong transaction hash") + } + + storedTransaction, err := transactionService.StoredTransaction(txHash) + if err != nil { + t.Fatal(err) + } + + if storedTransaction.GasLimit != transaction.MinGasLimit { + t.Fatalf("expected min gas limit %d, got %d", transaction.MinGasLimit, storedTransaction.GasLimit) + } + }) + + t.Run("send with max gas limit cap", func(t *testing.T) { + t.Parallel() + + // When estimation returns value that exceeds MaxGasLimit, cap it + highEstimate := uint64(15_000_000) // Above MaxGasLimit of 10M + expectedGasLimit := uint64(transaction.MaxGasLimit) + + signedTx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + To: &recipient, + Value: value, + Gas: expectedGasLimit, + GasFeeCap: gasFeeCap, + GasTipCap: suggestedGasTip, + Data: txData, + }) + request := &transaction.TxRequest{ + To: &recipient, + Data: txData, + Value: value, + } + store := storemock.NewStateStore() + + transactionService, err := transaction.NewService(logger, sender, + backendmock.New( + backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error { + if tx != signedTx { + t.Fatal("not sending signed transaction") + } + return nil + }), + backendmock.WithEstimateGasFunc(func(ctx context.Context, msg ethereum.CallMsg) (gas uint64, err error) { + return highEstimate, nil + }), + backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) { + return nonce, nil + }), + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return gasFeeCap, suggestedGasTip, nil + }), + ), + signerMockForTransaction(t, signedTx, sender, chainID), + store, + chainID, + monitormock.New(), + 0, + ) + if err != nil { + t.Fatal(err) + } + testutil.CleanupCloser(t, transactionService) + + txHash, err := transactionService.Send(context.Background(), request, 0) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) { + t.Fatal("returning wrong transaction hash") + } + + storedTransaction, err := transactionService.StoredTransaction(txHash) + if err != nil { + t.Fatal(err) + } + + if storedTransaction.GasLimit != transaction.MaxGasLimit { + t.Fatalf("expected max gas limit %d, got %d", transaction.MaxGasLimit, storedTransaction.GasLimit) + } + }) + + t.Run("send with provided gas limit", func(t *testing.T) { + t.Parallel() + + // When GasLimit is explicitly provided, use it with bounds validation + providedGasLimit := uint64(100_000) + + signedTx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + To: &recipient, + Value: value, + Gas: providedGasLimit, + GasFeeCap: gasFeeCap, + GasTipCap: suggestedGasTip, + Data: txData, + }) + request := &transaction.TxRequest{ + To: &recipient, + Data: txData, + Value: value, + GasLimit: providedGasLimit, + } + store := storemock.NewStateStore() + + transactionService, err := transaction.NewService(logger, sender, + backendmock.New( + backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error { + if tx != signedTx { + t.Fatal("not sending signed transaction") + } + return nil + }), + // EstimateGas should not be called when GasLimit is provided + backendmock.WithEstimateGasFunc(func(ctx context.Context, msg ethereum.CallMsg) (gas uint64, err error) { + t.Fatal("EstimateGas should not be called when GasLimit is provided") + return 0, nil + }), + backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) { + return nonce, nil + }), + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return gasFeeCap, suggestedGasTip, nil + }), + ), + signerMockForTransaction(t, signedTx, sender, chainID), + store, + chainID, + monitormock.New(), + 0, + ) + if err != nil { + t.Fatal(err) + } + testutil.CleanupCloser(t, transactionService) + + txHash, err := transactionService.Send(context.Background(), request, 0) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) { + t.Fatal("returning wrong transaction hash") + } + + storedTransaction, err := transactionService.StoredTransaction(txHash) + if err != nil { + t.Fatal(err) + } + + if storedTransaction.GasLimit != providedGasLimit { + t.Fatalf("expected provided gas limit %d, got %d", providedGasLimit, storedTransaction.GasLimit) + } + }) + + t.Run("send with MinEstimatedGasLimit enforced after buffer", func(t *testing.T) { + t.Parallel() + + // When estimated gas + buffer is below MinEstimatedGasLimit, enforce the minimum + lowEstimate := uint64(50_000) + minGas := uint64(100_000) + // lowEstimate + 33% = 66,500, which is < minGas, so minGas should be used + + signedTx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + To: &recipient, + Value: value, + Gas: minGas, + GasFeeCap: gasFeeCap, + GasTipCap: suggestedGasTip, + Data: txData, + }) + request := &transaction.TxRequest{ + To: &recipient, + Data: txData, + Value: value, + MinEstimatedGasLimit: minGas, + } + store := storemock.NewStateStore() + + transactionService, err := transaction.NewService(logger, sender, + backendmock.New( + backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error { + if tx != signedTx { + t.Fatal("not sending signed transaction") + } + return nil + }), + backendmock.WithEstimateGasFunc(func(ctx context.Context, msg ethereum.CallMsg) (gas uint64, err error) { + return lowEstimate, nil + }), + backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) { + return nonce, nil + }), + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return gasFeeCap, suggestedGasTip, nil + }), + ), + signerMockForTransaction(t, signedTx, sender, chainID), + store, + chainID, + monitormock.New(), + 0, + ) + if err != nil { + t.Fatal(err) + } + testutil.CleanupCloser(t, transactionService) + + txHash, err := transactionService.Send(context.Background(), request, 0) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) { + t.Fatal("returning wrong transaction hash") + } + + storedTransaction, err := transactionService.StoredTransaction(txHash) + if err != nil { + t.Fatal(err) + } + + if storedTransaction.GasLimit != minGas { + t.Fatalf("expected MinEstimatedGasLimit %d, got %d", minGas, storedTransaction.GasLimit) + } + }) + + t.Run("send with provided gas limit below minimum", func(t *testing.T) { + t.Parallel() + + // When provided GasLimit is below MinGasLimit, enforce MinGasLimit + lowGasLimit := uint64(10_000) // Below MinGasLimit of 21k + + signedTx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + To: &recipient, + Value: value, + Gas: transaction.MinGasLimit, + GasFeeCap: gasFeeCap, + GasTipCap: suggestedGasTip, + Data: txData, + }) + request := &transaction.TxRequest{ + To: &recipient, + Data: txData, + Value: value, + GasLimit: lowGasLimit, + } + store := storemock.NewStateStore() + + transactionService, err := transaction.NewService(logger, sender, + backendmock.New( + backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error { + if tx != signedTx { + t.Fatal("not sending signed transaction") + } + return nil + }), + backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) { + return nonce, nil + }), + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return gasFeeCap, suggestedGasTip, nil + }), + ), + signerMockForTransaction(t, signedTx, sender, chainID), + store, + chainID, + monitormock.New(), + 0, + ) + if err != nil { + t.Fatal(err) + } + testutil.CleanupCloser(t, transactionService) + + txHash, err := transactionService.Send(context.Background(), request, 0) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) { + t.Fatal("returning wrong transaction hash") + } + + storedTransaction, err := transactionService.StoredTransaction(txHash) + if err != nil { + t.Fatal(err) + } + + if storedTransaction.GasLimit != transaction.MinGasLimit { + t.Fatalf("expected min gas limit enforced %d, got %d", transaction.MinGasLimit, storedTransaction.GasLimit) + } + }) + + t.Run("send with provided gas limit above maximum", func(t *testing.T) { + t.Parallel() + + // When provided GasLimit is above MaxGasLimit, cap at MaxGasLimit + highGasLimit := uint64(15_000_000) // Above MaxGasLimit of 10M + + signedTx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + To: &recipient, + Value: value, + Gas: transaction.MaxGasLimit, + GasFeeCap: gasFeeCap, + GasTipCap: suggestedGasTip, + Data: txData, + }) + request := &transaction.TxRequest{ + To: &recipient, + Data: txData, + Value: value, + GasLimit: highGasLimit, + } + store := storemock.NewStateStore() + + transactionService, err := transaction.NewService(logger, sender, + backendmock.New( + backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error { + if tx != signedTx { + t.Fatal("not sending signed transaction") + } + return nil + }), + backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) { + return nonce, nil + }), + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return gasFeeCap, suggestedGasTip, nil + }), + ), + signerMockForTransaction(t, signedTx, sender, chainID), + store, + chainID, + monitormock.New(), + 0, + ) + if err != nil { + t.Fatal(err) + } + testutil.CleanupCloser(t, transactionService) + + txHash, err := transactionService.Send(context.Background(), request, 0) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) { + t.Fatal("returning wrong transaction hash") + } + + storedTransaction, err := transactionService.StoredTransaction(txHash) + if err != nil { + t.Fatal(err) + } + + if storedTransaction.GasLimit != transaction.MaxGasLimit { + t.Fatalf("expected max gas limit enforced %d, got %d", transaction.MaxGasLimit, storedTransaction.GasLimit) + } + }) } func TestTransactionWaitForReceipt(t *testing.T) { @@ -617,6 +1123,7 @@ func TestTransactionWaitForReceipt(t *testing.T) { return receiptC, nil, nil }), ), + 0, ) if err != nil { t.Fatal(err) @@ -690,6 +1197,7 @@ func TestTransactionResend(t *testing.T) { store, chainID, monitormock.New(), + 0, ) if err != nil { t.Fatal(err) @@ -777,6 +1285,7 @@ func TestTransactionCancel(t *testing.T) { store, chainID, monitormock.New(), + 0, ) if err != nil { t.Fatal(err) @@ -827,6 +1336,7 @@ func TestTransactionCancel(t *testing.T) { store, chainID, monitormock.New(), + 0, ) if err != nil { t.Fatal(err) @@ -912,6 +1422,7 @@ func TestTransactionService_UnwrapABIError(t *testing.T) { storemock.NewStateStore(), chainID, monitormock.New(), + 0, ) if err != nil { t.Fatal(err) diff --git a/pkg/transaction/wrapped/wrapped.go b/pkg/transaction/wrapped/wrapped.go index 1d4e452aef3..f573934a7d3 100644 --- a/pkg/transaction/wrapped/wrapped.go +++ b/pkg/transaction/wrapped/wrapped.go @@ -138,10 +138,10 @@ func (b *wrappedBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) } return gasTipCap, nil } -func (b *wrappedBackend) EstimateGasAtBlock(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) (uint64, error) { +func (b *wrappedBackend) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) { b.metrics.TotalRPCCalls.Inc() b.metrics.EstimateGasCalls.Inc() - gas, err := b.backend.EstimateGasAtBlock(ctx, msg, blockNumber) + gas, err := b.backend.EstimateGas(ctx, msg) if err != nil { b.metrics.TotalRPCErrors.Inc() return 0, err