From 24e44f226b51a93a43b8e306e6003b1f25a5b141 Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:48:53 -0300 Subject: [PATCH 1/3] fix(cli): show error cause on failed transaction --- pkg/ethutil/application.go | 6 +++--- pkg/ethutil/authority.go | 6 +++--- pkg/ethutil/ethutil.go | 30 +++++++++++++++--------------- pkg/ethutil/mnemonic.go | 4 ++-- pkg/ethutil/prt.go | 12 ++++++------ pkg/ethutil/quorum.go | 6 +++--- pkg/ethutil/selfhosted.go | 2 +- pkg/ethutil/transaction.go | 14 +++++++------- 8 files changed, 40 insertions(+), 40 deletions(-) diff --git a/pkg/ethutil/application.go b/pkg/ethutil/application.go index 6ba79b43e..ca283ded3 100644 --- a/pkg/ethutil/application.go +++ b/pkg/ethutil/application.go @@ -78,7 +78,7 @@ func (me *ApplicationDeployment) Deploy( result.Deployment = me factory, err := iapplicationfactory.NewIApplicationFactory(me.FactoryAddress, client) if err != nil { - return zero, nil, fmt.Errorf("failed to instantiate contract: %v", err) + return zero, nil, fmt.Errorf("failed to instantiate contract: %w", err) } if err := ValidateWithdrawalConfig(me.WithdrawalConfig); err != nil { @@ -105,12 +105,12 @@ func (me *ApplicationDeployment) Deploy( // deploy the contracts tx, err := factory.NewApplication0(txOpts, me.Consensus, me.OwnerAddress, me.TemplateHash, me.DataAvailability, me.WithdrawalConfig, me.Salt) if err != nil { - return zero, nil, fmt.Errorf("transaction failed: %v", err) + return zero, nil, fmt.Errorf("transaction failed: %w", err) } receipt, err := bind.WaitMined(ctx, client, tx) if err != nil { - return zero, nil, fmt.Errorf("failed to wait for transaction mining: %v", err) + return zero, nil, fmt.Errorf("failed to wait for transaction mining: %w", err) } if receipt.Status != 1 { diff --git a/pkg/ethutil/authority.go b/pkg/ethutil/authority.go index 3d42799fa..228d24596 100644 --- a/pkg/ethutil/authority.go +++ b/pkg/ethutil/authority.go @@ -44,7 +44,7 @@ func (me *AuthorityDeployment) Deploy( zero := common.Address{} factory, err := iauthorityfactory.NewIAuthorityFactory(me.FactoryAddress, client) if err != nil { - return common.Address{}, fmt.Errorf("failed to instantiate contract: %v", err) + return common.Address{}, fmt.Errorf("failed to instantiate contract: %w", err) } // check if addresses are available (have no code) @@ -64,12 +64,12 @@ func (me *AuthorityDeployment) Deploy( // deploy the contracts tx, err := factory.NewAuthority0(txOpts, me.OwnerAddress, new(big.Int).SetUint64(me.EpochLength), new(big.Int).SetUint64(me.ClaimStagingPeriod), me.Salt) if err != nil { - return common.Address{}, fmt.Errorf("failed to create new authority: %v", err) + return common.Address{}, fmt.Errorf("failed to create new authority: %w", err) } receipt, err := bind.WaitMined(ctx, client, tx) if err != nil { - return common.Address{}, fmt.Errorf("failed to mine new authority transaction: %v", err) + return common.Address{}, fmt.Errorf("failed to mine new authority transaction: %w", err) } if receipt.Status != 1 { diff --git a/pkg/ethutil/ethutil.go b/pkg/ethutil/ethutil.go index ce3c6f477..9493dd4de 100644 --- a/pkg/ethutil/ethutil.go +++ b/pkg/ethutil/ethutil.go @@ -48,7 +48,7 @@ func AddInput( } inputBox, err := iinputbox.NewIInputBox(inputBoxAddress, client) if err != nil { - return 0, 0, common.Hash{}, fmt.Errorf("failed to connect to InputBox contract: %v", err) + return 0, 0, common.Hash{}, fmt.Errorf("failed to connect to InputBox contract: %w", err) } receipt, err := sendTransaction( ctx, client, transactionOpts, big.NewInt(0), GasLimit, @@ -121,7 +121,7 @@ func getInputIndex( } inputAdded, err := inputBox.ParseInputAdded(*log) if err != nil { - return 0, fmt.Errorf("failed to parse input added event: %v", err) + return 0, fmt.Errorf("failed to parse input added event: %w", err) } // We assume that uint64 will fit all dapp inputs for now return inputAdded.Index.Uint64(), nil @@ -142,7 +142,7 @@ func GetInputFromInputBox( } inputBox, err := iinputbox.NewIInputBox(inputBoxAddress, client) if err != nil { - return nil, fmt.Errorf("failed to connect to InputBox contract: %v", err) + return nil, fmt.Errorf("failed to connect to InputBox contract: %w", err) } it, err := inputBox.FilterInputAdded( nil, @@ -150,7 +150,7 @@ func GetInputFromInputBox( []*big.Int{new(big.Int).SetUint64(inputIndex)}, ) if err != nil { - return nil, fmt.Errorf("failed to filter input added: %v", err) + return nil, fmt.Errorf("failed to filter input added: %w", err) } defer it.Close() if !it.Next() { @@ -183,7 +183,7 @@ func ValidateOutput( app, err := iapplication.NewIApplication(appAddr, client) if err != nil { - return fmt.Errorf("failed to connect to CartesiDapp contract: %v", err) + return fmt.Errorf("failed to connect to CartesiDapp contract: %w", err) } return app.ValidateOutput(&bind.CallOpts{Context: ctx}, output, proof) } @@ -213,7 +213,7 @@ func ExecuteOutput( app, err := iapplication.NewIApplication(appAddr, client) if err != nil { - return nil, fmt.Errorf("failed to connect to CartesiDapp contract: %v", err) + return nil, fmt.Errorf("failed to connect to CartesiDapp contract: %w", err) } receipt, err := sendTransaction( ctx, client, transactionOpts, big.NewInt(0), GasLimit, @@ -271,12 +271,12 @@ func GetConsensusAt( } app, err := iapplication.NewIApplication(appAddress, client) if err != nil { - return common.Address{}, fmt.Errorf("Failed to instantiate contract: %v", err) + return common.Address{}, fmt.Errorf("Failed to instantiate contract: %w", err) } opts := &bind.CallOpts{Context: ctx, BlockNumber: blockNumber} consensus, err := app.GetOutputsMerkleRootValidator(opts) if err != nil { - return common.Address{}, fmt.Errorf("error retrieving application epoch length: %v", err) + return common.Address{}, fmt.Errorf("error retrieving application epoch length: %w", err) } return consensus, nil } @@ -291,11 +291,11 @@ func GetDataAvailability( } app, err := iapplication.NewIApplication(appAddress, client) if err != nil { - return nil, fmt.Errorf("Failed to instantiate contract: %v", err) + return nil, fmt.Errorf("Failed to instantiate contract: %w", err) } dataAvailability, err := app.GetDataAvailability(&bind.CallOpts{Context: ctx}) if err != nil { - return nil, fmt.Errorf("error retrieving application epoch length: %v", err) + return nil, fmt.Errorf("error retrieving application epoch length: %w", err) } return dataAvailability, nil } @@ -310,11 +310,11 @@ func GetEpochLength( } consensus, err := iconsensus.NewIConsensus(consensusAddr, client) if err != nil { - return 0, fmt.Errorf("Failed to instantiate contract: %v", err) + return 0, fmt.Errorf("Failed to instantiate contract: %w", err) } epochLengthRaw, err := consensus.GetEpochLength(&bind.CallOpts{Context: ctx}) if err != nil { - return 0, fmt.Errorf("error retrieving application epoch length: %v", err) + return 0, fmt.Errorf("error retrieving application epoch length: %w", err) } return epochLengthRaw.Uint64(), nil } @@ -332,11 +332,11 @@ func GetClaimStagingPeriod( } consensus, err := iconsensus.NewIConsensus(consensusAddr, client) if err != nil { - return 0, fmt.Errorf("failed to instantiate contract: %v", err) + return 0, fmt.Errorf("failed to instantiate contract: %w", err) } raw, err := consensus.GetClaimStagingPeriod(&bind.CallOpts{Context: ctx}) if err != nil { - return 0, fmt.Errorf("error retrieving claim staging period: %v", err) + return 0, fmt.Errorf("error retrieving claim staging period: %w", err) } return raw.Uint64(), nil } @@ -355,7 +355,7 @@ func GetInputBoxDeploymentBlock( } block, err := inputbox.GetDeploymentBlockNumber(&bind.CallOpts{Context: ctx}) if err != nil { - return nil, fmt.Errorf("error retrieving inputbox deployment block: %v", err) + return nil, fmt.Errorf("error retrieving inputbox deployment block: %w", err) } return block, nil } diff --git a/pkg/ethutil/mnemonic.go b/pkg/ethutil/mnemonic.go index 09e2ac607..777e2b9cb 100644 --- a/pkg/ethutil/mnemonic.go +++ b/pkg/ethutil/mnemonic.go @@ -32,7 +32,7 @@ func MnemonicToPrivateKey(mnemonic string, accountIndex uint32) (*ecdsa.PrivateK masterKey, err := bip32.NewMasterKey(seed) if err != nil { - return nil, fmt.Errorf("failed to generate master key: %v", err) + return nil, fmt.Errorf("failed to generate master key: %w", err) } // get key at path m/44'/60'/0'/0/account @@ -48,7 +48,7 @@ func MnemonicToPrivateKey(mnemonic string, accountIndex uint32) (*ecdsa.PrivateK for i, level := range levels { key, err = key.NewChildKey(level) if err != nil { - return nil, fmt.Errorf("failed to get child %v: %v", i, err) + return nil, fmt.Errorf("failed to get child %v: %w", i, err) } } diff --git a/pkg/ethutil/prt.go b/pkg/ethutil/prt.go index bd807d562..3f7c72ec4 100644 --- a/pkg/ethutil/prt.go +++ b/pkg/ethutil/prt.go @@ -59,7 +59,7 @@ func (me *PRTApplicationDeployment) deployPRT( factory, err := idaveappfactory.NewIDaveAppFactory(me.FactoryAddress, client) if err != nil { - return zero, zero, fmt.Errorf("failed to instantiate contract binding: %v", err) + return zero, zero, fmt.Errorf("failed to instantiate contract binding: %w", err) } if err := ValidateWithdrawalConfig(me.WithdrawalConfig); err != nil { @@ -96,12 +96,12 @@ func (me *PRTApplicationDeployment) deployPRT( // deploy the contracts tx, err := factory.NewDaveApp(txOpts, me.TemplateHash, daveWC, me.Salt) if err != nil { - return zero, zero, fmt.Errorf("transaction failed: %v", err) + return zero, zero, fmt.Errorf("transaction failed: %w", err) } receipt, err := bind.WaitMined(ctx, client, tx) if err != nil { - return zero, zero, fmt.Errorf("failed to wait for transaction mining: %v", err) + return zero, zero, fmt.Errorf("failed to wait for transaction mining: %w", err) } if receipt.Status != 1 { @@ -140,18 +140,18 @@ func (me *PRTApplicationDeployment) Deploy( application, err := iapplication.NewIApplication(appAddress, client) if err != nil { - return zero, nil, fmt.Errorf("failed to instantiate application: %v", err) + return zero, nil, fmt.Errorf("failed to instantiate application: %w", err) } da, err := application.GetDataAvailability(nil) if err != nil { - return zero, nil, fmt.Errorf("failed to retrieve data availability: %v", err) + return zero, nil, fmt.Errorf("failed to retrieve data availability: %w", err) } result.DataAvailability = da result.InputBoxAddress, result.IInputBoxBlock, err = DecodeDA(client, da) if err != nil { - return zero, nil, fmt.Errorf("failed to decode data availability: %v", err) + return zero, nil, fmt.Errorf("failed to decode data availability: %w", err) } if err := VerifyDeployedWithdrawalConfig(ctx, client, appAddress, me.WithdrawalConfig); err != nil { diff --git a/pkg/ethutil/quorum.go b/pkg/ethutil/quorum.go index 36419d7d9..4eafca1bd 100644 --- a/pkg/ethutil/quorum.go +++ b/pkg/ethutil/quorum.go @@ -45,7 +45,7 @@ func (me *QuorumDeployment) Deploy( zero := common.Address{} factory, err := iquorumfactory.NewIQuorumFactory(me.FactoryAddress, client) if err != nil { - return zero, fmt.Errorf("failed to instantiate contract: %v", err) + return zero, fmt.Errorf("failed to instantiate contract: %w", err) } epochLength := new(big.Int).SetUint64(me.EpochLength) @@ -71,12 +71,12 @@ func (me *QuorumDeployment) Deploy( tx, err := factory.NewQuorum(txOpts, me.Validators, epochLength, claimStagingPeriod, me.Salt) if err != nil { - return zero, fmt.Errorf("failed to create new quorum: %v", err) + return zero, fmt.Errorf("failed to create new quorum: %w", err) } receipt, err := bind.WaitMined(ctx, client, tx) if err != nil { - return zero, fmt.Errorf("failed to mine new quorum transaction: %v", err) + return zero, fmt.Errorf("failed to mine new quorum transaction: %w", err) } if receipt.Status != 1 { diff --git a/pkg/ethutil/selfhosted.go b/pkg/ethutil/selfhosted.go index d3711d119..e7a556009 100644 --- a/pkg/ethutil/selfhosted.go +++ b/pkg/ethutil/selfhosted.go @@ -153,7 +153,7 @@ func (me *SelfhostedApplicationDeployment) Deploy( }, ) if err != nil { - return zero, nil, fmt.Errorf("failed to create a self hosted application: execution reverted") + return zero, nil, fmt.Errorf("failed to create a self hosted application: %w", err) } applicationFactory, err := iapplicationfactory.NewIApplicationFactory(result.ApplicationFactoryAddress, client) diff --git a/pkg/ethutil/transaction.go b/pkg/ethutil/transaction.go index 26c793d35..7c6bcace8 100644 --- a/pkg/ethutil/transaction.go +++ b/pkg/ethutil/transaction.go @@ -29,11 +29,11 @@ func sendTransaction( ) (*types.Receipt, error) { txOpts, err := _prepareTransaction(ctx, client, txOpts, txValue, gasLimit) if err != nil { - return nil, fmt.Errorf("failed to prepare transaction: %v", err) + return nil, fmt.Errorf("failed to prepare transaction: %w", err) } tx, err := doSend(txOpts) if err != nil { - return nil, fmt.Errorf("failed to send transaction: %v", err) + return nil, fmt.Errorf("failed to send transaction: %w", err) } receipt, err := _waitForTransaction(ctx, client, tx) if err != nil { @@ -52,11 +52,11 @@ func _prepareTransaction( ) (*bind.TransactOpts, error) { nonce, err := client.PendingNonceAt(ctx, txOpts.From) if err != nil { - return nil, fmt.Errorf("failed to get nonce: %v", err) + return nil, fmt.Errorf("failed to get nonce: %w", err) } gasPrice, err := client.SuggestGasPrice(ctx) if err != nil { - return nil, fmt.Errorf("failed to get gas price: %v", err) + return nil, fmt.Errorf("failed to get gas price: %w", err) } nonceBigInt := &big.Int{} nonceBigInt.SetUint64(nonce) @@ -76,7 +76,7 @@ func _waitForTransaction( for { _, isPending, err := client.TransactionByHash(ctx, tx.Hash()) if err != nil { - return nil, fmt.Errorf("fail to recover transaction: %v", err) + return nil, fmt.Errorf("fail to recover transaction: %w", err) } if !isPending { break @@ -90,12 +90,12 @@ func _waitForTransaction( } receipt, err := client.TransactionReceipt(ctx, tx.Hash()) if err != nil { - return nil, fmt.Errorf("failed to get receipt: %v", err) + return nil, fmt.Errorf("failed to get receipt: %w", err) } if receipt.Status == types.ReceiptStatusFailed { reason, err := _traceTransaction(client, tx.Hash()) if err != nil { - return nil, fmt.Errorf("transaction failed; failed to get reason: %v", err) + return nil, fmt.Errorf("transaction failed; failed to get reason: %w", err) } return nil, fmt.Errorf("transaction failed: %v", reason) } From 5de0ee58b4b8d6a9166f4a78fbf7b86038476e9a Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:49:22 -0300 Subject: [PATCH 2/3] fix(cli): use estimated gas limit by default --- .../root/deploy/application.go | 3 +- .../root/deploy/authority.go | 3 +- cmd/cartesi-rollups-cli/root/deploy/quorum.go | 3 +- .../root/deposit/deposit.go | 7 ++- .../root/execute/execute.go | 3 +- .../root/foreclose/foreclose.go | 3 +- .../root/provedriveroot/provedriveroot.go | 3 +- cmd/cartesi-rollups-cli/root/root.go | 7 +++ cmd/cartesi-rollups-cli/root/send/send.go | 3 +- .../root/withdraw/withdraw.go | 3 +- internal/cli/ethereum.go | 31 +++++++++++++ internal/cli/ethereum_test.go | 44 +++++++++++++++++++ internal/config/generate/Config.toml | 7 +++ internal/config/generated.go | 16 +++++++ pkg/ethutil/ethutil.go | 9 ++-- pkg/ethutil/selfhosted.go | 2 +- pkg/ethutil/transaction.go | 9 ++-- 17 files changed, 124 insertions(+), 32 deletions(-) create mode 100644 internal/cli/ethereum.go create mode 100644 internal/cli/ethereum_test.go diff --git a/cmd/cartesi-rollups-cli/root/deploy/application.go b/cmd/cartesi-rollups-cli/root/deploy/application.go index db243bd9e..28ba66864 100644 --- a/cmd/cartesi-rollups-cli/root/deploy/application.go +++ b/cmd/cartesi-rollups-cli/root/deploy/application.go @@ -13,7 +13,6 @@ import ( "github.com/cartesi/rollups-node/cmd/cartesi-rollups-cli/util" "github.com/cartesi/rollups-node/internal/cli" "github.com/cartesi/rollups-node/internal/config" - "github.com/cartesi/rollups-node/internal/config/auth" "github.com/cartesi/rollups-node/internal/model" "github.com/cartesi/rollups-node/internal/repository/factory" "github.com/cartesi/rollups-node/pkg/contracts/iapplicationfactory" @@ -146,7 +145,7 @@ func runDeployApplication(cmd *cobra.Command, args []string) { chainId, err := client.ChainID(ctx) cobra.CheckErr(err) - txOpts, err := auth.GetTransactOpts(ctx, chainId) + txOpts, err := cli.GetTransactOpts(ctx, chainId) cobra.CheckErr(err) // pre deployment checks diff --git a/cmd/cartesi-rollups-cli/root/deploy/authority.go b/cmd/cartesi-rollups-cli/root/deploy/authority.go index 061eca3e3..9955cb93b 100644 --- a/cmd/cartesi-rollups-cli/root/deploy/authority.go +++ b/cmd/cartesi-rollups-cli/root/deploy/authority.go @@ -10,7 +10,6 @@ import ( "github.com/cartesi/rollups-node/internal/cli" "github.com/cartesi/rollups-node/internal/config" - "github.com/cartesi/rollups-node/internal/config/auth" "github.com/cartesi/rollups-node/pkg/contracts/iauthorityfactory" "github.com/cartesi/rollups-node/pkg/ethutil" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -73,7 +72,7 @@ func runDeployAuthority(cmd *cobra.Command, args []string) { chainId, err := client.ChainID(ctx) cobra.CheckErr(err) - txOpts, err := auth.GetTransactOpts(ctx, chainId) + txOpts, err := cli.GetTransactOpts(ctx, chainId) cobra.CheckErr(err) deployment, err := buildAuthorityDeployment(cmd, txOpts) diff --git a/cmd/cartesi-rollups-cli/root/deploy/quorum.go b/cmd/cartesi-rollups-cli/root/deploy/quorum.go index c9ca798e5..698125248 100644 --- a/cmd/cartesi-rollups-cli/root/deploy/quorum.go +++ b/cmd/cartesi-rollups-cli/root/deploy/quorum.go @@ -10,7 +10,6 @@ import ( "github.com/cartesi/rollups-node/internal/cli" "github.com/cartesi/rollups-node/internal/config" - "github.com/cartesi/rollups-node/internal/config/auth" "github.com/cartesi/rollups-node/pkg/contracts/iquorumfactory" "github.com/cartesi/rollups-node/pkg/ethutil" "github.com/ethereum/go-ethereum/common" @@ -73,7 +72,7 @@ func runDeployQuorum(cmd *cobra.Command, args []string) { chainID, err := client.ChainID(ctx) cobra.CheckErr(err) - txOpts, err := auth.GetTransactOpts(ctx, chainID) + txOpts, err := cli.GetTransactOpts(ctx, chainID) cobra.CheckErr(err) deployment, err := buildQuorumDeployment(cmd) diff --git a/cmd/cartesi-rollups-cli/root/deposit/deposit.go b/cmd/cartesi-rollups-cli/root/deposit/deposit.go index b7303bb76..cbaac1c31 100644 --- a/cmd/cartesi-rollups-cli/root/deposit/deposit.go +++ b/cmd/cartesi-rollups-cli/root/deposit/deposit.go @@ -13,7 +13,6 @@ import ( "github.com/cartesi/rollups-node/cmd/cartesi-rollups-cli/util" "github.com/cartesi/rollups-node/internal/cli" "github.com/cartesi/rollups-node/internal/config" - "github.com/cartesi/rollups-node/internal/config/auth" "github.com/cartesi/rollups-node/pkg/contracts/iapplication" "github.com/cartesi/rollups-node/pkg/contracts/ierc20errors" "github.com/cartesi/rollups-node/pkg/contracts/ierc20metadata" @@ -114,7 +113,7 @@ func runERC20(cmd *cobra.Command, args []string) { cobra.CheckErr(err) chainID, err := client.ChainID(ctx) cobra.CheckErr(err) - txOpts, err := auth.GetTransactOpts(ctx, chainID) + txOpts, err := cli.GetTransactOpts(ctx, chainID) cobra.CheckErr(err) if !skipConfirmation { @@ -138,7 +137,7 @@ func runERC20(cmd *cobra.Command, args []string) { if approveParam { token, err := ierc20metadata.NewIERC20Metadata(tokenAddr, client) cobra.CheckErr(err) - approveOpts, err := auth.GetTransactOpts(ctx, chainID) + approveOpts, err := cli.GetTransactOpts(ctx, chainID) cobra.CheckErr(err) tx, err := token.Approve(approveOpts, portalAddr, amount) cobra.CheckErr(cli.DecorateRevert(err, @@ -154,7 +153,7 @@ func runERC20(cmd *cobra.Command, args []string) { portal, err := ierc20portal.NewIERC20Portal(portalAddr, client) cobra.CheckErr(err) - depositOpts, err := auth.GetTransactOpts(ctx, chainID) + depositOpts, err := cli.GetTransactOpts(ctx, chainID) cobra.CheckErr(err) tx, err := portal.DepositERC20Tokens(depositOpts, tokenAddr, appAddr, amount, execData) // The revert can come from three layers: the portal itself diff --git a/cmd/cartesi-rollups-cli/root/execute/execute.go b/cmd/cartesi-rollups-cli/root/execute/execute.go index 38c403750..c6cb0cde5 100644 --- a/cmd/cartesi-rollups-cli/root/execute/execute.go +++ b/cmd/cartesi-rollups-cli/root/execute/execute.go @@ -13,7 +13,6 @@ import ( "github.com/cartesi/rollups-node/internal/cli" "github.com/cartesi/rollups-node/internal/config" - "github.com/cartesi/rollups-node/internal/config/auth" "github.com/cartesi/rollups-node/internal/repository/factory" "github.com/cartesi/rollups-node/pkg/contracts/iapplication" "github.com/cartesi/rollups-node/pkg/ethutil" @@ -99,7 +98,7 @@ func run(cmd *cobra.Command, args []string) { chainId, err := client.ChainID(ctx) cobra.CheckErr(err) - txOpts, err := auth.GetTransactOpts(ctx, chainId) + txOpts, err := cli.GetTransactOpts(ctx, chainId) cobra.CheckErr(err) if !skipConfirmation { diff --git a/cmd/cartesi-rollups-cli/root/foreclose/foreclose.go b/cmd/cartesi-rollups-cli/root/foreclose/foreclose.go index 800d2dfbb..2917e4e8b 100644 --- a/cmd/cartesi-rollups-cli/root/foreclose/foreclose.go +++ b/cmd/cartesi-rollups-cli/root/foreclose/foreclose.go @@ -16,7 +16,6 @@ import ( "github.com/cartesi/rollups-node/cmd/cartesi-rollups-cli/util" "github.com/cartesi/rollups-node/internal/cli" "github.com/cartesi/rollups-node/internal/config" - "github.com/cartesi/rollups-node/internal/config/auth" "github.com/cartesi/rollups-node/pkg/contracts/iapplication" ) @@ -90,7 +89,7 @@ func run(cmd *cobra.Command, args []string) { chainId, err := client.ChainID(ctx) cobra.CheckErr(err) - txOpts, err := auth.GetTransactOpts(ctx, chainId) + txOpts, err := cli.GetTransactOpts(ctx, chainId) cobra.CheckErr(err) appContract, err := iapplication.NewIApplication(appAddr, client) diff --git a/cmd/cartesi-rollups-cli/root/provedriveroot/provedriveroot.go b/cmd/cartesi-rollups-cli/root/provedriveroot/provedriveroot.go index 46482fc53..3a2714e5d 100644 --- a/cmd/cartesi-rollups-cli/root/provedriveroot/provedriveroot.go +++ b/cmd/cartesi-rollups-cli/root/provedriveroot/provedriveroot.go @@ -17,7 +17,6 @@ import ( "github.com/cartesi/rollups-node/cmd/cartesi-rollups-cli/util" "github.com/cartesi/rollups-node/internal/cli" "github.com/cartesi/rollups-node/internal/config" - "github.com/cartesi/rollups-node/internal/config/auth" "github.com/cartesi/rollups-node/pkg/contracts/iapplication" ) @@ -102,7 +101,7 @@ func run(cmd *cobra.Command, args []string) { chainID, err := client.ChainID(ctx) cobra.CheckErr(err) - txOpts, err := auth.GetTransactOpts(ctx, chainID) + txOpts, err := cli.GetTransactOpts(ctx, chainID) cobra.CheckErr(err) appContract, err := iapplication.NewIApplication(appAddr, client) diff --git a/cmd/cartesi-rollups-cli/root/root.go b/cmd/cartesi-rollups-cli/root/root.go index cd2a2289d..58ec16a07 100644 --- a/cmd/cartesi-rollups-cli/root/root.go +++ b/cmd/cartesi-rollups-cli/root/root.go @@ -34,6 +34,7 @@ var ( verbose bool databaseConnection string blockchainEndpoint string + gasLimit uint64 inputBoxAddress string ) @@ -56,6 +57,12 @@ func init() { cobra.CheckErr(viper.BindPFlag(config.BLOCKCHAIN_HTTP_ENDPOINT, Cmd.PersistentFlags().Lookup("blockchain-http-endpoint"))) cobra.CheckErr(Cmd.PersistentFlags().MarkHidden("blockchain-http-endpoint")) + // Blockchain gas limit + Cmd.PersistentFlags().Uint64Var(&gasLimit, "gas-limit", 0, + "Blockchain gas limit") + cobra.CheckErr(viper.BindPFlag(config.BLOCKCHAIN_GAS_LIMIT, Cmd.PersistentFlags().Lookup("gas-limit"))) + cobra.CheckErr(Cmd.PersistentFlags().MarkHidden("gas-limit")) + // Input box address flag Cmd.PersistentFlags().StringVar(&inputBoxAddress, "inputbox", "", "Input Box contract address") diff --git a/cmd/cartesi-rollups-cli/root/send/send.go b/cmd/cartesi-rollups-cli/root/send/send.go index 615d19677..2c2cd4009 100644 --- a/cmd/cartesi-rollups-cli/root/send/send.go +++ b/cmd/cartesi-rollups-cli/root/send/send.go @@ -12,7 +12,6 @@ import ( "github.com/cartesi/rollups-node/internal/cli" "github.com/cartesi/rollups-node/internal/config" - "github.com/cartesi/rollups-node/internal/config/auth" "github.com/cartesi/rollups-node/internal/repository/factory" "github.com/cartesi/rollups-node/pkg/contracts/iapplication" "github.com/cartesi/rollups-node/pkg/contracts/iinputbox" @@ -143,7 +142,7 @@ func run(cmd *cobra.Command, args []string) { chainId, err := client.ChainID(ctx) cobra.CheckErr(err) - txOpts, err := auth.GetTransactOpts(ctx, chainId) + txOpts, err := cli.GetTransactOpts(ctx, chainId) cobra.CheckErr(err) // Ask for confirmation unless --yes flag is set diff --git a/cmd/cartesi-rollups-cli/root/withdraw/withdraw.go b/cmd/cartesi-rollups-cli/root/withdraw/withdraw.go index 6f9dc75ad..4ce1b6085 100644 --- a/cmd/cartesi-rollups-cli/root/withdraw/withdraw.go +++ b/cmd/cartesi-rollups-cli/root/withdraw/withdraw.go @@ -19,7 +19,6 @@ import ( "github.com/cartesi/rollups-node/cmd/cartesi-rollups-cli/util" "github.com/cartesi/rollups-node/internal/cli" "github.com/cartesi/rollups-node/internal/config" - "github.com/cartesi/rollups-node/internal/config/auth" "github.com/cartesi/rollups-node/pkg/contracts/iapplication" "github.com/cartesi/rollups-node/pkg/ethutil" ) @@ -109,7 +108,7 @@ func run(cmd *cobra.Command, args []string) { chainID, err := client.ChainID(ctx) cobra.CheckErr(err) - txOpts, err := auth.GetTransactOpts(ctx, chainID) + txOpts, err := cli.GetTransactOpts(ctx, chainID) cobra.CheckErr(err) appContract, err := iapplication.NewIApplication(appAddr, client) diff --git a/internal/cli/ethereum.go b/internal/cli/ethereum.go new file mode 100644 index 000000000..c3de148ad --- /dev/null +++ b/internal/cli/ethereum.go @@ -0,0 +1,31 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package cli + +import ( + "context" + "errors" + "math/big" + + "github.com/cartesi/rollups-node/internal/config" + "github.com/cartesi/rollups-node/internal/config/auth" + "github.com/ethereum/go-ethereum/accounts/abi/bind" +) + +func GetTransactOpts(ctx context.Context, chainId *big.Int) (*bind.TransactOpts, error) { + txOpts, err := auth.GetTransactOpts(ctx, chainId) + if err != nil { + return nil, err + } + + gasLimit, err := config.GetBlockchainGasLimit() + if err != nil && !errors.Is(err, config.ErrNotDefined) { + return nil, err + } + + if gasLimit > 0 { + txOpts.GasLimit = gasLimit + } + return txOpts, err +} diff --git a/internal/cli/ethereum_test.go b/internal/cli/ethereum_test.go new file mode 100644 index 000000000..e3350382a --- /dev/null +++ b/internal/cli/ethereum_test.go @@ -0,0 +1,44 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package cli + +import ( + "context" + "math/big" + "testing" + + "github.com/cartesi/rollups-node/internal/config" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +const testPrivateKey = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" + +func TestGetTransactOptsBlockchainGasLimit(t *testing.T) { + setupPrivateKeyAuth := func(t *testing.T) { + t.Helper() + viper.Reset() + config.SetDefaults() + viper.Set(config.AUTH_KIND, "private_key") + viper.Set(config.AUTH_PRIVATE_KEY, testPrivateKey) + } + + t.Run("default zero leaves gas limit unset", func(t *testing.T) { + setupPrivateKeyAuth(t) + + txOpts, err := GetTransactOpts(context.Background(), big.NewInt(31337)) + require.NoError(t, err) + require.Zero(t, txOpts.GasLimit) + }) + + t.Run("non-zero config sets gas limit", func(t *testing.T) { + setupPrivateKeyAuth(t) + viper.Set(config.BLOCKCHAIN_GAS_LIMIT, "123456") + + txOpts, err := GetTransactOpts(context.Background(), big.NewInt(31337)) + require.NoError(t, err) + require.Equal(t, uint64(123456), txOpts.GasLimit) + }) +} diff --git a/internal/config/generate/Config.toml b/internal/config/generate/Config.toml index b94b9d8f6..c5d298a78 100644 --- a/internal/config/generate/Config.toml +++ b/internal/config/generate/Config.toml @@ -254,6 +254,13 @@ description = """ Maximum number of blocks in a single query to the provider. Queries with larger ranges will be broken into multiple smaller queries. Zero for unlimited.""" used-by = ["evmreader", "claimer", "node", "prt"] +[rollups.CARTESI_BLOCKCHAIN_GAS_LIMIT] +default = "0" +go-type = "uint64" +description = """ +Gas limit to be used on chain transactions. Zero for the estimated limit provided by the chain. Default is zero.""" +used-by = ["cli"] + # # Contracts # diff --git a/internal/config/generated.go b/internal/config/generated.go index 9576ec7a8..cab15bdc8 100644 --- a/internal/config/generated.go +++ b/internal/config/generated.go @@ -71,6 +71,7 @@ const ( JSONRPC_MACHINE_LOG_LEVEL = "CARTESI_JSONRPC_MACHINE_LOG_LEVEL" ADVANCER_INPUT_BATCH_SIZE = "CARTESI_ADVANCER_INPUT_BATCH_SIZE" ADVANCER_POLLING_INTERVAL = "CARTESI_ADVANCER_POLLING_INTERVAL" + BLOCKCHAIN_GAS_LIMIT = "CARTESI_BLOCKCHAIN_GAS_LIMIT" BLOCKCHAIN_HTTP_MAX_RETRIES = "CARTESI_BLOCKCHAIN_HTTP_MAX_RETRIES" BLOCKCHAIN_HTTP_REQUEST_TIMEOUT = "CARTESI_BLOCKCHAIN_HTTP_REQUEST_TIMEOUT" BLOCKCHAIN_HTTP_RETRY_MAX_WAIT = "CARTESI_BLOCKCHAIN_HTTP_RETRY_MAX_WAIT" @@ -197,6 +198,8 @@ func SetDefaults() { viper.SetDefault(ADVANCER_POLLING_INTERVAL, "3") + viper.SetDefault(BLOCKCHAIN_GAS_LIMIT, "0") + viper.SetDefault(BLOCKCHAIN_HTTP_MAX_RETRIES, "4") viper.SetDefault(BLOCKCHAIN_HTTP_REQUEST_TIMEOUT, "120") @@ -2360,6 +2363,19 @@ func GetAdvancerPollingInterval() (Duration, error) { return notDefinedDuration(), fmt.Errorf("%s: %w", ADVANCER_POLLING_INTERVAL, ErrNotDefined) } +// GetBlockchainGasLimit returns the value for the environment variable CARTESI_BLOCKCHAIN_GAS_LIMIT. +func GetBlockchainGasLimit() (uint64, error) { + s := viper.GetString(BLOCKCHAIN_GAS_LIMIT) + if s != "" { + v, err := toUint64(s) + if err != nil { + return v, fmt.Errorf("failed to parse %s: %w", BLOCKCHAIN_GAS_LIMIT, err) + } + return v, nil + } + return notDefineduint64(), fmt.Errorf("%s: %w", BLOCKCHAIN_GAS_LIMIT, ErrNotDefined) +} + // GetBlockchainHttpMaxRetries returns the value for the environment variable CARTESI_BLOCKCHAIN_HTTP_MAX_RETRIES. func GetBlockchainHttpMaxRetries() (uint64, error) { s := viper.GetString(BLOCKCHAIN_HTTP_MAX_RETRIES) diff --git a/pkg/ethutil/ethutil.go b/pkg/ethutil/ethutil.go index 9493dd4de..b89042dd8 100644 --- a/pkg/ethutil/ethutil.go +++ b/pkg/ethutil/ethutil.go @@ -20,9 +20,6 @@ import ( "github.com/ethereum/go-ethereum/ethclient" ) -// Gas limit when sending transactions. -const GasLimit = 30_000_000 - // Dev mnemonic used by Foundry/Anvil. const FoundryMnemonic = "test test test test test test test test test test test junk" @@ -51,7 +48,7 @@ func AddInput( return 0, 0, common.Hash{}, fmt.Errorf("failed to connect to InputBox contract: %w", err) } receipt, err := sendTransaction( - ctx, client, transactionOpts, big.NewInt(0), GasLimit, + ctx, client, transactionOpts, big.NewInt(0), func(txOpts *bind.TransactOpts) (*types.Transaction, error) { return inputBox.AddInput(txOpts, application, input) }, @@ -87,7 +84,7 @@ func AddInputAsync( if err != nil { return common.Hash{}, fmt.Errorf("failed to connect to InputBox contract: %w", err) } - txOpts, err := _prepareTransaction(ctx, client, transactionOpts, big.NewInt(0), GasLimit) + txOpts, err := _prepareTransaction(ctx, client, transactionOpts, big.NewInt(0)) if err != nil { return common.Hash{}, fmt.Errorf("failed to prepare transaction: %w", err) } @@ -216,7 +213,7 @@ func ExecuteOutput( return nil, fmt.Errorf("failed to connect to CartesiDapp contract: %w", err) } receipt, err := sendTransaction( - ctx, client, transactionOpts, big.NewInt(0), GasLimit, + ctx, client, transactionOpts, big.NewInt(0), func(txOpts *bind.TransactOpts) (*types.Transaction, error) { return app.ExecuteOutput(txOpts, output, proof) }, diff --git a/pkg/ethutil/selfhosted.go b/pkg/ethutil/selfhosted.go index e7a556009..cd2582e8f 100644 --- a/pkg/ethutil/selfhosted.go +++ b/pkg/ethutil/selfhosted.go @@ -129,7 +129,7 @@ func (me *SelfhostedApplicationDeployment) Deploy( // deploy the contracts receipt, err := sendTransaction( - ctx, client, txOpts, big.NewInt(0), GasLimit, + ctx, client, txOpts, big.NewInt(0), func(txOpts *bind.TransactOpts) (*types.Transaction, error) { result.ApplicationFactoryAddress, err = factory.GetApplicationFactory(nil) if err != nil { diff --git a/pkg/ethutil/transaction.go b/pkg/ethutil/transaction.go index 7c6bcace8..e85066ef3 100644 --- a/pkg/ethutil/transaction.go +++ b/pkg/ethutil/transaction.go @@ -24,14 +24,15 @@ func sendTransaction( client *ethclient.Client, txOpts *bind.TransactOpts, txValue *big.Int, - gasLimit uint64, doSend func(txOpts *bind.TransactOpts) (*types.Transaction, error), ) (*types.Receipt, error) { - txOpts, err := _prepareTransaction(ctx, client, txOpts, txValue, gasLimit) + txOpts, err := _prepareTransaction(ctx, client, txOpts, txValue) if err != nil { return nil, fmt.Errorf("failed to prepare transaction: %w", err) } - tx, err := doSend(txOpts) + txOptsCopy := *txOpts + txOptsCopy.Context = ctx + tx, err := doSend(&txOptsCopy) if err != nil { return nil, fmt.Errorf("failed to send transaction: %w", err) } @@ -48,7 +49,6 @@ func _prepareTransaction( client *ethclient.Client, txOpts *bind.TransactOpts, txValue *big.Int, - gasLimit uint64, ) (*bind.TransactOpts, error) { nonce, err := client.PendingNonceAt(ctx, txOpts.From) if err != nil { @@ -62,7 +62,6 @@ func _prepareTransaction( nonceBigInt.SetUint64(nonce) txOpts.Nonce = nonceBigInt txOpts.Value = txValue - txOpts.GasLimit = gasLimit txOpts.GasPrice = gasPrice return txOpts, nil } From bfbc92b3e72c388e0cedd3faf54354718c56a95b Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:21:14 -0300 Subject: [PATCH 3/3] fix(ethclient): use the proper 'context.Context' on chain trasactions --- .../root/deploy/application.go | 2 +- .../root/execute/execute.go | 2 +- cmd/cartesi-rollups-cli/root/send/send.go | 6 +- internal/claimer/accept.go | 2 +- internal/claimer/blockchain.go | 34 ++++--- internal/claimer/mocks_test.go | 2 + internal/claimer/service.go | 14 +-- internal/claimer/submit.go | 2 +- internal/cli/ethereum.go | 7 +- internal/config/auth/auth.go | 16 ++- internal/kms/signtx.go | 54 ++++++++-- internal/kms/signtx_test.go | 99 ++++++++++++++++++- internal/prt/prt.go | 18 +++- internal/prt/service.go | 5 +- pkg/ethutil/anvil.go | 2 +- pkg/ethutil/application.go | 9 +- pkg/ethutil/ethutil.go | 16 +-- pkg/ethutil/ethutil_test.go | 85 +++++++++++++++- pkg/ethutil/prt.go | 7 +- pkg/ethutil/selfhosted.go | 4 +- pkg/ethutil/transaction.go | 40 ++++++-- 21 files changed, 354 insertions(+), 72 deletions(-) diff --git a/cmd/cartesi-rollups-cli/root/deploy/application.go b/cmd/cartesi-rollups-cli/root/deploy/application.go index 28ba66864..991a3b538 100644 --- a/cmd/cartesi-rollups-cli/root/deploy/application.go +++ b/cmd/cartesi-rollups-cli/root/deploy/application.go @@ -237,7 +237,7 @@ func runDeployApplication(cmd *cobra.Command, args []string) { if verboseParam || !asJSONParam { fmt.Fprint(os.Stderr, "deploying...") } - _, result, err := deployment.Deploy(ctx, client, txOpts) + _, result, err := deployment.Deploy(ctx, client, ethutil.NewStaticTransactOptsFactory(txOpts)) // The revert surface spans the variant's factory plus the constructors it // invokes; selectors are content-matched, so passing every factory ABI is // harmless and covers all three deployment variants. diff --git a/cmd/cartesi-rollups-cli/root/execute/execute.go b/cmd/cartesi-rollups-cli/root/execute/execute.go index c6cb0cde5..451abe678 100644 --- a/cmd/cartesi-rollups-cli/root/execute/execute.go +++ b/cmd/cartesi-rollups-cli/root/execute/execute.go @@ -116,7 +116,7 @@ func run(cmd *cobra.Command, args []string) { txHash, err := ethutil.ExecuteOutput( ctx, client, - txOpts, + ethutil.NewStaticTransactOptsFactory(txOpts), app.IApplicationAddress, outputIndex, output.RawData, diff --git a/cmd/cartesi-rollups-cli/root/send/send.go b/cmd/cartesi-rollups-cli/root/send/send.go index 2c2cd4009..c96af0395 100644 --- a/cmd/cartesi-rollups-cli/root/send/send.go +++ b/cmd/cartesi-rollups-cli/root/send/send.go @@ -145,6 +145,8 @@ func run(cmd *cobra.Command, args []string) { txOpts, err := cli.GetTransactOpts(ctx, chainId) cobra.CheckErr(err) + txOptsFactory := ethutil.NewStaticTransactOptsFactory(txOpts) + // Ask for confirmation unless --yes flag is set if !skipConfirmation { fmt.Printf("Preparing to send input to application %v (%v) with account %v\n", @@ -158,7 +160,7 @@ func run(cmd *cobra.Command, args []string) { } if asyncMode { - txHash, err := ethutil.AddInputAsync(ctx, client, txOpts, iboxAddr, app.IApplicationAddress, payload) + txHash, err := ethutil.AddInputAsync(ctx, client, txOptsFactory, iboxAddr, app.IApplicationAddress, payload) cobra.CheckErr(cli.DecorateRevert(err, iinputbox.IInputBoxMetaData, iapplication.IApplicationMetaData)) if asJSONParam { result := cli.SendResult{ @@ -174,7 +176,7 @@ func run(cmd *cobra.Command, args []string) { return } - inputIndex, blockNumber, txHash, err := ethutil.AddInput(ctx, client, txOpts, iboxAddr, app.IApplicationAddress, payload) + inputIndex, blockNumber, txHash, err := ethutil.AddInput(ctx, client, txOptsFactory, iboxAddr, app.IApplicationAddress, payload) cobra.CheckErr(cli.DecorateRevert(err, iinputbox.IInputBoxMetaData, iapplication.IApplicationMetaData)) if asJSONParam { diff --git a/internal/claimer/accept.go b/internal/claimer/accept.go index 9499a1278..18e1ad9d6 100644 --- a/internal/claimer/accept.go +++ b/internal/claimer/accept.go @@ -378,7 +378,7 @@ func (s *Service) broadcastAcceptClaimOrReconcileRevert( return claimRetryLater(err) } - txHash, err := s.blockchain.acceptClaimOnBlockchain(app, currEpoch) + txHash, err := s.blockchain.acceptClaimOnBlockchain(s.Context, app, currEpoch) if err != nil { outcome, stateErr := s.handleAcceptClaimRevert(err, app, currEpoch) switch outcome { diff --git a/internal/claimer/blockchain.go b/internal/claimer/blockchain.go index a87beb59f..29c59b19b 100644 --- a/internal/claimer/blockchain.go +++ b/internal/claimer/blockchain.go @@ -52,12 +52,14 @@ type iclaimerBlockchain interface { ) submitClaimToBlockchain( + ctx context.Context, ic *iconsensus.IConsensus, application *model.Application, epoch *model.Epoch, ) (common.Hash, error) acceptClaimOnBlockchain( + ctx context.Context, application *model.Application, epoch *model.Epoch, ) (common.Hash, error) @@ -100,27 +102,28 @@ type iclaimerBlockchain interface { } type claimerBlockchain struct { - client *ethclient.Client - txOpts *bind.TransactOpts - logger *slog.Logger - defaultBlock config.DefaultBlock + client *ethclient.Client + txOptsFactory ethutil.TransactOptsFactory + logger *slog.Logger + defaultBlock config.DefaultBlock } func (cb *claimerBlockchain) claimSubmitterAddress() (common.Address, bool) { - if cb.txOpts == nil { + if cb.txOptsFactory == nil { return common.Address{}, false } - return cb.txOpts.From, true + return cb.txOptsFactory.From(), true } func (cb *claimerBlockchain) submitClaimToBlockchain( + ctx context.Context, ic *iconsensus.IConsensus, application *model.Application, epoch *model.Epoch, ) (common.Hash, error) { txHash := common.Hash{} - if cb.txOpts == nil { - return txHash, fmt.Errorf("txOpts is required for claim submission") + if cb.txOptsFactory == nil { + return txHash, fmt.Errorf("txOptsFactory is required for claim submission") } if epoch.OutputsMerkleRoot == nil { return txHash, fmt.Errorf( @@ -140,8 +143,12 @@ func (cb *claimerBlockchain) submitClaimToBlockchain( for i, h := range epoch.OutputsMerkleProof { proof[i] = h } + txOpts, err := cb.txOptsFactory.NewTransactOpts(ctx) + if err != nil { + return txHash, fmt.Errorf("creating transaction options for claim submission: %w", err) + } lastBlockNumber := new(big.Int).SetUint64(epoch.LastBlock) - tx, err := ic.SubmitClaim(cb.txOpts, application.IApplicationAddress, + tx, err := ic.SubmitClaim(txOpts, application.IApplicationAddress, lastBlockNumber, *epoch.OutputsMerkleRoot, proof) if err != nil { cb.logger.Warn("submitClaimToBlockchain:failed", @@ -406,11 +413,12 @@ func (cb *claimerBlockchain) getConsensusAddress( // ClaimStagingPeriodNotOverYet if the math is off; the caller handles that // revert via handleAcceptClaimRevert. func (cb *claimerBlockchain) acceptClaimOnBlockchain( + ctx context.Context, application *model.Application, epoch *model.Epoch, ) (common.Hash, error) { txHash := common.Hash{} - if cb.txOpts == nil { + if cb.txOptsFactory == nil { return txHash, fmt.Errorf("txOpts is required for claim acceptance") } if epoch.MachineHash == nil { @@ -422,8 +430,12 @@ func (cb *claimerBlockchain) acceptClaimOnBlockchain( if err != nil { return txHash, fmt.Errorf("creating IConsensus binding for acceptClaim: %w", err) } + txOpts, err := cb.txOptsFactory.NewTransactOpts(ctx) + if err != nil { + return txHash, fmt.Errorf("creating transaction options for claim acceptance: %w", err) + } lastBlockNumber := new(big.Int).SetUint64(epoch.LastBlock) - tx, err := ic.AcceptClaim(cb.txOpts, application.IApplicationAddress, + tx, err := ic.AcceptClaim(txOpts, application.IApplicationAddress, lastBlockNumber, *epoch.MachineHash) if err != nil { cb.logger.Warn("acceptClaimOnBlockchain:failed", diff --git a/internal/claimer/mocks_test.go b/internal/claimer/mocks_test.go index 576ff79b6..36844791e 100644 --- a/internal/claimer/mocks_test.go +++ b/internal/claimer/mocks_test.go @@ -297,6 +297,7 @@ func (m *claimerBlockchainMock) findClaimAcceptedEventAndSucc( } func (m *claimerBlockchainMock) submitClaimToBlockchain( + ctx context.Context, instance *iconsensus.IConsensus, app *model.Application, epoch *model.Epoch, @@ -306,6 +307,7 @@ func (m *claimerBlockchainMock) submitClaimToBlockchain( } func (m *claimerBlockchainMock) acceptClaimOnBlockchain( + ctx context.Context, app *model.Application, epoch *model.Epoch, ) (common.Hash, error) { diff --git a/internal/claimer/service.go b/internal/claimer/service.go index 8495f4b15..aadcb505c 100644 --- a/internal/claimer/service.go +++ b/internal/claimer/service.go @@ -13,9 +13,9 @@ import ( "github.com/cartesi/rollups-node/internal/config/auth" "github.com/cartesi/rollups-node/internal/model" "github.com/cartesi/rollups-node/internal/repository" + "github.com/cartesi/rollups-node/pkg/ethutil" "github.com/cartesi/rollups-node/pkg/service" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/ethclient" ) @@ -126,9 +126,9 @@ func Create(ctx context.Context, c *CreateInfo) (*Service, error) { s.maxAcceptAttempts = defaultMaxAcceptAttempts } - var txOpts *bind.TransactOpts = nil + var txOptsFactory ethutil.TransactOptsFactory if s.submissionEnabled { - txOpts, err = auth.GetTransactOpts(ctx, chainId) + txOptsFactory, err = auth.GetTransactOptsFactory(ctx, chainId) if err != nil { return nil, fmt.Errorf("getting transaction options: %w", err) } @@ -136,10 +136,10 @@ func Create(ctx context.Context, c *CreateInfo) (*Service, error) { s.repository = c.Repository s.blockchain = &claimerBlockchain{ - logger: s.Logger, - client: c.EthConn, - txOpts: txOpts, - defaultBlock: nodeConfig.DefaultBlock, + logger: s.Logger, + client: c.EthConn, + txOptsFactory: txOptsFactory, + defaultBlock: nodeConfig.DefaultBlock, } return s, nil diff --git a/internal/claimer/submit.go b/internal/claimer/submit.go index c2c2c490f..035c65d31 100644 --- a/internal/claimer/submit.go +++ b/internal/claimer/submit.go @@ -425,7 +425,7 @@ func (s *Service) broadcastComputedClaim( "outputs_merkle_root", hashToHex(currEpoch.OutputsMerkleRoot), "last_block", currEpoch.LastBlock, ) - txHash, err := s.blockchain.submitClaimToBlockchain(ic, app, currEpoch) + txHash, err := s.blockchain.submitClaimToBlockchain(s.Context, ic, app, currEpoch) if err != nil { switch outcome, stateErr := s.handleSubmitClaimRevert(err, app, currEpoch); outcome { case submitClaimAlreadyOnChain: diff --git a/internal/cli/ethereum.go b/internal/cli/ethereum.go index c3de148ad..363d6ecfb 100644 --- a/internal/cli/ethereum.go +++ b/internal/cli/ethereum.go @@ -14,7 +14,12 @@ import ( ) func GetTransactOpts(ctx context.Context, chainId *big.Int) (*bind.TransactOpts, error) { - txOpts, err := auth.GetTransactOpts(ctx, chainId) + factory, err := auth.GetTransactOptsFactory(ctx, chainId) + if err != nil { + return nil, err + } + + txOpts, err := factory.NewTransactOpts(ctx) if err != nil { return nil, err } diff --git a/internal/config/auth/auth.go b/internal/config/auth/auth.go index ad19824f1..408f7b13a 100644 --- a/internal/config/auth/auth.go +++ b/internal/config/auth/auth.go @@ -21,7 +21,7 @@ import ( "github.com/cartesi/rollups-node/pkg/ethutil" ) -func GetTransactOpts(ctx context.Context, chainId *big.Int) (*bind.TransactOpts, error) { +func GetTransactOptsFactory(ctx context.Context, chainId *big.Int) (ethutil.TransactOptsFactory, error) { authKind, err := GetAuthKind() if err != nil { return nil, err @@ -40,7 +40,11 @@ func GetTransactOpts(ctx context.Context, chainId *big.Int) (*bind.TransactOpts, if err != nil { return nil, err } - return bind.NewKeyedTransactorWithChainID(privateKey, chainId) + txOpts, err := bind.NewKeyedTransactorWithChainID(privateKey, chainId) + if err != nil { + return nil, err + } + return ethutil.NewStaticTransactOptsFactory(txOpts), nil case AuthKindPrivateKeyVar: privateKey, err := GetAuthPrivateKey() if err != nil { @@ -50,7 +54,11 @@ func GetTransactOpts(ctx context.Context, chainId *big.Int) (*bind.TransactOpts, if err != nil { return nil, err } - return bind.NewKeyedTransactorWithChainID(key, chainId) + txOpts, err := bind.NewKeyedTransactorWithChainID(key, chainId) + if err != nil { + return nil, err + } + return ethutil.NewStaticTransactOptsFactory(txOpts), nil case AuthKindAWS: awsc, err := aws_cfg.LoadDefaultConfig(ctx) if err != nil { @@ -61,7 +69,7 @@ func GetTransactOpts(ctx context.Context, chainId *big.Int) (*bind.TransactOpts, if err != nil { return nil, err } - return signtx.CreateAWSTransactOpts( + return signtx.CreateAWSTransactOptsFactory( ctx, kmsConfig, aws.String(authAwsKmsKeyId.Value), diff --git a/internal/kms/signtx.go b/internal/kms/signtx.go index 4d2ad2fc5..8dfb11b27 100644 --- a/internal/kms/signtx.go +++ b/internal/kms/signtx.go @@ -27,7 +27,12 @@ import ( /* This is the signature for a function that takes a transaction, passes it * through a signer to obtain a message digest, creates a signature and embeds * it into the transaction itself. */ -type SignTxFn = func(tx *types.Transaction, s types.Signer) (*types.Transaction, error) +type SignTxFn = func(ctx context.Context, tx *types.Transaction, s types.Signer) (*types.Transaction, error) + +type Client interface { + GetPublicKey(context.Context, *kms.GetPublicKeyInput, ...func(*kms.Options)) (*kms.GetPublicKeyOutput, error) + Sign(context.Context, *kms.SignInput, ...func(*kms.Options)) (*kms.SignOutput, error) +} /* AWS sometimes reply with a `r` larger than 32bytes padded on the left with * zeros. Trim it down to a total of 32bytes */ @@ -90,7 +95,7 @@ func assembleSignature(r []byte, s []byte, hash []byte, key []byte) ([]byte, err * the signing key. */ func CreateAWSSignTxFn( ctx context.Context, - client *kms.Client, + client Client, arn *string, ) (SignTxFn, *ecdsa.PublicKey, common.Address, error) { publicKeyBytes, err := GetPublicKeyBytes(ctx, client, arn) @@ -101,7 +106,7 @@ func CreateAWSSignTxFn( if err != nil { return nil, nil, common.Address{}, err } - return func(tx *types.Transaction, signer types.Signer) (*types.Transaction, error) { + return func(ctx context.Context, tx *types.Transaction, signer types.Signer) (*types.Transaction, error) { hash := signer.Hash(tx).Bytes() signOutput, err := client.Sign(ctx, &kms.SignInput{ KeyId: arn, @@ -138,7 +143,7 @@ func CreateAWSSignTxFn( }, publicKey, crypto.PubkeyToAddress(*publicKey), nil } -func GetPublicKeyBytes(ctx context.Context, client *kms.Client, Arn *string) ([]byte, error) { +func GetPublicKeyBytes(ctx context.Context, client Client, Arn *string) ([]byte, error) { publicKeyOutput, err := client.GetPublicKey(ctx, &kms.GetPublicKeyInput{ KeyId: Arn, }) @@ -172,24 +177,55 @@ func GetPublicKeyBytes(ctx context.Context, client *kms.Client, Arn *string) ([] * similar to NewKeyedTransactorWithChainID */ func CreateAWSTransactOpts( ctx context.Context, - client *kms.Client, + client Client, arn *string, signer types.Signer, ) (*bind.TransactOpts, error) { - SignTxFn, _, keyAddress, err := CreateAWSSignTxFn(ctx, client, arn) + factory, err := CreateAWSTransactOptsFactory(ctx, client, arn, signer) if err != nil { return nil, err } + return factory.NewTransactOpts(ctx) +} + +func CreateAWSTransactOptsFactory( + ctx context.Context, + client Client, + arn *string, + signer types.Signer, +) (*AWSTransactOptsFactory, error) { + signTxFn, _, keyAddress, err := CreateAWSSignTxFn(ctx, client, arn) + if err != nil { + return nil, err + } + return &AWSTransactOptsFactory{ + signTxFn: signTxFn, + address: keyAddress, + signer: signer, + }, nil +} + +type AWSTransactOptsFactory struct { + signTxFn SignTxFn + address common.Address + signer types.Signer +} + +func (f *AWSTransactOptsFactory) From() common.Address { + return f.address +} + +func (f *AWSTransactOptsFactory) NewTransactOpts(ctx context.Context) (*bind.TransactOpts, error) { return &bind.TransactOpts{ - From: keyAddress, + From: f.address, Signer: func( address common.Address, tx *types.Transaction, ) (*types.Transaction, error) { - if address != keyAddress { + if address != f.address { return nil, bind.ErrNotAuthorized } - return SignTxFn(tx, signer) + return f.signTxFn(ctx, tx, f.signer) }, Context: ctx, }, nil diff --git a/internal/kms/signtx_test.go b/internal/kms/signtx_test.go index 9509d06d9..a4d7d422d 100644 --- a/internal/kms/signtx_test.go +++ b/internal/kms/signtx_test.go @@ -6,26 +6,30 @@ package kms import ( "context" "crypto/ecdsa" + "crypto/rand" + "encoding/asn1" "math/big" "testing" "github.com/cartesi/rollups-node/pkg/ethutil" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" awscfg "github.com/aws/aws-sdk-go-v2/config" awskms "github.com/aws/aws-sdk-go-v2/service/kms" + kmstypes "github.com/aws/aws-sdk-go-v2/service/kms/types" + "github.com/stretchr/testify/require" ) var ARN = "" /* Create a SignTxFn from a private key. Useful for testing */ func CreateSignTxFnFromPrivateKey(privateKey *ecdsa.PrivateKey) SignTxFn { - return func(tx *types.Transaction, s types.Signer) (*types.Transaction, error) { - return types.SignTx(tx, s, privateKey) + return func(_ context.Context, tx *ethtypes.Transaction, s ethtypes.Signer) (*ethtypes.Transaction, error) { + return ethtypes.SignTx(tx, s, privateKey) } } @@ -51,12 +55,12 @@ func sendFunds( panic(err) } var data []byte - tx := types.NewTransaction(nonce, recipient, value, gasLimit, gasPrice, data) + tx := ethtypes.NewTransaction(nonce, recipient, value, gasLimit, gasPrice, data) chainID, err := client.NetworkID(context.Background()) if err != nil { panic(err) } - signedTx, err := SignTx(tx, types.NewEIP155Signer(chainID)) + signedTx, err := SignTx(ctx, tx, ethtypes.NewEIP155Signer(chainID)) if err != nil { panic(err) } @@ -95,3 +99,88 @@ func TestSignTx(t *testing.T) { sendFunds(value10, SignTx, context.Background(), KMSAddress, anvilAddress) } + +func TestAWSTransactOptsFactorySignsWithSubmitContext(t *testing.T) { + privateKey, err := crypto.GenerateKey() + require.NoError(t, err) + + client := newFakeKMSClient(t, privateKey) + arn := "alias/test-key" + startupCtx, cancelStartup := context.WithCancel(context.Background()) + factory, err := CreateAWSTransactOptsFactory( + startupCtx, + client, + &arn, + ethtypes.NewEIP155Signer(big.NewInt(1)), + ) + require.NoError(t, err) + cancelStartup() + + type contextKey string + submitCtx := context.WithValue(context.Background(), contextKey("phase"), "submit") + opts, err := factory.NewTransactOpts(submitCtx) + require.NoError(t, err) + + tx := ethtypes.NewTransaction(0, common.Address{0x01}, big.NewInt(1), 21000, big.NewInt(1), nil) + _, err = opts.Signer(opts.From, tx) + require.NoError(t, err) + require.Equal(t, "submit", client.signContext.Value(contextKey("phase"))) + require.NoError(t, client.signContext.Err()) +} + +type fakeKMSClient struct { + t *testing.T + privateKey *ecdsa.PrivateKey + publicKey []byte + signContext context.Context +} + +func newFakeKMSClient(t *testing.T, privateKey *ecdsa.PrivateKey) *fakeKMSClient { + t.Helper() + publicKey, err := asn1.Marshal(struct { + Algorithm struct { + Algorithm asn1.ObjectIdentifier + Parameters asn1.ObjectIdentifier + } + SubjectPublicKey asn1.BitString + }{ + Algorithm: struct { + Algorithm asn1.ObjectIdentifier + Parameters asn1.ObjectIdentifier + }{ + Algorithm: asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}, + Parameters: asn1.ObjectIdentifier{1, 3, 132, 0, 10}, + }, + SubjectPublicKey: asn1.BitString{Bytes: crypto.FromECDSAPub(&privateKey.PublicKey)}, + }) + require.NoError(t, err) + return &fakeKMSClient{t: t, privateKey: privateKey, publicKey: publicKey} +} + +func (f *fakeKMSClient) GetPublicKey( + context.Context, + *awskms.GetPublicKeyInput, + ...func(*awskms.Options), +) (*awskms.GetPublicKeyOutput, error) { + return &awskms.GetPublicKeyOutput{PublicKey: f.publicKey}, nil +} + +func (f *fakeKMSClient) Sign( + ctx context.Context, + input *awskms.SignInput, + _ ...func(*awskms.Options), +) (*awskms.SignOutput, error) { + f.signContext = ctx + r, s, err := ecdsa.Sign(rand.Reader, f.privateKey, input.Message) + require.NoError(f.t, err) + signature, err := asn1.Marshal(struct { + R *big.Int + S *big.Int + }{R: r, S: s}) + require.NoError(f.t, err) + return &awskms.SignOutput{ + KeyId: input.KeyId, + SigningAlgorithm: kmstypes.SigningAlgorithmSpecEcdsaSha256, + Signature: signature, + }, nil +} diff --git a/internal/prt/prt.go b/internal/prt/prt.go index a03cf41be..5cc81d173 100644 --- a/internal/prt/prt.go +++ b/internal/prt/prt.go @@ -718,7 +718,14 @@ func (s *Service) trySettle(ctx context.Context, app *Application, mostRecentBlo s.Logger.Info("Sending Settle transaction", "application", app.Name, "epoch_index", epoch.Index, "outputs_merkle_root", epoch.OutputsMerkleRoot.String()) - tx, err := consensus.Settle(s.txOpts, result.EpochNumber, + if s.txOptsFactory == nil { + return fmt.Errorf("txOpts is required for settlement") + } + txOpts, err := s.txOptsFactory.NewTransactOpts(ctx) + if err != nil { + return fmt.Errorf("creating transaction options for settlement: %w", err) + } + tx, err := consensus.Settle(txOpts, result.EpochNumber, *epoch.OutputsMerkleRoot, hashSliceToByteSlice(epoch.OutputsMerkleProof)) if err != nil { return s.handleSettleRevert(ctx, app, result.EpochNumber.Uint64(), err) @@ -955,7 +962,14 @@ func (s *Service) reactToTournament(ctx context.Context, app *Application, mostR return err } - txOptsWithValue := *s.txOpts + if s.txOptsFactory == nil { + return fmt.Errorf("txOpts is required for joining tournament") + } + txOpts, err := s.txOptsFactory.NewTransactOpts(ctx) + if err != nil { + return fmt.Errorf("creating transaction options for joining tournament: %w", err) + } + txOptsWithValue := *txOpts txOptsWithValue.Value = bondValue // FIXME move this to constants diff --git a/internal/prt/service.go b/internal/prt/service.go index 4847b054f..6ce716825 100644 --- a/internal/prt/service.go +++ b/internal/prt/service.go @@ -15,7 +15,6 @@ import ( "github.com/cartesi/rollups-node/internal/repository" "github.com/cartesi/rollups-node/pkg/ethutil" "github.com/cartesi/rollups-node/pkg/service" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" ) @@ -35,7 +34,7 @@ type Service struct { adapterFactory AdapterFactory submissionEnabled bool filter ethutil.Filter - txOpts *bind.TransactOpts + txOptsFactory ethutil.TransactOptsFactory currentEpochIndex map[int64]uint64 // application.ID -> epochIndex settleInFlight map[int64]*common.Hash // application.ID -> txHash joinInFlight map[int64]*common.Hash // application.ID -> txHash @@ -112,7 +111,7 @@ func Create(ctx context.Context, c *CreateInfo) (*Service, error) { s.joinInFlight = map[int64]*common.Hash{} if s.submissionEnabled { - s.txOpts, err = auth.GetTransactOpts(ctx, chainID) + s.txOptsFactory, err = auth.GetTransactOptsFactory(ctx, chainID) if err != nil { return nil, err } diff --git a/pkg/ethutil/anvil.go b/pkg/ethutil/anvil.go index c20d49275..d07e31177 100644 --- a/pkg/ethutil/anvil.go +++ b/pkg/ethutil/anvil.go @@ -61,7 +61,7 @@ func CreateAnvilSnapshotAndDeployApp(ctx context.Context, client *ethclient.Clie EpochLength: 10, Salt: [32]byte{}, } - applicationAddress, _, err := deployment.Deploy(ctx, client, txOpts) + applicationAddress, _, err := deployment.Deploy(ctx, client, NewStaticTransactOptsFactory(txOpts)) if err != nil { _ = RevertToAnvilSnapshot(client.Client(), snapshotID) return zero, nil, fmt.Errorf("failed to deploy self-hosted application: %w", err) diff --git a/pkg/ethutil/application.go b/pkg/ethutil/application.go index ca283ded3..3e0e7cc5b 100644 --- a/pkg/ethutil/application.go +++ b/pkg/ethutil/application.go @@ -14,7 +14,7 @@ import ( ) type IApplicationDeployment interface { - Deploy(ctx context.Context, client *ethclient.Client, txOpts *bind.TransactOpts) (common.Address, IApplicationDeploymentResult, error) + Deploy(ctx context.Context, client *ethclient.Client, txOptsFactory TransactOptsFactory) (common.Address, IApplicationDeploymentResult, error) GetFactoryAddress() common.Address } type IApplicationDeploymentResult interface{} @@ -71,7 +71,7 @@ func (me *ApplicationDeploymentResult) String() string { func (me *ApplicationDeployment) Deploy( ctx context.Context, client *ethclient.Client, - txOpts *bind.TransactOpts, + txOptsFactory TransactOptsFactory, ) (common.Address, IApplicationDeploymentResult, error) { zero := common.Address{} result := &ApplicationDeploymentResult{} @@ -103,6 +103,11 @@ func (me *ApplicationDeployment) Deploy( } // deploy the contracts + txOpts, err := txOptsFactory.NewTransactOpts(ctx) + if err != nil { + return zero, nil, fmt.Errorf("failed to create transaction options: %w", err) + } + tx, err := factory.NewApplication0(txOpts, me.Consensus, me.OwnerAddress, me.TemplateHash, me.DataAvailability, me.WithdrawalConfig, me.Salt) if err != nil { return zero, nil, fmt.Errorf("transaction failed: %w", err) diff --git a/pkg/ethutil/ethutil.go b/pkg/ethutil/ethutil.go index b89042dd8..5d31f7e67 100644 --- a/pkg/ethutil/ethutil.go +++ b/pkg/ethutil/ethutil.go @@ -35,7 +35,7 @@ func TrimHex(s string) string { func AddInput( ctx context.Context, client *ethclient.Client, - transactionOpts *bind.TransactOpts, + txOptsFactory TransactOptsFactory, inputBoxAddress common.Address, application common.Address, input []byte, @@ -48,7 +48,7 @@ func AddInput( return 0, 0, common.Hash{}, fmt.Errorf("failed to connect to InputBox contract: %w", err) } receipt, err := sendTransaction( - ctx, client, transactionOpts, big.NewInt(0), + ctx, client, txOptsFactory, big.NewInt(0), func(txOpts *bind.TransactOpts) (*types.Transaction, error) { return inputBox.AddInput(txOpts, application, input) }, @@ -72,7 +72,7 @@ func AddInput( func AddInputAsync( ctx context.Context, client *ethclient.Client, - transactionOpts *bind.TransactOpts, + txOptsFactory TransactOptsFactory, inputBoxAddress common.Address, application common.Address, input []byte, @@ -84,11 +84,13 @@ func AddInputAsync( if err != nil { return common.Hash{}, fmt.Errorf("failed to connect to InputBox contract: %w", err) } - txOpts, err := _prepareTransaction(ctx, client, transactionOpts, big.NewInt(0)) + txOpts, err := _prepareTransaction(ctx, client, txOptsFactory, big.NewInt(0)) if err != nil { return common.Hash{}, fmt.Errorf("failed to prepare transaction: %w", err) } - tx, err := inputBox.AddInput(txOpts, application, input) + txOptsCopy := *txOpts + txOptsCopy.Context = ctx + tx, err := inputBox.AddInput(&txOptsCopy, application, input) if err != nil { return common.Hash{}, fmt.Errorf("failed to send transaction: %w", err) } @@ -190,7 +192,7 @@ func ValidateOutput( func ExecuteOutput( ctx context.Context, client *ethclient.Client, - transactionOpts *bind.TransactOpts, + txOptsFactory TransactOptsFactory, appAddr common.Address, index uint64, output []byte, @@ -213,7 +215,7 @@ func ExecuteOutput( return nil, fmt.Errorf("failed to connect to CartesiDapp contract: %w", err) } receipt, err := sendTransaction( - ctx, client, transactionOpts, big.NewInt(0), + ctx, client, txOptsFactory, big.NewInt(0), func(txOpts *bind.TransactOpts) (*types.Transaction, error) { return app.ExecuteOutput(txOpts, output, proof) }, diff --git a/pkg/ethutil/ethutil_test.go b/pkg/ethutil/ethutil_test.go index d8fbb9c17..cf9ddebb6 100644 --- a/pkg/ethutil/ethutil_test.go +++ b/pkg/ethutil/ethutil_test.go @@ -6,6 +6,8 @@ package ethutil import ( "context" "crypto/rand" + "errors" + "math/big" "os" "sync" "testing" @@ -15,7 +17,11 @@ import ( "github.com/cartesi/rollups-node/pkg/contracts/inputs" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -29,7 +35,7 @@ type EthUtilSuite struct { cancel context.CancelFunc client *ethclient.Client endpoint config.URL - txOpts *bind.TransactOpts + txOptsFactory TransactOptsFactory inputBoxAddr common.Address selfHostedAppFactory common.Address appAddr common.Address @@ -53,8 +59,9 @@ func (s *EthUtilSuite) SetupTest() { privateKey, err := MnemonicToPrivateKey(FoundryMnemonic, 0) s.Require().Nil(err) - s.txOpts, err = bind.NewKeyedTransactorWithChainID(privateKey, chainId) + txOpts, err := bind.NewKeyedTransactorWithChainID(privateKey, chainId) s.Require().Nil(err) + s.txOptsFactory = NewStaticTransactOptsFactory(txOpts) s.selfHostedAppFactory, err = config.GetContractsSelfHostedApplicationFactoryAddress() s.Require().Nil(err) @@ -82,7 +89,7 @@ func (s *EthUtilSuite) TearDownTest() { func (s *EthUtilSuite) TestAddInput() { - sender := s.txOpts.From + sender := s.txOptsFactory.From() payload := common.Hex2Bytes("deadbeef") indexChan := make(chan uint64) @@ -93,7 +100,7 @@ func (s *EthUtilSuite) TestAddInput() { go func() { waitGroup.Done() - inputIndex, _, _, err := AddInput(s.ctx, s.client, s.txOpts, s.inputBoxAddr, s.appAddr, payload) + inputIndex, _, _, err := AddInput(s.ctx, s.client, s.txOptsFactory, s.inputBoxAddr, s.appAddr, payload) if err != nil { errChan <- err return @@ -140,3 +147,73 @@ func (s *EthUtilSuite) TestMineNewBlock() { func TestEthUtilSuite(t *testing.T) { suite.Run(t, new(EthUtilSuite)) } + +func TestAddInputAsyncUsesContextForBindingTransaction(t *testing.T) { + timeout := 20 * time.Millisecond + + srv := rpc.NewServer() + backend := &addInputAsyncContextBackend{estimateGasTimeout: 10 * timeout} + require.NoError(t, srv.RegisterName("eth", backend)) + defer srv.Stop() + + client := ethclient.NewClient(rpc.DialInProc(srv)) + defer client.Close() + + privateKey, err := crypto.GenerateKey() + require.NoError(t, err) + txOpts, err := bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(1)) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + _, err = AddInputAsync( + ctx, + client, + NewStaticTransactOptsFactory(txOpts), + common.HexToAddress("0x1000000000000000000000000000000000000001"), + common.HexToAddress("0x2000000000000000000000000000000000000002"), + []byte("payload"), + ) + + require.ErrorIs(t, err, context.DeadlineExceeded) + require.True(t, backend.estimateGasCalled, "expected AddInputAsync to reach the binding gas-estimation boundary") +} + +type addInputAsyncContextBackend struct { + estimateGasCalled bool + estimateGasTimeout time.Duration +} + +func (b *addInputAsyncContextBackend) GetTransactionCount( + context.Context, + common.Address, + string, +) (hexutil.Uint64, error) { + return 0, nil +} + +func (b *addInputAsyncContextBackend) GasPrice(context.Context) (*hexutil.Big, error) { + return (*hexutil.Big)(big.NewInt(1)), nil +} + +func (b *addInputAsyncContextBackend) GetCode( + context.Context, + common.Address, + string, +) (hexutil.Bytes, error) { + return hexutil.Bytes{0x01}, nil +} + +func (b *addInputAsyncContextBackend) EstimateGas( + ctx context.Context, + _ map[string]interface{}, +) (hexutil.Uint64, error) { + b.estimateGasCalled = true + select { + case <-ctx.Done(): + return 0, ctx.Err() + case <-time.After(b.estimateGasTimeout): + return 0, errors.New("eth_estimateGas was not called with the AddInputAsync context") + } +} diff --git a/pkg/ethutil/prt.go b/pkg/ethutil/prt.go index 3f7c72ec4..128dc17ef 100644 --- a/pkg/ethutil/prt.go +++ b/pkg/ethutil/prt.go @@ -123,13 +123,18 @@ func (me *PRTApplicationDeployment) deployPRT( func (me *PRTApplicationDeployment) Deploy( ctx context.Context, client *ethclient.Client, - txOpts *bind.TransactOpts, + txOptsFactory TransactOptsFactory, ) (common.Address, IApplicationDeploymentResult, error) { zero := common.Address{} result := &PRTApplicationDeploymentResult{} result.Deployment = me var err error + txOpts, err := txOptsFactory.NewTransactOpts(ctx) + if err != nil { + return zero, nil, fmt.Errorf("failed to create transaction options: %w", err) + } + appAddress, consensusAddress, err := me.deployPRT(ctx, client, txOpts) if err != nil { return zero, nil, fmt.Errorf("failed to deploy Dave Application and consensus contracts: %w", err) diff --git a/pkg/ethutil/selfhosted.go b/pkg/ethutil/selfhosted.go index cd2582e8f..89817fd68 100644 --- a/pkg/ethutil/selfhosted.go +++ b/pkg/ethutil/selfhosted.go @@ -74,7 +74,7 @@ func (me *SelfhostedApplicationDeploymentResult) String() string { func (me *SelfhostedApplicationDeployment) Deploy( ctx context.Context, client *ethclient.Client, - txOpts *bind.TransactOpts, + txOptsFactory TransactOptsFactory, ) (common.Address, IApplicationDeploymentResult, error) { zero := common.Address{} result := &SelfhostedApplicationDeploymentResult{} @@ -129,7 +129,7 @@ func (me *SelfhostedApplicationDeployment) Deploy( // deploy the contracts receipt, err := sendTransaction( - ctx, client, txOpts, big.NewInt(0), + ctx, client, txOptsFactory, big.NewInt(0), func(txOpts *bind.TransactOpts) (*types.Transaction, error) { result.ApplicationFactoryAddress, err = factory.GetApplicationFactory(nil) if err != nil { diff --git a/pkg/ethutil/transaction.go b/pkg/ethutil/transaction.go index e85066ef3..eadc28897 100644 --- a/pkg/ethutil/transaction.go +++ b/pkg/ethutil/transaction.go @@ -16,23 +16,44 @@ import ( "github.com/ethereum/go-ethereum/ethclient" ) +type TransactOptsFactory interface { + From() common.Address + NewTransactOpts(ctx context.Context) (*bind.TransactOpts, error) +} + +type staticTransactOptsFactory struct { + opts *bind.TransactOpts +} + +func (f *staticTransactOptsFactory) From() common.Address { + return f.opts.From +} + +func (f *staticTransactOptsFactory) NewTransactOpts(ctx context.Context) (*bind.TransactOpts, error) { + opts := *f.opts + opts.Context = ctx + return &opts, nil +} + +func NewStaticTransactOptsFactory(txOpts *bind.TransactOpts) TransactOptsFactory { + return &staticTransactOptsFactory{opts: txOpts} +} + const PollInterval = 500 * time.Millisecond // Prepare the transaction, send it, and wait for the receipt. func sendTransaction( ctx context.Context, client *ethclient.Client, - txOpts *bind.TransactOpts, + txOptsFactory TransactOptsFactory, txValue *big.Int, doSend func(txOpts *bind.TransactOpts) (*types.Transaction, error), ) (*types.Receipt, error) { - txOpts, err := _prepareTransaction(ctx, client, txOpts, txValue) + txOpts, err := _prepareTransaction(ctx, client, txOptsFactory, txValue) if err != nil { return nil, fmt.Errorf("failed to prepare transaction: %w", err) } - txOptsCopy := *txOpts - txOptsCopy.Context = ctx - tx, err := doSend(&txOptsCopy) + tx, err := doSend(txOpts) if err != nil { return nil, fmt.Errorf("failed to send transaction: %w", err) } @@ -47,10 +68,10 @@ func sendTransaction( func _prepareTransaction( ctx context.Context, client *ethclient.Client, - txOpts *bind.TransactOpts, + txOptsFactory TransactOptsFactory, txValue *big.Int, ) (*bind.TransactOpts, error) { - nonce, err := client.PendingNonceAt(ctx, txOpts.From) + nonce, err := client.PendingNonceAt(ctx, txOptsFactory.From()) if err != nil { return nil, fmt.Errorf("failed to get nonce: %w", err) } @@ -60,6 +81,11 @@ func _prepareTransaction( } nonceBigInt := &big.Int{} nonceBigInt.SetUint64(nonce) + + txOpts, err := txOptsFactory.NewTransactOpts(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get transaction options: %w", err) + } txOpts.Nonce = nonceBigInt txOpts.Value = txValue txOpts.GasPrice = gasPrice