diff --git a/scenarios/revertingtx/contracts/always-revert.go b/scenarios/revertingtx/contracts/always-revert.go new file mode 100644 index 0000000..bb3eb9a --- /dev/null +++ b/scenarios/revertingtx/contracts/always-revert.go @@ -0,0 +1,224 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package contracts + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// RevertingtxMetaData contains all meta data concerning the Revertingtx contract. +var RevertingtxMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[],\"name\":\"alwaysRevert\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + Bin: "0x6080604052348015600f57600080fd5b5060b780601d6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80639fb3785314602d575b600080fd5b60336035565b005b60405162461bcd60e51b815260206004820152601c60248201527f546869732066756e6374696f6e20616c77617973207265766572747300000000604482015260640160405180910390fdfea26469706673582212206749d3ec93dd02d035ed0ef96b2fb34f9f3e0ab7973ff58f09801e86c2acc8b464736f6c63430008190033", +} + +// RevertingtxABI is the input ABI used to generate the binding from. +// Deprecated: Use RevertingtxMetaData.ABI instead. +var RevertingtxABI = RevertingtxMetaData.ABI + +// RevertingtxBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use RevertingtxMetaData.Bin instead. +var RevertingtxBin = RevertingtxMetaData.Bin + +// DeployRevertingtx deploys a new Ethereum contract, binding an instance of Revertingtx to it. +func DeployRevertingtx(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *Revertingtx, error) { + parsed, err := RevertingtxMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(RevertingtxBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Revertingtx{RevertingtxCaller: RevertingtxCaller{contract: contract}, RevertingtxTransactor: RevertingtxTransactor{contract: contract}, RevertingtxFilterer: RevertingtxFilterer{contract: contract}}, nil +} + +// Revertingtx is an auto generated Go binding around an Ethereum contract. +type Revertingtx struct { + RevertingtxCaller // Read-only binding to the contract + RevertingtxTransactor // Write-only binding to the contract + RevertingtxFilterer // Log filterer for contract events +} + +// RevertingtxCaller is an auto generated read-only Go binding around an Ethereum contract. +type RevertingtxCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// RevertingtxTransactor is an auto generated write-only Go binding around an Ethereum contract. +type RevertingtxTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// RevertingtxFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type RevertingtxFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// RevertingtxSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type RevertingtxSession struct { + Contract *Revertingtx // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// RevertingtxCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type RevertingtxCallerSession struct { + Contract *RevertingtxCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// RevertingtxTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type RevertingtxTransactorSession struct { + Contract *RevertingtxTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// RevertingtxRaw is an auto generated low-level Go binding around an Ethereum contract. +type RevertingtxRaw struct { + Contract *Revertingtx // Generic contract binding to access the raw methods on +} + +// RevertingtxCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type RevertingtxCallerRaw struct { + Contract *RevertingtxCaller // Generic read-only contract binding to access the raw methods on +} + +// RevertingtxTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type RevertingtxTransactorRaw struct { + Contract *RevertingtxTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewRevertingtx creates a new instance of Revertingtx, bound to a specific deployed contract. +func NewRevertingtx(address common.Address, backend bind.ContractBackend) (*Revertingtx, error) { + contract, err := bindRevertingtx(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Revertingtx{RevertingtxCaller: RevertingtxCaller{contract: contract}, RevertingtxTransactor: RevertingtxTransactor{contract: contract}, RevertingtxFilterer: RevertingtxFilterer{contract: contract}}, nil +} + +// NewRevertingtxCaller creates a new read-only instance of Revertingtx, bound to a specific deployed contract. +func NewRevertingtxCaller(address common.Address, caller bind.ContractCaller) (*RevertingtxCaller, error) { + contract, err := bindRevertingtx(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &RevertingtxCaller{contract: contract}, nil +} + +// NewRevertingtxTransactor creates a new write-only instance of Revertingtx, bound to a specific deployed contract. +func NewRevertingtxTransactor(address common.Address, transactor bind.ContractTransactor) (*RevertingtxTransactor, error) { + contract, err := bindRevertingtx(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &RevertingtxTransactor{contract: contract}, nil +} + +// NewRevertingtxFilterer creates a new log filterer instance of Revertingtx, bound to a specific deployed contract. +func NewRevertingtxFilterer(address common.Address, filterer bind.ContractFilterer) (*RevertingtxFilterer, error) { + contract, err := bindRevertingtx(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &RevertingtxFilterer{contract: contract}, nil +} + +// bindRevertingtx binds a generic wrapper to an already deployed contract. +func bindRevertingtx(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := RevertingtxMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Revertingtx *RevertingtxRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Revertingtx.Contract.RevertingtxCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Revertingtx *RevertingtxRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Revertingtx.Contract.RevertingtxTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Revertingtx *RevertingtxRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Revertingtx.Contract.RevertingtxTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Revertingtx *RevertingtxCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Revertingtx.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Revertingtx *RevertingtxTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Revertingtx.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Revertingtx *RevertingtxTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Revertingtx.Contract.contract.Transact(opts, method, params...) +} + +// AlwaysRevert is a paid mutator transaction binding the contract method 0x9fb37853. +// +// Solidity: function alwaysRevert() returns() +func (_Revertingtx *RevertingtxTransactor) AlwaysRevert(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Revertingtx.contract.Transact(opts, "alwaysRevert") +} + +// AlwaysRevert is a paid mutator transaction binding the contract method 0x9fb37853. +// +// Solidity: function alwaysRevert() returns() +func (_Revertingtx *RevertingtxSession) AlwaysRevert() (*types.Transaction, error) { + return _Revertingtx.Contract.AlwaysRevert(&_Revertingtx.TransactOpts) +} + +// AlwaysRevert is a paid mutator transaction binding the contract method 0x9fb37853. +// +// Solidity: function alwaysRevert() returns() +func (_Revertingtx *RevertingtxTransactorSession) AlwaysRevert() (*types.Transaction, error) { + return _Revertingtx.Contract.AlwaysRevert(&_Revertingtx.TransactOpts) +} diff --git a/scenarios/revertingtx/revertingtx.go b/scenarios/revertingtx/revertingtx.go new file mode 100644 index 0000000..fc20a9e --- /dev/null +++ b/scenarios/revertingtx/revertingtx.go @@ -0,0 +1,365 @@ +package revertingtx + +import ( + "context" + "fmt" + revertingtx "github.com/astriaorg/spamooor/scenarios/revertingtx/contracts" + "github.com/astriaorg/spamooor/utils" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "golang.org/x/crypto/sha3" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "math/big" + "os" + "sync" + "time" + + "github.com/astriaorg/spamooor/scenariotypes" + "github.com/astriaorg/spamooor/tester" + "github.com/astriaorg/spamooor/txbuilder" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/holiman/uint256" + "github.com/sirupsen/logrus" + "github.com/spf13/pflag" +) + +type ScenarioOptions struct { + TotalCount uint64 + Throughput uint64 + MaxPending uint64 + MaxWallets uint64 + Timeout uint64 + BaseFee uint64 + TipFee uint64 + ComposerAddress string + SendViaComposer bool + RollupId string +} + +type Scenario struct { + options ScenarioOptions + logger *logrus.Entry + tester *tester.Tester + composerConn *grpc.ClientConn + + revertingContractAddr common.Address + + pendingCount uint64 + pendingChan chan bool + pendingWGroup sync.WaitGroup +} + +func NewScenario() scenariotypes.Scenario { + return &Scenario{ + logger: logrus.WithField("scenario", "revertingtx"), + } +} + +func (s *Scenario) Flags(flags *pflag.FlagSet) error { + flags.Uint64VarP(&s.options.TotalCount, "count", "c", 0, "Total number of large transactions to send") + flags.Uint64VarP(&s.options.Throughput, "throughput", "t", 0, "Number of large transactions to send per slot") + flags.Uint64Var(&s.options.MaxPending, "max-pending", 0, "Maximum number of pending transactions") + flags.Uint64Var(&s.options.MaxWallets, "max-wallets", 0, "Maximum number of child wallets to use") + flags.Uint64Var(&s.options.Timeout, "timeout", 120, "Number of seconds to wait timing out the test") + flags.Uint64Var(&s.options.BaseFee, "basefee", 20, "Max fee per gas to use in large transactions (in gwei)") + flags.Uint64Var(&s.options.TipFee, "tipfee", 2, "Max tip per gas to use in large transactions (in gwei)") + flags.StringVar(&s.options.ComposerAddress, "composer-address", "localhost:50051", "Address of the composer service") + flags.BoolVar(&s.options.SendViaComposer, "send-via-composer", false, "Send transactions via composer") + flags.StringVar(&s.options.RollupId, "", "", "The rollup id of the evm rollup") + + return nil +} + +func (s *Scenario) Init(testerCfg *tester.TesterConfig) error { + if s.options.TotalCount == 0 && s.options.Throughput == 0 { + return fmt.Errorf("neither total count nor throughput limit set, must define at least one of them") + } + + if s.options.MaxWallets > 0 { + testerCfg.WalletCount = s.options.MaxWallets + } else if s.options.TotalCount > 0 { + if s.options.TotalCount < 1000 { + testerCfg.WalletCount = s.options.TotalCount + } else { + testerCfg.WalletCount = 1000 + } + } else { + if s.options.Throughput*10 < 1000 { + testerCfg.WalletCount = s.options.Throughput * 10 + } else { + testerCfg.WalletCount = 1000 + } + } + + if s.options.MaxPending > 0 { + s.pendingChan = make(chan bool, s.options.MaxPending) + } + + if s.options.SendViaComposer { + conn, err := grpc.NewClient(s.options.ComposerAddress, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return err + } + + s.composerConn = conn + } + + return nil +} + +func (s *Scenario) Setup(testerCfg *tester.Tester) error { + s.tester = testerCfg + s.logger.Infof("setting up scenario: revetingtx") + s.logger.Infof("deploying reverting tx contract...") + receipt, _, err := s.DeployRevertingTxContract() + if err != nil { + return err + } + + s.revertingContractAddr = receipt.ContractAddress + + s.logger.Infof("deployed reverting tx contract at %v", s.revertingContractAddr.String()) + + return nil +} + +func (s *Scenario) Run() error { + txIdxCounter := uint64(0) + counterMutex := sync.Mutex{} + waitGroup := sync.WaitGroup{} + pendingCount := uint64(0) + txCount := uint64(0) + startTime := time.Now() + + s.logger.Infof("starting scenario: revertingtx") + + for { + txIdx := txIdxCounter + txIdxCounter++ + + if s.pendingChan != nil { + // await pending transactions + s.pendingChan <- true + } + waitGroup.Add(1) + counterMutex.Lock() + pendingCount++ + counterMutex.Unlock() + + go func(txIdx uint64) { + defer func() { + counterMutex.Lock() + pendingCount-- + counterMutex.Unlock() + waitGroup.Done() + }() + + logger := s.logger + tx, client, err := s.sendTx(txIdx) + if client != nil { + logger = logger.WithField("rpc", client.GetName()) + } + if err != nil { + logger.Warnf("could not send transaction: %v", err) + <-s.pendingChan + return + } + + counterMutex.Lock() + txCount++ + counterMutex.Unlock() + logger.Infof("sent tx #%6d: %v", txIdx+1, tx.Hash().String()) + }(txIdx) + + count := txCount + pendingCount + if s.options.TotalCount > 0 && count >= s.options.TotalCount { + break + } + if s.options.Throughput > 0 { + for count/((uint64(time.Since(startTime).Seconds())/utils.SecondsPerSlot)+1) >= s.options.Throughput { + time.Sleep(100 * time.Millisecond) + } + } + } + waitGroup.Wait() + + s.logger.Infof("finished sending transactions, awaiting block inclusion...") + s.pendingWGroup.Wait() + s.logger.Infof("finished sending transactions, awaiting block inclusion...") + + return nil +} + +func (s *Scenario) DeployRevertingTxContract() (*types.Receipt, *txbuilder.Client, error) { + wallet := s.tester.GetRootWallet() + client := s.tester.GetClient(tester.SelectByIndex, 0) + + transactor, err := s.GetTransactor(wallet, true, big.NewInt(0)) + if err != nil { + return nil, nil, err + } + + _, deployTx, _, err := revertingtx.DeployRevertingtx(transactor, client.GetEthClient()) + if err != nil { + return nil, nil, err + } + + receipt, _, err := txbuilder.SendAndAwaitTx(txbuilder.SendTxOpts{ + Wallet: wallet, + Tx: deployTx, + Client: client, + BaseFee: int64(s.options.BaseFee), + TipFee: int64(s.options.TipFee), + Gas: 2000000, + }) + if err != nil { + return nil, nil, err + } + + return receipt, client, nil +} + +func (s *Scenario) sendTx(txIdx uint64) (*types.Transaction, *txbuilder.Client, error) { + client := s.tester.GetClient(tester.SelectByIndex, int(txIdx)) + wallet := s.tester.GetWallet(tester.SelectByIndex, int(txIdx)) + + var feeCap *big.Int + var tipCap *big.Int + + if s.options.BaseFee > 0 { + feeCap = new(big.Int).Mul(big.NewInt(int64(s.options.BaseFee)), big.NewInt(1000000000)) + } + if s.options.TipFee > 0 { + tipCap = new(big.Int).Mul(big.NewInt(int64(s.options.TipFee)), big.NewInt(1000000000)) + } + + if feeCap == nil || tipCap == nil { + var err error + feeCap, tipCap, err = client.GetSuggestedFee() + if err != nil { + return nil, client, err + } + } + + if feeCap.Cmp(big.NewInt(1000000000)) < 0 { + feeCap = big.NewInt(1000000000) + } + if tipCap.Cmp(big.NewInt(1000000000)) < 0 { + tipCap = big.NewInt(1000000000) + } + + transferFnSignature := []byte("alwaysRevert()") + hash := sha3.NewLegacyKeccak256() + hash.Write(transferFnSignature) + methodID := hash.Sum(nil)[:4] + var txCallData []byte + txCallData = append(txCallData, methodID...) + + txData, err := txbuilder.DynFeeTx(&txbuilder.TxMetadata{ + GasFeeCap: uint256.MustFromBig(feeCap), + GasTipCap: uint256.MustFromBig(tipCap), + Gas: 100000, + To: &s.revertingContractAddr, + Value: uint256.NewInt(0), + Data: txCallData, + }) + if err != nil { + return nil, nil, err + } + + tx, err := wallet.BuildDynamicFeeTx(txData) + if err != nil { + return nil, nil, err + } + + if s.options.SendViaComposer { + err = client.SendTransactionViaComposer(tx, s.composerConn, s.options.RollupId) + if err != nil { + return nil, client, err + } + } else { + err = client.SendTransaction(tx) + if err != nil { + return nil, client, err + } + } + + s.pendingWGroup.Add(1) + go s.awaitTx(txIdx, tx, client, wallet) + + return tx, client, nil +} + +func (s *Scenario) awaitTx(txIdx uint64, tx *types.Transaction, client *txbuilder.Client, wallet *txbuilder.Wallet) { + var awaitConfirmation = true + defer func() { + awaitConfirmation = false + if s.pendingChan != nil { + <-s.pendingChan + } + s.pendingWGroup.Done() + }() + if s.options.Timeout > 0 { + go s.timeTicker(txIdx, tx, &awaitConfirmation) + } + + receipt, blockNum, err := client.AwaitTransaction(tx) + if err != nil { + s.logger.WithField("client", client.GetName()).Warnf("error while awaiting tx receipt: %v", err) + return + } + + effectiveGasPrice := receipt.EffectiveGasPrice + if effectiveGasPrice == nil { + effectiveGasPrice = big.NewInt(0) + } + blobGasPrice := receipt.BlobGasPrice + if blobGasPrice == nil { + blobGasPrice = big.NewInt(0) + } + feeAmount := new(big.Int).Mul(effectiveGasPrice, big.NewInt(int64(receipt.GasUsed))) + totalAmount := new(big.Int).Add(tx.Value(), feeAmount) + wallet.SubBalance(totalAmount) + + gweiTotalFee := new(big.Int).Div(totalAmount, big.NewInt(1000000000)) + gweiBaseFee := new(big.Int).Div(effectiveGasPrice, big.NewInt(1000000000)) + gweiBlobFee := new(big.Int).Div(blobGasPrice, big.NewInt(1000000000)) + + txStatus := "failure" + if receipt.Status == 1 { + txStatus = "success" + } + + s.logger.WithField("client", client.GetName()).Infof(" transaction %d confirmed in block #%v with %s. total gas units: %d, total fee: %v gwei (base: %v, blob: %v)", txIdx+1, blockNum, txStatus, receipt.GasUsed, gweiTotalFee, gweiBaseFee, gweiBlobFee) +} + +func (s *Scenario) timeTicker(txIdx uint64, tx *types.Transaction, awaitConfirmation *bool) { + for { + time.Sleep(time.Duration(s.options.Timeout) * time.Second) + + if !*awaitConfirmation { + break + } + + s.logger.Infof("timeout reached for tx: %d with hash: %s, stopping test", txIdx, tx.Hash().String()) + os.Exit(1) + } +} + +func (s *Scenario) GetTransactor(wallet *txbuilder.Wallet, noSend bool, value *big.Int) (*bind.TransactOpts, error) { + transactor, err := bind.NewKeyedTransactorWithChainID(wallet.GetPrivateKey(), wallet.GetChainId()) + if err != nil { + return nil, err + } + transactor.Context = context.Background() + transactor.NoSend = noSend + transactor.Value = value + + return transactor, nil +} + +func (s *Scenario) GetRevertingContract() (*revertingtx.Revertingtx, error) { + client := s.tester.GetClient(tester.SelectByIndex, 0) + return revertingtx.NewRevertingtx(s.revertingContractAddr, client.GetEthClient()) +} diff --git a/scenarios/scenarios.go b/scenarios/scenarios.go index 5018cc9..0f97c38 100644 --- a/scenarios/scenarios.go +++ b/scenarios/scenarios.go @@ -5,6 +5,7 @@ import ( "github.com/astriaorg/spamooor/scenarios/eoatx" "github.com/astriaorg/spamooor/scenarios/erctx" "github.com/astriaorg/spamooor/scenarios/gasburnertx" + "github.com/astriaorg/spamooor/scenarios/revertingtx" "github.com/astriaorg/spamooor/scenarios/univ2tx" "github.com/astriaorg/spamooor/scenariotypes" ) @@ -15,4 +16,5 @@ var Scenarios = map[string]func() scenariotypes.Scenario{ "gasburnertx": gasburnertx.NewScenario, "univ2tx": univ2tx.NewScenario, "deploytx": deploytx.NewScenario, + "revertingtx": revertingtx.NewScenario, }