diff --git a/changelog/pmikolajczyk-nit-4242.md b/changelog/pmikolajczyk-nit-4242.md new file mode 100644 index 00000000000..6568ce30fca --- /dev/null +++ b/changelog/pmikolajczyk-nit-4242.md @@ -0,0 +1,2 @@ +### Added + - Nitro can restore parsed init message from genesis, without access to the consensus side \ No newline at end of file diff --git a/cmd/conf/init.go b/cmd/conf/init.go index 50b1a083a95..d547bfcbd7c 100644 --- a/cmd/conf/init.go +++ b/cmd/conf/init.go @@ -3,7 +3,9 @@ package conf import ( + "encoding/json" "fmt" + "math/big" "slices" "strings" "time" @@ -11,40 +13,94 @@ import ( "github.com/spf13/pflag" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" "github.com/offchainlabs/nitro/util" ) +type GenesisOverrideConfig struct { + SerializedChainConfig string `koanf:"serialized-chain-config"` + InitialL1BaseFee string `koanf:"initial-l1-base-fee"` +} + +var GenesisOverrideConfigDefault = GenesisOverrideConfig{ + SerializedChainConfig: "", + InitialL1BaseFee: "", +} + +func GenesisOverrideConfigAddOptions(prefix string, f *pflag.FlagSet) { + f.String(prefix+".serialized-chain-config", GenesisOverrideConfigDefault.SerializedChainConfig, "JSON-serialized chain config, can be used to provide chain config when genesis.json is not available") + f.String(prefix+".initial-l1-base-fee", GenesisOverrideConfigDefault.InitialL1BaseFee, "initial L1 base fee (in wei), must be set together with serialized-chain-config") +} + +// ParseInitialL1BaseFee parses the InitialL1BaseFee string into a *big.Int. +func (c *GenesisOverrideConfig) ParseInitialL1BaseFee() (*big.Int, error) { + if c.InitialL1BaseFee == "" { + return nil, fmt.Errorf("initial-l1-base-fee is not set") + } + fee, ok := new(big.Int).SetString(c.InitialL1BaseFee, 10) + if !ok { + return nil, fmt.Errorf("failed to parse initial-l1-base-fee %q as integer", c.InitialL1BaseFee) + } + if fee.Sign() < 0 { + return nil, fmt.Errorf("initial-l1-base-fee must be non-negative, got %s", c.InitialL1BaseFee) + } + return fee, nil +} + +func (c *GenesisOverrideConfig) Validate() error { + chainConfigSet := c.SerializedChainConfig != "" + baseFeeSet := c.InitialL1BaseFee != "" + if chainConfigSet != baseFeeSet { + return fmt.Errorf("genesis-override.serialized-chain-config and genesis-override.initial-l1-base-fee must both be set or both be empty") + } + if chainConfigSet { + var chainConfig params.ChainConfig + if err := json.Unmarshal([]byte(c.SerializedChainConfig), &chainConfig); err != nil { + return fmt.Errorf("failed to unmarshal serialized-chain-config: %w", err) + } + if _, err := c.ParseInitialL1BaseFee(); err != nil { + return fmt.Errorf("failed to parse initial-l1-base-fee: %w", err) + } + } + return nil +} + +func (c *GenesisOverrideConfig) IsSet() bool { + return c != nil && c.SerializedChainConfig != "" +} + type InitConfig struct { - Force bool `koanf:"force"` - Url string `koanf:"url"` - Latest string `koanf:"latest"` - LatestBase string `koanf:"latest-base"` - ValidateChecksum bool `koanf:"validate-checksum"` - DownloadPath string `koanf:"download-path"` - DownloadPoll time.Duration `koanf:"download-poll"` - DevInit bool `koanf:"dev-init"` - DevInitAddress string `koanf:"dev-init-address"` - DevMaxCodeSize uint64 `koanf:"dev-max-code-size"` - DevInitBlockNum uint64 `koanf:"dev-init-blocknum"` - Empty bool `koanf:"empty"` - ImportWasm bool `koanf:"import-wasm"` - AccountsPerSync uint `koanf:"accounts-per-sync"` - ImportFile string `koanf:"import-file"` - GenesisJsonFile string `koanf:"genesis-json-file"` - GenesisJsonFileDirectory string `koanf:"genesis-json-file-directory"` - ThenQuit bool `koanf:"then-quit"` - Prune string `koanf:"prune"` - PruneParallelStorageTraversal bool `koanf:"prune-parallel-storage-traversal"` - PruneBloomSize uint64 `koanf:"prune-bloom-size"` - PruneThreads int `koanf:"prune-threads"` - PruneTrieCleanCache int `koanf:"prune-trie-clean-cache"` - RecreateMissingStateFrom uint64 `koanf:"recreate-missing-state-from"` - RebuildLocalWasm string `koanf:"rebuild-local-wasm"` - ReorgToBatch int64 `koanf:"reorg-to-batch"` - ReorgToMessageBatch int64 `koanf:"reorg-to-message-batch"` - ReorgToBlockBatch int64 `koanf:"reorg-to-block-batch"` - ValidateGenesisAssertion bool `koanf:"validate-genesis-assertion"` + Force bool `koanf:"force"` + Url string `koanf:"url"` + Latest string `koanf:"latest"` + LatestBase string `koanf:"latest-base"` + ValidateChecksum bool `koanf:"validate-checksum"` + DownloadPath string `koanf:"download-path"` + DownloadPoll time.Duration `koanf:"download-poll"` + DevInit bool `koanf:"dev-init"` + DevInitAddress string `koanf:"dev-init-address"` + DevMaxCodeSize uint64 `koanf:"dev-max-code-size"` + DevInitBlockNum uint64 `koanf:"dev-init-blocknum"` + Empty bool `koanf:"empty"` + ImportWasm bool `koanf:"import-wasm"` + AccountsPerSync uint `koanf:"accounts-per-sync"` + ImportFile string `koanf:"import-file"` + GenesisJsonFile string `koanf:"genesis-json-file"` + GenesisJsonFileDirectory string `koanf:"genesis-json-file-directory"` + GenesisOverride GenesisOverrideConfig `koanf:"genesis-override"` + ThenQuit bool `koanf:"then-quit"` + Prune string `koanf:"prune"` + PruneParallelStorageTraversal bool `koanf:"prune-parallel-storage-traversal"` + PruneBloomSize uint64 `koanf:"prune-bloom-size"` + PruneThreads int `koanf:"prune-threads"` + PruneTrieCleanCache int `koanf:"prune-trie-clean-cache"` + RecreateMissingStateFrom uint64 `koanf:"recreate-missing-state-from"` + RebuildLocalWasm string `koanf:"rebuild-local-wasm"` + ReorgToBatch int64 `koanf:"reorg-to-batch"` + ReorgToMessageBatch int64 `koanf:"reorg-to-message-batch"` + ReorgToBlockBatch int64 `koanf:"reorg-to-block-batch"` + ValidateGenesisAssertion bool `koanf:"validate-genesis-assertion"` } var InitConfigDefault = InitConfig{ @@ -64,6 +120,7 @@ var InitConfigDefault = InitConfig{ ImportFile: "", GenesisJsonFile: "", GenesisJsonFileDirectory: "", + GenesisOverride: GenesisOverrideConfigDefault, AccountsPerSync: 100000, ThenQuit: false, Prune: "", @@ -97,6 +154,7 @@ func InitConfigAddOptions(prefix string, f *pflag.FlagSet) { f.String(prefix+".import-file", InitConfigDefault.ImportFile, "path for json data to import") f.String(prefix+".genesis-json-file", InitConfigDefault.GenesisJsonFile, "path for genesis json file") f.String(prefix+".genesis-json-file-directory", InitConfigDefault.GenesisJsonFileDirectory, "directory path for genesis json files - will search for a file named by the chain ID") + GenesisOverrideConfigAddOptions(prefix+".genesis-override", f) f.Uint(prefix+".accounts-per-sync", InitConfigDefault.AccountsPerSync, "during init - sync database every X accounts. Lower value for low-memory systems. 0 disables.") f.String(prefix+".prune", InitConfigDefault.Prune, "pruning for a given use: \"full\" for full nodes serving RPC requests, or \"validator\" for validators") f.Bool(prefix+".prune-parallel-storage-traversal", InitConfigDefault.PruneParallelStorageTraversal, "if true: use parallel pruning per account") @@ -119,6 +177,9 @@ func (c *InitConfig) Validate() error { if c.Empty && c.GenesisJsonFile != "" { return fmt.Errorf("init config cannot be both empty and have a genesis json file specified") } + if err := c.GenesisOverride.Validate(); err != nil { + return fmt.Errorf("invalid init.genesis-override: %w", err) + } if c.Force && c.RecreateMissingStateFrom > 0 { log.Warn("force init enabled, recreate-missing-state-from will have no effect") } diff --git a/cmd/nitro/init/init.go b/cmd/nitro/init/init.go index c48a8563a75..eff0c7169ab 100644 --- a/cmd/nitro/init/init.go +++ b/cmd/nitro/init/init.go @@ -4,6 +4,7 @@ package nitroinit import ( + "bytes" "context" "crypto/sha256" "encoding/hex" @@ -609,7 +610,7 @@ func rebuildLocalWasm(ctx context.Context, config *gethexec.Config, l2BlockChain return executionDB, l2BlockChain, nil } -func OpenInitializeExecutionDB(ctx context.Context, stack *node.Node, config *config.NodeConfig, chainId *big.Int, cacheConfig *core.BlockChainConfig, tracer *tracing.Hooks, persistentConfig *conf.PersistentConfig, l1Client *ethclient.Client, rollupAddrs chaininfo.RollupAddresses) (ethdb.Database, statetransfer.InitDataReader, *core.BlockChain, error) { +func OpenInitializeExecutionDB(ctx context.Context, stack *node.Node, config *config.NodeConfig, chainId *big.Int, cacheConfig *core.BlockChainConfig, tracer *tracing.Hooks, persistentConfig *conf.PersistentConfig, l1Client *ethclient.Client, rollupAddrs chaininfo.RollupAddresses, consensusParsedInitMessage *arbostypes.ParsedInitMessage) (ethdb.Database, statetransfer.InitDataReader, *core.BlockChain, error) { executionDB, wasmDB, l2BlockChain, chainConfig, err := OpenExistingExecutionDB(stack, config, chainId, cacheConfig, tracer, persistentConfig) if err != nil { return nil, nil, nil, err @@ -628,19 +629,36 @@ func OpenInitializeExecutionDB(ctx context.Context, stack *node.Node, config *co return nil, nil, nil, err } - var genesisArbOSInit *params.ArbOSInit + var genesis *core.Genesis - initDataReader, chainConfig, genesisArbOSInit, err = GetInit(config, executionDB) + initDataReader, chainConfig, genesis, err = GetInit(config, executionDB) if err != nil { return executionDB, nil, nil, err } - parsedInitMessage, err := GetConsensusParsedInitMsg(ctx, config.Node.ParentChainReader.Enable, chainId, l1Client, &rollupAddrs, chainConfig) + executionParsedInitMsg, err := GetExecutionParsedInitMsg(genesis, &config.Init.GenesisOverride, chainConfig) if err != nil { return executionDB, nil, nil, err } - l2BlockChain, err = getNewBlockchain(parsedInitMessage, config, initDataReader, chainConfig, genesisArbOSInit, executionDB, cacheConfig, tracer) + parsedInitMessage := executionParsedInitMsg + if consensusParsedInitMessage != nil { + if err := validateParsedInitMessagesMatch(executionParsedInitMsg, consensusParsedInitMessage); err != nil { + log.Warn("Execution and consensus parsed init messages do not match", "err", err) + } + parsedInitMessage = consensusParsedInitMessage + } + + if err := validateChainConfigCompatibility(parsedInitMessage, chainConfig); err != nil { + return executionDB, nil, nil, err + } + + var arbosInit *params.ArbOSInit + if genesis != nil { + arbosInit = genesis.ArbOSInit + } + + l2BlockChain, err = getNewBlockchain(parsedInitMessage, config, initDataReader, chainConfig, arbosInit, executionDB, cacheConfig, tracer) if err != nil { return executionDB, nil, nil, err } @@ -685,12 +703,12 @@ func pruneExecutionDB(ctx context.Context, executionDB ethdb.Database, stack *no return pruning.PruneExecutionDB(ctx, executionDB, stack, &config.Init, cacheConfig, persistentConfig, l1Client, rollupAddrs, config.Node.ValidatorRequired(), config.Node.MessageExtraction.Enable) } -func GetInit(config *config.NodeConfig, executionDB ethdb.Database) (statetransfer.InitDataReader, *params.ChainConfig, *params.ArbOSInit, error) { +func GetInit(config *config.NodeConfig, executionDB ethdb.Database) (statetransfer.InitDataReader, *params.ChainConfig, *core.Genesis, error) { var ( - initDataReader statetransfer.InitDataReader - chainConfig *params.ChainConfig - genesisArbOSInit *params.ArbOSInit - err error + initDataReader statetransfer.InitDataReader + chainConfig *params.ChainConfig + genesis *core.Genesis + err error ) if config.Init.ImportFile != "" { @@ -742,12 +760,11 @@ func GetInit(config *config.NodeConfig, executionDB ethdb.Database) (statetransf if err != nil { return nil, nil, nil, err } - var gen core.Genesis - if err := json.Unmarshal(genesisJson, &gen); err != nil { + if err := json.Unmarshal(genesisJson, &genesis); err != nil { return nil, nil, nil, err } var accounts []statetransfer.AccountInitializationInfo - for address, account := range gen.Alloc { + for address, account := range genesis.Alloc { accounts = append(accounts, statetransfer.AccountInitializationInfo{ Addr: address, EthBalance: account.Balance, @@ -761,11 +778,10 @@ func GetInit(config *config.NodeConfig, executionDB ethdb.Database) (statetransf initDataReader = statetransfer.NewMemoryInitDataReader(&statetransfer.ArbosInitializationInfo{ Accounts: accounts, }) - chainConfig, err = gen.GetConfig() + chainConfig, err = genesis.GetConfig() if err != nil { return nil, nil, nil, err } - genesisArbOSInit = gen.ArbOSInit } else { if initDataReader == nil { chainConfig = gethexec.TryReadStoredChainConfig(executionDB) @@ -788,7 +804,7 @@ func GetInit(config *config.NodeConfig, executionDB ethdb.Database) (statetransf chainConfig.ArbitrumChainParams.MaxCodeSize = config.Init.DevMaxCodeSize } - return initDataReader, chainConfig, genesisArbOSInit, nil + return initDataReader, chainConfig, genesis, nil } func GetGenesisFileNameFromDirectory(genesisFileDirectory string, chainId uint64) (string, error) { @@ -1033,58 +1049,139 @@ func OpenExistingExecutionDB(stack *node.Node, config *config.NodeConfig, chainI return nil, nil, nil, nil, nil } -func GetConsensusParsedInitMsg(ctx context.Context, parentChainReaderEnabled bool, chainId *big.Int, l1Client *ethclient.Client, rollupAddrs *chaininfo.RollupAddresses, chainConfig *params.ChainConfig) (*arbostypes.ParsedInitMessage, error) { +func GetParsedInitMsgFromParentChain(ctx context.Context, chainId *big.Int, l1Client *ethclient.Client, rollupAddrs *chaininfo.RollupAddresses) (*arbostypes.ParsedInitMessage, error) { var parsedInitMessage *arbostypes.ParsedInitMessage - if parentChainReaderEnabled { - delayedBridge, err := arbnode.NewDelayedBridge(l1Client, rollupAddrs.Bridge, rollupAddrs.DeployedAt) - if err != nil { - return nil, fmt.Errorf("failed creating delayed bridge while attempting to get serialized chain config from init message: %w", err) - } - deployedAt := new(big.Int).SetUint64(rollupAddrs.DeployedAt) - delayedMessages, err := delayedBridge.LookupMessagesInRange(ctx, deployedAt, deployedAt, nil) - if err != nil { - return nil, fmt.Errorf("failed getting delayed messages while attempting to get serialized chain config from init message: %w", err) - } - var initMessage *arbostypes.L1IncomingMessage - for _, msg := range delayedMessages { - if msg.Message.Header.Kind == arbostypes.L1MessageType_Initialize { - initMessage = msg.Message - break - } - } - if initMessage == nil { - return nil, fmt.Errorf("failed to get init message while attempting to get serialized chain config") + delayedBridge, err := arbnode.NewDelayedBridge(l1Client, rollupAddrs.Bridge, rollupAddrs.DeployedAt) + if err != nil { + return nil, fmt.Errorf("failed creating delayed bridge while attempting to get serialized chain config from init message: %w", err) + } + deployedAt := new(big.Int).SetUint64(rollupAddrs.DeployedAt) + delayedMessages, err := delayedBridge.LookupMessagesInRange(ctx, deployedAt, deployedAt, nil) + if err != nil { + return nil, fmt.Errorf("failed getting delayed messages while attempting to get serialized chain config from init message: %w", err) + } + var initMessage *arbostypes.L1IncomingMessage + for _, msg := range delayedMessages { + if msg.Message.Header.Kind == arbostypes.L1MessageType_Initialize { + initMessage = msg.Message + break } - parsedInitMessage, err = initMessage.ParseInitMessage() + } + if initMessage == nil { + return nil, fmt.Errorf("failed to get init message while attempting to get serialized chain config") + } + parsedInitMessage, err = initMessage.ParseInitMessage() + if err != nil { + return nil, err + } + if parsedInitMessage.ChainId.Cmp(chainId) != 0 { + return nil, fmt.Errorf("expected L2 chain ID %v but read L2 chain ID %v from init message in L1 inbox", chainId, parsedInitMessage.ChainId) + } + log.Info("Read serialized chain config from init message on parent chain", "json", string(parsedInitMessage.SerializedChainConfig), "initialL1BaseFee", parsedInitMessage.InitialL1BaseFee) + return parsedInitMessage, nil +} + +func GetExecutionParsedInitMsg(genesis *core.Genesis, genesisOverride *conf.GenesisOverrideConfig, chainConfig *params.ChainConfig) (*arbostypes.ParsedInitMessage, error) { + if genesis != nil { + parsedInitMessage, err := GetParsedInitMsgFromGenesis(genesis) if err != nil { return nil, err } - if parsedInitMessage.ChainId.Cmp(chainId) != 0 { - return nil, fmt.Errorf("expected L2 chain ID %v but read L2 chain ID %v from init message in L1 inbox", chainId, parsedInitMessage.ChainId) - } - if parsedInitMessage.ChainConfig != nil { - if err := parsedInitMessage.ChainConfig.CheckCompatible(chainConfig, chainConfig.ArbitrumChainParams.GenesisBlockNum, 0); err != nil { - return nil, fmt.Errorf("incompatible chain config read from init message in L1 inbox: %w", err) + if genesisOverride.IsSet() { + overrideMsg, err := GetParsedInitMsgFromGenesisOverride(genesisOverride) + if err != nil { + return nil, fmt.Errorf("error parsing genesis override: %w", err) + } + if err := validateParsedInitMessagesMatch(parsedInitMessage, overrideMsg); err != nil { + return nil, fmt.Errorf("genesis and genesis override mismatch: %w", err) } } - log.Info("Read serialized chain config from init message", "json", string(parsedInitMessage.SerializedChainConfig)) - } else { - serializedChainConfig, err := json.Marshal(chainConfig) - if err != nil { - return nil, err - } - parsedInitMessage = &arbostypes.ParsedInitMessage{ - ChainId: chainConfig.ChainID, - InitialL1BaseFee: arbostypes.DefaultInitialL1BaseFee, - ChainConfig: chainConfig, - SerializedChainConfig: serializedChainConfig, + log.Info("Retrieved parsed init message from genesis.json") + return parsedInitMessage, nil + } + if genesisOverride.IsSet() { + log.Info("Retrieved parsed init message from --init.genesis-override CLI flags") + return GetParsedInitMsgFromGenesisOverride(genesisOverride) + } + log.Info("Retrieved parsed init message from chain config") + return GetParsedInitMsgFromChainConfig(chainConfig) +} + +func validateChainConfigCompatibility(parsedInitMessage *arbostypes.ParsedInitMessage, chainConfig *params.ChainConfig) error { + if parsedInitMessage.ChainConfig != nil { + if err := parsedInitMessage.ChainConfig.CheckCompatible(chainConfig, chainConfig.ArbitrumChainParams.GenesisBlockNum, 0); err != nil { + return fmt.Errorf("local chain config is incompatible with the chain config from the init message: %w", err) } - log.Warn("Created fake init message as L1Reader is disabled and serialized chain config from init message is not available", "json", string(serializedChainConfig)) + } + return nil +} + +func GetParsedInitMsgFromGenesis(genesis *core.Genesis) (*arbostypes.ParsedInitMessage, error) { + chainConfig, err := genesis.GetConfig() + if err != nil { + return nil, err + } + + if genesis.ArbOSInit == nil || genesis.ArbOSInit.InitialL1BaseFee == nil { + return nil, fmt.Errorf("genesis is missing arbOSInit.initialL1BaseFee") } + parsedInitMessage := &arbostypes.ParsedInitMessage{ + ChainId: chainConfig.ChainID, + InitialL1BaseFee: genesis.ArbOSInit.InitialL1BaseFee, + ChainConfig: chainConfig, + SerializedChainConfig: []byte(genesis.SerializedChainConfig), + } return parsedInitMessage, nil } +func GetParsedInitMsgFromGenesisOverride(genesisOverride *conf.GenesisOverrideConfig) (*arbostypes.ParsedInitMessage, error) { + var chainConfig params.ChainConfig + if err := json.Unmarshal([]byte(genesisOverride.SerializedChainConfig), &chainConfig); err != nil { + return nil, fmt.Errorf("failed to deserialize chain config from genesis override: %w", err) + } + + fee, err := genesisOverride.ParseInitialL1BaseFee() + if err != nil { + return nil, err + } + + return &arbostypes.ParsedInitMessage{ + ChainId: chainConfig.ChainID, + InitialL1BaseFee: fee, + ChainConfig: &chainConfig, + SerializedChainConfig: []byte(genesisOverride.SerializedChainConfig), + }, nil +} + +func GetParsedInitMsgFromChainConfig(chainConfig *params.ChainConfig) (*arbostypes.ParsedInitMessage, error) { + serializedChainConfig, err := json.Marshal(chainConfig) + if err != nil { + return nil, err + } + + parsedInitMessage := &arbostypes.ParsedInitMessage{ + ChainId: chainConfig.ChainID, + InitialL1BaseFee: arbostypes.DefaultInitialL1BaseFee, + ChainConfig: chainConfig, + SerializedChainConfig: serializedChainConfig, + } + return parsedInitMessage, nil +} + +func validateParsedInitMessagesMatch(a, b *arbostypes.ParsedInitMessage) error { + if a.ChainId.Cmp(b.ChainId) != 0 { + return fmt.Errorf("chain ID mismatch: %v vs %v", a.ChainId, b.ChainId) + } + if a.InitialL1BaseFee.Cmp(b.InitialL1BaseFee) != 0 { + return fmt.Errorf("initial L1 base fee mismatch: %v vs %v", a.InitialL1BaseFee, b.InitialL1BaseFee) + } + if !bytes.Equal(a.SerializedChainConfig, b.SerializedChainConfig) { + return fmt.Errorf("serialized chain config mismatch: %s vs %s", string(a.SerializedChainConfig), string(b.SerializedChainConfig)) + } + return nil +} + func getGenesisAssertionCreationInfo(ctx context.Context, rollupAddress common.Address, l1Client *ethclient.Client, genesisHash common.Hash, sendRoot common.Hash) (*protocol.AssertionCreatedInfo, [32]byte, bool, error) { var assertionHash [32]byte diff --git a/cmd/nitro/init/init_test.go b/cmd/nitro/init/init_test.go index c7abe935fec..7eca11746b6 100644 --- a/cmd/nitro/init/init_test.go +++ b/cmd/nitro/init/init_test.go @@ -36,6 +36,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/offchainlabs/nitro/arbnode" + "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/cmd/conf" "github.com/offchainlabs/nitro/cmd/nitro/config" @@ -449,6 +450,7 @@ func TestOpenInitializeExecutionDBIncompatibleStateScheme(t *testing.T) { &nodeConfig.Persistent, l1Client, chaininfo.RollupAddresses{}, + nil, ) Require(t, err) blockchain.Stop() @@ -466,6 +468,7 @@ func TestOpenInitializeExecutionDBIncompatibleStateScheme(t *testing.T) { &nodeConfig.Persistent, l1Client, chaininfo.RollupAddresses{}, + nil, ) Require(t, err) blockchain.Stop() @@ -484,6 +487,7 @@ func TestOpenInitializeExecutionDBIncompatibleStateScheme(t *testing.T) { &nodeConfig.Persistent, l1Client, chaininfo.RollupAddresses{}, + nil, ) if !strings.Contains(err.Error(), "incompatible state scheme, stored: path, provided: hash") { t.Fatalf("Failed to detect incompatible state scheme") @@ -712,6 +716,7 @@ func TestOpenInitializeExecutionDbEmptyInit(t *testing.T) { &nodeConfig.Persistent, l1Client, chaininfo.RollupAddresses{}, + nil, ) Require(t, err) blockchain.Stop() @@ -1067,6 +1072,7 @@ func getInitHelper(t *testing.T, ownerAdress string, chainID uint64, emptyState &nodeConfig.Persistent, l1Client, chaininfo.RollupAddresses{}, + nil, ) Require(t, err) @@ -1080,7 +1086,13 @@ func getInitHelper(t *testing.T, ownerAdress string, chainID uint64, emptyState // We already call getInit once inside openInitializeExecutionDB but calling a // second time is okay since we're just loading configs - initDataReader, chainConfig, arbOsInit, err := GetInit(&nodeConfig, executionDB) + initDataReader, chainConfig, genesis, err := GetInit(&nodeConfig, executionDB) + + var arbOsInit *params.ArbOSInit + if genesis != nil { + arbOsInit = genesis.ArbOSInit + } + return initDataReader, chainConfig, arbOsInit, executionDB, cleanup, err } @@ -1283,8 +1295,8 @@ func TestGetInitWithGenesis(t *testing.T) { require.Equal(t, expectedChainConfig, chainConfig) - if arbOsInit != nil { - t.Fatalf("arbOsInit expected to be nil") + if arbOsInit == nil { + t.Fatalf("arbOsInit expected to be non-nil") } if initDataReader == nil { @@ -1358,3 +1370,276 @@ func gracefulShutdown(t *testing.T, ctx context.Context, server *http.Server) { t.Logf("HTTP server shutdown error: %v", err) } } + +func testChainConfigJSON(t *testing.T) string { + t.Helper() + chainConfig := chaininfo.ArbitrumDevTestChainConfig() + serialized, err := json.Marshal(chainConfig) + require.NoError(t, err) + return string(serialized) +} + +func TestGetParsedInitMsgFromGenesisOverride(t *testing.T) { + chainConfig := chaininfo.ArbitrumDevTestChainConfig() + serializedConfig := testChainConfigJSON(t) + + t.Run("with valid config", func(t *testing.T) { + override := &conf.GenesisOverrideConfig{ + SerializedChainConfig: serializedConfig, + InitialL1BaseFee: "50000000000", + } + msg, err := GetParsedInitMsgFromGenesisOverride(override) + require.NoError(t, err) + require.Equal(t, chainConfig.ChainID, msg.ChainId) + require.Equal(t, big.NewInt(50_000_000_000), msg.InitialL1BaseFee) + require.Equal(t, serializedConfig, string(msg.SerializedChainConfig)) + require.NotNil(t, msg.ChainConfig) + }) + + t.Run("with custom initial L1 base fee", func(t *testing.T) { + override := &conf.GenesisOverrideConfig{ + SerializedChainConfig: serializedConfig, + InitialL1BaseFee: "100000000000", // 100 GWei + } + msg, err := GetParsedInitMsgFromGenesisOverride(override) + require.NoError(t, err) + require.Equal(t, big.NewInt(100_000_000_000), msg.InitialL1BaseFee) + }) + + t.Run("with large initial L1 base fee", func(t *testing.T) { + largeFee := "999999999999999999999999999999" + override := &conf.GenesisOverrideConfig{ + SerializedChainConfig: serializedConfig, + InitialL1BaseFee: largeFee, + } + msg, err := GetParsedInitMsgFromGenesisOverride(override) + require.NoError(t, err) + expected, _ := new(big.Int).SetString(largeFee, 10) + require.Equal(t, expected, msg.InitialL1BaseFee) + }) + + t.Run("with invalid chain config JSON", func(t *testing.T) { + override := &conf.GenesisOverrideConfig{ + SerializedChainConfig: "not-valid-json", + } + _, err := GetParsedInitMsgFromGenesisOverride(override) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to deserialize chain config from genesis override") + }) + + t.Run("with invalid initial L1 base fee", func(t *testing.T) { + override := &conf.GenesisOverrideConfig{ + SerializedChainConfig: serializedConfig, + InitialL1BaseFee: "not-a-number", + } + _, err := GetParsedInitMsgFromGenesisOverride(override) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to parse initial-l1-base-fee") + }) +} + +func TestGetParsedInitMsgFromGenesis(t *testing.T) { + serializedConfig := testChainConfigJSON(t) + + t.Run("with ArbOSInit initial L1 base fee", func(t *testing.T) { + customFee := big.NewInt(100_000_000_000) + genesis := &core.Genesis{ + SerializedChainConfig: serializedConfig, + ArbOSInit: ¶ms.ArbOSInit{ + InitialL1BaseFee: customFee, + }, + } + msg, err := GetParsedInitMsgFromGenesis(genesis) + require.NoError(t, err) + require.Equal(t, customFee, msg.InitialL1BaseFee) + }) + + t.Run("without ArbOSInit returns error", func(t *testing.T) { + genesis := &core.Genesis{ + SerializedChainConfig: serializedConfig, + } + _, err := GetParsedInitMsgFromGenesis(genesis) + require.Error(t, err) + require.Contains(t, err.Error(), "genesis is missing arbOSInit.initialL1BaseFee") + }) +} + +func TestGetExecutionParsedInitMsg(t *testing.T) { + chainConfig := chaininfo.ArbitrumDevTestChainConfig() + serializedConfig := testChainConfigJSON(t) + customFee := big.NewInt(100_000_000_000) + + genesis := &core.Genesis{ + SerializedChainConfig: serializedConfig, + ArbOSInit: ¶ms.ArbOSInit{ + InitialL1BaseFee: customFee, + }, + } + + matchingOverride := &conf.GenesisOverrideConfig{ + SerializedChainConfig: serializedConfig, + InitialL1BaseFee: "100000000000", + } + + t.Run("genesis only", func(t *testing.T) { + msg, err := GetExecutionParsedInitMsg(genesis, nil, chainConfig) + require.NoError(t, err) + require.Equal(t, chainConfig.ChainID, msg.ChainId) + require.Equal(t, customFee, msg.InitialL1BaseFee) + }) + + t.Run("genesis with matching override", func(t *testing.T) { + msg, err := GetExecutionParsedInitMsg(genesis, matchingOverride, chainConfig) + require.NoError(t, err) + require.Equal(t, chainConfig.ChainID, msg.ChainId) + require.Equal(t, customFee, msg.InitialL1BaseFee) + }) + + t.Run("genesis with mismatching override returns error", func(t *testing.T) { + mismatchOverride := &conf.GenesisOverrideConfig{ + SerializedChainConfig: serializedConfig, + InitialL1BaseFee: "999", + } + _, err := GetExecutionParsedInitMsg(genesis, mismatchOverride, chainConfig) + require.Error(t, err) + require.Contains(t, err.Error(), "genesis and genesis override mismatch: initial L1 base fee mismatch") + }) + + t.Run("override only", func(t *testing.T) { + msg, err := GetExecutionParsedInitMsg(nil, matchingOverride, chainConfig) + require.NoError(t, err) + require.Equal(t, chainConfig.ChainID, msg.ChainId) + require.Equal(t, customFee, msg.InitialL1BaseFee) + }) + + t.Run("chain config fallback", func(t *testing.T) { + msg, err := GetExecutionParsedInitMsg(nil, nil, chainConfig) + require.NoError(t, err) + require.Equal(t, chainConfig.ChainID, msg.ChainId) + require.Equal(t, arbostypes.DefaultInitialL1BaseFee, msg.InitialL1BaseFee) + }) +} + +func TestValidateParsedInitMessagesMatch(t *testing.T) { + chainConfig := chaininfo.ArbitrumDevTestChainConfig() + serializedConfig, err := json.Marshal(chainConfig) + require.NoError(t, err) + + baseMsg := &arbostypes.ParsedInitMessage{ + ChainId: chainConfig.ChainID, + InitialL1BaseFee: arbostypes.DefaultInitialL1BaseFee, + ChainConfig: chainConfig, + SerializedChainConfig: serializedConfig, + } + + t.Run("matching messages", func(t *testing.T) { + other := &arbostypes.ParsedInitMessage{ + ChainId: chainConfig.ChainID, + InitialL1BaseFee: arbostypes.DefaultInitialL1BaseFee, + ChainConfig: chainConfig, + SerializedChainConfig: serializedConfig, + } + require.NoError(t, validateParsedInitMessagesMatch(baseMsg, other)) + }) + + t.Run("chain ID mismatch", func(t *testing.T) { + other := &arbostypes.ParsedInitMessage{ + ChainId: big.NewInt(999999), + InitialL1BaseFee: arbostypes.DefaultInitialL1BaseFee, + ChainConfig: chainConfig, + SerializedChainConfig: serializedConfig, + } + err := validateParsedInitMessagesMatch(baseMsg, other) + require.Error(t, err) + require.Contains(t, err.Error(), "chain ID mismatch") + }) + + t.Run("initial L1 base fee mismatch", func(t *testing.T) { + other := &arbostypes.ParsedInitMessage{ + ChainId: chainConfig.ChainID, + InitialL1BaseFee: big.NewInt(123), + ChainConfig: chainConfig, + SerializedChainConfig: serializedConfig, + } + err := validateParsedInitMessagesMatch(baseMsg, other) + require.Error(t, err) + require.Contains(t, err.Error(), "initial L1 base fee mismatch") + }) + + t.Run("serialized chain config mismatch", func(t *testing.T) { + other := &arbostypes.ParsedInitMessage{ + ChainId: chainConfig.ChainID, + InitialL1BaseFee: arbostypes.DefaultInitialL1BaseFee, + ChainConfig: chainConfig, + SerializedChainConfig: []byte(`{"chainId":999}`), + } + err := validateParsedInitMessagesMatch(baseMsg, other) + require.Error(t, err) + require.Contains(t, err.Error(), "serialized chain config mismatch") + }) +} + +func TestGenesisOverrideConfig(t *testing.T) { + t.Run("IsSet returns false for defaults", func(t *testing.T) { + c := conf.GenesisOverrideConfigDefault + require.False(t, c.IsSet()) + }) + + t.Run("IsSet returns true when serialized chain config is set", func(t *testing.T) { + c := conf.GenesisOverrideConfig{ + SerializedChainConfig: `{"chainId": 1}`, + } + require.True(t, c.IsSet()) + }) + + t.Run("IsSet returns false when only initial L1 base fee is set", func(t *testing.T) { + c := conf.GenesisOverrideConfig{ + InitialL1BaseFee: "0", + } + require.False(t, c.IsSet()) + }) + + t.Run("ParseInitialL1BaseFee errors when not set", func(t *testing.T) { + c := conf.GenesisOverrideConfig{} + _, err := c.ParseInitialL1BaseFee() + require.Error(t, err) + require.Contains(t, err.Error(), "initial-l1-base-fee is not set") + }) + + t.Run("ParseInitialL1BaseFee returns value when set", func(t *testing.T) { + c := conf.GenesisOverrideConfig{InitialL1BaseFee: "50000000000"} + fee, err := c.ParseInitialL1BaseFee() + require.NoError(t, err) + require.Equal(t, big.NewInt(50_000_000_000), fee) + }) + + t.Run("ParseInitialL1BaseFee returns zero when explicitly set to zero", func(t *testing.T) { + c := conf.GenesisOverrideConfig{InitialL1BaseFee: "0"} + fee, err := c.ParseInitialL1BaseFee() + require.NoError(t, err) + require.Equal(t, big.NewInt(0), fee) + }) + + t.Run("ParseInitialL1BaseFee supports large values", func(t *testing.T) { + largeFee := "999999999999999999999999999999" + c := conf.GenesisOverrideConfig{InitialL1BaseFee: largeFee} + fee, err := c.ParseInitialL1BaseFee() + require.NoError(t, err) + expected, _ := new(big.Int).SetString(largeFee, 10) + require.Equal(t, expected, fee) + }) + + t.Run("ParseInitialL1BaseFee errors on invalid string", func(t *testing.T) { + c := conf.GenesisOverrideConfig{InitialL1BaseFee: "not-a-number"} + _, err := c.ParseInitialL1BaseFee() + require.Error(t, err) + require.Contains(t, err.Error(), "failed to parse initial-l1-base-fee") + }) + + t.Run("ParseInitialL1BaseFee errors on negative value", func(t *testing.T) { + c := conf.GenesisOverrideConfig{InitialL1BaseFee: "-1"} + _, err := c.ParseInitialL1BaseFee() + require.Error(t, err) + require.Contains(t, err.Error(), "must be non-negative") + }) +} diff --git a/cmd/nitro/init/testdata/testGenesis.json b/cmd/nitro/init/testdata/testGenesis.json index 9e229efa66c..a3cb1a3329d 100644 --- a/cmd/nitro/init/testdata/testGenesis.json +++ b/cmd/nitro/init/testdata/testGenesis.json @@ -1,4 +1,7 @@ { + "arbOSInit": { + "initialL1BaseFee": 50000000000 + }, "serializedChainConfig": "{\"chainId\": 3503995874084926,\"homesteadBlock\": 0,\"eip150Block\": 0,\"eip155Block\": 0,\"eip158Block\": 0,\"byzantiumBlock\": 0,\"constantinopleBlock\": 0,\"petersburgBlock\": 0,\"istanbulBlock\": 0,\"muirGlacierBlock\": 0,\"berlinBlock\": 0,\"londonBlock\": 0,\"arrowGlacierBlock\": 0,\"grayGlacierBlock\": 0,\"mergeNetsplitBlock\": 0,\"shanghaiTime\": 0,\"cancunTime\": 60,\"pragueTime\": 120,\"terminalTotalDifficulty\": 131072,\"depositContractAddress\": \"0x0000000000000000000000000000000000000000\",\"ethash\": {},\"arbitrum\": {\"InitialArbOSVersion\": 6},\"blobSchedule\": {\"cancun\": { \"target\": 3, \"max\": 6, \"baseFeeUpdateFraction\": 3338477 },\"prague\": { \"target\": 6, \"max\": 9, \"baseFeeUpdateFraction\": 5007716 }}}", "nonce": "0x0", "timestamp": "0x0", diff --git a/cmd/nitro/nitro.go b/cmd/nitro/nitro.go index 69311aeb929..ad0a67c4aa8 100644 --- a/cmd/nitro/nitro.go +++ b/cmd/nitro/nitro.go @@ -41,6 +41,7 @@ import ( "github.com/offchainlabs/nitro/arbnode" nitroversionalerter "github.com/offchainlabs/nitro/arbnode/nitro-version-alerter" "github.com/offchainlabs/nitro/arbnode/resourcemanager" + "github.com/offchainlabs/nitro/arbos/arbostypes" blocksreexecutor "github.com/offchainlabs/nitro/blocks_reexecutor" "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/cmd/conf" @@ -427,7 +428,17 @@ func mainImpl() int { return 1 } - executionDB, initDataReader, l2BlockChain, err := nitroinit.OpenInitializeExecutionDB(ctx, stack, nodeConfig, new(big.Int).SetUint64(nodeConfig.Chain.ID), gethexec.DefaultCacheConfigFor(&nodeConfig.Execution.Caching), tracer, &nodeConfig.Persistent, l1Client, rollupAddrs) + chainId := new(big.Int).SetUint64(nodeConfig.Chain.ID) + var consensusParsedInitMsg *arbostypes.ParsedInitMessage + if nodeConfig.Node.ParentChainReader.Enable { + consensusParsedInitMsg, err = nitroinit.GetParsedInitMsgFromParentChain(ctx, chainId, l1Client, &rollupAddrs) + if err != nil { + log.Error("error getting parsed init message from parent chain", "err", err) + return 1 + } + } + + executionDB, initDataReader, l2BlockChain, err := nitroinit.OpenInitializeExecutionDB(ctx, stack, nodeConfig, chainId, gethexec.DefaultCacheConfigFor(&nodeConfig.Execution.Caching), tracer, &nodeConfig.Persistent, l1Client, rollupAddrs, consensusParsedInitMsg) if l2BlockChain != nil { deferFuncs = append(deferFuncs, func() { l2BlockChain.Stop() }) } diff --git a/system_tests/bold_challenge_protocol_test.go b/system_tests/bold_challenge_protocol_test.go index 43f06da7ce5..63ea875ce25 100644 --- a/system_tests/bold_challenge_protocol_test.go +++ b/system_tests/bold_challenge_protocol_test.go @@ -594,7 +594,7 @@ func create2ndNodeWithConfigForBoldProtocol( txOpts := l1info.GetDefaultTransactOpts("Sequencer", ctx) initReader := statetransfer.NewMemoryInitDataReader(l2InitData) - initMessage, err := nitroinit.GetConsensusParsedInitMsg(ctx, true, chainConfig.ChainID, l1client, first.DeployInfo, chainConfig) + initMessage, err := nitroinit.GetParsedInitMsgFromParentChain(ctx, chainConfig.ChainID, l1client, first.DeployInfo) Require(t, err) execConfig := ExecConfigDefaultNonSequencerTest(t, rawdb.HashScheme) diff --git a/system_tests/bold_customda_challenge_test.go b/system_tests/bold_customda_challenge_test.go index fee4e710e62..1961cb211e2 100644 --- a/system_tests/bold_customda_challenge_test.go +++ b/system_tests/bold_customda_challenge_test.go @@ -203,7 +203,7 @@ func createNodeBWithSharedContracts( txOpts := l1info.GetDefaultTransactOpts("Sequencer", ctx) initReader := statetransfer.NewMemoryInitDataReader(l2InitData) - initMessage, err := nitroinit.GetConsensusParsedInitMsg(ctx, true, chainConfig.ChainID, l1client, first.DeployInfo, chainConfig) + initMessage, err := nitroinit.GetParsedInitMsgFromParentChain(ctx, chainConfig.ChainID, l1client, first.DeployInfo) Require(t, err) execConfig := ExecConfigDefaultNonSequencerTest(t, rawdb.HashScheme) diff --git a/system_tests/common_test.go b/system_tests/common_test.go index f9012f36ed1..2d9fc1fbb9f 100644 --- a/system_tests/common_test.go +++ b/system_tests/common_test.go @@ -1991,7 +1991,7 @@ func deployOnParentChain( parentChainInfo.SetContract("SequencerInbox", addresses.SequencerInbox) parentChainInfo.SetContract("Inbox", addresses.Inbox) parentChainInfo.SetContract("UpgradeExecutor", addresses.UpgradeExecutor) - initMessage, err := nitroinit.GetConsensusParsedInitMsg(ctx, true, chainConfig.ChainID, parentChainClient, addresses, chainConfig) + initMessage, err := nitroinit.GetParsedInitMsgFromParentChain(ctx, chainConfig.ChainID, parentChainClient, addresses) Require(t, err) return addresses, initMessage diff --git a/system_tests/genesis_assertion_test.go b/system_tests/genesis_assertion_test.go index 75ee65e5472..81d29e3729a 100644 --- a/system_tests/genesis_assertion_test.go +++ b/system_tests/genesis_assertion_test.go @@ -289,7 +289,7 @@ func createL2NodeWithRollupAddresses( Require(t, execConfig.Validate()) stackConfig := testhelpers.CreateStackConfigForTest("") stackConfig.DBEngine = rawdb.DBPebble - initMessage, err := nitroinit.GetConsensusParsedInitMsg(ctx, true, l2infoIn.Signer.ChainID(), l1client, addresses, chainConfig) + initMessage, err := nitroinit.GetParsedInitMsgFromParentChain(ctx, l2infoIn.Signer.ChainID(), l1client, addresses) Require(t, err) var l2executionDB ethdb.Database diff --git a/system_tests/nitro_init_test.go b/system_tests/nitro_init_test.go index 3f2ee6a5c9e..86bc6a57b8c 100644 --- a/system_tests/nitro_init_test.go +++ b/system_tests/nitro_init_test.go @@ -4,7 +4,6 @@ package arbtest import ( "context" - "encoding/json" "math/big" "reflect" "testing" @@ -14,41 +13,12 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/node" - "github.com/offchainlabs/nitro/arbos/arbostypes" - "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/cmd/nitro/config" "github.com/offchainlabs/nitro/cmd/nitro/init" "github.com/offchainlabs/nitro/execution/gethexec" ) -func TestGetConsensusParsedInitMsgNoParentChain(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - builder := NewNodeBuilder(ctx).DefaultConfig(t, false) - cleanup := builder.Build(t) - defer cleanup() - - initMessage, err := nitroinit.GetConsensusParsedInitMsg(ctx, false, builder.chainConfig.ChainID, nil, &chaininfo.RollupAddresses{}, builder.chainConfig) - Require(t, err) - - serializedChainConfig, err := json.Marshal(builder.chainConfig) - Require(t, err) - - // We create an initMessage since builder doesn't create an initMessage without an L1 - expectedInitMessage := &arbostypes.ParsedInitMessage{ - ChainId: builder.chainConfig.ChainID, - InitialL1BaseFee: arbostypes.DefaultInitialL1BaseFee, - ChainConfig: builder.chainConfig, - SerializedChainConfig: serializedChainConfig, - } - - if success := reflect.DeepEqual(initMessage, expectedInitMessage); !success { - t.Fatalf("diff found in initMessage %v and builder.initMessage: %v", initMessage, builder.initMessage) - } -} - -func TestGetConsensusParsedInitMsgWithParentChain(t *testing.T) { +func TestGetParsedInitMsgFromParentChain(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -57,7 +27,7 @@ func TestGetConsensusParsedInitMsgWithParentChain(t *testing.T) { cleanup := builder.Build(t) defer cleanup() - initMessage, err := nitroinit.GetConsensusParsedInitMsg(ctx, true, builder.chainConfig.ChainID, builder.L1.Client, builder.addresses, builder.chainConfig) + initMessage, err := nitroinit.GetParsedInitMsgFromParentChain(ctx, builder.chainConfig.ChainID, builder.L1.Client, builder.addresses) Require(t, err) if success := reflect.DeepEqual(initMessage, builder.initMessage); !success {