diff --git a/cmd/sendTxs.go b/cmd/sendTxs.go index cdeca5c..ebdec21 100644 --- a/cmd/sendTxs.go +++ b/cmd/sendTxs.go @@ -27,9 +27,10 @@ import ( // sendTxsCmd represents the sendTxs command var sendTxsCmd = &cobra.Command{ Use: "sendTxs", - Short: "send large volumes of different tx types to different nodes", + Short: "Send large volumes of different tx types to different nodes for testing purposes", Long: `Loads tx configuration from .toml config file -Generates txs from configuration and sends them to designated node according to set frequency and number`, +Generates txs from configuration and sends them to designated node according to set frequency and number +Support standard, optimism L2, optimism L1 to L2, and EIP1559 transactions`, Run: func(cmd *cobra.Command, args []string) { sendTxs() }, @@ -45,10 +46,12 @@ func sendTxs() { quitChan := make(chan bool) txSpammer.Loop(wg, quitChan) - shutdown := make(chan os.Signal) - signal.Notify(shutdown, os.Interrupt) - <-shutdown - close(quitChan) + go func() { + shutdown := make(chan os.Signal) + signal.Notify(shutdown, os.Interrupt) + <-shutdown + close(quitChan) + }() wg.Wait() } diff --git a/environment/example.toml b/environment/example.toml index b77a652..d72d394 100644 --- a/environment/example.toml +++ b/environment/example.toml @@ -4,6 +4,7 @@ [L2ContractDeployment] type = "L2" httpPath = "" + chainID = 420 to = "" amount = "0" gasLimit = 0 @@ -13,11 +14,16 @@ writeSenderPath = "" frequency = 1 totalNumber = 1 - chainID = 420 + delay = 0 + l1Sender = "" + l1RollupTxId = 0 + sigHashType = 0 + queueOrigin = 0 [L2ContractPutCall] type = "L2" httpPath = "" + chainID = 420 to = "" amount = "0" gasLimit = 0 @@ -26,54 +32,17 @@ senderKeyPath = "" writeSenderPath = "" frequency = 15 - totalNumber = 1500 - chainID = 420 + totalNumber = 1 + delay = 60 + l1Sender = "" + l1RollupTxId = 0 + sigHashType = 0 + queueOrigin = 0 [L2ContractGetCall] type = "L2" httpPath = "" - to = "" - amount = "0" - gasLimit = 0 - gasPrice = "0" - data = "" - senderKeyPath = "" - writeSenderPath = "" - frequency = 15 - totalNumber = 1500 chainID = 420 - -[GethTx] - type = "geth" - httpPath = "" - to = "" - amount = "0" - gasLimit = 0 - gasPrice = "0" - data = "" - senderKeyPath = "" - writeSenderPath = "" - frequency = 0 - totalNumber = 0 - chainID = 1 - -[EIP1559] - type = "1559" - httpPath = "" - to = "" - amount = "0" - gasLimit = 0 - gasPremium = "0" - feeCap = "0" - data = "" - senderKeyPath = "" - writeSenderPath = "" - frequency = 0 - totalNumber = 0 - -[L1ToL2] - type = "L2" - httpPath = "" to = "" amount = "0" gasLimit = 0 @@ -81,11 +50,13 @@ data = "" senderKeyPath = "" writeSenderPath = "" + frequency = 60 + totalNumber = 2 + delay = 30 l1Sender = "" l1RollupTxId = 0 sigHashType = 0 - frequency = 0 - totalNumber = 0 + queueOrigin = 0 [log] level = "info" diff --git a/pkg/config.go b/pkg/config.go index ae32aa1..f31ef72 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -24,6 +24,8 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rpc" "github.com/spf13/viper" @@ -39,6 +41,9 @@ type TxParams struct { // Type of the tx Type TxType + // Chain ID + ChainID uint64 + // Universal tx fields To *common.Address GasLimit uint64 @@ -49,23 +54,25 @@ type TxParams struct { // Optimism-specific metadata fields L1SenderAddr *common.Address - L1RollupTxId uint64 - SigHashType uint8 + L1RollupTxId *hexutil.Uint64 + SigHashType types.SignatureHashType + QueueOrigin types.QueueOrigin // EIP1559-specific fields GasPremium *big.Int FeeCap *big.Int // Sender key, if left the senderKeyPath is empty we generate a new key - SenderKey *ecdsa.PrivateKey + SenderKey *ecdsa.PrivateKey + StartingNonce uint64 // Sending params // How often we send a tx of this type Frequency time.Duration // Total number of txs of this type to send TotalNumber uint64 - // Txs of different types will be sent according to their order (starting at 0) - Order uint64 + // Delay before beginning to send + Delay time.Duration } const ( @@ -87,6 +94,10 @@ const ( sigHashTypeSuffix = ".sigHashType" frequencySuffix = ".frequency" totalNumberSuffix = ".totalNumber" + delaySuffix = ".delay" + startingNonceSuffix = ".startingNonce" + queueOriginSuffix = ".queueOrigin" + chainIDSuffix = ".chainID" ) // NewConfig returns a new tx spammer config @@ -109,7 +120,7 @@ func NewTxParams() ([]TxParams, error) { return nil, err } - // Get tx type + // Get tx type and chain id txTypeStr := viper.GetString(txName + typeSuffix) if txTypeStr == "" { return nil, fmt.Errorf("need tx type for tx %s", txName) @@ -118,6 +129,7 @@ func NewTxParams() ([]TxParams, error) { if err != nil { return nil, err } + chainID := viper.GetUint64(txName + chainIDSuffix) // Get basic fields toStr := viper.GetString(txName + toSuffix) @@ -176,9 +188,10 @@ func NewTxParams() ([]TxParams, error) { sender := common.HexToAddress(l1SenderStr) l1Sender = &sender } - l1RollupTxId := viper.GetUint64(txName + l1RollupTxIdSuffix) + l1rtid := (hexutil.Uint64)(l1RollupTxId) sigHashType := viper.GetUint(txName + sigHashTypeSuffix) + queueOrigin := viper.GetInt64(txName + queueOriginSuffix) // If gasPrice was empty, attempt to load EIP1559 fields var feeCap, gasPremium *big.Int @@ -201,26 +214,32 @@ func NewTxParams() ([]TxParams, error) { } } - // Load the sending paramas + // Load starting nonce and sending params + startingNonce := viper.GetUint64(txName + startingNonceSuffix) frequency := viper.GetDuration(txName + frequencySuffix) totalNumber := viper.GetUint64(txName + totalNumberSuffix) + delay := viper.GetDuration(txName + delaySuffix) txParams[i] = TxParams{ - Client: rpcClient, - Type: txType, - Name: txName, - To: toAddr, - Amount: amount, - GasLimit: gasLimit, - GasPrice: gasPrice, - GasPremium: gasPremium, - FeeCap: feeCap, - Data: data, - L1SenderAddr: l1Sender, - L1RollupTxId: l1RollupTxId, - SigHashType: uint8(sigHashType), - Frequency: frequency, - TotalNumber: totalNumber, + Client: rpcClient, + Type: txType, + Name: txName, + To: toAddr, + Amount: amount, + GasLimit: gasLimit, + GasPrice: gasPrice, + GasPremium: gasPremium, + FeeCap: feeCap, + Data: data, + L1SenderAddr: l1Sender, + L1RollupTxId: &l1rtid, + SigHashType: (types.SignatureHashType)(uint8(sigHashType)), + Frequency: frequency, + TotalNumber: totalNumber, + Delay: delay, + StartingNonce: startingNonce, + QueueOrigin: (types.QueueOrigin)(queueOrigin), + ChainID: chainID, } } return txParams, nil diff --git a/pkg/sender.go b/pkg/sender.go index 4547d1b..577da66 100644 --- a/pkg/sender.go +++ b/pkg/sender.go @@ -18,40 +18,78 @@ package tx_spammer import ( "context" + "fmt" + "sync" + "time" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/rpc" + "github.com/sirupsen/logrus" ) +// TxSender type for type TxSender struct { - TxGen *TxGenerator + TxGen *TxGenerator + TxParams []TxParams } +// NewTxSender returns a new tx sender func NewTxSender(params []TxParams) *TxSender { return &TxSender{ - TxGen: NewTxGenerator(params), + TxGen: NewTxGenerator(params), + TxParams: params, } } -func (s *TxSender) Send(quitChan <-chan bool) <-chan error { + +func (s *TxSender) Send(quitChan <-chan bool) (<-chan bool, <-chan error) { + // done channel to signal completion of all jobs + doneChan := make(chan bool) + // err channel returned to calling context errChan := make(chan error) - go func() { - for s.TxGen.Next() { - select { - case <-quitChan: + // for each tx param set, spin up a goroutine to generate and send the tx at the specified delay and frequency + wg := new(sync.WaitGroup) + for _, txParams := range s.TxParams { + wg.Add(1) + go func(p TxParams) { + defer wg.Done() + // send the first tx after the delay + timer := time.NewTimer(p.Delay) + <-timer.C + if err := s.genAndSend(p); err != nil { + errChan <- fmt.Errorf("tx %s initial genAndSend error: %v", p.Name, err) return - default: } - if err := sendRawTransaction(s.TxGen.Current()); err != nil { - errChan <- err + // send any remaining ones at the provided frequency, also check for quit signal + ticker := time.NewTicker(p.Frequency) + for i := uint64(1); i < p.TotalNumber; i++ { + select { + case <-ticker.C: + if err := s.genAndSend(p); err != nil { + errChan <- fmt.Errorf("tx %s number %d genAndSend error: %v", p.Name, i, err) + return + } + case <-quitChan: + return + } } - } - if s.TxGen.Error() != nil { - errChan <- s.TxGen.Error() - } + }(txParams) + } + go func() { + wg.Wait() + close(doneChan) }() - return errChan + return doneChan, errChan } -func sendRawTransaction(rpcClient *rpc.Client, txRlp []byte) error { +func (s *TxSender) genAndSend(p TxParams) error { + tx, err := s.TxGen.GenerateTx(p) + if err != nil { + return err + } + return sendRawTransaction(p.Client, tx, p.Name) +} + +func sendRawTransaction(rpcClient *rpc.Client, txRlp []byte, name string) error { + logrus.Infof("sending tx %s", name) return rpcClient.CallContext(context.Background(), nil, "eth_sendRawTransaction", hexutil.Encode(txRlp)) } diff --git a/pkg/service.go b/pkg/service.go index 10b2564..5e9728e 100644 --- a/pkg/service.go +++ b/pkg/service.go @@ -38,9 +38,9 @@ func NewTxSpammer(params []TxParams) Service { func (s *Spammer) Loop(wg *sync.WaitGroup, quitChan <-chan bool) { forwardQuit := make(chan bool) - errChan := s.Sender.Send(forwardQuit) + doneChan, errChan := s.Sender.Send(forwardQuit) + wg.Add(1) go func() { - wg.Add(1) defer wg.Done() for { select { @@ -48,6 +48,8 @@ func (s *Spammer) Loop(wg *sync.WaitGroup, quitChan <-chan bool) { logrus.Error(err) case forwardQuit <- <-quitChan: return + case <-doneChan: + return } } }() diff --git a/pkg/tx_generator.go b/pkg/tx_generator.go index 169db1c..4c7f37f 100644 --- a/pkg/tx_generator.go +++ b/pkg/tx_generator.go @@ -16,34 +16,71 @@ package tx_spammer -import "github.com/ethereum/go-ethereum/rpc" +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" +) // TxGenerator generates and signs txs type TxGenerator struct { - TxParams []TxParams - currentTx []byte - currentClient *rpc.Client - err error + // keep track of account nonces locally so we aren't spamming to determine the nonce + // this assumes these accounts are not sending txs outside this process + nonces map[common.Address]uint64 } +// NewTxGenerator creates a new tx generator func NewTxGenerator(params []TxParams) *TxGenerator { + nonces := make(map[common.Address]uint64) + for _, p := range params { + nonces[p.Sender] = p.StartingNonce + } return &TxGenerator{ - TxParams: params, + nonces: nonces, } } -func (gen TxGenerator) Next() bool { - return false -} - -func (gen TxGenerator) Current() (*rpc.Client, []byte) { - return gen.currentClient, gen.currentTx -} - -func (gen TxGenerator) Error() error { - return gen.err +// GenerateTx generates tx from the provided params +func (tg TxGenerator) GenerateTx(params TxParams) ([]byte, error) { + tx := make([]byte, 0) + switch params.Type { + case Standard, OptimismL1ToL2, OptimismL2: + return tg.gen(params) + case EIP1559: + return tg.gen1559(params) + default: + return nil, fmt.Errorf("unsupported tx type: %s", params.Type.String()) + } + return tx, nil } func (gen TxGenerator) gen(params TxParams) ([]byte, error) { - return nil, nil + nonce := gen.nonces[params.Sender] + tx := new(types.Transaction) + if params.To == nil { + tx = types.NewContractCreation(nonce, params.Amount, params.GasLimit, params.GasPrice, params.Data, params.L1SenderAddr, params.L1RollupTxId, params.QueueOrigin) + } else { + tx = types.NewTransaction(nonce, *params.To, params.Amount, params.GasLimit, params.GasPrice, params.Data, params.L1SenderAddr, params.L1RollupTxId, params.QueueOrigin, params.SigHashType) + } + signer, err := TxSigner(params.Type, params.ChainID) + if err != nil { + return nil, err + } + signedTx, err := types.SignTx(tx, signer, params.SenderKey) + if err != nil { + return nil, err + } + txRlp, err := rlp.EncodeToBytes(signedTx) + if err != nil { + return nil, err + } + gen.nonces[params.Sender]++ + return txRlp, nil +} + +func (gen TxGenerator) gen1559(params TxParams) ([]byte, error) { + // TODO: support EIP1559; new to make a new major version, vendor it, or release with different pkg name so that we can import both optimism and eip1559 geth + return nil, fmt.Errorf("1559 support not yet available") } diff --git a/pkg/tx_type.go b/pkg/tx_type.go index 9b0ec61..0cabf0c 100644 --- a/pkg/tx_type.go +++ b/pkg/tx_type.go @@ -24,13 +24,28 @@ import ( type TxType int const ( - Unkown TxType = iota + Unsupported TxType = iota Standard OptimismL2 OptimismL1ToL2 EIP1559 ) +func (tt TxType) String() string { + switch tt { + case Standard: + return "Standard" + case OptimismL2: + return "L2" + case OptimismL1ToL2: + return "L1toL2" + case EIP1559: + return "EIP1559" + default: + return "Unsupported" + } +} + // TxTypeFromString returns the tx enum type from provided string func TxTypeFromString(str string) (TxType, error) { switch strings.ToLower(str) { @@ -43,6 +58,6 @@ func TxTypeFromString(str string) (TxType, error) { case "eip1559": return EIP1559, nil default: - return Unkown, fmt.Errorf("unsupported tx type: %s", str) + return Unsupported, fmt.Errorf("unsupported tx type: %s", str) } } diff --git a/pkg/util.go b/pkg/util.go index 0a3d813..a074399 100644 --- a/pkg/util.go +++ b/pkg/util.go @@ -21,31 +21,14 @@ import ( "math/big" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/params" ) // ChainConfig returns the appropriate ethereum chain config for the provided chain id -func ChainConfig(chainID uint64) (*params.ChainConfig, error) { - switch chainID { - case 1: - return params.MainnetChainConfig, nil - case 3: - return params.TestnetChainConfig, nil // Ropsten - case 4: - return params.RinkebyChainConfig, nil - case 5, 420: - return params.GoerliChainConfig, nil - default: - return nil, fmt.Errorf("chain config for chainid %d not available", chainID) - } -} - -// ChainConfig returns the appropriate ethereum chain config for the provided chain id -func TxSigner(chainID uint64) (types.Signer, error) { - switch chainID { - case 1, 3, 4, 5: +func TxSigner(kind TxType, chainID uint64) (types.Signer, error) { + switch kind { + case Standard, EIP1559: return types.NewEIP155Signer(new(big.Int).SetUint64(chainID)), nil - case 420: + case OptimismL2, OptimismL1ToL2: return types.NewOVMSigner(new(big.Int).SetUint64(chainID)), nil default: return nil, fmt.Errorf("chain config for chainid %d not available", chainID)