Skip to content

Commit

Permalink
Merge pull request #43 from maticnetwork/jhilliard/fork-analysis
Browse files Browse the repository at this point in the history
Fork Analysis
  • Loading branch information
praetoriansentry authored Feb 24, 2023
2 parents 84b42a4 + 6badb96 commit 41ce3f6
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 76 deletions.
118 changes: 42 additions & 76 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,97 +330,63 @@ 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

```bash
cobra-cli init
cobra-cli add version
cobra-cli add hash
cobra-cli add mnemonic
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
```

This is the content of my `~/.cobra.yaml` file

```yaml
---
author: Polygon <[email protected]>
license: lgpl-3.0
useViper: true
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)
```

# Testing with Geth
While working on some of the Polygon CLI tools, we'll run geth in dev
mode in order to make sure the various functions work properly. First,
we'll startup geth.
If we want to break down input data we can run something like this:

```shell
# 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
go run main.go abi --data 0x3c158267000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000063ed0f8f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006eec03843b9aca0082520894d2f852ec7b4e457f6e7ff8b243c49ff5692926ea87038d7ea4c68000808204c58080642dfe2cca094f2419aad1322ec68e3b37974bd9c918e0686b9bbf02b8bd1145622a3dd64202da71549c010494fd1475d3bf232aa9028204a872fd2e531abfd31c000000000000000000000000000000000000 < contract.abi
```

```sh
git submodule update --init --recursive
cd v3-contracts
npm i
npx hardhat compile --show-stack-traces
```
In addition to the function selector data, we'll also get a breakdown of input data:

Simple startup script for borv3 from our testing
```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
}
]
}
```

```bash
#!/bin/bash
# Fork

num=4
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.

rm -rf test-dir-*
rm genesis.json
```shell
go run main.go fork 0x053d84d5215684c8ae810a4729f7c9b54d65a80b128a27aeddcd7dc295a0cebd https://polygon-rpc.com
```

# rm borv3
# go build -o borv3 cmd/borv3/main.go
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.

./borv3 init-account --datadir test-dir --num $num
./borv3 init-genesis --premine 0x85da99c8a7c2c95964c8efd687e95e632fc533d6
# Testing with Geth

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
While working on some of the Polygon CLI tools, we'll run geth in dev
mode in order to make sure the various functions work properly. First,
we'll startup geth.

# ps aux | grep borv3 | grep -v grep | awk '{print $2}' | xargs kill -9
```
```shell
# 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

In the logs, we'll see a line that says IPC endpoint opened:
Expand Down Expand Up @@ -471,7 +437,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
Expand Down
184 changes: 184 additions & 0 deletions cmd/fork/fork.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package fork

import (
"context"
"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"
"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 0x053d84d5215684c8ae810a4729f7c9b54d65a80b128a27aeddcd7dc295a0cebd https://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().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
}
return walkTheBlocks(blockHash, c)
},
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")

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 {
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() {
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
}
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()
}
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()
// 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 {
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 {
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 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)
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})

}
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package cmd

import (
"fmt"
"github.com/maticnetwork/polygon-cli/cmd/fork"
"os"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 41ce3f6

Please sign in to comment.