github.com/ethereum/go-ethereum@v1.14.4-0.20240516095835-473ee8fc07a3/cmd/era/main.go (about)

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