package testnet import ( "context" "encoding/hex" "encoding/json" "fmt" "math/big" "os" "strings" "time" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" "github.com/ghodss/yaml" "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v4/beacon-chain/state" "github.com/prysmaticlabs/prysm/v4/cmd/flags" "github.com/prysmaticlabs/prysm/v4/config/params" "github.com/prysmaticlabs/prysm/v4/container/trie" "github.com/prysmaticlabs/prysm/v4/io/file" ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v4/runtime/interop" "github.com/prysmaticlabs/prysm/v4/runtime/version" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) var ( generateGenesisStateFlags = struct { DepositJsonFile string ChainConfigFile string ConfigName string NumValidators uint64 GenesisTime uint64 GenesisTimeDelay uint64 OutputSSZ string OutputJSON string OutputYaml string ForkName string OverrideEth1Data bool ExecutionEndpoint string GethGenesisJsonIn string GethGenesisJsonOut string }{} log = logrus.WithField("prefix", "genesis") outputSSZFlag = &cli.StringFlag{ Name: "output-ssz", Destination: &generateGenesisStateFlags.OutputSSZ, Usage: "Output filename of the SSZ marshaling of the generated genesis state", Value: "", } outputYamlFlag = &cli.StringFlag{ Name: "output-yaml", Destination: &generateGenesisStateFlags.OutputYaml, Usage: "Output filename of the YAML marshaling of the generated genesis state", Value: "", } outputJsonFlag = &cli.StringFlag{ Name: "output-json", Destination: &generateGenesisStateFlags.OutputJSON, Usage: "Output filename of the JSON marshaling of the generated genesis state", Value: "", } generateGenesisStateCmd = &cli.Command{ Name: "generate-genesis", Usage: "Generate a beacon chain genesis state", Action: func(cliCtx *cli.Context) error { if err := cliActionGenerateGenesisState(cliCtx); err != nil { log.WithError(err).Fatal("Could not generate beacon chain genesis state") } return nil }, Flags: []cli.Flag{ &cli.StringFlag{ Name: "chain-config-file", Destination: &generateGenesisStateFlags.ChainConfigFile, Usage: "The path to a YAML file with chain config values", }, &cli.StringFlag{ Name: "deposit-json-file", Destination: &generateGenesisStateFlags.DepositJsonFile, Usage: "Path to deposit_data.json file generated by the staking-deposit-cli tool for optionally specifying validators in genesis state", }, &cli.StringFlag{ Name: "config-name", Usage: "Config kind to be used for generating the genesis state. Default: mainnet. Options include mainnet, interop, minimal, prater, sepolia. --chain-config-file will override this flag.", Destination: &generateGenesisStateFlags.ConfigName, Value: params.MainnetName, }, &cli.Uint64Flag{ Name: "num-validators", Usage: "Number of validators to deterministically generate in the genesis state", Destination: &generateGenesisStateFlags.NumValidators, Required: true, }, &cli.Uint64Flag{ Name: "genesis-time", Destination: &generateGenesisStateFlags.GenesisTime, Usage: "Unix timestamp seconds used as the genesis time in the genesis state. If unset, defaults to now()", }, &cli.Uint64Flag{ Name: "genesis-time-delay", Destination: &generateGenesisStateFlags.GenesisTimeDelay, Usage: "Delay genesis time by N seconds", }, &cli.BoolFlag{ Name: "override-eth1data", Destination: &generateGenesisStateFlags.OverrideEth1Data, Usage: "Overrides Eth1Data with values from execution client. If unset, defaults to false", Value: false, }, &cli.StringFlag{ Name: "geth-genesis-json-in", Destination: &generateGenesisStateFlags.GethGenesisJsonIn, Usage: "Path to a \"genesis.json\" file, containing a json representation of Geth's core.Genesis", }, &cli.StringFlag{ Name: "geth-genesis-json-out", Destination: &generateGenesisStateFlags.GethGenesisJsonOut, Usage: "Path to write generated \"genesis.json\" file, containing a json representation of Geth's core.Genesis", }, &cli.StringFlag{ Name: "execution-endpoint", Destination: &generateGenesisStateFlags.ExecutionEndpoint, Usage: "Endpoint to preferred execution client. If unset, defaults to Geth", Value: "http://localhost:8545", }, flags.EnumValue{ Name: "fork", Usage: fmt.Sprintf("Name of the BeaconState schema to use in output encoding [%s]", strings.Join(versionNames(), ",")), Enum: versionNames(), Value: versionNames()[0], Destination: &generateGenesisStateFlags.ForkName, }.GenericFlag(), outputSSZFlag, outputYamlFlag, outputJsonFlag, }, } ) func versionNames() []string { enum := version.All() names := make([]string, len(enum)) for i := range enum { names[i] = version.String(enum[i]) } return names } // Represents a json object of hex string and uint64 values for // validators on Ethereum. This file can be generated using the official staking-deposit-cli. type depositDataJSON struct { PubKey string `json:"pubkey"` Amount uint64 `json:"amount"` WithdrawalCredentials string `json:"withdrawal_credentials"` DepositDataRoot string `json:"deposit_data_root"` Signature string `json:"signature"` } func cliActionGenerateGenesisState(cliCtx *cli.Context) error { outputJson := generateGenesisStateFlags.OutputJSON outputYaml := generateGenesisStateFlags.OutputYaml outputSSZ := generateGenesisStateFlags.OutputSSZ noOutputFlag := outputSSZ == "" && outputJson == "" && outputYaml == "" if noOutputFlag { return fmt.Errorf( "no %s, %s, %s flag(s) specified. At least one is required", outputJsonFlag.Name, outputYamlFlag.Name, outputSSZFlag.Name, ) } if err := setGlobalParams(); err != nil { return fmt.Errorf("could not set config params: %v", err) } st, err := generateGenesis(cliCtx.Context) if err != nil { return fmt.Errorf("could not generate genesis state: %v", err) } if outputJson != "" { if err := writeToOutputFile(outputJson, st, json.Marshal); err != nil { return err } } if outputYaml != "" { if err := writeToOutputFile(outputYaml, st, yaml.Marshal); err != nil { return err } } if outputSSZ != "" { type MinimumSSZMarshal interface { MarshalSSZ() ([]byte, error) } marshalFn := func(o interface{}) ([]byte, error) { marshaler, ok := o.(MinimumSSZMarshal) if !ok { return nil, errors.New("not a marshaler") } return marshaler.MarshalSSZ() } if err := writeToOutputFile(outputSSZ, st, marshalFn); err != nil { return err } } log.Info("Command completed") return nil } func setGlobalParams() error { chainConfigFile := generateGenesisStateFlags.ChainConfigFile if chainConfigFile != "" { log.Infof("Specified a chain config file: %s", chainConfigFile) return params.LoadChainConfigFile(chainConfigFile, nil) } cfg, err := params.ByName(generateGenesisStateFlags.ConfigName) if err != nil { return fmt.Errorf("unable to find config using name %s: %v", generateGenesisStateFlags.ConfigName, err) } return params.SetActive(cfg.Copy()) } func generateGenesis(ctx context.Context) (state.BeaconState, error) { f := &generateGenesisStateFlags if f.GenesisTime == 0 { f.GenesisTime = uint64(time.Now().Unix()) log.Info("No genesis time specified, defaulting to now()") } log.Infof("Delaying genesis %v by %v seconds", f.GenesisTime, f.GenesisTimeDelay) f.GenesisTime += f.GenesisTimeDelay log.Infof("Genesis is now %v", f.GenesisTime) v, err := version.FromString(f.ForkName) if err != nil { return nil, err } opts := make([]interop.PremineGenesisOpt, 0) nv := f.NumValidators if f.DepositJsonFile != "" { expanded, err := file.ExpandPath(f.DepositJsonFile) if err != nil { return nil, err } log.Printf("reading deposits from JSON at %s", expanded) b, err := os.ReadFile(expanded) // #nosec G304 if err != nil { return nil, err } roots, dds, err := depositEntriesFromJSON(b) if err != nil { return nil, err } opts = append(opts, interop.WithDepositData(dds, roots)) } else if nv == 0 { return nil, fmt.Errorf( "expected --num-validators > 0 or --deposit-json-file to have been provided", ) } gen := &core.Genesis{} if f.GethGenesisJsonIn != "" { gbytes, err := os.ReadFile(f.GethGenesisJsonIn) // #nosec G304 if err != nil { return nil, errors.Wrapf(err, "failed to read %s", f.GethGenesisJsonIn) } if err := json.Unmarshal(gbytes, gen); err != nil { return nil, err } // set timestamps for genesis and shanghai fork gen.Timestamp = f.GenesisTime gen.Config.ShanghaiTime = interop.GethShanghaiTime(f.GenesisTime, params.BeaconConfig()) gen.Config.CancunTime = interop.GethCancunTime(f.GenesisTime, params.BeaconConfig()) fields := logrus.Fields{} if gen.Config.ShanghaiTime != nil { fields["shanghai"] = fmt.Sprintf("%d", *gen.Config.ShanghaiTime) } if gen.Config.CancunTime != nil { fields["cancun"] = fmt.Sprintf("%d", *gen.Config.CancunTime) } log.WithFields(fields).Info("Setting fork geth times") if v > version.Altair { // set ttd to zero so EL goes post-merge immediately gen.Config.TerminalTotalDifficulty = big.NewInt(0) gen.Config.TerminalTotalDifficultyPassed = true } } else { gen = interop.GethTestnetGenesis(f.GenesisTime, params.BeaconConfig()) } if f.GethGenesisJsonOut != "" { gbytes, err := json.MarshalIndent(gen, "", "\t") if err != nil { return nil, err } if err := os.WriteFile(f.GethGenesisJsonOut, gbytes, os.ModePerm); err != nil { return nil, errors.Wrapf(err, "failed to write %s", f.GethGenesisJsonOut) } } gb := gen.ToBlock() // TODO: expose the PregenesisCreds option with a cli flag - for now defaulting to no withdrawal credentials at genesis genesisState, err := interop.NewPreminedGenesis(ctx, f.GenesisTime, nv, 0, v, gb, opts...) if err != nil { return nil, err } if f.OverrideEth1Data { log.Print("Overriding Eth1Data with data from execution client") conn, err := rpc.Dial(generateGenesisStateFlags.ExecutionEndpoint) if err != nil { return nil, errors.Wrapf( err, "could not dial %s please make sure you are running your execution client", generateGenesisStateFlags.ExecutionEndpoint) } client := ethclient.NewClient(conn) header, err := client.HeaderByNumber(ctx, big.NewInt(0)) if err != nil { return nil, errors.Wrap(err, "could not get header by number") } t, err := trie.NewTrie(params.BeaconConfig().DepositContractTreeDepth) if err != nil { return nil, errors.Wrap(err, "could not create deposit tree") } depositRoot, err := t.HashTreeRoot() if err != nil { return nil, errors.Wrap(err, "could not get hash tree root") } e1d := ðpb.Eth1Data{ DepositRoot: depositRoot[:], DepositCount: 0, BlockHash: header.Hash().Bytes(), } if err := genesisState.SetEth1Data(e1d); err != nil { return nil, err } if err := genesisState.SetEth1DepositIndex(0); err != nil { return nil, err } } return genesisState, err } func depositEntriesFromJSON(enc []byte) ([][]byte, []*ethpb.Deposit_Data, error) { var depositJSON []*depositDataJSON if err := json.Unmarshal(enc, &depositJSON); err != nil { return nil, nil, err } dds := make([]*ethpb.Deposit_Data, len(depositJSON)) roots := make([][]byte, len(depositJSON)) for i, val := range depositJSON { root, data, err := depositJSONToDepositData(val) if err != nil { return nil, nil, err } dds[i] = data roots[i] = root } return roots, dds, nil } func depositJSONToDepositData(input *depositDataJSON) ([]byte, *ethpb.Deposit_Data, error) { root, err := hex.DecodeString(strings.TrimPrefix(input.DepositDataRoot, "0x")) if err != nil { return nil, nil, err } pk, err := hex.DecodeString(strings.TrimPrefix(input.PubKey, "0x")) if err != nil { return nil, nil, err } creds, err := hex.DecodeString(strings.TrimPrefix(input.WithdrawalCredentials, "0x")) if err != nil { return nil, nil, err } sig, err := hex.DecodeString(strings.TrimPrefix(input.Signature, "0x")) if err != nil { return nil, nil, err } return root, ðpb.Deposit_Data{ PublicKey: pk, WithdrawalCredentials: creds, Amount: input.Amount, Signature: sig, }, nil } func writeToOutputFile( fPath string, data interface{}, marshalFn func(o interface{}) ([]byte, error), ) error { encoded, err := marshalFn(data) if err != nil { return err } if err := file.WriteFile(fPath, encoded); err != nil { return err } log.Printf("Done writing genesis state to %s", fPath) return nil }