Commit ae96d784 authored by Matthew Slipper's avatar Matthew Slipper

l2geth: Add commands to initialize from a URL, and dump the current chainstate

This PR adds two commands to l2geth:

- `geth init <url> <genesisHash>`, which will download a remote genesis JSON file, compare its hash to the one specified on the CLI, then reinitialize the chain config.
- `geth dump-chain-cfg`, which will dump the current chain config to stdout.

These commands will make it easier for us to adopt the upcoming gas schedule hardfork.

Fixes: ENG-1906
parent a9da94ef
---
'@eth-optimism/l2geth': patch
---
Add reinitialize-by-url command, add dump chain state command
...@@ -17,15 +17,23 @@ ...@@ -17,15 +17,23 @@
package main package main
import ( import (
"bytes"
"crypto/sha256"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/ioutil"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"runtime" "runtime"
"strconv" "strconv"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/ethereum-optimism/optimism/l2geth/common/hexutil"
"github.com/ethereum-optimism/optimism/l2geth/cmd/utils" "github.com/ethereum-optimism/optimism/l2geth/cmd/utils"
"github.com/ethereum-optimism/optimism/l2geth/common" "github.com/ethereum-optimism/optimism/l2geth/common"
"github.com/ethereum-optimism/optimism/l2geth/console" "github.com/ethereum-optimism/optimism/l2geth/console"
...@@ -45,7 +53,7 @@ var ( ...@@ -45,7 +53,7 @@ var (
Action: utils.MigrateFlags(initGenesis), Action: utils.MigrateFlags(initGenesis),
Name: "init", Name: "init",
Usage: "Bootstrap and initialize a new genesis block", Usage: "Bootstrap and initialize a new genesis block",
ArgsUsage: "<genesisPath>", ArgsUsage: "<genesisPathOrUrl> (<genesisHash>)",
Flags: []cli.Flag{ Flags: []cli.Flag{
utils.DataDirFlag, utils.DataDirFlag,
}, },
...@@ -55,7 +63,22 @@ The init command initializes a new genesis block and definition for the network. ...@@ -55,7 +63,22 @@ The init command initializes a new genesis block and definition for the network.
This is a destructive action and changes the network in which you will be This is a destructive action and changes the network in which you will be
participating. participating.
It expects the genesis file as argument.`, It expects either a path or an HTTP URL to the genesis file as an argument. If an
HTTP URL is specified for the genesis file, then a hex-encoded SHA256 hash of the
genesis file must be included as a second argument. The hash provided on the CLI
will be checked against the hash of the genesis file downloaded from the URL.`,
}
dumpChainCfgCommand = cli.Command{
Action: utils.MigrateFlags(dumpChainCfg),
Name: "dump-chain-cfg",
Usage: "Dumps the current chain config to standard out.",
Flags: []cli.Flag{
utils.DataDirFlag,
},
Category: "BLOCKCHAIN COMMANDS",
Description: `
This command dumps the currently configured chain state to standard output. It
will fail if there is no genesis block configured.`,
} }
importCommand = cli.Command{ importCommand = cli.Command{
Action: utils.MigrateFlags(importChain), Action: utils.MigrateFlags(importChain),
...@@ -194,15 +217,50 @@ Use "ethereum dump 0" to dump the genesis block.`, ...@@ -194,15 +217,50 @@ Use "ethereum dump 0" to dump the genesis block.`,
// the zero'd block (i.e. genesis) or will fail hard if it can't succeed. // the zero'd block (i.e. genesis) or will fail hard if it can't succeed.
func initGenesis(ctx *cli.Context) error { func initGenesis(ctx *cli.Context) error {
// Make sure we have a valid genesis JSON // Make sure we have a valid genesis JSON
genesisPath := ctx.Args().First() genesisPathOrURL := ctx.Args().First()
if len(genesisPath) == 0 { if len(genesisPathOrURL) == 0 {
utils.Fatalf("Must supply path to genesis JSON file") utils.Fatalf("Must supply path or URL to genesis JSON file")
} }
file, err := os.Open(genesisPath)
if err != nil { var file io.ReadCloser
utils.Fatalf("Failed to read genesis file: %v", err) if matched, _ := regexp.MatchString("^http(s)?://", genesisPathOrURL); matched {
genesisHashStr := ctx.Args().Get(1)
if genesisHashStr == "" {
utils.Fatalf("Must specify a genesis hash argument if the genesis path argument is an URL.")
}
genesisHashData, err := hexutil.Decode(genesisHashStr)
if err != nil {
utils.Fatalf("Error decoding genesis hash: %v", err)
}
log.Info("Fetching genesis file", "url", genesisPathOrURL)
genesisData, err := fetchGenesis(genesisPathOrURL)
if err != nil {
utils.Fatalf("Failed to fetch genesis file: %v", err)
}
hash := sha256.New()
hash.Write(genesisData)
actualHash := hash.Sum(nil)
if !bytes.Equal(actualHash, genesisHashData) {
utils.Fatalf(
"Genesis hashes do not match. Need: %s, got: %s",
genesisHashStr,
hexutil.Encode(actualHash),
)
}
file = ioutil.NopCloser(bytes.NewReader(genesisData))
} else {
var err error
file, err = os.Open(genesisPathOrURL)
if err != nil {
utils.Fatalf("Failed to read genesis file: %v", err)
}
defer file.Close()
} }
defer file.Close()
genesis := new(core.Genesis) genesis := new(core.Genesis)
if err := json.NewDecoder(file).Decode(genesis); err != nil { if err := json.NewDecoder(file).Decode(genesis); err != nil {
...@@ -227,6 +285,30 @@ func initGenesis(ctx *cli.Context) error { ...@@ -227,6 +285,30 @@ func initGenesis(ctx *cli.Context) error {
return nil return nil
} }
// dumpChainCfg dumps chain config to standard output.
func dumpChainCfg(ctx *cli.Context) error {
stack := makeFullNode(ctx)
defer stack.Close()
db, err := stack.OpenDatabase("chaindata", 0, 0, "")
if err != nil {
utils.Fatalf("Failed to open database: %v", err)
}
stored := rawdb.ReadCanonicalHash(db, 0)
var zeroHash common.Hash
if stored == zeroHash {
utils.Fatalf("No genesis block configured.")
}
chainCfg := rawdb.ReadChainConfig(db, stored)
out, err := json.MarshalIndent(chainCfg, "", " ")
if err != nil {
utils.Fatalf("Failed to marshal chain config: %v", out)
}
fmt.Println(string(out))
return nil
}
func importChain(ctx *cli.Context) error { func importChain(ctx *cli.Context) error {
if len(ctx.Args()) < 1 { if len(ctx.Args()) < 1 {
utils.Fatalf("This command requires an argument.") utils.Fatalf("This command requires an argument.")
...@@ -557,3 +639,15 @@ func hashish(x string) bool { ...@@ -557,3 +639,15 @@ func hashish(x string) bool {
_, err := strconv.Atoi(x) _, err := strconv.Atoi(x)
return err != nil return err != nil
} }
func fetchGenesis(url string) ([]byte, error) {
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
package main
import (
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
)
func TestChainInit(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
f, err := os.Open("testdata/init.json")
if err != nil {
panic(err)
}
defer f.Close()
io.Copy(w, f)
}))
tests := []struct {
name string
url string
hash string
errorMsg string
}{
{
"no genesis hash specified",
server.URL,
"",
"Must specify a genesis hash argument if the genesis path argument is an URL",
},
{
"invalid genesis hash specified",
server.URL,
"not hex yo",
"Error decoding genesis hash",
},
{
"bad URL",
"https://honk",
"0x1234",
"Failed to fetch genesis file",
},
{
"mis-matched hashes",
server.URL,
"0x1234",
"Genesis hashes do not match",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
datadir := tmpdir(t)
geth := runGeth(t, "init", tt.url, tt.hash, "--datadir", datadir)
geth.ExpectRegexp(tt.errorMsg)
})
}
t.Run("URL and hash args OK", func(t *testing.T) {
datadir := tmpdir(t)
geth := runGeth(t, "init", server.URL, "0x1f0201852c30e203a701ac283aeafafaf55b2ad3ae2f4e8f15c61e761434fb62", "--datadir", datadir)
geth.ExpectExit()
geth = runGeth(t, "dump-chain-cfg", "--datadir", datadir)
geth.ExpectRegexp("\"muirGlacierBlock\": 500")
})
t.Run("file arg OK", func(t *testing.T) {
datadir := tmpdir(t)
geth := runGeth(t, "init", "testdata/init.json", "--datadir", datadir)
geth.ExpectExit()
geth = runGeth(t, "dump-chain-cfg", "--datadir", datadir)
geth.ExpectRegexp("\"muirGlacierBlock\": 500")
})
}
func TestDumpChainCfg(t *testing.T) {
datadir := tmpdir(t)
geth := runGeth(t, "init", "testdata/init.json", "--datadir", datadir)
geth.ExpectExit()
geth = runGeth(t, "dump-chain-cfg", "--datadir", datadir)
geth.Expect(`{
"chainId": 69,
"homesteadBlock": 0,
"eip150Block": 0,
"eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"istanbulBlock": 0,
"muirGlacierBlock": 500,
"clique": {
"period": 0,
"epoch": 30000
}
}`)
}
...@@ -216,6 +216,7 @@ func init() { ...@@ -216,6 +216,7 @@ func init() {
app.Commands = []cli.Command{ app.Commands = []cli.Command{
// See chaincmd.go: // See chaincmd.go:
initCommand, initCommand,
dumpChainCfgCommand,
importCommand, importCommand,
exportCommand, exportCommand,
importPreimagesCommand, importPreimagesCommand,
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment