diff --git a/cmd/devnet/args/args.go b/cmd/devnet/args/args.go new file mode 100644 index 00000000000..59208ff41ef --- /dev/null +++ b/cmd/devnet/args/args.go @@ -0,0 +1,154 @@ +package args + +import ( + "fmt" + "reflect" + "strings" + "unicode" + "unicode/utf8" +) + +type Args []string + +func AsArgs(args interface{}) (Args, error) { + + argsValue := reflect.ValueOf(args) + + if argsValue.Kind() == reflect.Ptr { + argsValue = argsValue.Elem() + } + + if argsValue.Kind() != reflect.Struct { + return nil, fmt.Errorf("Args type must be struct or struc pointer, got %T", args) + } + + return gatherArgs(argsValue, func(v reflect.Value, field reflect.StructField) (string, error) { + tag := field.Tag.Get("arg") + + if tag == "-" { + return "", nil + } + + // only process public fields (reflection won't return values of unsafe fields without unsafe operations) + if r, _ := utf8.DecodeRuneInString(field.Name); !(unicode.IsLetter(r) && unicode.IsUpper(r)) { + return "", nil + } + + var key string + var positional bool + + for _, key = range strings.Split(tag, ",") { + if key == "" { + continue + } + + key = strings.TrimLeft(key, " ") + + if pos := strings.Index(key, ":"); pos != -1 { + key = key[:pos] + } + + switch { + case strings.HasPrefix(key, "---"): + return "", fmt.Errorf("%s.%s: too many hyphens", v.Type().Name(), field.Name) + case strings.HasPrefix(key, "--"): + + case strings.HasPrefix(key, "-"): + if len(key) != 2 { + return "", fmt.Errorf("%s.%s: short arguments must be one character only", v.Type().Name(), field.Name) + } + case key == "positional": + key = "" + positional = true + default: + return "", fmt.Errorf("unrecognized tag '%s' on field %s", key, tag) + } + } + + if len(key) == 0 && !positional { + key = "--" + strings.ToLower(field.Name) + } + + var value string + + switch fv := v.FieldByIndex(field.Index); fv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if fv.Int() == 0 { + break + } + fallthrough + default: + value = fmt.Sprintf("%v", fv.Interface()) + } + + flagValue, isFlag := field.Tag.Lookup("flag") + + if isFlag { + if value != "true" { + if flagValue == "true" { + value = flagValue + } + } + } + + if len(value) == 0 { + if defaultString, hasDefault := field.Tag.Lookup("default"); hasDefault { + value = defaultString + } + + if len(value) == 0 { + return "", nil + } + } + + if len(key) == 0 { + return value, nil + } + + if isFlag { + if value == "true" { + return key, nil + } + + return "", nil + } + + if len(value) == 0 { + return key, nil + } + + return fmt.Sprintf("%s=%s", key, value), nil + }) +} + +func gatherArgs(v reflect.Value, visit func(v reflect.Value, field reflect.StructField) (string, error)) (args Args, err error) { + for i := 0; i < v.NumField(); i++ { + field := v.Type().Field(i) + + var gathered Args + + fieldType := field.Type + + if fieldType.Kind() == reflect.Ptr { + fieldType.Elem() + } + + if fieldType.Kind() == reflect.Struct { + gathered, err = gatherArgs(v.FieldByIndex(field.Index), visit) + } else { + var value string + + if value, err = visit(v, field); len(value) > 0 { + gathered = Args{value} + } + } + + if err != nil { + return nil, err + } + + args = append(args, gathered...) + } + + return args, nil +} diff --git a/cmd/devnet/args/node.go b/cmd/devnet/args/node.go new file mode 100644 index 00000000000..a7a9defb5b7 --- /dev/null +++ b/cmd/devnet/args/node.go @@ -0,0 +1,137 @@ +package args + +import ( + "fmt" + "net" + "path/filepath" + "strconv" + + "github.com/ledgerwatch/erigon/cmd/devnet/requests" + "github.com/ledgerwatch/erigon/params/networkname" +) + +type Node struct { + requests.RequestGenerator `arg:"-"` + BuildDir string `arg:"positional" default:"./build/bin/devnet"` + DataDir string `arg:"--datadir" default:"./dev"` + Chain string `arg:"--chain" default:"dev"` + Port int `arg:"--port"` + AllowedPorts string `arg:"--p2p.allowed-ports"` + NAT string `arg:"--nat" default:"none"` + ConsoleVerbosity string `arg:"--log.console.verbosity" default:"0"` + DirVerbosity string `arg:"--log.dir.verbosity"` + LogDirPath string `arg:"--log.dir.path"` + LogDirPrefix string `arg:"--log.dir.prefix"` + P2PProtocol string `arg:"--p2p.protocol" default:"68"` + Downloader string `arg:"--no-downloader" default:"true"` + WS string `arg:"--ws" flag:"" default:"true"` + PrivateApiAddr string `arg:"--private.api.addr" default:"localhost:9090"` + HttpPort int `arg:"--http.port" default:"8545"` + HttpVHosts string `arg:"--http.vhosts"` + AuthRpcPort int `arg:"--authrpc.port" default:"8551"` + AuthRpcVHosts string `arg:"--authrpc.vhosts"` + WSPort int `arg:"-" default:"8546"` // flag not defined + GRPCPort int `arg:"-" default:"8547"` // flag not defined + TCPPort int `arg:"-" default:"8548"` // flag not defined + StaticPeers string `arg:"--staticpeers"` + WithoutHeimdall bool `arg:"--bor.withoutheimdall" flag:"" default:"false"` +} + +func (node *Node) configure(base Node, nodeNumber int) error { + node.DataDir = filepath.Join(base.DataDir, fmt.Sprintf("%d", nodeNumber)) + + node.LogDirPath = filepath.Join(base.DataDir, "logs") + node.LogDirPrefix = fmt.Sprintf("node-%d", nodeNumber) + + node.Chain = base.Chain + + node.StaticPeers = base.StaticPeers + + var err error + + node.PrivateApiAddr, _, err = portFromBase(base.PrivateApiAddr, nodeNumber, 1) + + if err != nil { + return err + } + + apiPort := base.HttpPort + (nodeNumber * 5) + + node.HttpPort = apiPort + node.WSPort = apiPort + 1 + node.GRPCPort = apiPort + 2 + node.TCPPort = apiPort + 3 + node.AuthRpcPort = apiPort + 4 + + return nil +} + +type Miner struct { + Node + Mine bool `arg:"--mine" flag:"true"` + DevPeriod int `arg:"--dev.period"` + BorPeriod int `arg:"--bor.period"` + BorMinBlockSize int `arg:"--bor.minblocksize"` + HttpApi string `arg:"--http.api" default:"admin,eth,erigon,web3,net,debug,trace,txpool,parity,ots"` + AccountSlots int `arg:"--txpool.accountslots" default:"16"` +} + +func (m Miner) Configure(baseNode Node, nodeNumber int) (int, interface{}, error) { + err := m.configure(baseNode, nodeNumber) + + if err != nil { + return -1, nil, err + } + + switch m.Chain { + case networkname.DevChainName: + if m.DevPeriod == 0 { + m.DevPeriod = 30 + } + } + + return m.HttpPort, m, nil +} + +func (n Miner) IsMiner() bool { + return true +} + +type NonMiner struct { + Node + HttpApi string `arg:"--http.api" default:"admin,eth,debug,net,trace,web3,erigon,txpool"` + TorrentPort string `arg:"--torrent.port" default:"42070"` + NoDiscover string `arg:"--nodiscover" flag:"" default:"true"` +} + +func (n NonMiner) Configure(baseNode Node, nodeNumber int) (int, interface{}, error) { + err := n.configure(baseNode, nodeNumber) + + if err != nil { + return -1, nil, err + } + + return n.HttpPort, n, nil +} + +func (n NonMiner) IsMiner() bool { + return false +} + +func portFromBase(baseAddr string, increment int, portCount int) (string, int, error) { + apiHost, apiPort, err := net.SplitHostPort(baseAddr) + + if err != nil { + return "", -1, err + } + + portNo, err := strconv.Atoi(apiPort) + + if err != nil { + return "", -1, err + } + + portNo += (increment * portCount) + + return fmt.Sprintf("%s:%d", apiHost, portNo), portNo, nil +} diff --git a/cmd/devnet/node/node_test.go b/cmd/devnet/args/node_test.go similarity index 87% rename from cmd/devnet/node/node_test.go rename to cmd/devnet/args/node_test.go index 8c241c03c7a..707c4da40eb 100644 --- a/cmd/devnet/node/node_test.go +++ b/cmd/devnet/args/node_test.go @@ -1,4 +1,4 @@ -package node_test +package args_test import ( "errors" @@ -6,21 +6,21 @@ import ( "path/filepath" "testing" - "github.com/ledgerwatch/erigon/cmd/devnet/devnetutils" - "github.com/ledgerwatch/erigon/cmd/devnet/node" + "github.com/ledgerwatch/erigon/cmd/devnet/args" ) func TestNodeArgs(t *testing.T) { asMap := map[string]struct{}{} - args, _ := devnetutils.AsArgs(node.Miner{ - Node: node.Node{ + nodeArgs, _ := args.AsArgs(args.Miner{ + Node: args.Node{ DataDir: filepath.Join("data", fmt.Sprintf("%d", 1)), PrivateApiAddr: "localhost:9092", }, + DevPeriod: 30, }) - for _, arg := range args { + for _, arg := range nodeArgs { asMap[arg] = struct{}{} } @@ -36,15 +36,15 @@ func TestNodeArgs(t *testing.T) { t.Fatal(asMap, "not found") } - args, _ = devnetutils.AsArgs(node.NonMiner{ - Node: node.Node{ + nodeArgs, _ = args.AsArgs(args.NonMiner{ + Node: args.Node{ DataDir: filepath.Join("data", fmt.Sprintf("%d", 2)), StaticPeers: "enode", PrivateApiAddr: "localhost:9091", }, }) - for _, arg := range args { + for _, arg := range nodeArgs { asMap[arg] = struct{}{} } @@ -160,8 +160,10 @@ func miningNodeArgs(dataDir string, nodeNumber int) []string { downloaderArg, _ := parameterFromArgument("--no-downloader", "true") httpPortArg, _ := parameterFromArgument("--http.port", "8545") authrpcPortArg, _ := parameterFromArgument("--authrpc.port", "8551") + natArg, _ := parameterFromArgument("--nat", "none") + accountSlotsArg, _ := parameterFromArgument("--txpool.accountslots", "16") - return []string{buildDirArg, dataDirArg, chainType, privateApiAddr, httpPortArg, authrpcPortArg, mine, httpApi, ws, devPeriod, consoleVerbosity, p2pProtocol, downloaderArg} + return []string{buildDirArg, dataDirArg, chainType, privateApiAddr, httpPortArg, authrpcPortArg, mine, httpApi, ws, natArg, devPeriod, consoleVerbosity, p2pProtocol, downloaderArg, accountSlotsArg} } // nonMiningNodeArgs returns custom args for starting a non-mining node @@ -176,8 +178,10 @@ func nonMiningNodeArgs(dataDir string, nodeNumber int, enode string) []string { p2pProtocol, _ := parameterFromArgument("--p2p.protocol", "68") downloaderArg, _ := parameterFromArgument("--no-downloader", "true") httpPortArg, _ := parameterFromArgument("--http.port", "8545") - httpApi, _ := parameterFromArgument(httpApiArg, "eth,debug,net,trace,web3,erigon") + httpApi, _ := parameterFromArgument(httpApiArg, "admin,eth,debug,net,trace,web3,erigon,txpool") authrpcPortArg, _ := parameterFromArgument("--authrpc.port", "8551") + natArg, _ := parameterFromArgument("--nat", "none") + ws := wsArg - return []string{buildDirArg, dataDirArg, chainType, privateApiAddr, httpPortArg, authrpcPortArg, httpApi, staticPeers, noDiscover, consoleVerbosity, torrentPort, p2pProtocol, downloaderArg} + return []string{buildDirArg, dataDirArg, chainType, privateApiAddr, httpPortArg, authrpcPortArg, httpApi, ws, natArg, staticPeers, noDiscover, consoleVerbosity, torrentPort, p2pProtocol, downloaderArg} } diff --git a/cmd/devnet/commands/account.go b/cmd/devnet/commands/account.go index 24a41169ac4..b90d4b93b88 100644 --- a/cmd/devnet/commands/account.go +++ b/cmd/devnet/commands/account.go @@ -1,21 +1,33 @@ package commands import ( + "context" + libcommon "github.com/ledgerwatch/erigon-lib/common" - "github.com/ledgerwatch/erigon/cmd/devnet/node" + "github.com/ledgerwatch/erigon/cmd/devnet/devnet" "github.com/ledgerwatch/erigon/cmd/devnet/requests" - "github.com/ledgerwatch/log/v3" + "github.com/ledgerwatch/erigon/cmd/devnet/scenarios" ) -const ( - addr = "0x71562b71999873DB5b286dF957af199Ec94617F7" -) +func init() { + scenarios.MustRegisterStepHandlers( + scenarios.StepHandler(GetBalance), + ) +} + +//const ( +// addr = "0x71562b71999873DB5b286dF957af199Ec94617F7" +//) + +func GetBalance(ctx context.Context, addr string, blockNum requests.BlockNumber, checkBal uint64) { + logger := devnet.Logger(ctx) -func callGetBalance(node *node.Node, addr string, blockNum requests.BlockNumber, checkBal uint64, logger log.Logger) { logger.Info("Getting balance", "addeess", addr) + address := libcommon.HexToAddress(addr) - bal, err := node.GetBalance(address, blockNum) + bal, err := devnet.SelectMiner(ctx).GetBalance(address, blockNum) + if err != nil { logger.Error("FAILURE", "error", err) return diff --git a/cmd/devnet/commands/all.go b/cmd/devnet/commands/all.go index b2a9ebbe618..af3e674a449 100644 --- a/cmd/devnet/commands/all.go +++ b/cmd/devnet/commands/all.go @@ -1,31 +1,21 @@ package commands import ( - "github.com/ledgerwatch/erigon/cmd/devnet/models" - "github.com/ledgerwatch/erigon/cmd/devnet/node" - "github.com/ledgerwatch/erigon/cmd/devnet/requests" + "github.com/ledgerwatch/erigon/cmd/devnet/devnet" + "github.com/ledgerwatch/erigon/cmd/devnet/scenarios" "github.com/ledgerwatch/erigon/cmd/devnet/services" "github.com/ledgerwatch/log/v3" ) -// ExecuteAllMethods runs all the simulation tests for erigon devnet -func ExecuteAllMethods(nw *node.Network, logger log.Logger) { - // test connection to JSON RPC - logger.Info("PINGING JSON RPC...") - if err := pingErigonRpc(nw.Node(0), logger); err != nil { - return - } - logger.Info("") - - // get balance of the receiver's account - callGetBalance(nw.Node(0), addr, requests.BlockNumbers.Latest, 0, logger) - logger.Info("") - - // confirm that the txpool is empty - logger.Info("CONFIRMING TXPOOL IS EMPTY BEFORE SENDING TRANSACTION...") - services.CheckTxPoolContent(nw.Node(0), 0, 0, 0, logger) - logger.Info("") +func init() { + scenarios.RegisterStepHandlers( + scenarios.StepHandler(services.CheckTxPoolContent), + scenarios.StepHandler(services.InitSubscriptions), + ) +} +// ExecuteAllMethods runs all the simulation tests for erigon devnet +func ExecuteAllMethods(nw *devnet.Network, logger log.Logger) { /* * Cannot run contract tx after running regular tx because contract tx simulates a new backend * and it expects the nonce to be 0. @@ -40,13 +30,6 @@ func ExecuteAllMethods(nw *node.Network, logger log.Logger) { //} //fmt.Println() - _, err := callSendTxWithDynamicFee(nw.Node(0), recipientAddress, models.DevAddress, logger) - if err != nil { - logger.Error("callSendTxWithDynamicFee", "error", err) - return - } - logger.Info("") - // initiate a contract transaction //fmt.Println("INITIATING A CONTRACT TRANSACTION...") //_, err := callContractTx() @@ -55,7 +38,4 @@ func ExecuteAllMethods(nw *node.Network, logger log.Logger) { // return //} //fmt.Println() - - logger.Info("SEND SIGNAL TO QUIT ALL RUNNING NODES") - models.QuitNodeChan <- true } diff --git a/cmd/devnet/commands/block.go b/cmd/devnet/commands/block.go index 9ced304ce2b..b9580e3eb56 100644 --- a/cmd/devnet/commands/block.go +++ b/cmd/devnet/commands/block.go @@ -1,6 +1,7 @@ package commands import ( + "context" "fmt" "time" @@ -8,20 +9,22 @@ import ( "github.com/ledgerwatch/erigon-lib/common/hexutility" "github.com/ledgerwatch/log/v3" + "github.com/ledgerwatch/erigon/cmd/devnet/devnet" "github.com/ledgerwatch/erigon/cmd/devnet/devnetutils" - "github.com/ledgerwatch/erigon/cmd/devnet/models" - "github.com/ledgerwatch/erigon/cmd/devnet/node" "github.com/ledgerwatch/erigon/cmd/devnet/requests" + "github.com/ledgerwatch/erigon/cmd/devnet/scenarios" "github.com/ledgerwatch/erigon/cmd/devnet/services" "github.com/ledgerwatch/erigon/common/hexutil" ) -const ( - recipientAddress = "0x71562b71999873DB5b286dF957af199Ec94617F7" - sendValue uint64 = 10000 -) +func init() { + scenarios.MustRegisterStepHandlers( + scenarios.StepHandler(SendTxWithDynamicFee), + scenarios.StepHandler(AwaitBlocks), + ) +} -func callSendTx(node *node.Node, value uint64, toAddr, fromAddr string, logger log.Logger) (*libcommon.Hash, error) { +func callSendTx(node devnet.Node, value uint64, toAddr, fromAddr string, logger log.Logger) (*libcommon.Hash, error) { logger.Info("Sending tx", "value", value, "to", toAddr, "from", fromAddr) // get the latest nonce for the next transaction @@ -32,7 +35,7 @@ func callSendTx(node *node.Node, value uint64, toAddr, fromAddr string, logger l } // create a non-contract transaction and sign it - signedTx, _, _, _, err := services.CreateTransaction(models.NonContractTx, toAddr, value, nonce) + signedTx, _, err := services.CreateTransaction(toAddr, value, nonce) if err != nil { logger.Error("failed to create a transaction", "error", err) return nil, err @@ -53,33 +56,40 @@ func callSendTx(node *node.Node, value uint64, toAddr, fromAddr string, logger l return hash, nil } -func callSendTxWithDynamicFee(node *node.Node, toAddr, fromAddr string, logger log.Logger) ([]*libcommon.Hash, error) { +func SendTxWithDynamicFee(ctx context.Context, toAddr, fromAddr string, amount uint64) ([]*libcommon.Hash, error) { // get the latest nonce for the next transaction + node := devnet.SelectNode(ctx) + logger := devnet.Logger(ctx) + nonce, err := services.GetNonce(node, libcommon.HexToAddress(fromAddr)) + if err != nil { logger.Error("failed to get latest nonce", "error", err) return nil, err } - lowerThanBaseFeeTxs, higherThanBaseFeeTxs, err := services.CreateManyEIP1559TransactionsRefWithBaseFee2(node, toAddr, &nonce, logger) + lowerThanBaseFeeTxs, higherThanBaseFeeTxs, err := services.CreateManyEIP1559TransactionsRefWithBaseFee2(ctx, toAddr, &nonce) if err != nil { logger.Error("failed CreateManyEIP1559TransactionsRefWithBaseFee", "error", err) return nil, err } - higherThanBaseFeeHashlist, err := services.SendManyTransactions(node, higherThanBaseFeeTxs, logger) + higherThanBaseFeeHashlist, err := services.SendManyTransactions(ctx, higherThanBaseFeeTxs) if err != nil { logger.Error("failed SendManyTransactions(higherThanBaseFeeTxs)", "error", err) return nil, err } - lowerThanBaseFeeHashlist, err := services.SendManyTransactions(node, lowerThanBaseFeeTxs, logger) + lowerThanBaseFeeHashlist, err := services.SendManyTransactions(ctx, lowerThanBaseFeeTxs) + if err != nil { logger.Error("failed SendManyTransactions(lowerThanBaseFeeTxs)", "error", err) return nil, err } - services.CheckTxPoolContent(node, 100, 0, 100, logger) + services.CheckTxPoolContent(ctx, 100, 0, 100) + + services.CheckTxPoolContent(ctx, -1, -1, -1) hashmap := make(map[libcommon.Hash]bool) for _, hash := range higherThanBaseFeeHashlist { @@ -92,37 +102,51 @@ func callSendTxWithDynamicFee(node *node.Node, toAddr, fromAddr string, logger l logger.Info("SUCCESS: All transactions in pending pool included in blocks") + return append(lowerThanBaseFeeHashlist, higherThanBaseFeeHashlist...), nil +} + +func AwaitBlocks(ctx context.Context, sleepTime time.Duration) error { + logger := devnet.Logger(ctx) + for i := 1; i <= 20; i++ { + node := devnet.SelectNode(ctx) + blockNumber, err := node.BlockNumber() + if err != nil { logger.Error("FAILURE => error getting block number", "error", err) } else { logger.Info("Got block number", "blockNum", blockNumber) } + pendingSize, queuedSize, baseFeeSize, err := node.TxpoolContent() + if err != nil { logger.Error("FAILURE getting txpool content", "error", err) } else { logger.Info("Txpool subpool sizes", "pending", pendingSize, "queued", queuedSize, "basefee", baseFeeSize) } - time.Sleep(5 * time.Second) + + time.Sleep(sleepTime) } - return append(lowerThanBaseFeeHashlist, higherThanBaseFeeHashlist...), nil + return nil } -func callContractTx(node *node.Node, logger log.Logger) (*libcommon.Hash, error) { +func callContractTx(node devnet.Node, logger log.Logger) (*libcommon.Hash, error) { // hashset to hold hashes for search after mining hashes := make(map[libcommon.Hash]bool) // get the latest nonce for the next transaction - nonce, err := services.GetNonce(node, libcommon.HexToAddress(models.DevAddress)) + nonce, err := services.GetNonce(node, libcommon.HexToAddress(services.DevAddress)) + if err != nil { logger.Error("failed to get latest nonce", "error", err) return nil, err } // subscriptionContract is the handler to the contract for further operations - signedTx, address, subscriptionContract, transactOpts, err := services.CreateTransaction(models.ContractTx, "", 0, nonce) + signedTx, address, subscriptionContract, transactOpts, err := services.DeploySubsriptionContract(nonce) + if err != nil { logger.Error("failed to create transaction", "error", err) return nil, err @@ -130,18 +154,22 @@ func callContractTx(node *node.Node, logger log.Logger) (*libcommon.Hash, error) // send the contract transaction to the node hash, err := node.SendTransaction(signedTx) + if err != nil { logger.Error("failed to send transaction", "error", err) return nil, err } + hashes[*hash] = true logger.Info("") eventHash, err := services.EmitFallbackEvent(node, subscriptionContract, transactOpts, logger) + if err != nil { logger.Error("failed to emit events", "error", err) return nil, err } + hashes[*eventHash] = true txToBlockMap, err := services.SearchReservesForTransactionHash(hashes, logger) @@ -151,13 +179,13 @@ func callContractTx(node *node.Node, logger log.Logger) (*libcommon.Hash, error) blockNum := (*txToBlockMap)[*eventHash] - block, err := node.GetBlockByNumber(requests.HexToInt(blockNum), true) + block, err := node.GetBlockByNumber(devnetutils.HexToInt(blockNum), true) if err != nil { return nil, err } expectedLog := requests.BuildLog(*eventHash, blockNum, address, - devnetutils.GenerateTopic(models.SolContractMethodSignature), hexutility.Bytes{}, hexutil.Uint(1), + devnetutils.GenerateTopic(services.SolContractMethodSignature), hexutility.Bytes{}, hexutil.Uint(1), block.Result.Hash, hexutil.Uint(0), false) if err = node.GetAndCompareLogs(0, 20, expectedLog); err != nil { @@ -166,8 +194,3 @@ func callContractTx(node *node.Node, logger log.Logger) (*libcommon.Hash, error) return hash, nil } - -func makeEIP1559Checks() { - // run the check for baseFee effect twice - -} diff --git a/cmd/devnet/commands/ping.go b/cmd/devnet/commands/ping.go new file mode 100644 index 00000000000..9298f29e952 --- /dev/null +++ b/cmd/devnet/commands/ping.go @@ -0,0 +1,22 @@ +package commands + +import ( + "context" + + "github.com/ledgerwatch/erigon/cmd/devnet/devnet" + "github.com/ledgerwatch/erigon/cmd/devnet/scenarios" +) + +func init() { + scenarios.MustRegisterStepHandlers( + scenarios.StepHandler(PingErigonRpc), + ) +} + +func PingErigonRpc(ctx context.Context) error { + err := devnet.SelectNode(ctx).PingErigonRpc().Err + if err != nil { + devnet.Logger(ctx).Error("FAILURE", "error", err) + } + return err +} diff --git a/cmd/devnet/commands/request.go b/cmd/devnet/commands/request.go deleted file mode 100644 index 693c61183c4..00000000000 --- a/cmd/devnet/commands/request.go +++ /dev/null @@ -1,14 +0,0 @@ -package commands - -import ( - "github.com/ledgerwatch/erigon/cmd/devnet/node" - "github.com/ledgerwatch/log/v3" -) - -func pingErigonRpc(node *node.Node, logger log.Logger) error { - err := node.PingErigonRpc().Err - if err != nil { - logger.Error("FAILURE", "error", err) - } - return err -} diff --git a/cmd/devnet/devnet/context.go b/cmd/devnet/devnet/context.go new file mode 100644 index 00000000000..bb103b03f5f --- /dev/null +++ b/cmd/devnet/devnet/context.go @@ -0,0 +1,154 @@ +package devnet + +import ( + go_context "context" + + "github.com/ledgerwatch/erigon/cmd/devnet/devnetutils" + "github.com/ledgerwatch/log/v3" + "github.com/urfave/cli/v2" +) + +type ctxKey int + +const ( + ckLogger ctxKey = iota + ckNetwork + ckNode + ckCliContext +) + +type Context interface { + go_context.Context + WithValue(key, value interface{}) Context +} + +type context struct { + go_context.Context +} + +func (c *context) WithValue(key, value interface{}) Context { + return &context{go_context.WithValue(c, key, value)} +} + +func AsContext(ctx go_context.Context) Context { + if ctx, ok := ctx.(Context); ok { + return ctx + } + + return &context{ctx} +} + +func WithNetwork(ctx go_context.Context, nw *Network) Context { + return &context{go_context.WithValue(go_context.WithValue(ctx, ckNetwork, nw), ckLogger, nw.Logger)} +} + +func Logger(ctx go_context.Context) log.Logger { + if logger, ok := ctx.Value(ckLogger).(log.Logger); ok { + return logger + } + + return log.Root() +} + +type cnode struct { + selector interface{} + node Node +} + +func WithCurrentNode(ctx go_context.Context, selector interface{}) Context { + return &context{go_context.WithValue(ctx, ckNode, &cnode{selector: selector})} +} + +func WithCliContext(ctx go_context.Context, cliCtx *cli.Context) Context { + return &context{go_context.WithValue(ctx, ckCliContext, cliCtx)} +} + +func CurrentNode(ctx go_context.Context) Node { + if cn, ok := ctx.Value(ckNode).(*cnode); ok { + if cn.node == nil { + if network, ok := ctx.Value(ckNetwork).(*Network); ok { + cn.node = network.SelectNode(ctx, cn.selector) + } + } + + return cn.node + } + + return nil +} + +func SelectNode(ctx go_context.Context, selector ...interface{}) Node { + if network, ok := ctx.Value(ckNetwork).(*Network); ok { + if len(selector) > 0 { + return network.SelectNode(ctx, selector[0]) + } + + if current := CurrentNode(ctx); current != nil { + return current + } + + return network.AnyNode(ctx) + } + + return nil +} + +func SelectMiner(ctx go_context.Context, selector ...interface{}) Node { + if network, ok := ctx.Value(ckNetwork).(*Network); ok { + if len(selector) > 0 { + miners := network.Miners() + switch selector := selector[0].(type) { + case int: + if selector < len(miners) { + return miners[selector] + } + case NodeSelector: + for _, node := range miners { + if selector.Test(ctx, node) { + return node + } + } + } + } + + if current := CurrentNode(ctx); current != nil && current.IsMiner() { + return current + } + + if miners := network.Miners(); len(miners) > 0 { + return miners[devnetutils.RandomInt(len(miners)-1)] + } + } + + return nil +} + +func SelectNonMiner(ctx go_context.Context, selector ...interface{}) Node { + if network, ok := ctx.Value(ckNetwork).(*Network); ok { + if len(selector) > 0 { + nonMiners := network.NonMiners() + switch selector := selector[0].(type) { + case int: + if selector < len(nonMiners) { + return nonMiners[selector] + } + case NodeSelector: + for _, node := range nonMiners { + if selector.Test(ctx, node) { + return node + } + } + } + } + + if current := CurrentNode(ctx); current != nil && !current.IsMiner() { + return current + } + + if nonMiners := network.NonMiners(); len(nonMiners) > 0 { + return nonMiners[devnetutils.RandomInt(len(nonMiners)-1)] + } + } + + return nil +} diff --git a/cmd/devnet/devnet/network.go b/cmd/devnet/devnet/network.go new file mode 100644 index 00000000000..8a12d602a30 --- /dev/null +++ b/cmd/devnet/devnet/network.go @@ -0,0 +1,238 @@ +package devnet + +import ( + go_context "context" + "errors" + "fmt" + "net" + "net/url" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/ledgerwatch/erigon-lib/common/dbg" + "github.com/ledgerwatch/erigon/cmd/devnet/args" + "github.com/ledgerwatch/erigon/cmd/devnet/devnetutils" + "github.com/ledgerwatch/erigon/cmd/devnet/requests" + "github.com/ledgerwatch/erigon/cmd/devnet/scenarios" + erigonapp "github.com/ledgerwatch/erigon/turbo/app" + erigoncli "github.com/ledgerwatch/erigon/turbo/cli" + "github.com/ledgerwatch/log/v3" +) + +type Network struct { + DataDir string + Chain string + Logger log.Logger + BasePrivateApiAddr string + BaseRPCAddr string + Nodes []Node + wg sync.WaitGroup + peers []string +} + +// Start starts the process for two erigon nodes running on the dev chain +func (nw *Network) Start() error { + + type configurable interface { + Configure(baseNode args.Node, nodeNumber int) (int, interface{}, error) + } + + apiHost, apiPort, err := net.SplitHostPort(nw.BaseRPCAddr) + + if err != nil { + return err + } + + apiPortNo, err := strconv.Atoi(apiPort) + + if err != nil { + return err + } + + baseNode := args.Node{ + DataDir: nw.DataDir, + Chain: nw.Chain, + HttpPort: apiPortNo, + PrivateApiAddr: nw.BasePrivateApiAddr, + } + + for i, node := range nw.Nodes { + if configurable, ok := node.(configurable); ok { + nodePort, args, err := configurable.Configure(baseNode, i) + + if err == nil { + node, err = nw.startNode(fmt.Sprintf("http://%s:%d", apiHost, nodePort), args, i) + } + + if err != nil { + nw.Stop() + return err + } + + nw.Nodes[i] = node + + // get the enode of the node + // - note this has the side effect of waiting for the node to start + if enode, err := getEnode(node); err == nil { + nw.peers = append(nw.peers, enode) + baseNode.StaticPeers = strings.Join(nw.peers, ",") + + // TODO do we need to call AddPeer to the nodes to make them aware of this one + // the current model only works for an appending node network where the peers gossip + // connections - not sure if this is the case ? + } + } + } + + return nil +} + +// startNode starts an erigon node on the dev chain +func (nw *Network) startNode(nodeAddr string, cfg interface{}, nodeNumber int) (Node, error) { + + args, err := devnetutils.AsArgs(cfg) + + if err != nil { + return nil, err + } + + nw.wg.Add(1) + + node := node{ + requests.NewRequestGenerator(nodeAddr, nw.Logger), + cfg, + &nw.wg, + nil, + } + + go func() { + nw.Logger.Info("Running node", "number", nodeNumber, "args", args) + + // catch any errors and avoid panics if an error occurs + defer func() { + panicResult := recover() + if panicResult == nil { + return + } + + nw.Logger.Error("catch panic", "err", panicResult, "stack", dbg.Stack()) + nw.Stop() + os.Exit(1) + }() + + app := erigonapp.MakeApp(fmt.Sprintf("node-%d", nodeNumber), node.run, erigoncli.DefaultFlags) + + if err := app.Run(args); err != nil { + _, printErr := fmt.Fprintln(os.Stderr, err) + if printErr != nil { + nw.Logger.Warn("Error writing app run error to stderr", "err", printErr) + } + } + }() + + return node, nil +} + +// getEnode returns the enode of the mining node +func getEnode(n Node) (string, error) { + reqCount := 0 + + for { + nodeInfo, err := n.AdminNodeInfo() + + if err != nil { + if reqCount < 10 { + var urlErr *url.Error + if errors.As(err, &urlErr) { + var opErr *net.OpError + if errors.As(urlErr.Err, &opErr) { + var callErr *os.SyscallError + if errors.As(opErr.Err, &callErr) { + if callErr.Syscall == "connectex" { + reqCount++ + time.Sleep(time.Duration(devnetutils.RandomInt(5)) * time.Second) + continue + } + } + } + } + } + + return "", err + } + + enode, err := devnetutils.UniqueIDFromEnode(nodeInfo.Enode) + + if err != nil { + return "", err + } + + return enode, nil + } +} + +func (nw *Network) Run(ctx go_context.Context, scenario scenarios.Scenario) error { + return scenarios.Run(WithNetwork(ctx, nw), &scenario) +} + +func (nw *Network) Stop() { + type stoppable interface { + Stop() + } + + for _, n := range nw.Nodes { + if stoppable, ok := n.(stoppable); ok { + stoppable.Stop() + } + } + + nw.wg.Wait() +} + +func (nw *Network) AnyNode(ctx go_context.Context) Node { + return nw.SelectNode(ctx, devnetutils.RandomInt(len(nw.Nodes)-1)) +} + +func (nw *Network) SelectNode(ctx go_context.Context, selector interface{}) Node { + switch selector := selector.(type) { + case int: + if selector < len(nw.Nodes) { + return nw.Nodes[selector] + } + case NodeSelector: + for _, node := range nw.Nodes { + if selector.Test(ctx, node) { + return node + } + } + } + + return nil +} + +func (nw *Network) Miners() []Node { + var miners []Node + + for _, node := range nw.Nodes { + if node.IsMiner() { + miners = append(miners, node) + } + } + + return miners +} + +func (nw *Network) NonMiners() []Node { + var nonMiners []Node + + for _, node := range nw.Nodes { + if !node.IsMiner() { + nonMiners = append(nonMiners, node) + } + } + + return nonMiners +} diff --git a/cmd/devnet/devnet/node.go b/cmd/devnet/devnet/node.go new file mode 100644 index 00000000000..d14761fdc3a --- /dev/null +++ b/cmd/devnet/devnet/node.go @@ -0,0 +1,85 @@ +package devnet + +import ( + go_context "context" + "sync" + + "github.com/ledgerwatch/erigon/cmd/devnet/args" + "github.com/ledgerwatch/erigon/cmd/devnet/requests" + "github.com/ledgerwatch/erigon/params" + "github.com/ledgerwatch/erigon/turbo/debug" + enode "github.com/ledgerwatch/erigon/turbo/node" + "github.com/ledgerwatch/log/v3" + "github.com/urfave/cli/v2" +) + +type Node interface { + requests.RequestGenerator + IsMiner() bool +} + +type NodeSelector interface { + Test(ctx go_context.Context, node Node) bool +} + +type NodeSelectorFunc func(ctx go_context.Context, node Node) bool + +func (f NodeSelectorFunc) Test(ctx go_context.Context, node Node) bool { + return f(ctx, node) +} + +type node struct { + requests.RequestGenerator + args interface{} + wg *sync.WaitGroup + ethNode *enode.ErigonNode +} + +func (n *node) Stop() { + if n.ethNode != nil { + toClose := n.ethNode + n.ethNode = nil + toClose.Close() + } + + if n.wg != nil { + wg := n.wg + n.wg = nil + wg.Done() + } +} + +func (n node) IsMiner() bool { + _, isMiner := n.args.(args.Miner) + return isMiner +} + +// run configures, creates and serves an erigon node +func (n *node) run(ctx *cli.Context) error { + var logger log.Logger + var err error + + if logger, err = debug.Setup(ctx, false /* rootLogger */); err != nil { + return err + } + + logger.Info("Build info", "git_branch", params.GitBranch, "git_tag", params.GitTag, "git_commit", params.GitCommit) + + nodeCfg := enode.NewNodConfigUrfave(ctx, logger) + ethCfg := enode.NewEthConfigUrfave(ctx, nodeCfg, logger) + + n.ethNode, err = enode.New(nodeCfg, ethCfg, logger) + + if err != nil { + logger.Error("Node startup", "err", err) + return err + } + + err = n.ethNode.Serve() + + if err != nil { + logger.Error("error while serving Devnet node", "err", err) + } + + return err +} diff --git a/cmd/devnet/devnetutils/utils.go b/cmd/devnet/devnetutils/utils.go index c66d7eefb04..960e0b5abaf 100644 --- a/cmd/devnet/devnetutils/utils.go +++ b/cmd/devnet/devnetutils/utils.go @@ -2,8 +2,8 @@ package devnetutils import ( "crypto/rand" + "encoding/binary" "fmt" - "math/big" "net" "os" "path/filepath" @@ -46,6 +46,13 @@ func ClearDevDB(dataDir string, logger log.Logger) error { return nil } +// HexToInt converts a hexadecimal string to uint64 +func HexToInt(hexStr string) uint64 { + cleaned := strings.ReplaceAll(hexStr, "0x", "") // remove the 0x prefix + result, _ := strconv.ParseUint(cleaned, 16, 64) + return result +} + // UniqueIDFromEnode returns the unique ID from a node's enode, removing the `?discport=0` part func UniqueIDFromEnode(enode string) (string, error) { if len(enode) == 0 { @@ -85,6 +92,16 @@ func UniqueIDFromEnode(enode string) (string, error) { return enode[:i], nil } +func RandomInt(max int) int { + if max == 0 { + return 0 + } + + var n uint16 + binary.Read(rand.Reader, binary.LittleEndian, &n) + return int(n) % (max + 1) +} + // NamespaceAndSubMethodFromMethod splits a parent method into namespace and the actual method func NamespaceAndSubMethodFromMethod(method string) (string, string, error) { parts := strings.SplitN(method, "_", 2) @@ -105,14 +122,7 @@ func RandomNumberInRange(min, max uint64) (uint64, error) { return 0, fmt.Errorf("Invalid range: upper bound %d less or equal than lower bound %d", max, min) } - diff := int64(max - min) - - n, err := rand.Int(rand.Reader, big.NewInt(diff)) - if err != nil { - return 0, err - } - - return uint64(n.Int64() + int64(min)), nil + return uint64(RandomInt(int(max-min)) + int(min)), nil } type Args []string @@ -176,7 +186,17 @@ func AsArgs(args interface{}) (Args, error) { key = "--" + strings.ToLower(field.Name) } - value := fmt.Sprintf("%v", v.FieldByIndex(field.Index).Interface()) + var value string + + switch fv := v.FieldByIndex(field.Index); fv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if fv.Int() == 0 { + break + } + fallthrough + default: + value = fmt.Sprintf("%v", fv.Interface()) + } flagValue, isFlag := field.Tag.Lookup("flag") @@ -188,7 +208,7 @@ func AsArgs(args interface{}) (Args, error) { } } - if len(value) == 0 || value == "0" { + if len(value) == 0 { if defaultString, hasDefault := field.Tag.Lookup("default"); hasDefault { value = defaultString } diff --git a/cmd/devnet/devnetutils/utils_test.go b/cmd/devnet/devnetutils/utils_test.go index a756e4c9dbe..94f22d7155a 100644 --- a/cmd/devnet/devnetutils/utils_test.go +++ b/cmd/devnet/devnetutils/utils_test.go @@ -7,6 +7,23 @@ import ( "github.com/stretchr/testify/require" ) +func TestHexToInt(t *testing.T) { + testCases := []struct { + hexStr string + expected uint64 + }{ + {"0x0", 0}, + {"0x32424", 205860}, + {"0x200", 512}, + {"0x39", 57}, + } + + for _, testCase := range testCases { + got := HexToInt(testCase.hexStr) + require.EqualValues(t, testCase.expected, got) + } +} + func TestUniqueIDFromEnode(t *testing.T) { testCases := []struct { input string diff --git a/cmd/devnet/main.go b/cmd/devnet/main.go index 2a405526449..28db249ac8c 100644 --- a/cmd/devnet/main.go +++ b/cmd/devnet/main.go @@ -1,15 +1,20 @@ package main import ( + "context" "fmt" "os" "path/filepath" + dbg "runtime/debug" "time" - "github.com/ledgerwatch/erigon/cmd/devnet/commands" + _ "github.com/ledgerwatch/erigon/cmd/devnet/commands" + + "github.com/ledgerwatch/erigon/cmd/devnet/args" + "github.com/ledgerwatch/erigon/cmd/devnet/devnet" "github.com/ledgerwatch/erigon/cmd/devnet/devnetutils" - "github.com/ledgerwatch/erigon/cmd/devnet/node" "github.com/ledgerwatch/erigon/cmd/devnet/requests" + "github.com/ledgerwatch/erigon/cmd/devnet/scenarios" "github.com/ledgerwatch/erigon/cmd/devnet/services" "github.com/ledgerwatch/erigon/params/networkname" "github.com/ledgerwatch/log/v3" @@ -21,20 +26,32 @@ import ( "github.com/urfave/cli/v2" ) -var DataDirFlag = flags.DirectoryFlag{ - Name: "datadir", - Usage: "Data directory for the devnet", - Value: flags.DirectoryString(""), - Required: true, -} +var ( + DataDirFlag = flags.DirectoryFlag{ + Name: "datadir", + Usage: "Data directory for the devnet", + Value: flags.DirectoryString(""), + Required: true, + } + + ChainFlag = cli.StringFlag{ + Name: "chain", + Usage: "The devnet chain to run (dev,bor-devnet)", + Value: networkname.DevChainName, + } + + WithoutHeimdallFlag = cli.BoolFlag{ + Name: "bor.withoutheimdall", + Usage: "Run without Heimdall service", + } +) type PanicHandler struct { } func (ph PanicHandler) Log(r *log.Record) error { - fmt.Println("LEGACY LOG:", r.Msg) - //fmt.Printf("Stack: %s\n", dbg.Stack()) - //os.Exit(1) + fmt.Printf("Msg: %s\nStack: %s\n", r.Msg, dbg.Stack()) + os.Exit(1) return nil } @@ -49,6 +66,8 @@ func main() { } app.Flags = []cli.Flag{ &DataDirFlag, + &ChainFlag, + &WithoutHeimdallFlag, } app.After = func(ctx *cli.Context) error { @@ -61,12 +80,19 @@ func main() { } } +const ( + recipientAddress = "0x71562b71999873DB5b286dF957af199Ec94617F7" + sendValue uint64 = 10000 +) + func action(ctx *cli.Context) error { dataDir := ctx.String("datadir") logsDir := filepath.Join(dataDir, "logs") + if err := os.MkdirAll(logsDir, 0755); err != nil { return err } + logger := logging.SetupLoggerCtx("devnet", ctx, false /* rootLogger */) // Make root logger fail @@ -77,33 +103,133 @@ func action(ctx *cli.Context) error { return err } - network := &node.Network{ - DataDir: dataDir, - Chain: networkname.DevChainName, - //Chain: networkname.BorDevnetChainName, - Logger: logger, - BasePrivateApiAddr: "localhost:9090", - BaseRPCAddr: "localhost:8545", - Nodes: []node.NetworkNode{ - &node.Miner{}, - &node.NonMiner{}, - }, + network, err := selectNetwork(ctx, logger) + + if err != nil { + return err } // start the network with each node in a go routine - network.Start() - - // sleep for seconds to allow the nodes fully start up - time.Sleep(time.Second * 10) + logger.Info("Starting Network") + if err := network.Start(); err != nil { + return fmt.Errorf("Network start failed: %w", err) + } - // start up the subscription services for the different sub methods - services.InitSubscriptions([]requests.SubMethod{requests.Methods.ETHNewHeads}, logger) + runCtx := devnet.WithCliContext(context.Background(), ctx) - // execute all rpc methods amongst the two nodes - commands.ExecuteAllMethods(network, logger) + if ctx.String(ChainFlag.Name) == networkname.DevChainName { + // the dev network currently inserts blocks very slowly when run in multi node mode - needs investigaton + // this effectively makes it a ingle node network by routing all traffic to node 0 + // devnet.WithCurrentNode(devnet.WithCliContext(context.Background(), ctx), 0) + services.MaxNumberOfEmptyBlockChecks = 30 + } - // wait for all goroutines to complete before exiting - network.Wait() + network.Run( + runCtx, + scenarios.Scenario{ + Name: "all", + Steps: []*scenarios.Step{ + {Text: "InitSubscriptions", Args: []any{[]requests.SubMethod{requests.Methods.ETHNewHeads}}}, + {Text: "PingErigonRpc"}, + {Text: "CheckTxPoolContent", Args: []any{0, 0, 0}}, + {Text: "SendTxWithDynamicFee", Args: []any{recipientAddress, services.DevAddress, sendValue}}, + {Text: "AwaitBlocks", Args: []any{2 * time.Second}}, + }, + }) + + logger.Info("Stopping Network") + network.Stop() return nil } + +func selectNetwork(ctx *cli.Context, logger log.Logger) (*devnet.Network, error) { + dataDir := ctx.String(DataDirFlag.Name) + chain := ctx.String(ChainFlag.Name) + + switch chain { + case networkname.BorDevnetChainName: + if ctx.Bool(WithoutHeimdallFlag.Name) { + return &devnet.Network{ + DataDir: dataDir, + Chain: networkname.BorDevnetChainName, + Logger: logger, + BasePrivateApiAddr: "localhost:10090", + BaseRPCAddr: "localhost:8545", + Nodes: []devnet.Node{ + args.Miner{ + Node: args.Node{ + ConsoleVerbosity: "0", + DirVerbosity: "5", + WithoutHeimdall: true, + }, + AccountSlots: 200, + }, + args.NonMiner{ + Node: args.Node{ + ConsoleVerbosity: "0", + DirVerbosity: "5", + WithoutHeimdall: true, + }, + }, + }, + }, nil + } else { + return &devnet.Network{ + DataDir: dataDir, + Chain: networkname.BorDevnetChainName, + Logger: logger, + BasePrivateApiAddr: "localhost:10090", + BaseRPCAddr: "localhost:8545", + Nodes: []devnet.Node{ + args.Miner{ + Node: args.Node{ + ConsoleVerbosity: "0", + DirVerbosity: "5", + }, + AccountSlots: 200, + }, + args.Miner{ + Node: args.Node{ + ConsoleVerbosity: "0", + DirVerbosity: "5", + }, + AccountSlots: 200, + }, + args.NonMiner{ + Node: args.Node{ + ConsoleVerbosity: "0", + DirVerbosity: "5", + }, + }, + }, + }, nil + } + + case networkname.DevChainName: + return &devnet.Network{ + DataDir: dataDir, + Chain: networkname.DevChainName, + Logger: logger, + BasePrivateApiAddr: "localhost:10090", + BaseRPCAddr: "localhost:8545", + Nodes: []devnet.Node{ + args.Miner{ + Node: args.Node{ + ConsoleVerbosity: "0", + DirVerbosity: "5", + }, + AccountSlots: 200, + }, + args.NonMiner{ + Node: args.Node{ + ConsoleVerbosity: "0", + DirVerbosity: "5", + }, + }, + }, + }, nil + } + + return nil, fmt.Errorf(`Unknown network: "%s"`, chain) +} diff --git a/cmd/devnet/models/errors.go b/cmd/devnet/models/errors.go deleted file mode 100644 index eb385c5128e..00000000000 --- a/cmd/devnet/models/errors.go +++ /dev/null @@ -1,8 +0,0 @@ -package models - -import "errors" - -var ( - // ErrInvalidTransactionType for invalid transaction types - ErrInvalidTransactionType = errors.New("invalid transaction type") -) diff --git a/cmd/devnet/models/model.go b/cmd/devnet/models/model.go deleted file mode 100644 index 9d832977b4f..00000000000 --- a/cmd/devnet/models/model.go +++ /dev/null @@ -1,77 +0,0 @@ -package models - -import ( - libcommon "github.com/ledgerwatch/erigon-lib/common" - "github.com/ledgerwatch/erigon/accounts/abi/bind/backends" - "github.com/ledgerwatch/erigon/cmd/devnet/requests" - "github.com/ledgerwatch/erigon/common/hexutil" - "github.com/ledgerwatch/erigon/core" - "github.com/ledgerwatch/erigon/crypto" - "github.com/ledgerwatch/erigon/rpc" -) - -type ( - // TransactionType is the type of transaction attempted to be made, can be regular or contract - TransactionType string -) - -const ( - // MaxNumberOfBlockChecks is the max number of blocks to look for a transaction in - MaxNumberOfBlockChecks = 3 - - // hexPrivateKey is the hex value for the private key - hexPrivateKey = "26e86e45f6fc45ec6e2ecd128cec80fa1d1505e5507dcd2ae58c3130a7a97b48" - // DevAddress is the developer address for sending - DevAddress = "0x67b1d87101671b127f5f8714789C7192f7ad340e" - - // NonContractTx is the transaction type for sending ether - NonContractTx TransactionType = "non-contract" - // ContractTx is the transaction type for sending ether - ContractTx TransactionType = "contract" - // DynamicFee is the transaction type for dynamic fee - DynamicFee TransactionType = "dynamic-fee" - - // SolContractMethodSignature is the function signature for the event in the solidity contract definition - SolContractMethodSignature = "SubscriptionEvent()" -) - -var ( - // DevSignedPrivateKey is the signed private key for signing transactions - DevSignedPrivateKey, _ = crypto.HexToECDSA(hexPrivateKey) - // gspec is the geth dev genesis block - gspec = core.DeveloperGenesisBlock(uint64(0), libcommon.HexToAddress(DevAddress)) - // ContractBackend is a simulated backend created using a simulated blockchain - ContractBackend = backends.NewSimulatedBackendWithConfig(gspec.Alloc, gspec.Config, 1_000_000) - - // MethodSubscriptionMap is a container for all the subscription methods - MethodSubscriptionMap *map[requests.SubMethod]*MethodSubscription - - // NewHeadsChan is the block cache the eth_NewHeads - NewHeadsChan chan interface{} - - //QuitNodeChan is the channel for receiving a quit signal on all nodes - QuitNodeChan chan bool -) - -// MethodSubscription houses the client subscription, name and channel for its delivery -type MethodSubscription struct { - Client *rpc.Client - ClientSub *rpc.ClientSubscription - Name requests.SubMethod - SubChan chan interface{} -} - -// NewMethodSubscription returns a new MethodSubscription instance -func NewMethodSubscription(name requests.SubMethod) *MethodSubscription { - return &MethodSubscription{ - Name: name, - SubChan: make(chan interface{}), - } -} - -// Block represents a simple block for queries -type Block struct { - Number *hexutil.Big - Transactions []libcommon.Hash - BlockHash libcommon.Hash -} diff --git a/cmd/devnet/node/network.go b/cmd/devnet/node/network.go deleted file mode 100644 index 74cfecbfe44..00000000000 --- a/cmd/devnet/node/network.go +++ /dev/null @@ -1,58 +0,0 @@ -package node - -import ( - "sync" - - "github.com/ledgerwatch/erigon/cmd/devnet/models" - "github.com/ledgerwatch/log/v3" -) - -type Network struct { - DataDir string - Chain string - Logger log.Logger - BasePrivateApiAddr string - BaseRPCAddr string - Nodes []NetworkNode - wg sync.WaitGroup - peers []string -} - -// Start starts the process for two erigon nodes running on the dev chain -func (nw *Network) Start() { - for i, node := range nw.Nodes { - node.Start(nw, i) - - // get the enode of the node - // - note this has the side effect of waiting for the node to start - if enode, err := node.getEnode(); err != nil { - nw.peers = append(nw.peers, enode) - - // TODO we need to call AddPeer to the nodes to make them aware of this one - // the current model only works for a 2 node network - } - - } - - quitOnSignal(&nw.wg) -} - -func (nw *Network) Wait() { - nw.wg.Wait() -} - -func (nw *Network) Node(nodeNumber int) *Node { - return nw.Nodes[nodeNumber].node() -} - -// QuitOnSignal stops the node goroutines after all checks have been made on the devnet -func quitOnSignal(wg *sync.WaitGroup) { - models.QuitNodeChan = make(chan bool) - go func() { - for <-models.QuitNodeChan { - // TODO this assumes 2 nodes and it should be node.Stop() - wg.Done() - wg.Done() - } - }() -} diff --git a/cmd/devnet/node/node.go b/cmd/devnet/node/node.go deleted file mode 100644 index dc3d95a5a29..00000000000 --- a/cmd/devnet/node/node.go +++ /dev/null @@ -1,271 +0,0 @@ -package node - -import ( - "crypto/rand" - "errors" - "fmt" - "math/big" - "net" - "net/url" - "os" - "path/filepath" - "strconv" - "strings" - "sync" - "time" - - "github.com/urfave/cli/v2" - - "github.com/ledgerwatch/erigon-lib/common/dbg" - "github.com/ledgerwatch/erigon/cmd/devnet/devnetutils" - "github.com/ledgerwatch/erigon/cmd/devnet/requests" - "github.com/ledgerwatch/erigon/params" - "github.com/ledgerwatch/erigon/params/networkname" - erigonapp "github.com/ledgerwatch/erigon/turbo/app" - erigoncli "github.com/ledgerwatch/erigon/turbo/cli" - "github.com/ledgerwatch/erigon/turbo/debug" - "github.com/ledgerwatch/erigon/turbo/node" - "github.com/ledgerwatch/log/v3" -) - -type NetworkNode interface { - Start(nw *Network, nodeNumber int) error - getEnode() (string, error) - node() *Node -} - -type Node struct { - *requests.RequestGenerator `arg:"-"` - BuildDir string `arg:"positional" default:"./build/bin/devnet"` - DataDir string `arg:"--datadir" default:"./dev"` - Chain string `arg:"--chain" default:"dev"` - ConsoleVerbosity string `arg:"--log.console.verbosity" default:"0"` - DirVerbosity string `arg:"--log.dir.verbosity"` - LogDirPath string `arg:"--log.dir.path"` //default:"./cmd/devnet/debug_logs" - P2PProtocol string `arg:"--p2p.protocol" default:"68"` - Downloader string `arg:"--no-downloader" default:"true"` - PrivateApiAddr string `arg:"--private.api.addr" default:"localhost:9090"` - HttpPort int `arg:"--http.port" default:"8545"` - AuthRpcPort int `arg:"--authrpc.port" default:"8551"` - WSPort int `arg:"-" default:"8546"` // flag not defined - GRPCPort int `arg:"-" default:"8547"` // flag not defined - TCPPort int `arg:"-" default:"8548"` // flag not defined - StaticPeers string `arg:"--staticpeers"` - WithoutHeimdall bool `arg:"--bor.withoutheimdall" flag:"" default:"false"` -} - -// getEnode returns the enode of the mining node -func (node Node) getEnode() (string, error) { - reqCount := 0 - - for { - nodeInfo, err := node.AdminNodeInfo() - - if err != nil { - if reqCount < 10 { - var urlErr *url.Error - if errors.As(err, &urlErr) { - var opErr *net.OpError - if errors.As(urlErr.Err, &opErr) { - var callErr *os.SyscallError - if errors.As(opErr.Err, &callErr) { - if callErr.Syscall == "connectex" { - reqCount++ - delay, _ := rand.Int(rand.Reader, big.NewInt(4)) - time.Sleep(time.Duration(delay.Int64()+1) * time.Second) - continue - } - } - } - } - } - - return "", err - } - - enode, err := devnetutils.UniqueIDFromEnode(nodeInfo.Enode) - - if err != nil { - return "", err - } - - return enode, nil - } -} - -func (node *Node) configure(nw *Network, nodeNumber int) (err error) { - node.DataDir = filepath.Join(nw.DataDir, fmt.Sprintf("%d", nodeNumber)) - - //TODO add a log.dir.prefix arg and set it to the node name (node-%d) - //node.LogDirPath = filepath.Join(nw.DataDir, "logs") - - node.Chain = nw.Chain - - node.StaticPeers = strings.Join(nw.peers, ",") - - node.PrivateApiAddr, _, err = portFromBase(nw.BasePrivateApiAddr, nodeNumber, 1) - - if err != nil { - return err - } - - httpApiAddr, apiPort, err := portFromBase(nw.BaseRPCAddr, nodeNumber, 5) - - if err != nil { - return err - } - - node.HttpPort = apiPort - node.WSPort = apiPort + 1 - node.GRPCPort = apiPort + 2 - node.TCPPort = apiPort + 3 - node.AuthRpcPort = apiPort + 4 - - node.RequestGenerator = requests.NewRequestGenerator("http://"+httpApiAddr, nw.Logger) - - return nil -} - -type Miner struct { - Node - Mine bool `arg:"--mine" flag:"true"` - DevPeriod string `arg:"--dev.period" default:"30"` - HttpApi string `arg:"--http.api" default:"admin,eth,erigon,web3,net,debug,trace,txpool,parity,ots"` - WS string `arg:"--ws" flag:"" default:"true"` -} - -func (node *Miner) node() *Node { - return &node.Node -} - -func (node *Miner) Start(nw *Network, nodeNumber int) (err error) { - err = node.configure(nw, nodeNumber) - - if err != nil { - return err - } - - switch node.Chain { - case networkname.BorDevnetChainName: - node.WithoutHeimdall = true - } - - args, err := devnetutils.AsArgs(node) - - if err != nil { - return err - } - - nw.wg.Add(1) - - go startNode(&nw.wg, args, nodeNumber, nw.Logger) - - return nil -} - -type NonMiner struct { - Node - HttpApi string `arg:"--http.api" default:"eth,debug,net,trace,web3,erigon"` - TorrentPort string `arg:"--torrent.port" default:"42070"` - NoDiscover string `arg:"--nodiscover" flag:"" default:"true"` -} - -func (node *NonMiner) node() *Node { - return &node.Node -} - -func (node *NonMiner) Start(nw *Network, nodeNumber int) (err error) { - err = node.configure(nw, nodeNumber) - - if err != nil { - return err - } - - args, err := devnetutils.AsArgs(node) - - if err != nil { - return err - } - - nw.wg.Add(1) - - go startNode(&nw.wg, args, nodeNumber, nw.Logger) - - return nil -} - -func portFromBase(baseAddr string, increment int, portCount int) (string, int, error) { - apiHost, apiPort, err := net.SplitHostPort(baseAddr) - - if err != nil { - return "", -1, err - } - - portNo, err := strconv.Atoi(apiPort) - - if err != nil { - return "", -1, err - } - - portNo += (increment * portCount) - - return fmt.Sprintf("%s:%d", apiHost, portNo), portNo, nil -} - -// startNode starts an erigon node on the dev chain -func startNode(wg *sync.WaitGroup, args []string, nodeNumber int, logger log.Logger) { - logger.Info("Running node", "number", nodeNumber, "args", args) - - // catch any errors and avoid panics if an error occurs - defer func() { - panicResult := recover() - if panicResult == nil { - wg.Done() - return - } - - logger.Error("catch panic", "err", panicResult, "stack", dbg.Stack()) - wg.Done() - //TODO - this should be at process end not here wg should be enough - os.Exit(1) - }() - - app := erigonapp.MakeApp(fmt.Sprintf("node-%d", nodeNumber), runNode, erigoncli.DefaultFlags) - - if err := app.Run(args); err != nil { - _, printErr := fmt.Fprintln(os.Stderr, err) - if printErr != nil { - logger.Warn("Error writing app run error to stderr", "err", printErr) - } - wg.Done() - //TODO - this should be at process end not here wg should be enough - os.Exit(1) - } -} - -// runNode configures, creates and serves an erigon node -func runNode(ctx *cli.Context) error { - // Initializing the node and providing the current git commit there - - var logger log.Logger - var err error - if logger, err = debug.Setup(ctx, false /* rootLogger */); err != nil { - return err - } - logger.Info("Build info", "git_branch", params.GitBranch, "git_tag", params.GitTag, "git_commit", params.GitCommit) - - nodeCfg := node.NewNodConfigUrfave(ctx, logger) - ethCfg := node.NewEthConfigUrfave(ctx, nodeCfg, logger) - - ethNode, err := node.New(nodeCfg, ethCfg, logger) - if err != nil { - logger.Error("Devnet startup", "err", err) - return err - } - - err = ethNode.Serve() - if err != nil { - logger.Error("error while serving Devnet node", "err", err) - } - return err -} diff --git a/cmd/devnet/requests/account.go b/cmd/devnet/requests/account.go index 150cb71f1e7..3c31bed43a7 100644 --- a/cmd/devnet/requests/account.go +++ b/cmd/devnet/requests/account.go @@ -23,7 +23,7 @@ type EthTransaction struct { Value hexutil.Big `json:"value"` } -func (reqGen *RequestGenerator) GetBalance(address libcommon.Address, blockNum BlockNumber) (uint64, error) { +func (reqGen *requestGenerator) GetBalance(address libcommon.Address, blockNum BlockNumber) (uint64, error) { var b EthBalance method, body := reqGen.getBalance(address, blockNum) @@ -38,7 +38,7 @@ func (reqGen *RequestGenerator) GetBalance(address libcommon.Address, blockNum B return b.Balance.ToInt().Uint64(), nil } -func (req *RequestGenerator) getBalance(address libcommon.Address, blockNum BlockNumber) (RPCMethod, string) { +func (req *requestGenerator) getBalance(address libcommon.Address, blockNum BlockNumber) (RPCMethod, string) { const template = `{"jsonrpc":"2.0","method":%q,"params":["0x%x","%v"],"id":%d}` return Methods.ETHGetBalance, fmt.Sprintf(template, Methods.ETHGetBalance, address, blockNum, req.reqID) } diff --git a/cmd/devnet/requests/admin.go b/cmd/devnet/requests/admin.go index 662bdd16a99..cba12a5720f 100644 --- a/cmd/devnet/requests/admin.go +++ b/cmd/devnet/requests/admin.go @@ -12,7 +12,7 @@ type AdminNodeInfoResponse struct { Result p2p.NodeInfo `json:"result"` } -func (reqGen *RequestGenerator) AdminNodeInfo() (p2p.NodeInfo, error) { +func (reqGen *requestGenerator) AdminNodeInfo() (p2p.NodeInfo, error) { var b AdminNodeInfoResponse method, body := reqGen.adminNodeInfo() @@ -23,7 +23,7 @@ func (reqGen *RequestGenerator) AdminNodeInfo() (p2p.NodeInfo, error) { return b.Result, nil } -func (req *RequestGenerator) adminNodeInfo() (RPCMethod, string) { +func (req *requestGenerator) adminNodeInfo() (RPCMethod, string) { const template = `{"jsonrpc":"2.0","method":%q,"id":%d}` return Methods.AdminNodeInfo, fmt.Sprintf(template, Methods.AdminNodeInfo, req.reqID) } diff --git a/cmd/devnet/requests/block.go b/cmd/devnet/requests/block.go index 3012e6fc8da..992b33b7b0f 100644 --- a/cmd/devnet/requests/block.go +++ b/cmd/devnet/requests/block.go @@ -38,7 +38,7 @@ type EthSendRawTransaction struct { TxnHash libcommon.Hash `json:"result"` } -func (reqGen *RequestGenerator) BlockNumber() (uint64, error) { +func (reqGen *requestGenerator) BlockNumber() (uint64, error) { var b EthBlockNumber method, body := reqGen.blockNumber() @@ -52,12 +52,12 @@ func (reqGen *RequestGenerator) BlockNumber() (uint64, error) { return number, nil } -func (req *RequestGenerator) blockNumber() (RPCMethod, string) { +func (req *requestGenerator) blockNumber() (RPCMethod, string) { const template = `{"jsonrpc":"2.0","method":%q,"id":%d}` return Methods.ETHBlockNumber, fmt.Sprintf(template, Methods.ETHBlockNumber, req.reqID) } -func (reqGen *RequestGenerator) GetBlockByNumber(blockNum uint64, withTxs bool) (EthBlockByNumber, error) { +func (reqGen *requestGenerator) GetBlockByNumber(blockNum uint64, withTxs bool) (EthBlockByNumber, error) { var b EthBlockByNumber method, body := reqGen.getBlockByNumber(blockNum, withTxs) @@ -73,17 +73,17 @@ func (reqGen *RequestGenerator) GetBlockByNumber(blockNum uint64, withTxs bool) return b, nil } -func (req *RequestGenerator) getBlockByNumber(blockNum uint64, withTxs bool) (RPCMethod, string) { +func (req *requestGenerator) getBlockByNumber(blockNum uint64, withTxs bool) (RPCMethod, string) { const template = `{"jsonrpc":"2.0","method":%q,"params":["0x%x",%t],"id":%d}` return Methods.ETHGetBlockByNumber, fmt.Sprintf(template, Methods.ETHGetBlockByNumber, blockNum, withTxs, req.reqID) } -func (req *RequestGenerator) getBlockByNumberI(blockNum string, withTxs bool) (RPCMethod, string) { +func (req *requestGenerator) getBlockByNumberI(blockNum string, withTxs bool) (RPCMethod, string) { const template = `{"jsonrpc":"2.0","method":%q,"params":["%s",%t],"id":%d}` return Methods.ETHGetBlockByNumber, fmt.Sprintf(template, Methods.ETHGetBlockByNumber, blockNum, withTxs, req.reqID) } -func (reqGen *RequestGenerator) GetBlockByNumberDetails(blockNum string, withTxs bool) (map[string]interface{}, error) { +func (reqGen *requestGenerator) GetBlockByNumberDetails(blockNum string, withTxs bool) (map[string]interface{}, error) { var b struct { CommonResponse Result interface{} `json:"result"` @@ -107,7 +107,7 @@ func (reqGen *RequestGenerator) GetBlockByNumberDetails(blockNum string, withTxs return m, nil } -func (reqGen *RequestGenerator) GetTransactionCount(address libcommon.Address, blockNum BlockNumber) (EthGetTransactionCount, error) { +func (reqGen *requestGenerator) GetTransactionCount(address libcommon.Address, blockNum BlockNumber) (EthGetTransactionCount, error) { var b EthGetTransactionCount method, body := reqGen.getTransactionCount(address, blockNum) @@ -122,16 +122,16 @@ func (reqGen *RequestGenerator) GetTransactionCount(address libcommon.Address, b return b, nil } -func (req *RequestGenerator) getTransactionCount(address libcommon.Address, blockNum BlockNumber) (RPCMethod, string) { +func (req *requestGenerator) getTransactionCount(address libcommon.Address, blockNum BlockNumber) (RPCMethod, string) { const template = `{"jsonrpc":"2.0","method":%q,"params":["0x%x","%v"],"id":%d}` return Methods.ETHGetTransactionCount, fmt.Sprintf(template, Methods.ETHGetTransactionCount, address, blockNum, req.reqID) } -func (reqGen *RequestGenerator) SendTransaction(signedTx *types.Transaction) (*libcommon.Hash, error) { +func (reqGen *requestGenerator) SendTransaction(signedTx types.Transaction) (*libcommon.Hash, error) { var b EthSendRawTransaction var buf bytes.Buffer - if err := (*signedTx).MarshalBinary(&buf); err != nil { + if err := signedTx.MarshalBinary(&buf); err != nil { return nil, fmt.Errorf("failed to marshal binary: %v", err) } @@ -140,10 +140,23 @@ func (reqGen *RequestGenerator) SendTransaction(signedTx *types.Transaction) (*l return nil, fmt.Errorf("could not make to request to eth_sendRawTransaction: %v", res.Err) } + zeroHash := true + + for _, hb := range b.TxnHash { + if hb != 0 { + zeroHash = false + break + } + } + + if zeroHash { + return nil, fmt.Errorf("Request: %d, hash: %s, nonce %d: returned a zero transaction hash", b.RequestId, signedTx.Hash().Hex(), signedTx.GetNonce()) + } + return &b.TxnHash, nil } -func (req *RequestGenerator) sendRawTransaction(signedTx []byte) (RPCMethod, string) { +func (req *requestGenerator) sendRawTransaction(signedTx []byte) (RPCMethod, string) { const template = `{"jsonrpc":"2.0","method":%q,"params":["0x%x"],"id":%d}` return Methods.ETHSendRawTransaction, fmt.Sprintf(template, Methods.ETHSendRawTransaction, signedTx, req.reqID) } diff --git a/cmd/devnet/requests/event.go b/cmd/devnet/requests/event.go index 9823bd84eba..cfe380eccfa 100644 --- a/cmd/devnet/requests/event.go +++ b/cmd/devnet/requests/event.go @@ -4,10 +4,10 @@ import ( "encoding/json" "fmt" "strconv" - "strings" libcommon "github.com/ledgerwatch/erigon-lib/common" "github.com/ledgerwatch/erigon-lib/common/hexutility" + "github.com/ledgerwatch/erigon/cmd/devnet/devnetutils" "github.com/ledgerwatch/erigon/common/hexutil" "github.com/ledgerwatch/log/v3" ) @@ -51,7 +51,7 @@ func BuildLog(hash libcommon.Hash, blockNum string, address libcommon.Address, t Address: address, Topics: topics, Data: data, - BlockNumber: hexutil.Uint64(HexToInt(blockNum)), + BlockNumber: hexutil.Uint64(devnetutils.HexToInt(blockNum)), TxHash: hash, TxIndex: txIndex, BlockHash: blockHash, @@ -60,14 +60,7 @@ func BuildLog(hash libcommon.Hash, blockNum string, address libcommon.Address, t } } -// HexToInt converts a hexadecimal string to uint64 -func HexToInt(hexStr string) uint64 { - cleaned := strings.ReplaceAll(hexStr, "0x", "") // remove the 0x prefix - result, _ := strconv.ParseUint(cleaned, 16, 64) - return result -} - -func (reqGen *RequestGenerator) GetAndCompareLogs(fromBlock uint64, toBlock uint64, expected Log) error { +func (reqGen *requestGenerator) GetAndCompareLogs(fromBlock uint64, toBlock uint64, expected Log) error { reqGen.logger.Info("GETTING AND COMPARING LOGS") var b EthGetLogs @@ -148,7 +141,7 @@ func hashSlicesAreEqual(s1, s2 []libcommon.Hash) bool { return true } -func (req *RequestGenerator) getLogs(fromBlock, toBlock uint64, address libcommon.Address) (RPCMethod, string) { +func (req *requestGenerator) getLogs(fromBlock, toBlock uint64, address libcommon.Address) (RPCMethod, string) { const template = `{"jsonrpc":"2.0","method":%q,"params":[{"fromBlock":"0x%x","toBlock":"0x%x","address":"0x%x"}],"id":%d}` return Methods.ETHGetLogs, fmt.Sprintf(template, Methods.ETHGetLogs, fromBlock, toBlock, address, req.reqID) } diff --git a/cmd/devnet/requests/mock_request.go b/cmd/devnet/requests/mock_request.go deleted file mode 100644 index efcbbd31609..00000000000 --- a/cmd/devnet/requests/mock_request.go +++ /dev/null @@ -1,16 +0,0 @@ -package requests - -import ( - "fmt" - - "github.com/ledgerwatch/log/v3" -) - -func PingErigonRpc(reqGen *RequestGenerator, logger log.Logger) error { - res := reqGen.PingErigonRpc() - if res.Err != nil { - return fmt.Errorf("failed to ping erigon rpc url: %v", res.Err) - } - logger.Info("SUCCESS => OK") - return nil -} diff --git a/cmd/devnet/requests/request_generator.go b/cmd/devnet/requests/request_generator.go index 60d15d60795..c51cb49bf36 100644 --- a/cmd/devnet/requests/request_generator.go +++ b/cmd/devnet/requests/request_generator.go @@ -8,6 +8,9 @@ import ( "strings" "time" + libcommon "github.com/ledgerwatch/erigon-lib/common" + "github.com/ledgerwatch/erigon/core/types" + "github.com/ledgerwatch/erigon/p2p" "github.com/ledgerwatch/log/v3" "github.com/valyala/fastjson" ) @@ -34,7 +37,20 @@ type EthError struct { Message string `json:"message"` } -type RequestGenerator struct { +type RequestGenerator interface { + PingErigonRpc() CallResult + GetBalance(address libcommon.Address, blockNum BlockNumber) (uint64, error) + AdminNodeInfo() (p2p.NodeInfo, error) + GetBlockByNumberDetails(blockNum string, withTxs bool) (map[string]interface{}, error) + GetBlockByNumber(blockNum uint64, withTxs bool) (EthBlockByNumber, error) + BlockNumber() (uint64, error) + GetTransactionCount(address libcommon.Address, blockNum BlockNumber) (EthGetTransactionCount, error) + SendTransaction(signedTx types.Transaction) (*libcommon.Hash, error) + GetAndCompareLogs(fromBlock uint64, toBlock uint64, expected Log) error + TxpoolContent() (int, int, int, error) +} + +type requestGenerator struct { reqID int client *http.Client logger log.Logger @@ -100,7 +116,7 @@ var Methods = struct { ETHNewHeads: "eth_newHeads", } -func (req *RequestGenerator) call(method RPCMethod, body string, response interface{}) CallResult { +func (req *requestGenerator) call(method RPCMethod, body string, response interface{}) CallResult { start := time.Now() err := post(req.client, req.target, string(method), body, response, req.logger) req.reqID++ @@ -114,7 +130,7 @@ func (req *RequestGenerator) call(method RPCMethod, body string, response interf } } -func (req *RequestGenerator) PingErigonRpc() CallResult { +func (req *requestGenerator) PingErigonRpc() CallResult { start := time.Now() res := CallResult{ RequestID: req.reqID, @@ -156,17 +172,15 @@ func (req *RequestGenerator) PingErigonRpc() CallResult { return res } -func NewRequestGenerator(target string, logger log.Logger) *RequestGenerator { - var client = &http.Client{ - Timeout: time.Second * 600, - } - reqGen := RequestGenerator{ - client: client, +func NewRequestGenerator(target string, logger log.Logger) RequestGenerator { + return &requestGenerator{ + client: &http.Client{ + Timeout: time.Second * 600, + }, reqID: 1, logger: logger, target: target, } - return &reqGen } func post(client *http.Client, url, method, request string, response interface{}, logger log.Logger) error { diff --git a/cmd/devnet/requests/request_generator_test.go b/cmd/devnet/requests/request_generator_test.go index f6188c6adf1..8cc16e0f654 100644 --- a/cmd/devnet/requests/request_generator_test.go +++ b/cmd/devnet/requests/request_generator_test.go @@ -7,8 +7,8 @@ import ( "github.com/stretchr/testify/require" ) -func MockRequestGenerator(reqId int) *RequestGenerator { - return &RequestGenerator{ +func MockRequestGenerator(reqId int) *requestGenerator { + return &requestGenerator{ reqID: reqId, client: nil, } @@ -258,20 +258,3 @@ func TestParseResponse(t *testing.T) { require.EqualValues(t, testCase.expected, got) } } - -func TestHexToInt(t *testing.T) { - testCases := []struct { - hexStr string - expected uint64 - }{ - {"0x0", 0}, - {"0x32424", 205860}, - {"0x200", 512}, - {"0x39", 57}, - } - - for _, testCase := range testCases { - got := HexToInt(testCase.hexStr) - require.EqualValues(t, testCase.expected, got) - } -} diff --git a/cmd/devnet/requests/tx.go b/cmd/devnet/requests/tx.go index 06e33fa1b9f..2d32a776221 100644 --- a/cmd/devnet/requests/tx.go +++ b/cmd/devnet/requests/tx.go @@ -9,7 +9,7 @@ type EthTxPool struct { Result interface{} `json:"result"` } -func (reqGen *RequestGenerator) TxpoolContent() (int, int, int, error) { +func (reqGen *requestGenerator) TxpoolContent() (int, int, int, error) { var ( b EthTxPool pending map[string]interface{} @@ -22,7 +22,11 @@ func (reqGen *RequestGenerator) TxpoolContent() (int, int, int, error) { return len(pending), len(queued), len(baseFee), fmt.Errorf("failed to fetch txpool content: %v", res.Err) } - resp := b.Result.(map[string]interface{}) + resp, ok := b.Result.(map[string]interface{}) + + if !ok { + return 0, 0, 0, fmt.Errorf("Unexpected result type: %T", b.Result) + } pendingLen := 0 queuedLen := 0 @@ -52,7 +56,7 @@ func (reqGen *RequestGenerator) TxpoolContent() (int, int, int, error) { return pendingLen, queuedLen, baseFeeLen, nil } -func (req *RequestGenerator) txpoolContent() (RPCMethod, string) { +func (req *requestGenerator) txpoolContent() (RPCMethod, string) { const template = `{"jsonrpc":"2.0","method":%q,"params":[],"id":%d}` return Methods.TxpoolContent, fmt.Sprintf(template, Methods.TxpoolContent, req.reqID) } diff --git a/cmd/devnet/scenarios/context.go b/cmd/devnet/scenarios/context.go new file mode 100644 index 00000000000..622f0a66281 --- /dev/null +++ b/cmd/devnet/scenarios/context.go @@ -0,0 +1,7 @@ +package scenarios + +import "context" + +func stepRunners(ctx context.Context) []*stepRunner { + return nil +} diff --git a/cmd/devnet/scenarios/errors.go b/cmd/devnet/scenarios/errors.go new file mode 100644 index 00000000000..5b7941608a6 --- /dev/null +++ b/cmd/devnet/scenarios/errors.go @@ -0,0 +1,24 @@ +package scenarios + +import "fmt" + +// ErrUndefined is returned in case if step definition was not found +var ErrUndefined = fmt.Errorf("step is undefined") + +type ScenarioError struct { + error + Result ScenarioResult + Cause error +} + +func (e *ScenarioError) Unwrap() error { + return e.error +} + +func NewScenarioError(err error, result ScenarioResult, cause error) *ScenarioError { + return &ScenarioError{err, result, cause} +} + +func (e *ScenarioError) Error() string { + return fmt.Sprintf("%s: Cause: %s", e.error, e.Cause) +} diff --git a/cmd/devnet/scenarios/results.go b/cmd/devnet/scenarios/results.go new file mode 100644 index 00000000000..2f2c414e9dd --- /dev/null +++ b/cmd/devnet/scenarios/results.go @@ -0,0 +1,54 @@ +package scenarios + +import ( + "time" +) + +type ScenarioResult struct { + ScenarioId string + StartedAt time.Time + + StepResults []StepResult +} + +type StepResult struct { + Status StepStatus + FinishedAt time.Time + Err error + + ScenarioId string + + Step *Step +} + +func NewStepResult(scenarioId string, step *Step) StepResult { + return StepResult{FinishedAt: TimeNowFunc(), ScenarioId: scenarioId, Step: step} +} + +type StepStatus int + +const ( + Passed StepStatus = iota + Failed + Skipped + Undefined + Pending +) + +// String ... +func (st StepStatus) String() string { + switch st { + case Passed: + return "passed" + case Failed: + return "failed" + case Skipped: + return "skipped" + case Undefined: + return "undefined" + case Pending: + return "pending" + default: + return "unknown" + } +} diff --git a/cmd/devnet/scenarios/run.go b/cmd/devnet/scenarios/run.go new file mode 100644 index 00000000000..26729163711 --- /dev/null +++ b/cmd/devnet/scenarios/run.go @@ -0,0 +1,120 @@ +package scenarios + +import ( + "context" + "sync" + + "github.com/ledgerwatch/erigon/cmd/devnet/devnetutils" +) + +type SimulationInitializer func(*SimulationContext) + +func Run(ctx context.Context, scenarios ...*Scenario) error { + return runner{scenarios: scenarios}.runWithOptions(ctx, getDefaultOptions()) +} + +type runner struct { + randomize bool + stopOnFailure bool + + scenarios []*Scenario + + simulationInitializer SimulationInitializer +} + +func (r *runner) concurrent(ctx context.Context, rate int) (err error) { + var copyLock sync.Mutex + + queue := make(chan int, rate) + scenarios := make([]*Scenario, len(r.scenarios)) + + if r.randomize { + for i := range r.scenarios { + j := devnetutils.RandomInt(i + 1) + scenarios[i] = r.scenarios[j] + } + + } else { + copy(scenarios, r.scenarios) + } + + simulationContext := SimulationContext{ + suite: &suite{ + randomize: r.randomize, + defaultContext: ctx, + stepRunners: stepRunners(ctx), + }, + } + + for i, s := range scenarios { + scenario := *s + + queue <- i // reserve space in queue + + runScenario := func(err *error, Scenario *Scenario) { + defer func() { + <-queue // free a space in queue + }() + + if r.stopOnFailure && *err != nil { + return + } + + // Copy base suite. + suite := *simulationContext.suite + + if r.simulationInitializer != nil { + sc := SimulationContext{suite: &suite} + r.simulationInitializer(&sc) + } + + _, serr := suite.runScenario(&scenario) + if suite.shouldFail(serr) { + copyLock.Lock() + *err = serr + copyLock.Unlock() + } + } + + if rate == 1 { + // Running within the same goroutine for concurrency 1 + // to preserve original stacks and simplify debugging. + runScenario(&err, &scenario) + } else { + go runScenario(&err, &scenario) + } + } + + // wait until last are processed + for i := 0; i < rate; i++ { + queue <- i + } + + close(queue) + + return err +} + +func (runner runner) runWithOptions(ctx context.Context, opt *Options) error { + //var output io.Writer = os.Stdout + //if nil != opt.Output { + // output = opt.Output + //} + + if opt.Concurrency < 1 { + opt.Concurrency = 1 + } + + return runner.concurrent(ctx, opt.Concurrency) +} + +type Options struct { + Concurrency int +} + +func getDefaultOptions() *Options { + opt := &Options{ + Concurrency: 1, + } + return opt +} diff --git a/cmd/devnet/scenarios/scenario.go b/cmd/devnet/scenarios/scenario.go new file mode 100644 index 00000000000..1446bb53c49 --- /dev/null +++ b/cmd/devnet/scenarios/scenario.go @@ -0,0 +1,150 @@ +package scenarios + +import ( + "context" + "errors" + "fmt" + "path" + "reflect" + "regexp" + "runtime" + "unicode" +) + +var ( + ErrUnmatchedStepArgumentNumber = errors.New("func received more arguments than expected") + ErrCannotConvert = errors.New("cannot convert argument") + ErrUnsupportedArgumentType = errors.New("unsupported argument type") +) + +var stepRunnerRegistry = map[reflect.Value]*stepRunner{} + +type stepHandler struct { + handler reflect.Value + matchExpressions []string +} + +func RegisterStepHandlers(handlers ...stepHandler) error { + for _, h := range handlers { + var exprs []*regexp.Regexp + + if kind := h.handler.Kind(); kind != reflect.Func { + return fmt.Errorf("Can't register non-function %s as step handler", kind) + } + + if len(h.matchExpressions) == 0 { + name := path.Ext(runtime.FuncForPC(h.handler.Pointer()).Name())[1:] + + if unicode.IsLower(rune(name[0])) { + return fmt.Errorf("Can't register unexported function %s as step handler", name) + } + + h.matchExpressions = []string{ + name, + } + } + + for _, e := range h.matchExpressions { + exp, err := regexp.Compile(e) + + if err != nil { + return err + } + + exprs = append(exprs, exp) + } + + stepRunnerRegistry[h.handler] = &stepRunner{ + Handler: h.handler, + Exprs: exprs, + } + } + + return nil +} + +func MustRegisterStepHandlers(handlers ...stepHandler) { + if err := RegisterStepHandlers(handlers...); err != nil { + panic(fmt.Errorf("Step handler registration failed: %w", err)) + } +} + +func StepHandler(handler interface{}, matchExpressions ...string) stepHandler { + return stepHandler{reflect.ValueOf(handler), matchExpressions} +} + +type Scenario struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Steps []*Step `json:"steps"` +} + +type Step struct { + Id string `json:"id"` + Args []interface{} `json:"args,omitempty"` + Text string `json:"text"` + Description string `json:"description,omitempty"` +} + +type stepRunner struct { + Exprs []*regexp.Regexp + Handler reflect.Value + // multistep related + Nested bool + Undefined []string +} + +var typeOfBytes = reflect.TypeOf([]byte(nil)) + +var typeOfContext = reflect.TypeOf((*context.Context)(nil)).Elem() + +func (c *stepRunner) Run(ctx context.Context, args []interface{}) (context.Context, interface{}) { + var values = make([]reflect.Value, 0, len(args)) + + typ := c.Handler.Type() + numIn := typ.NumIn() + hasCtxIn := numIn > 0 && typ.In(0).Implements(typeOfContext) + + if hasCtxIn { + values = append(values, reflect.ValueOf(ctx)) + numIn-- + } + + if len(args) < numIn { + return ctx, fmt.Errorf("Expected %d arguments, matched %d from step", typ.NumIn(), len(args)) + } + + for _, arg := range args { + values = append(values, reflect.ValueOf(arg)) + } + + res := c.Handler.Call(values) + + if len(res) == 0 { + return ctx, nil + } + + r := res[0].Interface() + + if rctx, ok := r.(context.Context); ok { + if len(res) == 1 { + return rctx, nil + } + + res = res[1:] + ctx = rctx + } + + if len(res) == 1 { + return ctx, res[0].Interface() + } + + var results = make([]interface{}, 0, len(res)) + + for _, value := range res { + results = append(results, value.Interface()) + } + + return ctx, results +} diff --git a/cmd/devnet/scenarios/stack.go b/cmd/devnet/scenarios/stack.go new file mode 100644 index 00000000000..3b50082fd8f --- /dev/null +++ b/cmd/devnet/scenarios/stack.go @@ -0,0 +1,141 @@ +package scenarios + +import ( + "fmt" + "go/build" + "io" + "path" + "path/filepath" + "runtime" + "strings" +) + +// Frame represents a program counter inside a stack frame. +type stackFrame uintptr + +// pc returns the program counter for this frame; +// multiple frames may have the same PC value. +func (f stackFrame) pc() uintptr { return uintptr(f) - 1 } + +// file returns the full path to the file that contains the +// function for this Frame's pc. +func (f stackFrame) file() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + file, _ := fn.FileLine(f.pc()) + return file +} + +func trimGoPath(file string) string { + for _, p := range filepath.SplitList(build.Default.GOPATH) { + file = strings.Replace(file, filepath.Join(p, "src")+string(filepath.Separator), "", 1) + } + return file +} + +// line returns the line number of source code of the +// function for this Frame's pc. +func (f stackFrame) line() int { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return 0 + } + _, line := fn.FileLine(f.pc()) + return line +} + +// Format formats the frame according to the fmt.Formatter interface. +// +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+s path of source file relative to the compile time GOPATH +// %+v equivalent to %+s:%d +func (f stackFrame) Format(s fmt.State, verb rune) { + funcname := func(name string) string { + i := strings.LastIndex(name, "/") + name = name[i+1:] + i = strings.Index(name, ".") + return name[i+1:] + } + + switch verb { + case 's': + switch { + case s.Flag('+'): + pc := f.pc() + fn := runtime.FuncForPC(pc) + if fn == nil { + io.WriteString(s, "unknown") + } else { + file, _ := fn.FileLine(pc) + fmt.Fprintf(s, "%s\n\t%s", fn.Name(), trimGoPath(file)) + } + default: + io.WriteString(s, path.Base(f.file())) + } + case 'd': + fmt.Fprintf(s, "%d", f.line()) + case 'n': + name := runtime.FuncForPC(f.pc()).Name() + io.WriteString(s, funcname(name)) + case 'v': + f.Format(s, 's') + io.WriteString(s, ":") + f.Format(s, 'd') + } +} + +// stack represents a stack of program counters. +type stack []uintptr + +func (s *stack) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case st.Flag('+'): + for _, pc := range *s { + f := stackFrame(pc) + fmt.Fprintf(st, "\n%+v", f) + } + } + } +} + +func callStack() *stack { + const depth = 32 + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + var st stack = pcs[0:n] + return &st +} + +// fundamental is an error that has a message and a stack, but no caller. +type traceError struct { + msg string + *stack +} + +func (f *traceError) Error() string { return f.msg } + +func (f *traceError) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + io.WriteString(s, f.msg) + f.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, f.msg) + case 'q': + fmt.Fprintf(s, "%q", f.msg) + } +} diff --git a/cmd/devnet/scenarios/suite.go b/cmd/devnet/scenarios/suite.go new file mode 100644 index 00000000000..6062b19b2da --- /dev/null +++ b/cmd/devnet/scenarios/suite.go @@ -0,0 +1,378 @@ +package scenarios + +import ( + "context" + "errors" + "fmt" + "testing" + "time" +) + +type SimulationContext struct { + suite *suite +} + +type BeforeScenarioHook func(context.Context, *Scenario) (context.Context, error) +type AfterScenarioHook func(context.Context, *Scenario, error) (context.Context, error) +type BeforeStepHook func(context.Context, *Step) (context.Context, error) +type AfterStepHook func(context.Context, *Step, StepStatus, error) (context.Context, error) + +var TimeNowFunc = func() time.Time { + return time.Now() +} + +type suite struct { + stepRunners []*stepRunner + failed bool + randomize bool + strict bool + stopOnFailure bool + testingT *testing.T + defaultContext context.Context + + // suite event handlers + beforeScenarioHandlers []BeforeScenarioHook + afterScenarioHandlers []AfterScenarioHook + beforeStepHandlers []BeforeStepHook + afterStepHandlers []AfterStepHook +} + +func (s *suite) runSteps(ctx context.Context, scenario *Scenario, steps []*Step) (context.Context, []StepResult, error) { + var results = make([]StepResult, 0, len(steps)) + var err error + + for i, step := range steps { + isLast := i == len(steps)-1 + isFirst := i == 0 + + var stepResult StepResult + + ctx, stepResult = s.runStep(ctx, scenario, step, err, isFirst, isLast) + + switch { + case stepResult.Err == nil: + case errors.Is(stepResult.Err, ErrUndefined): + // do not overwrite failed error + if err == nil { + err = stepResult.Err + } + default: + err = stepResult.Err + } + + results = append(results, stepResult) + } + + return ctx, results, err +} + +func (s *suite) runStep(ctx context.Context, scenario *Scenario, step *Step, prevStepErr error, isFirst, isLast bool) (rctx context.Context, sr StepResult) { + var match *stepRunner + + sr = StepResult{Status: Undefined} + rctx = ctx + + // user multistep definitions may panic + defer func() { + if e := recover(); e != nil { + sr.Err = &traceError{ + msg: fmt.Sprintf("%v", e), + stack: callStack(), + } + } + + earlyReturn := prevStepErr != nil || sr.Err == ErrUndefined + + if !earlyReturn { + sr = NewStepResult(scenario.Id, step) + } + + // Run after step handlers. + rctx, sr.Err = s.runAfterStepHooks(ctx, step, sr.Status, sr.Err) + + // Trigger after scenario on failing or last step to attach possible hook error to step. + if isLast || (sr.Status != Skipped && sr.Status != Undefined && sr.Err != nil) { + rctx, sr.Err = s.runAfterScenarioHooks(rctx, scenario, sr.Err) + } + + if earlyReturn { + return + } + + switch sr.Err { + case nil: + sr.Status = Passed + default: + sr.Status = Failed + } + }() + + // run before scenario handlers + if isFirst { + ctx, sr.Err = s.runBeforeScenarioHooks(ctx, scenario) + } + + // run before step handlers + ctx, sr.Err = s.runBeforeStepHooks(ctx, step, sr.Err) + + if sr.Err != nil { + sr = NewStepResult(step.Id, step) + sr.Status = Failed + return ctx, sr + } + + ctx, undef, match, err := s.maybeUndefined(ctx, step.Text, step.Args) + + if err != nil { + return ctx, sr + } else if len(undef) > 0 { + sr = NewStepResult(scenario.Id, step) + sr.Status = Undefined + sr.Err = ErrUndefined + return ctx, sr + } + + if prevStepErr != nil { + sr = NewStepResult(scenario.Id, step) + sr.Status = Skipped + return ctx, sr + } + + ctx, sr.Err = s.maybeSubSteps(match.Run(ctx, step.Args)) + + return ctx, sr +} + +func (s *suite) maybeUndefined(ctx context.Context, text string, args []interface{}) (context.Context, []string, *stepRunner, error) { + step := s.matchStep(text) + + if nil == step { + return ctx, []string{text}, nil, nil + } + + var undefined []string + + if !step.Nested { + return ctx, undefined, step, nil + } + + ctx, steps := step.Run(ctx, args) + + for _, next := range steps.([]Step) { + ctx, undef, _, err := s.maybeUndefined(ctx, next.Text, nil) + if err != nil { + return ctx, undefined, nil, err + } + undefined = append(undefined, undef...) + } + + return ctx, undefined, nil, nil +} + +func (s *suite) matchStep(text string) *stepRunner { + for _, r := range s.stepRunners { + for _, expr := range r.Exprs { + if m := expr.FindStringSubmatch(text); len(m) > 0 { + return r + } + } + } + + for _, r := range stepRunnerRegistry { + for _, expr := range r.Exprs { + if m := expr.FindStringSubmatch(text); len(m) > 0 { + return r + } + } + } + + return nil +} + +func (s *suite) maybeSubSteps(ctx context.Context, result interface{}) (context.Context, error) { + if nil == result { + return ctx, nil + } + + if err, ok := result.(error); ok { + return ctx, err + } + + steps, ok := result.([]Step) + if !ok { + return ctx, fmt.Errorf("unexpected error, should have been []string: %T - %+v", result, result) + } + + var err error + + for _, step := range steps { + if def := s.matchStep(step.Text); def == nil { + return ctx, ErrUndefined + } else if ctx, err = s.maybeSubSteps(def.Run(ctx, step.Args)); err != nil { + return ctx, fmt.Errorf("%s: %+v", step.Text, err) + } + } + return ctx, nil +} + +func (s *suite) runScenario(scenario *Scenario) (sr *ScenarioResult, err error) { + ctx := s.defaultContext + if ctx == nil { + ctx = context.Background() + } + + ctx, cancel := context.WithCancel(ctx) + + defer cancel() + + if len(scenario.Steps) == 0 { + return &ScenarioResult{ScenarioId: scenario.Id, StartedAt: TimeNowFunc()}, ErrUndefined + } + + // Before scenario hooks are called in context of first evaluated step + // so that error from handler can be added to step. + + sr = &ScenarioResult{ScenarioId: scenario.Id, StartedAt: TimeNowFunc()} + + // scenario + if s.testingT != nil { + // Running scenario as a subtest. + s.testingT.Run(scenario.Name, func(t *testing.T) { + ctx, sr.StepResults, err = s.runSteps(ctx, scenario, scenario.Steps) + if s.shouldFail(err) { + t.Error(err) + } + }) + } else { + ctx, sr.StepResults, err = s.runSteps(ctx, scenario, scenario.Steps) + } + + // After scenario handlers are called in context of last evaluated step + // so that error from handler can be added to step. + + return sr, err +} + +func (s *suite) shouldFail(err error) bool { + if err == nil { + return false + } + + if errors.Is(err, ErrUndefined) { + return s.strict + } + + return true +} + +func (s *suite) runBeforeStepHooks(ctx context.Context, step *Step, err error) (context.Context, error) { + hooksFailed := false + + for _, f := range s.beforeStepHandlers { + hctx, herr := f(ctx, step) + if herr != nil { + hooksFailed = true + + if err == nil { + err = herr + } else { + err = fmt.Errorf("%v, %w", herr, err) + } + } + + if hctx != nil { + ctx = hctx + } + } + + if hooksFailed { + err = fmt.Errorf("before step hook failed: %w", err) + } + + return ctx, err +} + +func (s *suite) runAfterStepHooks(ctx context.Context, step *Step, status StepStatus, err error) (context.Context, error) { + for _, f := range s.afterStepHandlers { + hctx, herr := f(ctx, step, status, err) + + // Adding hook error to resulting error without breaking hooks loop. + if herr != nil { + if err == nil { + err = herr + } else { + err = fmt.Errorf("%v, %w", herr, err) + } + } + + if hctx != nil { + ctx = hctx + } + } + + return ctx, err +} + +func (s *suite) runBeforeScenarioHooks(ctx context.Context, scenario *Scenario) (context.Context, error) { + var err error + + // run before scenario handlers + for _, f := range s.beforeScenarioHandlers { + hctx, herr := f(ctx, scenario) + if herr != nil { + if err == nil { + err = herr + } else { + err = fmt.Errorf("%v, %w", herr, err) + } + } + + if hctx != nil { + ctx = hctx + } + } + + if err != nil { + err = fmt.Errorf("before scenario hook failed: %w", err) + } + + return ctx, err +} + +func (s *suite) runAfterScenarioHooks(ctx context.Context, scenario *Scenario, lastStepErr error) (context.Context, error) { + err := lastStepErr + + hooksFailed := false + isStepErr := true + + // run after scenario handlers + for _, f := range s.afterScenarioHandlers { + hctx, herr := f(ctx, scenario, err) + + // Adding hook error to resulting error without breaking hooks loop. + if herr != nil { + hooksFailed = true + + if err == nil { + isStepErr = false + err = herr + } else { + if isStepErr { + err = fmt.Errorf("step error: %w", err) + isStepErr = false + } + err = fmt.Errorf("%v, %w", herr, err) + } + } + + if hctx != nil { + ctx = hctx + } + } + + if hooksFailed { + err = fmt.Errorf("after scenario hook failed: %w", err) + } + + return ctx, err +} diff --git a/cmd/devnet/services/account.go b/cmd/devnet/services/account.go index cb1af862352..e8d7d04ed3b 100644 --- a/cmd/devnet/services/account.go +++ b/cmd/devnet/services/account.go @@ -5,11 +5,11 @@ import ( libcommon "github.com/ledgerwatch/erigon-lib/common" - "github.com/ledgerwatch/erigon/cmd/devnet/node" + "github.com/ledgerwatch/erigon/cmd/devnet/devnet" "github.com/ledgerwatch/erigon/cmd/devnet/requests" ) -func GetNonce(node *node.Node, address libcommon.Address) (uint64, error) { +func GetNonce(node devnet.Node, address libcommon.Address) (uint64, error) { res, err := node.GetTransactionCount(address, requests.BlockNumbers.Latest) if err != nil { return 0, fmt.Errorf("failed to get transaction count for address 0x%x: %v", address, err) diff --git a/cmd/devnet/services/block.go b/cmd/devnet/services/block.go index bad6572283a..416ed65a945 100644 --- a/cmd/devnet/services/block.go +++ b/cmd/devnet/services/block.go @@ -11,12 +11,15 @@ import ( "github.com/ledgerwatch/log/v3" "github.com/ledgerwatch/erigon/accounts/abi/bind" + "github.com/ledgerwatch/erigon/accounts/abi/bind/backends" "github.com/ledgerwatch/erigon/cmd/devnet/contracts" + "github.com/ledgerwatch/erigon/cmd/devnet/devnet" "github.com/ledgerwatch/erigon/cmd/devnet/devnetutils" - "github.com/ledgerwatch/erigon/cmd/devnet/models" - "github.com/ledgerwatch/erigon/cmd/devnet/node" "github.com/ledgerwatch/erigon/cmd/devnet/requests" + "github.com/ledgerwatch/erigon/common/hexutil" + "github.com/ledgerwatch/erigon/core" "github.com/ledgerwatch/erigon/core/types" + "github.com/ledgerwatch/erigon/crypto" "github.com/ledgerwatch/erigon/params" "github.com/ledgerwatch/erigon/rpc" ) @@ -24,21 +27,39 @@ import ( const gasPrice = 912_345_678 const gasAmount = 875_000_000 +const ( + // hexPrivateKey is the hex value for the private key + hexPrivateKey = "26e86e45f6fc45ec6e2ecd128cec80fa1d1505e5507dcd2ae58c3130a7a97b48" + // DevAddress is the developer address for sending + DevAddress = "0x67b1d87101671b127f5f8714789C7192f7ad340e" + + // SolContractMethodSignature is the function signature for the event in the solidity contract definition + SolContractMethodSignature = "SubscriptionEvent()" +) + var ( + // DevSignedPrivateKey is the signed private key for signing transactions + DevSignedPrivateKey, _ = crypto.HexToECDSA(hexPrivateKey) + // gspec is the geth dev genesis block + gspec = core.DeveloperGenesisBlock(uint64(0), libcommon.HexToAddress(DevAddress)) + // ContractBackend is a simulated backend created using a simulated blockchain + ContractBackend = backends.NewSimulatedBackendWithConfig(gspec.Alloc, gspec.Config, 1_000_000) + signer = types.LatestSigner(params.AllCliqueProtocolChanges) ) -func CreateManyEIP1559TransactionsRefWithBaseFee(node *node.Node, addr string, startingNonce *uint64, logger log.Logger) ([]*types.Transaction, []*types.Transaction, error) { +func CreateManyEIP1559TransactionsRefWithBaseFee(ctx context.Context, addr string, startingNonce *uint64, logger log.Logger) ([]types.Transaction, []types.Transaction, error) { toAddress := libcommon.HexToAddress(addr) - baseFeePerGas, err := BaseFeeFromBlock(node, logger) + baseFeePerGas, err := BaseFeeFromBlock(ctx) if err != nil { return nil, nil, fmt.Errorf("failed BaseFeeFromBlock: %v", err) } - logger.Info("BaseFeePerGas", "val", baseFeePerGas) + devnet.Logger(ctx).Info("BaseFeePerGas", "val", baseFeePerGas) + + lowerBaseFeeTransactions, higherBaseFeeTransactions, err := signEIP1559TxsLowerAndHigherThanBaseFee2(ctx, 1, 1, baseFeePerGas, startingNonce, toAddress) - lowerBaseFeeTransactions, higherBaseFeeTransactions, err := signEIP1559TxsLowerAndHigherThanBaseFee2(1, 1, baseFeePerGas, startingNonce, toAddress, logger) if err != nil { return nil, nil, fmt.Errorf("failed signEIP1559TxsLowerAndHigherThanBaseFee2: %v", err) } @@ -46,17 +67,18 @@ func CreateManyEIP1559TransactionsRefWithBaseFee(node *node.Node, addr string, s return lowerBaseFeeTransactions, higherBaseFeeTransactions, nil } -func CreateManyEIP1559TransactionsRefWithBaseFee2(node *node.Node, addr string, startingNonce *uint64, logger log.Logger) ([]*types.Transaction, []*types.Transaction, error) { +func CreateManyEIP1559TransactionsRefWithBaseFee2(ctx context.Context, addr string, startingNonce *uint64) ([]types.Transaction, []types.Transaction, error) { toAddress := libcommon.HexToAddress(addr) - baseFeePerGas, err := BaseFeeFromBlock(node, logger) + baseFeePerGas, err := BaseFeeFromBlock(ctx) if err != nil { return nil, nil, fmt.Errorf("failed BaseFeeFromBlock: %v", err) } - logger.Info("BaseFeePerGas2", "val", baseFeePerGas) + devnet.Logger(ctx).Info("BaseFeePerGas2", "val", baseFeePerGas) + + lowerBaseFeeTransactions, higherBaseFeeTransactions, err := signEIP1559TxsLowerAndHigherThanBaseFee2(ctx, 100, 100, baseFeePerGas, startingNonce, toAddress) - lowerBaseFeeTransactions, higherBaseFeeTransactions, err := signEIP1559TxsLowerAndHigherThanBaseFee2(100, 100, baseFeePerGas, startingNonce, toAddress, logger) if err != nil { return nil, nil, fmt.Errorf("failed signEIP1559TxsLowerAndHigherThanBaseFee2: %v", err) } @@ -64,45 +86,31 @@ func CreateManyEIP1559TransactionsRefWithBaseFee2(node *node.Node, addr string, return lowerBaseFeeTransactions, higherBaseFeeTransactions, nil } -// CreateTransaction creates a transaction depending on the type of transaction being passed -func CreateTransaction(txType models.TransactionType, addr string, value, nonce uint64) (*types.Transaction, libcommon.Address, *contracts.Subscription, *bind.TransactOpts, error) { - switch txType { - case models.NonContractTx: - tx, address, err := createNonContractTx(addr, value, nonce) - if err != nil { - return nil, libcommon.Address{}, nil, nil, fmt.Errorf("failed to create non-contract transaction: %v", err) - } - return tx, address, nil, nil, nil - case models.ContractTx: - return createContractTx(nonce) - default: - return nil, libcommon.Address{}, nil, nil, models.ErrInvalidTransactionType - } -} - // createNonContractTx returns a signed transaction and the recipient address -func createNonContractTx(addr string, value, nonce uint64) (*types.Transaction, libcommon.Address, error) { +func CreateTransaction(addr string, value, nonce uint64) (types.Transaction, libcommon.Address, error) { toAddress := libcommon.HexToAddress(addr) // create a new transaction using the parameters to send transaction := types.NewTransaction(nonce, toAddress, uint256.NewInt(value), params.TxGas, uint256.NewInt(gasPrice), nil) // sign the transaction using the developer 0signed private key - signedTx, err := types.SignTx(transaction, *signer, models.DevSignedPrivateKey) + signedTx, err := types.SignTx(transaction, *signer, DevSignedPrivateKey) if err != nil { return nil, libcommon.Address{}, fmt.Errorf("failed to sign non-contract transaction: %v", err) } - return &signedTx, toAddress, nil + return signedTx, toAddress, nil } -func signEIP1559TxsLowerAndHigherThanBaseFee2(amountLower, amountHigher int, baseFeePerGas uint64, nonce *uint64, toAddress libcommon.Address, logger log.Logger) ([]*types.Transaction, []*types.Transaction, error) { - higherBaseFeeTransactions, err := signEIP1559TxsHigherThanBaseFee(amountHigher, baseFeePerGas, nonce, toAddress, logger) +func signEIP1559TxsLowerAndHigherThanBaseFee2(ctx context.Context, amountLower, amountHigher int, baseFeePerGas uint64, nonce *uint64, toAddress libcommon.Address) ([]types.Transaction, []types.Transaction, error) { + higherBaseFeeTransactions, err := signEIP1559TxsHigherThanBaseFee(ctx, amountHigher, baseFeePerGas, nonce, toAddress) + if err != nil { return nil, nil, fmt.Errorf("failed signEIP1559TxsHigherThanBaseFee: %v", err) } - lowerBaseFeeTransactions, err := signEIP1559TxsLowerThanBaseFee(amountLower, baseFeePerGas, nonce, toAddress, logger) + lowerBaseFeeTransactions, err := signEIP1559TxsLowerThanBaseFee(ctx, amountLower, baseFeePerGas, nonce, toAddress) + if err != nil { return nil, nil, fmt.Errorf("failed signEIP1559TxsLowerThanBaseFee: %v", err) } @@ -111,8 +119,8 @@ func signEIP1559TxsLowerAndHigherThanBaseFee2(amountLower, amountHigher int, bas } // signEIP1559TxsLowerThanBaseFee creates n number of transactions with gasFeeCap lower than baseFeePerGas -func signEIP1559TxsLowerThanBaseFee(n int, baseFeePerGas uint64, nonce *uint64, toAddress libcommon.Address, logger log.Logger) ([]*types.Transaction, error) { - var signedTransactions []*types.Transaction +func signEIP1559TxsLowerThanBaseFee(ctx context.Context, n int, baseFeePerGas uint64, nonce *uint64, toAddress libcommon.Address) ([]types.Transaction, error) { + var signedTransactions []types.Transaction var ( minFeeCap = baseFeePerGas - 300_000_000 @@ -132,14 +140,15 @@ func signEIP1559TxsLowerThanBaseFee(n int, baseFeePerGas uint64, nonce *uint64, transaction := types.NewEIP1559Transaction(*signer.ChainID(), *nonce, toAddress, uint256.NewInt(value), uint64(210_000), uint256.NewInt(gasPrice), new(uint256.Int), uint256.NewInt(gasFeeCap), nil) - logger.Info("LOWER", "transaction", i, "nonce", transaction.Nonce, "value", transaction.Value, "feecap", transaction.FeeCap) + devnet.Logger(ctx).Info("LOWER", "transaction", i, "nonce", transaction.Nonce, "value", transaction.Value, "feecap", transaction.FeeCap) + + signedTransaction, err := types.SignTx(transaction, *signer, DevSignedPrivateKey) - signedTransaction, err := types.SignTx(transaction, *signer, models.DevSignedPrivateKey) if err != nil { return nil, err } - signedTransactions = append(signedTransactions, &signedTransaction) + signedTransactions = append(signedTransactions, signedTransaction) *nonce++ } @@ -147,8 +156,8 @@ func signEIP1559TxsLowerThanBaseFee(n int, baseFeePerGas uint64, nonce *uint64, } // signEIP1559TxsHigherThanBaseFee creates amount number of transactions with gasFeeCap higher than baseFeePerGas -func signEIP1559TxsHigherThanBaseFee(n int, baseFeePerGas uint64, nonce *uint64, toAddress libcommon.Address, logger log.Logger) ([]*types.Transaction, error) { - var signedTransactions []*types.Transaction +func signEIP1559TxsHigherThanBaseFee(ctx context.Context, n int, baseFeePerGas uint64, nonce *uint64, toAddress libcommon.Address) ([]types.Transaction, error) { + var signedTransactions []types.Transaction var ( minFeeCap = baseFeePerGas @@ -168,22 +177,22 @@ func signEIP1559TxsHigherThanBaseFee(n int, baseFeePerGas uint64, nonce *uint64, transaction := types.NewEIP1559Transaction(*signer.ChainID(), *nonce, toAddress, uint256.NewInt(value), uint64(210_000), uint256.NewInt(gasPrice), new(uint256.Int), uint256.NewInt(gasFeeCap), nil) - logger.Info("HIGHER", "transaction", i, "nonce", transaction.Nonce, "value", transaction.Value, "feecap", transaction.FeeCap) + devnet.Logger(ctx).Info("HIGHER", "transaction", i, "nonce", transaction.Nonce, "value", transaction.Value, "feecap", transaction.FeeCap) - signedTransaction, err := types.SignTx(transaction, *signer, models.DevSignedPrivateKey) + signedTransaction, err := types.SignTx(transaction, *signer, DevSignedPrivateKey) if err != nil { return nil, err } - signedTransactions = append(signedTransactions, &signedTransaction) + signedTransactions = append(signedTransactions, signedTransaction) *nonce++ } return signedTransactions, nil } -// createContractTx creates and signs a transaction using the developer address, returns the contract and the signed transaction -func createContractTx(nonce uint64) (*types.Transaction, libcommon.Address, *contracts.Subscription, *bind.TransactOpts, error) { +// DeploySubsriptionContract creates and signs a transaction using the developer address, returns the contract and the signed transaction +func DeploySubsriptionContract(nonce uint64) (types.Transaction, libcommon.Address, *contracts.Subscription, *bind.TransactOpts, error) { // initialize transactOpts transactOpts, err := initializeTransactOps(nonce) if err != nil { @@ -191,25 +200,25 @@ func createContractTx(nonce uint64) (*types.Transaction, libcommon.Address, *con } // deploy the contract and get the contract handler - address, txToSign, subscriptionContract, err := contracts.DeploySubscription(transactOpts, models.ContractBackend) + address, txToSign, subscriptionContract, err := contracts.DeploySubscription(transactOpts, ContractBackend) if err != nil { return nil, libcommon.Address{}, nil, nil, fmt.Errorf("failed to deploy subscription: %v", err) } // sign the transaction with the private key - signedTx, err := types.SignTx(txToSign, *signer, models.DevSignedPrivateKey) + signedTx, err := types.SignTx(txToSign, *signer, DevSignedPrivateKey) if err != nil { return nil, libcommon.Address{}, nil, nil, fmt.Errorf("failed to sign tx: %v", err) } - return &signedTx, address, subscriptionContract, transactOpts, nil + return signedTx, address, subscriptionContract, transactOpts, nil } // initializeTransactOps initializes the transactOpts object for a contract transaction func initializeTransactOps(nonce uint64) (*bind.TransactOpts, error) { var chainID = big.NewInt(1337) - transactOpts, err := bind.NewKeyedTransactorWithChainID(models.DevSignedPrivateKey, chainID) + transactOpts, err := bind.NewKeyedTransactorWithChainID(DevSignedPrivateKey, chainID) if err != nil { return nil, fmt.Errorf("cannot create transactor with chainID %d, error: %v", chainID, err) } @@ -221,13 +230,20 @@ func initializeTransactOps(nonce uint64) (*bind.TransactOpts, error) { return transactOpts, nil } +// Block represents a simple block for queries +type Block struct { + Number *hexutil.Big + Transactions []libcommon.Hash + BlockHash libcommon.Hash +} + // txHashInBlock checks if the block with block number has the transaction hash in its list of transactions func txHashInBlock(client *rpc.Client, hashmap map[libcommon.Hash]bool, blockNumber string, txToBlockMap map[libcommon.Hash]string, logger log.Logger) (uint64, int, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // releases the resources held by the context var ( - currBlock models.Block + currBlock Block numFound int ) err := client.CallContext(ctx, &currBlock, string(requests.Methods.ETHGetBlockByNumber), blockNumber, false) @@ -244,7 +260,7 @@ func txHashInBlock(client *rpc.Client, hashmap map[libcommon.Hash]bool, blockNum txToBlockMap[txnHash] = blockNumber delete(hashmap, txnHash) if len(hashmap) == 0 { - return requests.HexToInt(blockNumber), numFound, nil + return devnetutils.HexToInt(blockNumber), numFound, nil } } } @@ -253,7 +269,7 @@ func txHashInBlock(client *rpc.Client, hashmap map[libcommon.Hash]bool, blockNum } // EmitFallbackEvent emits an event from the contract using the fallback method -func EmitFallbackEvent(node *node.Node, subContract *contracts.Subscription, opts *bind.TransactOpts, logger log.Logger) (*libcommon.Hash, error) { +func EmitFallbackEvent(node devnet.Node, subContract *contracts.Subscription, opts *bind.TransactOpts, logger log.Logger) (*libcommon.Hash, error) { logger.Info("EMITTING EVENT FROM FALLBACK...") // adding one to the nonce before initiating another transaction @@ -264,12 +280,12 @@ func EmitFallbackEvent(node *node.Node, subContract *contracts.Subscription, opt return nil, fmt.Errorf("failed to emit event from fallback: %v", err) } - signedTx, err := types.SignTx(tx, *signer, models.DevSignedPrivateKey) + signedTx, err := types.SignTx(tx, *signer, DevSignedPrivateKey) if err != nil { return nil, fmt.Errorf("failed to sign fallback transaction: %v", err) } - hash, err := node.SendTransaction(&signedTx) + hash, err := node.SendTransaction(signedTx) if err != nil { return nil, fmt.Errorf("failed to send fallback transaction: %v", err) } @@ -277,9 +293,9 @@ func EmitFallbackEvent(node *node.Node, subContract *contracts.Subscription, opt return hash, nil } -func BaseFeeFromBlock(node *node.Node, logger log.Logger) (uint64, error) { +func BaseFeeFromBlock(ctx context.Context) (uint64, error) { var val uint64 - res, err := node.GetBlockByNumberDetails("latest", false) + res, err := devnet.SelectNode(ctx).GetBlockByNumberDetails("latest", false) if err != nil { return 0, fmt.Errorf("failed to get base fee from block: %v\n", err) } @@ -287,21 +303,23 @@ func BaseFeeFromBlock(node *node.Node, logger log.Logger) (uint64, error) { if v, ok := res["baseFeePerGas"]; !ok { return val, fmt.Errorf("baseFeePerGas field missing from response") } else { - val = requests.HexToInt(v.(string)) + val = devnetutils.HexToInt(v.(string)) } return val, err } -func SendManyTransactions(node *node.Node, signedTransactions []*types.Transaction, logger log.Logger) ([]*libcommon.Hash, error) { +func SendManyTransactions(ctx context.Context, signedTransactions []types.Transaction) ([]*libcommon.Hash, error) { + logger := devnet.Logger(ctx) + logger.Info("Sending multiple transactions to the txpool...") hashes := make([]*libcommon.Hash, len(signedTransactions)) for idx, tx := range signedTransactions { - hash, err := node.SendTransaction(tx) + hash, err := devnet.SelectNode(ctx).SendTransaction(tx) if err != nil { logger.Error("failed SendTransaction", "error", err) - return nil, err + //return nil, err } hashes[idx] = hash } diff --git a/cmd/devnet/services/event.go b/cmd/devnet/services/event.go index 4b000ca5cfb..3625122be0b 100644 --- a/cmd/devnet/services/event.go +++ b/cmd/devnet/services/event.go @@ -6,34 +6,43 @@ import ( libcommon "github.com/ledgerwatch/erigon-lib/common" + "github.com/ledgerwatch/erigon/cmd/devnet/devnet" "github.com/ledgerwatch/erigon/cmd/devnet/devnetutils" - "github.com/ledgerwatch/erigon/cmd/devnet/models" "github.com/ledgerwatch/erigon/cmd/devnet/requests" "github.com/ledgerwatch/erigon/rpc" "github.com/ledgerwatch/log/v3" ) -func InitSubscriptions(methods []requests.SubMethod, logger log.Logger) { +var ( + // MaxNumberOfBlockChecks is the max number of blocks to look for a transaction in + MaxNumberOfEmptyBlockChecks = 25 + Subscriptions *map[requests.SubMethod]*Subscription +) + +// Subscription houses the client subscription, name and channel for its delivery +type Subscription struct { + Client *rpc.Client + ClientSub *rpc.ClientSubscription + Name requests.SubMethod + SubChan chan interface{} +} + +// NewSubscription returns a new Subscription instance +func NewSubscription(name requests.SubMethod) *Subscription { + return &Subscription{ + Name: name, + SubChan: make(chan interface{}), + } +} + +func InitSubscriptions(ctx context.Context, methods []requests.SubMethod) { + logger := devnet.Logger(ctx) + logger.Info("CONNECTING TO WEBSOCKETS AND SUBSCRIBING TO METHODS...") if err := subscribeAll(methods, logger); err != nil { logger.Error("failed to subscribe to all methods", "error", err) return } - - // Initializing subscription methods - logger.Info("INITIATE LISTENS ON SUBSCRIPTION CHANNELS") - models.NewHeadsChan = make(chan interface{}) - - go func() { - methodSub := (*models.MethodSubscriptionMap)[requests.Methods.ETHNewHeads] - if methodSub == nil { - logger.Error("method subscription should not be nil") - return - } - - block := <-methodSub.SubChan - models.NewHeadsChan <- block - }() } func SearchReservesForTransactionHash(hashes map[libcommon.Hash]bool, logger log.Logger) (*map[libcommon.Hash]string, error) { @@ -47,8 +56,8 @@ func SearchReservesForTransactionHash(hashes map[libcommon.Hash]bool, logger log } // subscribe connects to a websocket client and returns the subscription handler and a channel buffer -func subscribe(client *rpc.Client, method requests.SubMethod, args ...interface{}) (*models.MethodSubscription, error) { - methodSub := models.NewMethodSubscription(method) +func subscribe(client *rpc.Client, method requests.SubMethod, args ...interface{}) (*Subscription, error) { + methodSub := NewSubscription(method) namespace, subMethod, err := devnetutils.NamespaceAndSubMethodFromMethod(string(method)) if err != nil { @@ -67,7 +76,7 @@ func subscribe(client *rpc.Client, method requests.SubMethod, args ...interface{ return methodSub, nil } -func subscribeToMethod(method requests.SubMethod, logger log.Logger) (*models.MethodSubscription, error) { +func subscribeToMethod(method requests.SubMethod, logger log.Logger) (*Subscription, error) { client, err := rpc.DialWebsocket(context.Background(), "ws://localhost:8545", "", logger) if err != nil { return nil, fmt.Errorf("failed to dial websocket: %v", err) @@ -90,28 +99,42 @@ func searchBlockForHashes(hashmap map[libcommon.Hash]bool, logger log.Logger) (* txToBlock := make(map[libcommon.Hash]string, len(hashmap)) - toFind := len(hashmap) - methodSub := (*models.MethodSubscriptionMap)[requests.Methods.ETHNewHeads] + methodSub := (*Subscriptions)[requests.Methods.ETHNewHeads] if methodSub == nil { return nil, fmt.Errorf("client subscription should not be nil") } + headsSub := (*Subscriptions)[requests.Methods.ETHNewHeads] + + // get a block from the new heads channel + if headsSub == nil { + return nil, fmt.Errorf("no block heads subscription") + } + var blockCount int for { - // get a block from the new heads channel - block := <-models.NewHeadsChan - blockCount++ // increment the number of blocks seen to check against the max number of blocks to iterate over + block := <-headsSub.SubChan blockNum := block.(map[string]interface{})["number"].(string) _, numFound, foundErr := txHashInBlock(methodSub.Client, hashmap, blockNum, txToBlock, logger) + if foundErr != nil { return nil, fmt.Errorf("failed to find hash in block with number %q: %v", foundErr, blockNum) } - toFind -= numFound // remove the amount of found txs from the amount we're looking for - if toFind == 0 { // this means we have found all the txs we're looking for + + if len(hashmap) == 0 { // this means we have found all the txs we're looking for logger.Info("All the transactions created have been included in blocks") return &txToBlock, nil } - if blockCount == models.MaxNumberOfBlockChecks { + + if numFound == 0 { + blockCount++ // increment the number of blocks seen to check against the max number of blocks to iterate over + } + + if blockCount == MaxNumberOfEmptyBlockChecks { + for h := range hashmap { + logger.Error("Missing Tx", "txHash", h) + } + return nil, fmt.Errorf("timeout when searching for tx") } } @@ -119,10 +142,10 @@ func searchBlockForHashes(hashmap map[libcommon.Hash]bool, logger log.Logger) (* // UnsubscribeAll closes all the client subscriptions and empties their global subscription channel func UnsubscribeAll() { - if models.MethodSubscriptionMap == nil { + if Subscriptions == nil { return } - for _, methodSub := range *models.MethodSubscriptionMap { + for _, methodSub := range *Subscriptions { if methodSub != nil { methodSub.ClientSub.Unsubscribe() for len(methodSub.SubChan) > 0 { @@ -135,14 +158,14 @@ func UnsubscribeAll() { // subscribeAll subscribes to the range of methods provided func subscribeAll(methods []requests.SubMethod, logger log.Logger) error { - m := make(map[requests.SubMethod]*models.MethodSubscription) - models.MethodSubscriptionMap = &m + m := make(map[requests.SubMethod]*Subscription) + Subscriptions = &m for _, method := range methods { sub, err := subscribeToMethod(method, logger) if err != nil { return err } - (*models.MethodSubscriptionMap)[method] = sub + (*Subscriptions)[method] = sub } return nil diff --git a/cmd/devnet/services/tx.go b/cmd/devnet/services/tx.go index b8037d42eca..9c574665526 100644 --- a/cmd/devnet/services/tx.go +++ b/cmd/devnet/services/tx.go @@ -1,30 +1,34 @@ package services import ( - "github.com/ledgerwatch/erigon/cmd/devnet/node" - "github.com/ledgerwatch/log/v3" + "context" + + "github.com/ledgerwatch/erigon/cmd/devnet/devnet" ) -func CheckTxPoolContent(node *node.Node, expectedPendingSize, expectedQueuedSize, expectedBaseFeeSize int, logger log.Logger) { - pendingSize, queuedSize, baseFeeSize, err := node.TxpoolContent() +func CheckTxPoolContent(ctx context.Context, expectedPendingSize, expectedQueuedSize, expectedBaseFeeSize int) { + pendingSize, queuedSize, baseFeeSize, err := devnet.SelectMiner(ctx).TxpoolContent() + + logger := devnet.Logger(ctx) + if err != nil { logger.Error("FAILURE getting txpool content", "error", err) return } - if pendingSize != expectedPendingSize { + if expectedPendingSize >= 0 && pendingSize != expectedPendingSize { logger.Error("FAILURE mismatched pending subpool size", "expected", expectedPendingSize, "got", pendingSize) return } - if queuedSize != expectedQueuedSize { + if expectedQueuedSize >= 0 && queuedSize != expectedQueuedSize { logger.Error("FAILURE mismatched queued subpool size", "expected", expectedQueuedSize, "got", queuedSize) return } - if baseFeeSize != expectedBaseFeeSize { + if expectedBaseFeeSize >= 0 && baseFeeSize != expectedBaseFeeSize { logger.Error("FAILURE mismatched basefee subpool size", "expected", expectedBaseFeeSize, "got", baseFeeSize) } - logger.Info("SUCCESS => subpool sizes", "pending", pendingSize, "queued", queuedSize, "basefee", baseFeeSize) + logger.Info("Subpool sizes", "pending", pendingSize, "queued", queuedSize, "basefee", baseFeeSize) } diff --git a/docs/examples/single-process.md b/docs/examples/single-process.md index 94a108f081c..6465128fba7 100644 --- a/docs/examples/single-process.md +++ b/docs/examples/single-process.md @@ -18,6 +18,7 @@ How to run Erigon in a single process (all parts of the system run as one). --ws \ --http.api=eth,debug,net,trace,web3,erigon \ --log.dir.path=/desired/path/to/logs + --log.dir.prefix=filename ``` ## Notes diff --git a/turbo/logging/flags.go b/turbo/logging/flags.go index bf070e2fe1e..b7e9e56727a 100644 --- a/turbo/logging/flags.go +++ b/turbo/logging/flags.go @@ -38,6 +38,11 @@ var ( Usage: "Path to store user and error logs to disk", } + LogDirPrefixFlag = cli.StringFlag{ + Name: "log.dir.prefix", + Usage: "The file name prefix for logs stored to disk", + } + LogDirVerbosityFlag = cli.StringFlag{ Name: "log.dir.verbosity", Usage: "Set the log verbosity for logs stored to disk", @@ -52,5 +57,6 @@ var Flags = []cli.Flag{ &LogVerbosityFlag, &LogConsoleVerbosityFlag, &LogDirPathFlag, + &LogDirPrefixFlag, &LogDirVerbosityFlag, } diff --git a/turbo/logging/logging.go b/turbo/logging/logging.go index 588f6c86284..fb9e467e7e8 100644 --- a/turbo/logging/logging.go +++ b/turbo/logging/logging.go @@ -52,6 +52,11 @@ func SetupLoggerCtx(filePrefix string, ctx *cli.Context, rootHandler bool) log.L } else { logger = log.New() } + + if logDirPrefix := ctx.String(LogDirPrefixFlag.Name); len(logDirPrefix) > 0 { + filePrefix = logDirPrefix + } + initSeparatedLogging(logger, filePrefix, dirPath, consoleLevel, dirLevel, consoleJson, dirJson) return logger } @@ -100,6 +105,11 @@ func SetupLoggerCmd(filePrefix string, cmd *cobra.Command) log.Logger { dirPath = filepath.Join(datadir, "logs") } } + + if logDirPrefix := cmd.Flags().Lookup(LogDirPrefixFlag.Name).Value.String(); len(logDirPrefix) > 0 { + filePrefix = logDirPrefix + } + initSeparatedLogging(log.Root(), filePrefix, dirPath, consoleLevel, dirLevel, consoleJson, dirJson) return log.Root() } @@ -110,6 +120,7 @@ func SetupLogger(filePrefix string) log.Logger { var logConsoleVerbosity = flag.String(LogConsoleVerbosityFlag.Name, "", LogConsoleVerbosityFlag.Usage) var logDirVerbosity = flag.String(LogDirVerbosityFlag.Name, "", LogDirVerbosityFlag.Usage) var logDirPath = flag.String(LogDirPathFlag.Name, "", LogDirPathFlag.Usage) + var logDirPrefix = flag.String(LogDirPrefixFlag.Name, "", LogDirPrefixFlag.Usage) var logVerbosity = flag.String(LogVerbosityFlag.Name, "", LogVerbosityFlag.Usage) var logConsoleJson = flag.Bool(LogConsoleJsonFlag.Name, false, LogConsoleJsonFlag.Usage) var logJson = flag.Bool(LogJsonFlag.Name, false, LogJsonFlag.Usage) @@ -133,6 +144,10 @@ func SetupLogger(filePrefix string) log.Logger { dirLevel = log.LvlInfo } + if logDirPrefix != nil && len(*logDirPrefix) > 0 { + filePrefix = *logDirPrefix + } + initSeparatedLogging(log.Root(), filePrefix, *logDirPath, consoleLevel, dirLevel, consoleJson, *dirJson) return log.Root() } diff --git a/turbo/node/node.go b/turbo/node/node.go index 0f40a7c0d7f..a403a700afc 100644 --- a/turbo/node/node.go +++ b/turbo/node/node.go @@ -25,7 +25,7 @@ type ErigonNode struct { // Serve runs the node and blocks the execution. It returns when the node is existed. func (eri *ErigonNode) Serve() error { - defer eri.stack.Close() + defer eri.Close() eri.run() @@ -34,6 +34,10 @@ func (eri *ErigonNode) Serve() error { return nil } +func (eri *ErigonNode) Close() { + eri.stack.Close() +} + func (eri *ErigonNode) run() { node.StartNode(eri.stack) // we don't have accounts locally and we don't do mining