From 2f9e7dc79a66a7a0e0744c78d8d326dcd5e7e4a6 Mon Sep 17 00:00:00 2001 From: John Hilliard Date: Thu, 23 Feb 2023 17:31:31 -0500 Subject: [PATCH 1/8] docs: adding some notes about the abi command --- README.md | 109 +++++++++++++++++------------------------------------- 1 file changed, 33 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index b2195e24..b418beb8 100644 --- a/README.md +++ b/README.md @@ -330,26 +330,45 @@ go run main.go metrics-to-dash -i avail-light-metrics.txt -p avail_light. -t "Av ``` -# Adding Commands +# ABI -Script to setup this repo +This command is useful for analyzing Solidity ABIs and decoding function selectors and input data. Most commonly, we need +this capability while analyzing raw blocks when we don't know which method is being called, but we know the smart contract. +We can the command like this to get the function signatures and selectors: +```shell +go run main.go abi --file contract.abi +``` -```bash -cobra-cli init -cobra-cli add version -cobra-cli add hash -cobra-cli add mnemonic +This would output some information that woudl let us know the various function selectors for this contract: +```txt +Selector:19d8ac61 Signature:function lastTimestamp() view returns(uint64) +Selector:a066215c Signature:function setVerifyBatchTimeTarget(uint64 newVerifyBatchTimeTarget) returns() +Selector:715018a6 Signature:function renounceOwnership() returns() +Selector:cfa8ed47 Signature:function trustedSequencer() view returns(address) +``` + +If we want to break down input data we can run something like this: + +```shell +go run main.go abi --data 0x3c158267000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000063ed0f8f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006eec03843b9aca0082520894d2f852ec7b4e457f6e7ff8b243c49ff5692926ea87038d7ea4c68000808204c58080642dfe2cca094f2419aad1322ec68e3b37974bd9c918e0686b9bbf02b8bd1145622a3dd64202da71549c010494fd1475d3bf232aa9028204a872fd2e531abfd31c000000000000000000000000000000000000 < contract.abi ``` -This is the content of my `~/.cobra.yaml` file +In addition to the function selector data, we'll also get a breakdown of input data: -```yaml ---- -author: Polygon -license: lgpl-3.0 -useViper: true +```json +{ + "batches": [ + { + "transactions": "7AOEO5rKAIJSCJTS+FLse05Ff25/+LJDxJ/1aSkm6ocDjX6kxoAAgIIExYCAZC3+LMoJTyQZqtEyLsaOOzeXS9nJGOBoa5u/Ari9EUViKj3WQgLacVScAQSU/RR1078jKqkCggSocv0uUxq/0xw=", + "globalExitRoot": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "timestamp": 1676480399, + "minForcedTimestamp": 0 + } + ] +} ``` + # Testing with Geth While working on some of the Polygon CLI tools, we'll run geth in dev @@ -360,68 +379,6 @@ we'll startup geth. # Geth ./build/bin/geth --dev --dev.period 2 --http --http.addr localhost --http.port 8545 --http.api admin,debug,web3,eth,txpool,personal,miner,net --verbosity 5 --rpc.gascap 50000000 --rpc.txfeecap 0 --miner.gaslimit 10 --miner.gasprice 1 --gpo.blocks 1 --gpo.percentile 1 --gpo.maxprice 10 --gpo.ignoreprice 2 --dev.gaslimit 50000000 - -# v3 -go run main.go server --dev \ - --dev.period 2 \ - --ws --ws.port 8546 \ - --http --http.port 8545 \ - --jsonrpc.modules eth,web3,personal,net \ - --log-level debug \ - --ipcpath ./borv3ipc \ - --rpc.gascap 18446744073709551615 \ - --rpc.txfeecap 0 \ - --miner.gaslimit 10 \ - --miner.gasprice 1 \ - --gpo.blocks 1 \ - --gpo.percentile 1 \ - --gpo.maxprice 10 \ - --gpo.ignoreprice 2 -``` - -If you don't have `v3` make sure you grab it. - -- Build the binaries - - ```sh - git clone https://github.com/maticnetwork/v3.git - cd v3 - make all - ``` - -- Generate the contracts as `borv3 init-genesis` requires them - - ```sh - git submodule update --init --recursive - cd v3-contracts - npm i - npx hardhat compile --show-stack-traces - ``` - -Simple startup script for borv3 from our testing - -```bash -#!/bin/bash - -num=4 - -rm -rf test-dir-* -rm genesis.json - -# rm borv3 -# go build -o borv3 cmd/borv3/main.go - -./borv3 init-account --datadir test-dir --num $num -./borv3 init-genesis --premine 0x85da99c8a7c2c95964c8efd687e95e632fc533d6 - -seq 1 $num | while read -r line -do - ./borv3 server --chain genesis.json --datadir test-dir-$line --port 3030$line --mine --http --http.port 854$line --jsonrpc.modules eth --rpc.gascap 18446744073709551615 & -done - -# ps aux | grep borv3 | grep -v grep | awk '{print $2}' | xargs kill -9 -``` - In the logs, we'll see a line that says IPC endpoint opened: ```example @@ -471,7 +428,7 @@ Useful RPCs when testing ```shell curl -v -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0", "id": 1, "method": "net_version", "params": []}' https://polygon-rpc.com -curl -v -H 'Content-Type: application/json' -d '{"id": 1, "method": "eth_blockNumber", "params": []}' https://polygon-rpc.com +curl -v -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0", "id": 1, "method": "eth_blockNumber", "params": []}' https://polygon-rpc.com curl -v -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0", "id": 1, "method": "eth_getBlockByNumber", "params": ["0x1DE8531", true]}' https://polygon-rpc.com curl -v -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0", "id": 1, "method": "clique_getSigner", "params": ["0x1DE8531", true]}' https://polygon-rpc.com curl -v -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0", "id": 1, "method": "eth_getBalance", "params": ["0x85da99c8a7c2c95964c8efd687e95e632fc533d6", "latest"]}' https://polygon-rpc.com From 40b4919de6208477655562d8c7ef3b728ccf12b9 Mon Sep 17 00:00:00 2001 From: John Hilliard Date: Thu, 23 Feb 2023 18:46:14 -0500 Subject: [PATCH 2/8] feat: adding wip to analyze fork --- cmd/fork/fork.go | 110 +++++++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 2 + 2 files changed, 112 insertions(+) create mode 100644 cmd/fork/fork.go diff --git a/cmd/fork/fork.go b/cmd/fork/fork.go new file mode 100644 index 00000000..3455b938 --- /dev/null +++ b/cmd/fork/fork.go @@ -0,0 +1,110 @@ +package fork + +import ( + "context" + "fmt" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "os" + "time" + + "github.com/ethereum/go-ethereum/ethclient" +) + +var ( + rpcURL string + blockHash ethcommon.Hash + retryLimit = 30 + + errRetryLimitExceeded = fmt.Errorf("Unable to process request after hitting retry limit") +) +var ForkCmd = &cobra.Command{ + Use: "fork blockhash http://polygon-rpc.com", + Short: "Take a forked block and walk up the chain to do analysis", + Long: ` +TODO +`, + RunE: func(cmd *cobra.Command, args []string) error { + log.Info().Msg("Hi there") + log.Info().Str("rpc", rpcURL).Str("blockHash", blockHash.String()).Msg("Starting Analysis") + c, err := ethclient.Dial(rpcURL) + if err != nil { + log.Error().Err(err).Str("rpc", rpcURL).Msg("Could not rpc dial connection") + return err + } + walkTheBlocks(blockHash, c) + return nil + }, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 2 { + return fmt.Errorf("two arguments required a block hash and an RPC URL") + } + blockHash = ethcommon.HexToHash(args[0]) + rpcURL = args[1] + return nil + }, +} + +func walkTheBlocks(inputBlockHash ethcommon.Hash, client *ethclient.Client) error { + log.Info().Msg("Starting block analysis") + ctx := context.Background() + bn, err := client.BlockNumber(ctx) + if err != nil { + log.Error().Err(err).Msg("unable to get current block number from chain") + return err + } + log.Info().Uint64("headBlock", bn).Msg("retrieved current head of the chain") + + for { + potentialForkedBlock, err := getBlockByHash(ctx, inputBlockHash, client) + if err != nil { + log.Error().Err(err).Str("blockhash", inputBlockHash.String()).Msg("unable to fetch block") + return err + } + log.Info().Uint64("number", potentialForkedBlock.NumberU64()).Msg("successfully retrieved starting block hash") + + canonicalBlock, err := client.BlockByNumber(ctx, potentialForkedBlock.Number()) + if err != nil { + log.Error().Err(err).Uint64("number", potentialForkedBlock.NumberU64()).Msg("unable to retrieve block by number") + return err + } + if potentialForkedBlock.Hash().String() == canonicalBlock.Hash().String() { + log.Info().Uint64("number", canonicalBlock.NumberU64()).Str("blockHash", canonicalBlock.Hash().String()).Msg("the current block seems to be canonical in the chain. Stopping analysis") + break + } else { + log.Info(). + Uint64("number", potentialForkedBlock.NumberU64()). + Str("forkedBlockHash", potentialForkedBlock.Hash().String()). + Str("canonicalBlockHash", canonicalBlock.Hash().String()). + Msg("Identified forked block. Continuing traversal") + } + // Ever higher + inputBlockHash = potentialForkedBlock.ParentHash() + } + return nil +} + +// getBlockByHash will try to get a block by hash in a loop. Unless we have a dedicated node that we know has the forked blocks it's going to be tricky to consistently get results from a fork. So it requires some brute force +func getBlockByHash(ctx context.Context, bh ethcommon.Hash, client *ethclient.Client) (*types.Block, error) { + for i := 0; i < retryLimit; i = i + 1 { + block, err := client.BlockByHash(ctx, bh) + if err != nil { + log.Warn().Err(err).Int("attempt", i).Str("blockhash", bh.String()).Msg("unable to fetch block") + } else { + return block, nil + } + time.Sleep(2 * time.Second) + } + log.Error().Err(errRetryLimitExceeded).Str("blockhash", bh.String()).Int("retryLimit", retryLimit).Msg("unable to fetch block after retrying") + return nil, errRetryLimitExceeded +} + +func init() { + // flagSet := ForkCmd.PersistentFlags() + zerolog.SetGlobalLevel(zerolog.TraceLevel) + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + +} diff --git a/cmd/root.go b/cmd/root.go index 60ca1125..db92be52 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -18,6 +18,7 @@ package cmd import ( "fmt" + "github.com/maticnetwork/polygon-cli/cmd/fork" "os" "github.com/spf13/cobra" @@ -83,6 +84,7 @@ func init() { rootCmd.AddCommand(abi.ABICmd) rootCmd.AddCommand(version.VersionCmd) rootCmd.AddCommand(wallet.WalletCmd) + rootCmd.AddCommand(fork.ForkCmd) } // initConfig reads in config file and ENV variables if set. From 6731d475278cb75ae8b49fbe8692f70e83629c81 Mon Sep 17 00:00:00 2001 From: John Hilliard Date: Thu, 23 Feb 2023 19:55:14 -0500 Subject: [PATCH 3/8] feat: adding file output --- cmd/fork/fork.go | 70 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/cmd/fork/fork.go b/cmd/fork/fork.go index 3455b938..2224e778 100644 --- a/cmd/fork/fork.go +++ b/cmd/fork/fork.go @@ -2,6 +2,7 @@ package fork import ( "context" + "encoding/json" "fmt" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -14,6 +15,13 @@ import ( "github.com/ethereum/go-ethereum/ethclient" ) +type ( + jsonBlock struct { + json.RawMessage + Transactions types.Transactions `json:"transactions"` + } +) + var ( rpcURL string blockHash ethcommon.Hash @@ -21,6 +29,7 @@ var ( errRetryLimitExceeded = fmt.Errorf("Unable to process request after hitting retry limit") ) + var ForkCmd = &cobra.Command{ Use: "fork blockhash http://polygon-rpc.com", Short: "Take a forked block and walk up the chain to do analysis", @@ -58,6 +67,12 @@ func walkTheBlocks(inputBlockHash ethcommon.Hash, client *ethclient.Client) erro } log.Info().Uint64("headBlock", bn).Msg("retrieved current head of the chain") + folderName := fmt.Sprintf("fork-analysis-%d", time.Now().Unix()) + if err := os.Mkdir(folderName, os.ModePerm); err != nil { + log.Error().Err(err).Msg("Unable to create output folder") + return err + } + for { potentialForkedBlock, err := getBlockByHash(ctx, inputBlockHash, client) if err != nil { @@ -69,17 +84,32 @@ func walkTheBlocks(inputBlockHash ethcommon.Hash, client *ethclient.Client) erro canonicalBlock, err := client.BlockByNumber(ctx, potentialForkedBlock.Number()) if err != nil { log.Error().Err(err).Uint64("number", potentialForkedBlock.NumberU64()).Msg("unable to retrieve block by number") + return err } if potentialForkedBlock.Hash().String() == canonicalBlock.Hash().String() { + err = writeBlock(folderName, canonicalBlock, true) + if err != nil { + log.Error().Err(err).Msg("failed to save final canonical block") + } log.Info().Uint64("number", canonicalBlock.NumberU64()).Str("blockHash", canonicalBlock.Hash().String()).Msg("the current block seems to be canonical in the chain. Stopping analysis") break - } else { - log.Info(). - Uint64("number", potentialForkedBlock.NumberU64()). - Str("forkedBlockHash", potentialForkedBlock.Hash().String()). - Str("canonicalBlockHash", canonicalBlock.Hash().String()). - Msg("Identified forked block. Continuing traversal") + } + log.Info(). + Uint64("number", potentialForkedBlock.NumberU64()). + Str("forkedBlockHash", potentialForkedBlock.Hash().String()). + Str("canonicalBlockHash", canonicalBlock.Hash().String()). + Msg("Identified forked block. Continuing traversal") + + err = writeBlock(folderName, potentialForkedBlock, false) + if err != nil { + log.Error().Err(err).Msg("unable to save forked block") + return err + } + err = writeBlock(folderName, canonicalBlock, true) + if err != nil { + log.Error().Err(err).Msg("unable to save canonical block") + return err } // Ever higher inputBlockHash = potentialForkedBlock.ParentHash() @@ -87,6 +117,34 @@ func walkTheBlocks(inputBlockHash ethcommon.Hash, client *ethclient.Client) erro return nil } +func writeBlock(folderName string, block *types.Block, isCanonical bool) error { + rawHeader, err := block.Header().MarshalJSON() + if err != nil { + log.Error().Err(err).Msg("unable to json marshal the header") + return err + } + fields := make(map[string]interface{}, 0) + err = json.Unmarshal(rawHeader, &fields) + if err != nil { + log.Error().Err(err).Msg("unable to convert header to map type") + return err + } + fields["transactions"] = block.Transactions() + + jsonData, err := json.Marshal(fields) + if err != nil { + log.Error().Err(err).Msg("Unable to marshal block to json") + return err + } + blockType := "c" + if !isCanonical { + blockType = "f" + } + fileName := fmt.Sprintf("%s/%d-%s-%s.json", folderName, block.NumberU64(), blockType, block.Hash().String()) + err = os.WriteFile(fileName, jsonData, 0744) + return err +} + // getBlockByHash will try to get a block by hash in a loop. Unless we have a dedicated node that we know has the forked blocks it's going to be tricky to consistently get results from a fork. So it requires some brute force func getBlockByHash(ctx context.Context, bh ethcommon.Hash, client *ethclient.Client) (*types.Block, error) { for i := 0; i < retryLimit; i = i + 1 { From b1ca0e49fbe8521ac6b9499be71377223fb28cae Mon Sep 17 00:00:00 2001 From: John Hilliard Date: Thu, 23 Feb 2023 20:11:11 -0500 Subject: [PATCH 4/8] feat: adding signer recovery --- cmd/fork/fork.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/cmd/fork/fork.go b/cmd/fork/fork.go index 2224e778..b0f11b0d 100644 --- a/cmd/fork/fork.go +++ b/cmd/fork/fork.go @@ -5,7 +5,9 @@ import ( "encoding/json" "fmt" ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/clique" "github.com/ethereum/go-ethereum/core/types" + ethcrypto "github.com/ethereum/go-ethereum/crypto" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -130,6 +132,13 @@ func writeBlock(folderName string, block *types.Block, isCanonical bool) error { return err } fields["transactions"] = block.Transactions() + // TODO in the future if this is used in other chains or with different types of consensus this would need to be revised + signer, err := ecrecover(block) + if err != nil { + log.Error().Err(err).Msg("Unable to recover signature") + return err + } + fields["_signer"] = ethcommon.BytesToAddress(signer) jsonData, err := json.Marshal(fields) if err != nil { @@ -160,6 +169,22 @@ func getBlockByHash(ctx context.Context, bh ethcommon.Hash, client *ethclient.Cl return nil, errRetryLimitExceeded } +func ecrecover(block *types.Block) ([]byte, error) { + header := block.Header() + sigStart := len(header.Extra) - ethcrypto.SignatureLength + if sigStart < 0 || sigStart > len(header.Extra) { + return nil, fmt.Errorf("unable to recover signature") + } + signature := header.Extra[sigStart:] + pubkey, err := ethcrypto.Ecrecover(clique.SealHash(header).Bytes(), signature) + if err != nil { + return nil, err + } + signer := ethcrypto.Keccak256(pubkey[1:])[12:] + + return signer, nil +} + func init() { // flagSet := ForkCmd.PersistentFlags() zerolog.SetGlobalLevel(zerolog.TraceLevel) From 14818364b2d0fba29f9240bb8602334d444acf60 Mon Sep 17 00:00:00 2001 From: John Hilliard Date: Thu, 23 Feb 2023 21:39:33 -0500 Subject: [PATCH 5/8] chore: lints --- cmd/fork/fork.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/cmd/fork/fork.go b/cmd/fork/fork.go index b0f11b0d..da67ea8a 100644 --- a/cmd/fork/fork.go +++ b/cmd/fork/fork.go @@ -17,13 +17,6 @@ import ( "github.com/ethereum/go-ethereum/ethclient" ) -type ( - jsonBlock struct { - json.RawMessage - Transactions types.Transactions `json:"transactions"` - } -) - var ( rpcURL string blockHash ethcommon.Hash @@ -46,8 +39,7 @@ TODO log.Error().Err(err).Str("rpc", rpcURL).Msg("Could not rpc dial connection") return err } - walkTheBlocks(blockHash, c) - return nil + return walkTheBlocks(blockHash, c) }, Args: func(cmd *cobra.Command, args []string) error { if len(args) != 2 { From ad1e90310325e4114c803b8eb0f732a939d8ce6c Mon Sep 17 00:00:00 2001 From: John Hilliard Date: Thu, 23 Feb 2023 21:45:15 -0500 Subject: [PATCH 6/8] docs: adding small comment for fork --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index b418beb8..364631ef 100644 --- a/README.md +++ b/README.md @@ -368,6 +368,15 @@ In addition to the function selector data, we'll also get a breakdown of input d } ``` +# Fork + +Occasionally, we'll want to analyze the details of a side chain to understand in detail who was proposing the blocks, what was the difficultly, and just generally get better understanding of block propogation. + +```shell +go run main.go fork [HASH] [RPC-NODE] +``` + +In order to use this, you'll need to have a blockhash of a block that was part of a fork / side chain. Once you have that, you can run `fork` against a node to get the details of the fork and the canonical chain. # Testing with Geth From df073770ba8d1fe4bede6510780ec6640d05cd59 Mon Sep 17 00:00:00 2001 From: John Hilliard Date: Fri, 24 Feb 2023 09:46:13 -0500 Subject: [PATCH 7/8] docs: adding working example --- README.md | 2 +- cmd/fork/fork.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 364631ef..7c6f7522 100644 --- a/README.md +++ b/README.md @@ -373,7 +373,7 @@ In addition to the function selector data, we'll also get a breakdown of input d Occasionally, we'll want to analyze the details of a side chain to understand in detail who was proposing the blocks, what was the difficultly, and just generally get better understanding of block propogation. ```shell -go run main.go fork [HASH] [RPC-NODE] +go run main.go fork 0x053d84d5215684c8ae810a4729f7c9b54d65a80b128a27aeddcd7dc295a0cebd https://polygon-rpc.com ``` In order to use this, you'll need to have a blockhash of a block that was part of a fork / side chain. Once you have that, you can run `fork` against a node to get the details of the fork and the canonical chain. diff --git a/cmd/fork/fork.go b/cmd/fork/fork.go index da67ea8a..3b673da2 100644 --- a/cmd/fork/fork.go +++ b/cmd/fork/fork.go @@ -26,7 +26,7 @@ var ( ) var ForkCmd = &cobra.Command{ - Use: "fork blockhash http://polygon-rpc.com", + Use: "fork 0x053d84d5215684c8ae810a4729f7c9b54d65a80b128a27aeddcd7dc295a0cebd https://polygon-rpc.com", Short: "Take a forked block and walk up the chain to do analysis", Long: ` TODO From 6badb96e52d10ea70d053d4365c6384e32bb037a Mon Sep 17 00:00:00 2001 From: John Hilliard Date: Fri, 24 Feb 2023 09:55:55 -0500 Subject: [PATCH 8/8] chore: dropping junk line --- cmd/fork/fork.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/fork/fork.go b/cmd/fork/fork.go index 3b673da2..26e2adaf 100644 --- a/cmd/fork/fork.go +++ b/cmd/fork/fork.go @@ -32,7 +32,6 @@ var ForkCmd = &cobra.Command{ TODO `, RunE: func(cmd *cobra.Command, args []string) error { - log.Info().Msg("Hi there") log.Info().Str("rpc", rpcURL).Str("blockHash", blockHash.String()).Msg("Starting Analysis") c, err := ethclient.Dial(rpcURL) if err != nil {