-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #43 from maticnetwork/jhilliard/fork-analysis
Fork Analysis
- Loading branch information
Showing
3 changed files
with
228 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters