diff --git a/node/node.go b/node/node.go index 4e5238d1dad..5f30f95d4f7 100644 --- a/node/node.go +++ b/node/node.go @@ -20,6 +20,7 @@ import ( cfg "github.com/cometbft/cometbft/config" cs "github.com/cometbft/cometbft/consensus" "github.com/cometbft/cometbft/crypto" + "github.com/cometbft/cometbft/crypto/tmhash" "github.com/cometbft/cometbft/evidence" cmtjson "github.com/cometbft/cometbft/libs/json" @@ -1372,6 +1373,7 @@ func makeNodeInfo( //------------------------------------------------------------------------------ var genesisDocKey = []byte("genesisDoc") +var genesisDocHashKey = []byte("genesisDocHash") // LoadStateFromDBOrGenesisDocProvider attempts to load the state from the // database, or creates one using the given genesisDocProvider. On success this also @@ -1380,57 +1382,44 @@ func LoadStateFromDBOrGenesisDocProvider( stateDB dbm.DB, genesisDocProvider GenesisDocProvider, ) (sm.State, *types.GenesisDoc, error) { - // Get genesis doc - genDoc, err := loadGenesisDoc(stateDB) + // Get genesis doc hash + genDocHash, err := stateDB.Get(genesisDocHashKey) if err != nil { - genDoc, err = genesisDocProvider() - if err != nil { - return sm.State{}, nil, err - } - // save genesis doc to prevent a certain class of user errors (e.g. when it - // was changed, accidentally or not). Also good for audit trail. - if err := saveGenesisDoc(stateDB, genDoc); err != nil { - return sm.State{}, nil, err - } + return sm.State{}, nil, fmt.Errorf("error retrieving genesis doc hash: %w", err) } - stateStore := sm.NewStore(stateDB, sm.StoreOptions{ - DiscardABCIResponses: false, - }) - state, err := stateStore.LoadFromDBOrGenesisDoc(genDoc) + genDoc, err := genesisDocProvider() if err != nil { return sm.State{}, nil, err } - return state, genDoc, nil -} -// panics if failed to unmarshal bytes -func loadGenesisDoc(db dbm.DB) (*types.GenesisDoc, error) { - b, err := db.Get(genesisDocKey) - if err != nil { - panic(err) - } - if len(b) == 0 { - return nil, errors.New("genesis doc not found") + if err := genDoc.ValidateAndComplete(); err != nil { + return sm.State{}, nil, fmt.Errorf("error in genesis doc: %w", err) } - var genDoc *types.GenesisDoc - err = cmtjson.Unmarshal(b, &genDoc) + + genDocBytes, err := cmtjson.Marshal(genDoc) if err != nil { - panic(fmt.Sprintf("Failed to load genesis doc due to unmarshaling error: %v (bytes: %X)", err, b)) + return sm.State{}, nil, fmt.Errorf("failed to save genesis doc hash due to marshaling error: %w", err) } - return genDoc, nil -} -// panics if failed to marshal the given genesis document -func saveGenesisDoc(db dbm.DB, genDoc *types.GenesisDoc) error { - b, err := cmtjson.Marshal(genDoc) - if err != nil { - return fmt.Errorf("failed to save genesis doc due to marshaling error: %w", err) + incomingGenDocHash := tmhash.Sum(genDocBytes) + if len(genDocHash) == 0 { + // Save the genDoc hash in the store if it doesn't already exist for future verification + if err := stateDB.SetSync(genesisDocHashKey, incomingGenDocHash); err != nil { + return sm.State{}, nil, fmt.Errorf("failed to save genesis doc hash to db: %w", err) + } + } else { + if !bytes.Equal(genDocHash, incomingGenDocHash) { + return sm.State{}, nil, fmt.Errorf("genesis doc hash in db does not match loaded genesis doc") + } } - if err := db.SetSync(genesisDocKey, b); err != nil { - return err + stateStore := sm.NewStore(stateDB, sm.StoreOptions{ + DiscardABCIResponses: false, + }) + state, err := stateStore.LoadFromDBOrGenesisDoc(genDoc) + if err != nil { + return sm.State{}, nil, err } - - return nil + return state, genDoc, nil } func createAndStartPrivValidatorSocketClient( diff --git a/node/node_test.go b/node/node_test.go index ea22a157fef..3fbe91c80ea 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -17,8 +17,11 @@ import ( "github.com/cometbft/cometbft/abci/example/kvstore" cfg "github.com/cometbft/cometbft/config" "github.com/cometbft/cometbft/crypto/ed25519" + "github.com/cometbft/cometbft/crypto/tmhash" "github.com/cometbft/cometbft/evidence" + cmtjson "github.com/cometbft/cometbft/libs/json" "github.com/cometbft/cometbft/libs/log" + cmtos "github.com/cometbft/cometbft/libs/os" cmtrand "github.com/cometbft/cometbft/libs/rand" mempl "github.com/cometbft/cometbft/mempool" mempoolv0 "github.com/cometbft/cometbft/mempool/v0" @@ -452,6 +455,71 @@ func TestNodeNewNodeCustomReactors(t *testing.T) { assert.Contains(t, channels, cr.Channels[0].ID) } +func TestNodeNewNodeGenesisHashMismatch(t *testing.T) { + config := cfg.ResetTestRoot("node_new_node_genesis_hash") + defer os.RemoveAll(config.RootDir) + + // Use goleveldb so we can reuse the same db for the second NewNode() + config.DBBackend = string(dbm.GoLevelDBBackend) + + nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) + require.NoError(t, err) + + n, err := NewNode( + config, + privval.LoadOrGenFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()), + nodeKey, + proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()), + DefaultGenesisDocProviderFunc(config), + DefaultDBProvider, + DefaultMetricsProvider(config.Instrumentation), + log.TestingLogger(), + ) + require.NoError(t, err) + + // Start and stop to close the db for later reading + err = n.Start() + require.NoError(t, err) + + err = n.Stop() + require.NoError(t, err) + + // Ensure the genesis doc hash is saved to db + stateDB, err := DefaultDBProvider(&DBContext{ID: "state", Config: config}) + require.NoError(t, err) + + genDocHash, err := stateDB.Get(genesisDocHashKey) + require.NoError(t, err) + require.NotNil(t, genDocHash, "genesis doc hash should be saved in db") + require.Len(t, genDocHash, tmhash.Size) + + err = stateDB.Close() + require.NoError(t, err) + + // Modify the genesis file chain ID to get a different hash + genBytes := cmtos.MustReadFile(config.GenesisFile()) + var genesisDoc types.GenesisDoc + err = cmtjson.Unmarshal(genBytes, &genesisDoc) + require.NoError(t, err) + + genesisDoc.ChainID = "different-chain-id" + err = genesisDoc.SaveAs(config.GenesisFile()) + require.NoError(t, err) + + _, err = NewNode( + config, + privval.LoadOrGenFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()), + nodeKey, + proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()), + DefaultGenesisDocProviderFunc(config), + DefaultDBProvider, + DefaultMetricsProvider(config.Instrumentation), + log.TestingLogger(), + ) + require.Error(t, err, "NewNode should error when genesisDoc is changed") + require.Equal(t, "genesis doc hash in db does not match loaded genesis doc", err.Error()) +} + func state(nVals int, height int64) (sm.State, dbm.DB, []types.PrivValidator) { privVals := make([]types.PrivValidator, nVals) vals := make([]types.GenesisValidator, nVals)