diff --git a/main.go b/main.go new file mode 100644 index 0000000..ed5f364 --- /dev/null +++ b/main.go @@ -0,0 +1,22 @@ +// Copyright © 2020 Vulcanize, Inc +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import "github.com/vulcanize/tx_spammer/cmd" + +func main() { + cmd.Execute() +} diff --git a/pkg/config.go b/pkg/config.go new file mode 100644 index 0000000..cca9416 --- /dev/null +++ b/pkg/config.go @@ -0,0 +1,227 @@ +// VulcanizeDB +// Copyright © 2020 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package tx_spammer + +import ( + "crypto/ecdsa" + "fmt" + "math/big" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rpc" + "github.com/spf13/viper" +) + +type TxParams struct { + // Name of this tx in the .toml file + Name string + + // HTTP Client for this tx type + Client *rpc.Client + + // Type of the tx + Type TxType + + // Universal tx fields + To *common.Address + GasLimit uint64 + GasPrice *big.Int // nil if eip1559 + Amount *big.Int + Data []byte + Sender common.Address + + // Optimism-specific metadata fields + L1SenderAddr *common.Address + L1RollupTxId uint64 + SigHashType uint8 + + // 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 + + // 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 +} + +const ( + ETH_TX_LIST = "ETH_TX_LIST" + + typeSuffix = ".type" + httpPathSuffix = ".http" + toSuffix = ".to" + amountSuffix = ".amount" + gasLimitSuffix = ".gasLimit" + gasPriceSuffix = ".gasPrice" + gasPremiumSuffix = ".gasPremium" + feeCapSuffix = ".feeCap" + dataSuffix = ".data" + senderKeyPathSuffix = ".senderKeyPath" + writeSenderPathSuffix = ".writeSenderPath" + l1SenderSuffix = ".l1Sender" + l1RollupTxIdSuffix = ".l1RollupTxId" + sigHashTypeSuffix = ".sigHashType" + frequencySuffix = ".frequency" + totalNumberSuffix = ".totalNumber" +) + +// NewConfig returns a new tx spammer config +func NewTxParams() ([]TxParams, error) { + viper.BindEnv("eth.txs", ETH_TX_LIST) + + txs := viper.GetStringSlice("eth.txs") + txParams := make([]TxParams, len(txs)) + for i, txName := range txs { + // Get http client + httpPathStr := viper.GetString(txName+httpPathSuffix) + if httpPathStr == "" { + return nil, fmt.Errorf("tx %s is missing an httpPath", txName) + } + if !strings.HasPrefix(httpPathStr, "http://") { + httpPathStr = "http://" + httpPathStr + } + rpcClient, err := rpc.Dial(httpPathStr) + if err != nil { + return nil, err + } + + // Get tx type + txTypeStr := viper.GetString(txName+typeSuffix) + if txTypeStr == "" { + return nil, fmt.Errorf("need tx type for tx %s", txName) + } + txType, err := TxTypeFromString(txTypeStr) + if err != nil { + return nil, err + } + + // Get basic fields + toStr := viper.GetString(txName+toSuffix) + var toAddr *common.Address + if toStr != "" { + to := common.HexToAddress(toStr) + toAddr = &to + } + amountStr := viper.GetString(txName+amountSuffix) + amount := new(big.Int) + if amountStr != "" { + if _, ok := amount.SetString(amountStr, 10); !ok { + return nil, fmt.Errorf("amount (%s) for tx %s is not valid", amountStr, txName) + } + } + gasPriceStr := viper.GetString(txName+gasPriceSuffix) + var gasPrice *big.Int + if gasPriceStr != "" { + gasPrice = new(big.Int) + if _, ok := gasPrice.SetString(gasPriceStr, 10); !ok { + return nil, fmt.Errorf("gasPrice (%s) for tx %s is not valid", gasPriceStr, txName) + } + } + gasLimit := viper.GetUint64(txName+gasLimitSuffix) + hex := viper.GetString(txName+dataSuffix) + data := make([]byte, 0) + if hex != "" { + data = common.Hex2Bytes(hex) + } + + // Load or generate sender key + senderKeyPath := viper.GetString(txName+senderKeyPathSuffix) + var key *ecdsa.PrivateKey + if senderKeyPath != "" { + key, err = crypto.LoadECDSA(senderKeyPath) + if err != nil { + return nil, fmt.Errorf("unable to load ecdsa at %s key for tx %s", senderKeyPath, txName) + } + } else { + key, err = crypto.GenerateKey() + if err != nil { + return nil, fmt.Errorf("unable to generate ecdsa key for tx %s", txName) + } + writePath := viper.GetString(txName+writeSenderPathSuffix) + if writePath != "" { + if err := crypto.SaveECDSA(writePath, key); err != nil { + return nil, err + } + } + } + + // Attempt to load Optimism fields + l1SenderStr := viper.GetString(txName+l1SenderSuffix) + var l1Sender *common.Address + if l1SenderStr != "" { + sender := common.HexToAddress(l1SenderStr) + l1Sender = &sender + } + + l1RollupTxId := viper.GetUint64(txName+l1RollupTxIdSuffix) + sigHashType := viper.GetUint(txName+sigHashTypeSuffix) + + // If gasPrice was empty, attempt to load EIP1559 fields + var feeCap, gasPremium *big.Int + if gasPrice == nil { + feeCapStr := viper.GetString(txName+feeCapSuffix) + gasPremiumString := viper.GetString(txName+gasPremiumSuffix) + if feeCapStr == "" { + return nil, fmt.Errorf("tx %s is missing feeCapStr", txName) + } + if gasPremiumString == "" { + return nil, fmt.Errorf("tx %s is missing gasPremiumStr", txName) + } + feeCap = new(big.Int) + gasPremium = new(big.Int) + if _, ok := feeCap.SetString(feeCapStr, 10); !ok { + return nil, fmt.Errorf("unable to set feeCap to %s for tx %s", feeCapStr, txName) + } + if _, ok := gasPremium.SetString(gasPremiumString, 10); !ok { + return nil, fmt.Errorf("unable to set gasPremium to %s for tx %s", gasPremiumString, txName) + } + } + + // Load the sending paramas + frequency := viper.GetDuration(txName+frequencySuffix) + totalNumber := viper.GetUint64(txName+totalNumberSuffix) + + 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, + } + } + return txParams, nil +} diff --git a/pkg/sender.go b/pkg/sender.go new file mode 100644 index 0000000..3c1bd25 --- /dev/null +++ b/pkg/sender.go @@ -0,0 +1,99 @@ +// VulcanizeDB +// Copyright © 2020 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package tx_spammer + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rpc" +) + +// SendTxArgs represents the arguments to submit a transaction +type SendTxArgs struct { + From common.MixedcaseAddress `json:"from"` + To *common.MixedcaseAddress `json:"to"` + Gas hexutil.Uint64 `json:"gas"` + GasPrice hexutil.Big `json:"gasPrice"` + Value hexutil.Big `json:"value"` + Nonce hexutil.Uint64 `json:"nonce"` + // We accept "data" and "input" for backwards-compatibility reasons. + Data *hexutil.Bytes `json:"data"` + Input *hexutil.Bytes `json:"input,omitempty"` +} + +/* +// SendTransaction creates a transaction for the given argument, sign it and submit it to the +// transaction pool. +func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) { + // Look up the wallet containing the requested signer + account := accounts.Account{Address: args.From} + + wallet, err := s.b.AccountManager().Find(account) + if err != nil { + return common.Hash{}, err + } + + if args.Nonce == nil { + // Hold the addresse's mutex around signing to prevent concurrent assignment of + // the same nonce to multiple accounts. + s.nonceLock.LockAddr(args.From) + defer s.nonceLock.UnlockAddr(args.From) + } + + // Set some sanity defaults and terminate on failure + if err := args.setDefaults(ctx, s.b); err != nil { + return common.Hash{}, err + } + // Assemble the transaction and sign with the wallet + tx := args.toTransaction() + + signed, err := wallet.SignTx(account, tx, s.b.ChainConfig().ChainID) + if err != nil { + return common.Hash{}, err + } + return SubmitTransaction(ctx, s.b, signed) +} + */ + +type TxSender struct { + TxGen *TxGenerator +} + +func NewTxSender(params []TxParams) *TxSender { + return &TxSender{ + TxGen: NewTxGenerator(params), + } +} +func (s *TxSender) Send() <-chan error { + errChan := make(chan error) + go func() { + for s.TxGen.Next() { + if err := sendRawTransaction(s.TxGen.Current()); err != nil { + errChan <- err + } + } + if s.TxGen.Error() != nil { + errChan <- s.TxGen.Error() + } + }() +} + +func sendRawTransaction(rpcClient *rpc.Client, txRlp []byte) error { + return rpcClient.CallContext(context.Background(), nil, "eth_sendRawTransaction", hexutil.Encode(txRlp)) +} \ No newline at end of file diff --git a/pkg/service.go b/pkg/service.go new file mode 100644 index 0000000..380c257 --- /dev/null +++ b/pkg/service.go @@ -0,0 +1,34 @@ +// VulcanizeDB +// Copyright © 2020 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package tx_spammer + +import "sync" + +type Service interface { + Loop(wg *sync.WaitGroup) error +} + +type Tx struct { + Spammer *Sender + Generator *TxGenerator + Config *Config +} + +func NewTxSpammer(params []TxParams) (TxSpammer, error) { + + return &txSpammer{}, nil +} diff --git a/pkg/touch b/pkg/touch new file mode 100644 index 0000000..e69de29 diff --git a/pkg/tx_generator.go b/pkg/tx_generator.go new file mode 100644 index 0000000..b81a3f4 --- /dev/null +++ b/pkg/tx_generator.go @@ -0,0 +1,49 @@ +// VulcanizeDB +// Copyright © 2020 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package tx_spammer + +import "github.com/ethereum/go-ethereum/rpc" + +// TxGenerator generates and signs txs +type TxGenerator struct { + TxParams []TxParams + currentTx []byte + currentClient *rpc.Client + err error +} + +func NewTxGenerator(params []TxParams) *TxGenerator { + return &TxGenerator{ + TxParams: params, + } +} + +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 +} + +func (gen TxGenerator) gen(params TxParams) ([]byte, error) { + return nil, nil +} \ No newline at end of file diff --git a/pkg/tx_type.go b/pkg/tx_type.go new file mode 100644 index 0000000..860d60a --- /dev/null +++ b/pkg/tx_type.go @@ -0,0 +1,48 @@ +// VulcanizeDB +// Copyright © 2020 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package tx_spammer + +import ( + "fmt" + "strings" +) + +type TxType int + +const ( + Unkown TxType = iota + Standard + OptimismL2 + OptimismL1ToL2 + EIP1559 +) + +// TxTypeFromString returns the tx enum type from provided string +func TxTypeFromString(str string) (TxType, error) { + switch strings.ToLower(str) { + case "geth", "standard", "parity": + return Standard, nil + case "l2": + return OptimismL2, nil + case "l1", "l2tol1": + return OptimismL1ToL2, nil + case "eip1559": + return EIP1559, nil + default: + return Unkown, fmt.Errorf("unsupported tx type: %s", str) + } +} \ No newline at end of file diff --git a/pkg/util.go b/pkg/util.go new file mode 100644 index 0000000..6dc418a --- /dev/null +++ b/pkg/util.go @@ -0,0 +1,43 @@ +package tx_spammer + +import ( + "fmt" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "math/big" +) + +// 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: + return params.GoerliChainConfig, nil + case 420: + 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: + return params.MainnetChainConfig, nil + case 3: + return params.TestnetChainConfig, nil // Ropsten + case 4: + return params.RinkebyChainConfig, nil + case 5: + return params.GoerliChainConfig, nil + case 420: + return types.NewOVMSigner(big.NewInt()), nil + default: + return nil, fmt.Errorf("chain config for chainid %d not available", chainID) + } +} \ No newline at end of file