Skip to content

Commit 1f50aa7

Browse files
lightclientholiman
andauthored
cmd,internal/era: implement export-history subcommand (#26621)
* all: implement era format, add history importer/export * internal/era/e2store: refactor e2store to provide ReadAt interface * internal/era/e2store: export HeaderSize * internal/era: refactor era to use ReadAt interface * internal/era: elevate anonymous func to named * cmd/utils: don't store entire era file in-memory during import / export * internal/era: better abstraction between era and e2store * cmd/era: properly close era files * cmd/era: don't let defers stack * cmd/geth: add description for import-history * cmd/utils: better bytes buffer * internal/era: error if accumulator has more records than max allowed * internal/era: better doc comment * internal/era/e2store: rm superfluous reader, rm superfluous testcases, add fuzzer * internal/era: avoid some repetition * internal/era: simplify clauses * internal/era: unexport things * internal/era,cmd/utils,cmd/era: change to iterator interface for reading era entries * cmd/utils: better defer handling in history test * internal/era,cmd: add number method to era iterator to get the current block number * internal/era/e2store: avoid double allocation during write * internal/era,cmd/utils: fix lint issues * internal/era: add ReaderAt func so entry value can be read lazily Co-authored-by: lightclient <lightclient@protonmail.com> Co-authored-by: Martin Holst Swende <martin@swende.se> * internal/era: improve iterator interface * internal/era: fix rlp decode of header and correctly read total difficulty * cmd/era: fix rebase errors * cmd/era: clearer comments * cmd,internal: fix comment typos --------- Co-authored-by: Martin Holst Swende <martin@swende.se>
1 parent 199e0c9 commit 1f50aa7

File tree

15 files changed

+2145
-0
lines changed

15 files changed

+2145
-0
lines changed

cmd/era/main.go

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
// Copyright 2023 The go-ethereum Authors
2+
// This file is part of go-ethereum.
3+
//
4+
// go-ethereum is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// go-ethereum is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package main
18+
19+
import (
20+
"encoding/json"
21+
"fmt"
22+
"math/big"
23+
"os"
24+
"path"
25+
"strconv"
26+
"strings"
27+
"time"
28+
29+
"github.com/ethereum/go-ethereum/common"
30+
"github.com/ethereum/go-ethereum/core/types"
31+
"github.com/ethereum/go-ethereum/internal/era"
32+
"github.com/ethereum/go-ethereum/internal/ethapi"
33+
"github.com/ethereum/go-ethereum/internal/flags"
34+
"github.com/ethereum/go-ethereum/params"
35+
"github.com/ethereum/go-ethereum/trie"
36+
"github.com/urfave/cli/v2"
37+
)
38+
39+
var app = flags.NewApp("go-ethereum era tool")
40+
41+
var (
42+
dirFlag = &cli.StringFlag{
43+
Name: "dir",
44+
Usage: "directory storing all relevant era1 files",
45+
Value: "eras",
46+
}
47+
networkFlag = &cli.StringFlag{
48+
Name: "network",
49+
Usage: "network name associated with era1 files",
50+
Value: "mainnet",
51+
}
52+
eraSizeFlag = &cli.IntFlag{
53+
Name: "size",
54+
Usage: "number of blocks per era",
55+
Value: era.MaxEra1Size,
56+
}
57+
txsFlag = &cli.BoolFlag{
58+
Name: "txs",
59+
Usage: "print full transaction values",
60+
}
61+
)
62+
63+
var (
64+
blockCommand = &cli.Command{
65+
Name: "block",
66+
Usage: "get block data",
67+
ArgsUsage: "<number>",
68+
Action: block,
69+
Flags: []cli.Flag{
70+
txsFlag,
71+
},
72+
}
73+
infoCommand = &cli.Command{
74+
Name: "info",
75+
ArgsUsage: "<epoch>",
76+
Usage: "get epoch information",
77+
Action: info,
78+
}
79+
verifyCommand = &cli.Command{
80+
Name: "verify",
81+
ArgsUsage: "<expected>",
82+
Usage: "verifies each era1 against expected accumulator root",
83+
Action: verify,
84+
}
85+
)
86+
87+
func init() {
88+
app.Commands = []*cli.Command{
89+
blockCommand,
90+
infoCommand,
91+
verifyCommand,
92+
}
93+
app.Flags = []cli.Flag{
94+
dirFlag,
95+
networkFlag,
96+
eraSizeFlag,
97+
}
98+
}
99+
100+
func main() {
101+
if err := app.Run(os.Args); err != nil {
102+
fmt.Fprintf(os.Stderr, "%v\n", err)
103+
os.Exit(1)
104+
}
105+
}
106+
107+
// block prints the specified block from an era1 store.
108+
func block(ctx *cli.Context) error {
109+
num, err := strconv.ParseUint(ctx.Args().First(), 10, 64)
110+
if err != nil {
111+
return fmt.Errorf("invalid block number: %w", err)
112+
}
113+
e, err := open(ctx, num/uint64(ctx.Int(eraSizeFlag.Name)))
114+
if err != nil {
115+
return fmt.Errorf("error opening era1: %w", err)
116+
}
117+
defer e.Close()
118+
// Read block with number.
119+
block, err := e.GetBlockByNumber(num)
120+
if err != nil {
121+
return fmt.Errorf("error reading block %d: %w", num, err)
122+
}
123+
// Convert block to JSON and print.
124+
val := ethapi.RPCMarshalBlock(block, ctx.Bool(txsFlag.Name), ctx.Bool(txsFlag.Name), params.MainnetChainConfig)
125+
b, err := json.MarshalIndent(val, "", " ")
126+
if err != nil {
127+
return fmt.Errorf("error marshaling json: %w", err)
128+
}
129+
fmt.Println(string(b))
130+
return nil
131+
}
132+
133+
// info prints some high-level information about the era1 file.
134+
func info(ctx *cli.Context) error {
135+
epoch, err := strconv.ParseUint(ctx.Args().First(), 10, 64)
136+
if err != nil {
137+
return fmt.Errorf("invalid epoch number: %w", err)
138+
}
139+
e, err := open(ctx, epoch)
140+
if err != nil {
141+
return err
142+
}
143+
defer e.Close()
144+
acc, err := e.Accumulator()
145+
if err != nil {
146+
return fmt.Errorf("error reading accumulator: %w", err)
147+
}
148+
td, err := e.InitialTD()
149+
if err != nil {
150+
return fmt.Errorf("error reading total difficulty: %w", err)
151+
}
152+
info := struct {
153+
Accumulator common.Hash `json:"accumulator"`
154+
TotalDifficulty *big.Int `json:"totalDifficulty"`
155+
StartBlock uint64 `json:"startBlock"`
156+
Count uint64 `json:"count"`
157+
}{
158+
acc, td, e.Start(), e.Count(),
159+
}
160+
b, _ := json.MarshalIndent(info, "", " ")
161+
fmt.Println(string(b))
162+
return nil
163+
}
164+
165+
// open opens an era1 file at a certain epoch.
166+
func open(ctx *cli.Context, epoch uint64) (*era.Era, error) {
167+
var (
168+
dir = ctx.String(dirFlag.Name)
169+
network = ctx.String(networkFlag.Name)
170+
)
171+
entries, err := era.ReadDir(dir, network)
172+
if err != nil {
173+
return nil, fmt.Errorf("error reading era dir: %w", err)
174+
}
175+
if epoch >= uint64(len(entries)) {
176+
return nil, fmt.Errorf("epoch out-of-bounds: last %d, want %d", len(entries)-1, epoch)
177+
}
178+
return era.Open(path.Join(dir, entries[epoch]))
179+
}
180+
181+
// verify checks each era1 file in a directory to ensure it is well-formed and
182+
// that the accumulator matches the expected value.
183+
func verify(ctx *cli.Context) error {
184+
if ctx.Args().Len() != 1 {
185+
return fmt.Errorf("missing accumulators file")
186+
}
187+
188+
roots, err := readHashes(ctx.Args().First())
189+
if err != nil {
190+
return fmt.Errorf("unable to read expected roots file: %w", err)
191+
}
192+
193+
var (
194+
dir = ctx.String(dirFlag.Name)
195+
network = ctx.String(networkFlag.Name)
196+
start = time.Now()
197+
reported = time.Now()
198+
)
199+
200+
entries, err := era.ReadDir(dir, network)
201+
if err != nil {
202+
return fmt.Errorf("error reading %s: %w", dir, err)
203+
}
204+
205+
if len(entries) != len(roots) {
206+
return fmt.Errorf("number of era1 files should match the number of accumulator hashes")
207+
}
208+
209+
// Verify each epoch matches the expected root.
210+
for i, want := range roots {
211+
// Wrap in function so defers don't stack.
212+
err := func() error {
213+
name := entries[i]
214+
e, err := era.Open(path.Join(dir, name))
215+
if err != nil {
216+
return fmt.Errorf("error opening era1 file %s: %w", name, err)
217+
}
218+
defer e.Close()
219+
// Read accumulator and check against expected.
220+
if got, err := e.Accumulator(); err != nil {
221+
return fmt.Errorf("error retrieving accumulator for %s: %w", name, err)
222+
} else if got != want {
223+
return fmt.Errorf("invalid root %s: got %s, want %s", name, got, want)
224+
}
225+
// Recompute accumulator.
226+
if err := checkAccumulator(e); err != nil {
227+
return fmt.Errorf("error verify era1 file %s: %w", name, err)
228+
}
229+
// Give the user some feedback that something is happening.
230+
if time.Since(reported) >= 8*time.Second {
231+
fmt.Printf("Verifying Era1 files \t\t verified=%d,\t elapsed=%s\n", i, common.PrettyDuration(time.Since(start)))
232+
reported = time.Now()
233+
}
234+
return nil
235+
}()
236+
if err != nil {
237+
return err
238+
}
239+
}
240+
241+
return nil
242+
}
243+
244+
// checkAccumulator verifies the accumulator matches the data in the Era.
245+
func checkAccumulator(e *era.Era) error {
246+
var (
247+
err error
248+
want common.Hash
249+
td *big.Int
250+
tds = make([]*big.Int, 0)
251+
hashes = make([]common.Hash, 0)
252+
)
253+
if want, err = e.Accumulator(); err != nil {
254+
return fmt.Errorf("error reading accumulator: %w", err)
255+
}
256+
if td, err = e.InitialTD(); err != nil {
257+
return fmt.Errorf("error reading total difficulty: %w", err)
258+
}
259+
it, err := era.NewIterator(e)
260+
if err != nil {
261+
return fmt.Errorf("error making era iterator: %w", err)
262+
}
263+
// To fully verify an era the following attributes must be checked:
264+
// 1) the block index is constructed correctly
265+
// 2) the tx root matches the value in the block
266+
// 3) the receipts root matches the value in the block
267+
// 4) the starting total difficulty value is correct
268+
// 5) the accumulator is correct by recomputing it locally, which verifies
269+
// the blocks are all correct (via hash)
270+
//
271+
// The attributes 1), 2), and 3) are checked for each block. 4) and 5) require
272+
// accumulation across the entire set and are verified at the end.
273+
for it.Next() {
274+
// 1) next() walks the block index, so we're able to implicitly verify it.
275+
if it.Error() != nil {
276+
return fmt.Errorf("error reading block %d: %w", it.Number(), err)
277+
}
278+
block, receipts, err := it.BlockAndReceipts()
279+
if it.Error() != nil {
280+
return fmt.Errorf("error reading block %d: %w", it.Number(), err)
281+
}
282+
// 2) recompute tx root and verify against header.
283+
tr := types.DeriveSha(block.Transactions(), trie.NewStackTrie(nil))
284+
if tr != block.TxHash() {
285+
return fmt.Errorf("tx root in block %d mismatch: want %s, got %s", block.NumberU64(), block.TxHash(), tr)
286+
}
287+
// 3) recompute receipt root and check value against block.
288+
rr := types.DeriveSha(receipts, trie.NewStackTrie(nil))
289+
if rr != block.ReceiptHash() {
290+
return fmt.Errorf("receipt root in block %d mismatch: want %s, got %s", block.NumberU64(), block.ReceiptHash(), rr)
291+
}
292+
hashes = append(hashes, block.Hash())
293+
td.Add(td, block.Difficulty())
294+
tds = append(tds, new(big.Int).Set(td))
295+
}
296+
// 4+5) Verify accumulator and total difficulty.
297+
got, err := era.ComputeAccumulator(hashes, tds)
298+
if err != nil {
299+
return fmt.Errorf("error computing accumulator: %w", err)
300+
}
301+
if got != want {
302+
return fmt.Errorf("expected accumulator root does not match calculated: got %s, want %s", got, want)
303+
}
304+
return nil
305+
}
306+
307+
// readHashes reads a file of newline-delimited hashes.
308+
func readHashes(f string) ([]common.Hash, error) {
309+
b, err := os.ReadFile(f)
310+
if err != nil {
311+
return nil, fmt.Errorf("unable to open accumulators file")
312+
}
313+
s := strings.Split(string(b), "\n")
314+
// Remove empty last element, if present.
315+
if s[len(s)-1] == "" {
316+
s = s[:len(s)-1]
317+
}
318+
// Convert to hashes.
319+
r := make([]common.Hash, len(s))
320+
for i := range s {
321+
r[i] = common.HexToHash(s[i])
322+
}
323+
return r, nil
324+
}

0 commit comments

Comments
 (0)