From f1e2f03f1fe787027903d2780bbf3196a49e9d6d Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Tue, 22 Feb 2022 19:45:50 +0800 Subject: [PATCH 1/2] core: store genesis allocation and recommit them if necessary --- core/blockchain.go | 10 +++++ core/genesis.go | 69 ++++++++++++++++++++++++++------ core/genesis_test.go | 30 ++++++++++++++ core/rawdb/accessors_metadata.go | 13 ++++++ core/rawdb/database.go | 2 + core/rawdb/schema.go | 10 ++++- 6 files changed, 119 insertions(+), 15 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index fa7e39fb0189f..4db9f4c18870d 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -542,6 +542,16 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, root common.Hash, repair bo } } if beyondRoot || newHeadBlock.NumberU64() == 0 { + // Recommit the genesis state into disk in case the rewinding destination + // is genesis. In the future this rewinding destination can be the earliest + // block stored in the chain if the historical chain pruning is enabled. + // In that case the logic needs to be improved here. + if newHeadBlock.NumberU64() == 0 { + if err := CommitGenesisState(bc.db, bc.genesisBlock.Hash()); err != nil { + log.Crit("Failed to commit genesis state", "err", err) + } + log.Debug("Recommitted genesis state to disk") + } log.Debug("Rewound to block with state", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash()) break } diff --git a/core/genesis.go b/core/genesis.go index 1d17f298a4fb3..a18e46cc9bd65 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -80,6 +80,58 @@ func (ga *GenesisAlloc) UnmarshalJSON(data []byte) error { return nil } +// flush adds allocated genesis accounts into a fresh new statedb and +// commit the state changes into the given database handler. +func (ga *GenesisAlloc) flush(db ethdb.Database) (common.Hash, error) { + statedb, err := state.New(common.Hash{}, state.NewDatabase(db), nil) + if err != nil { + return common.Hash{}, err + } + for addr, account := range *ga { + statedb.AddBalance(addr, account.Balance) + statedb.SetCode(addr, account.Code) + statedb.SetNonce(addr, account.Nonce) + for key, value := range account.Storage { + statedb.SetState(addr, key, value) + } + } + root, err := statedb.Commit(false) + if err != nil { + return common.Hash{}, err + } + err = statedb.Database().TrieDB().Commit(root, true, nil) + if err != nil { + return common.Hash{}, err + } + return root, nil +} + +// write writes the json marshaled genesis state into database +// with the given block hash as the unique identifier. +func (ga *GenesisAlloc) write(db ethdb.KeyValueWriter, hash common.Hash) error { + blob, err := json.Marshal(ga) + if err != nil { + return err + } + rawdb.WriteGenesisState(db, hash, blob) + return nil +} + +// CommitGenesisState loads the stored genesis state with the given block +// hash and commits them into the given database handler. +func CommitGenesisState(db ethdb.Database, hash common.Hash) error { + blob := rawdb.ReadGenesisState(db, hash) + if len(blob) == 0 { + return errors.New("not found") + } + var alloc GenesisAlloc + if err := alloc.UnmarshalJSON(blob); err != nil { + return err + } + _, err := alloc.flush(db) + return err +} + // GenesisAccount is an account in the state of the genesis block. type GenesisAccount struct { Code []byte `json:"code,omitempty"` @@ -264,19 +316,10 @@ func (g *Genesis) ToBlock(db ethdb.Database) *types.Block { if db == nil { db = rawdb.NewMemoryDatabase() } - statedb, err := state.New(common.Hash{}, state.NewDatabase(db), nil) + root, err := g.Alloc.flush(db) if err != nil { panic(err) } - for addr, account := range g.Alloc { - statedb.AddBalance(addr, account.Balance) - statedb.SetCode(addr, account.Code) - statedb.SetNonce(addr, account.Nonce) - for key, value := range account.Storage { - statedb.SetState(addr, key, value) - } - } - root := statedb.IntermediateRoot(false) head := &types.Header{ Number: new(big.Int).SetUint64(g.Number), Nonce: types.EncodeNonce(g.Nonce), @@ -304,9 +347,6 @@ func (g *Genesis) ToBlock(db ethdb.Database) *types.Block { head.BaseFee = new(big.Int).SetUint64(params.InitialBaseFee) } } - statedb.Commit(false) - statedb.Database().TrieDB().Commit(root, true, nil) - return types.NewBlock(head, nil, nil, nil, trie.NewStackTrie(nil)) } @@ -327,6 +367,9 @@ func (g *Genesis) Commit(db ethdb.Database) (*types.Block, error) { if config.Clique != nil && len(block.Extra()) == 0 { return nil, errors.New("can't start clique chain without signers") } + if err := g.Alloc.write(db, block.Hash()); err != nil { + return nil, err + } rawdb.WriteTd(db, block.Hash(), block.NumberU64(), block.Difficulty()) rawdb.WriteBlock(db, block) rawdb.WriteReceipts(db, block.Hash(), block.NumberU64(), nil) diff --git a/core/genesis_test.go b/core/genesis_test.go index f3d6b23e5fe03..e8010e3d4ebd4 100644 --- a/core/genesis_test.go +++ b/core/genesis_test.go @@ -213,3 +213,33 @@ func TestGenesis_Commit(t *testing.T) { t.Errorf("inequal difficulty; stored: %v, genesisBlock: %v", stored, genesisBlock.Difficulty()) } } + +func TestReadWriteGenesisAlloc(t *testing.T) { + var ( + db = rawdb.NewMemoryDatabase() + alloc = &GenesisAlloc{ + {1}: {Balance: big.NewInt(1), Storage: map[common.Hash]common.Hash{{1}: {1}}}, + {2}: {Balance: big.NewInt(2), Storage: map[common.Hash]common.Hash{{2}: {2}}}, + } + hash = common.HexToHash("0xdeadbeef") + ) + alloc.write(db, hash) + + var reload GenesisAlloc + err := reload.UnmarshalJSON(rawdb.ReadGenesisState(db, hash)) + if err != nil { + t.Fatalf("Failed to load genesis state %v", err) + } + if len(reload) != len(*alloc) { + t.Fatal("Unexpected genesis allocation") + } + for addr, account := range reload { + want, ok := (*alloc)[addr] + if !ok { + t.Fatal("Account is not found") + } + if !reflect.DeepEqual(want, account) { + t.Fatal("Unexpected account") + } + } +} diff --git a/core/rawdb/accessors_metadata.go b/core/rawdb/accessors_metadata.go index 3b0fcf0f2d1f8..f5a161adb6889 100644 --- a/core/rawdb/accessors_metadata.go +++ b/core/rawdb/accessors_metadata.go @@ -81,6 +81,19 @@ func WriteChainConfig(db ethdb.KeyValueWriter, hash common.Hash, cfg *params.Cha } } +// ReadGenesisState retrieves the genesis state based on the given genesis hash. +func ReadGenesisState(db ethdb.KeyValueReader, hash common.Hash) []byte { + data, _ := db.Get(genesisKey(hash)) + return data +} + +// WriteGenesisState writes the genesis state into the disk. +func WriteGenesisState(db ethdb.KeyValueWriter, hash common.Hash, data []byte) { + if err := db.Put(genesisKey(hash), data); err != nil { + log.Crit("Failed to store genesis state", "err", err) + } +} + // crashList is a list of unclean-shutdown-markers, for rlp-encoding to the // database type crashList struct { diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 5ef64d26a2057..89b6087383cc3 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -375,6 +375,8 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { preimages.Add(size) case bytes.HasPrefix(key, configPrefix) && len(key) == (len(configPrefix)+common.HashLength): metadata.Add(size) + case bytes.HasPrefix(key, genesisPrefix) && len(key) == (len(genesisPrefix)+common.HashLength): + metadata.Add(size) case bytes.HasPrefix(key, bloomBitsPrefix) && len(key) == (len(bloomBitsPrefix)+10+common.HashLength): bloomBits.Add(size) case bytes.HasPrefix(key, BloomBitsIndexPrefix): diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index b35fcba45f79a..09d020b706f63 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -93,8 +93,9 @@ var ( SnapshotStoragePrefix = []byte("o") // SnapshotStoragePrefix + account hash + storage hash -> storage trie value CodePrefix = []byte("c") // CodePrefix + code hash -> account code - PreimagePrefix = []byte("secure-key-") // PreimagePrefix + hash -> preimage - configPrefix = []byte("ethereum-config-") // config prefix for the db + PreimagePrefix = []byte("secure-key-") // PreimagePrefix + hash -> preimage + configPrefix = []byte("ethereum-config-") // config prefix for the db + genesisPrefix = []byte("ethereum-genesis-") // genesis state prefix for the db // Chain index prefixes (use `i` + single byte to avoid mixing data types). BloomBitsIndexPrefix = []byte("iB") // BloomBitsIndexPrefix is the data table of a chain indexer to track its progress @@ -233,3 +234,8 @@ func IsCodeKey(key []byte) (bool, []byte) { func configKey(hash common.Hash) []byte { return append(configPrefix, hash.Bytes()...) } + +// genesisKey = genesisPrefix + hash +func genesisKey(hash common.Hash) []byte { + return append(genesisPrefix, hash.Bytes()...) +} From 1e5c0cc52bc1b219f9ef32e64a58fe4d3f7cb502 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Mon, 28 Feb 2022 11:02:24 +0800 Subject: [PATCH 2/2] core: recover predefined genesis allocation if possible --- core/blockchain.go | 17 ++++++++++------- core/genesis.go | 35 +++++++++++++++++++++++++++++------ 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 4db9f4c18870d..ddbd1eda0f6b3 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -542,15 +542,18 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, root common.Hash, repair bo } } if beyondRoot || newHeadBlock.NumberU64() == 0 { - // Recommit the genesis state into disk in case the rewinding destination - // is genesis. In the future this rewinding destination can be the earliest - // block stored in the chain if the historical chain pruning is enabled. - // In that case the logic needs to be improved here. if newHeadBlock.NumberU64() == 0 { - if err := CommitGenesisState(bc.db, bc.genesisBlock.Hash()); err != nil { - log.Crit("Failed to commit genesis state", "err", err) + // Recommit the genesis state into disk in case the rewinding destination + // is genesis block and the relevant state is gone. In the future this + // rewinding destination can be the earliest block stored in the chain + // if the historical chain pruning is enabled. In that case the logic + // needs to be improved here. + if !bc.HasState(bc.genesisBlock.Root()) { + if err := CommitGenesisState(bc.db, bc.genesisBlock.Hash()); err != nil { + log.Crit("Failed to commit genesis state", "err", err) + } + log.Debug("Recommitted genesis state to disk") } - log.Debug("Recommitted genesis state to disk") } log.Debug("Rewound to block with state", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash()) break diff --git a/core/genesis.go b/core/genesis.go index a18e46cc9bd65..39d158d3a4d34 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -120,13 +120,36 @@ func (ga *GenesisAlloc) write(db ethdb.KeyValueWriter, hash common.Hash) error { // CommitGenesisState loads the stored genesis state with the given block // hash and commits them into the given database handler. func CommitGenesisState(db ethdb.Database, hash common.Hash) error { - blob := rawdb.ReadGenesisState(db, hash) - if len(blob) == 0 { - return errors.New("not found") - } var alloc GenesisAlloc - if err := alloc.UnmarshalJSON(blob); err != nil { - return err + blob := rawdb.ReadGenesisState(db, hash) + if len(blob) != 0 { + if err := alloc.UnmarshalJSON(blob); err != nil { + return err + } + } else { + // Genesis allocation is missing and there are several possibilities: + // the node is legacy which doesn't persist the genesis allocation or + // the persisted allocation is just lost. + // - supported networks(mainnet, testnets), recover with defined allocations + // - private network, can't recover + var genesis *Genesis + switch hash { + case params.MainnetGenesisHash: + genesis = DefaultGenesisBlock() + case params.RopstenGenesisHash: + genesis = DefaultRopstenGenesisBlock() + case params.RinkebyGenesisHash: + genesis = DefaultRinkebyGenesisBlock() + case params.GoerliGenesisHash: + genesis = DefaultGoerliGenesisBlock() + case params.SepoliaGenesisHash: + genesis = DefaultSepoliaGenesisBlock() + } + if genesis != nil { + alloc = genesis.Alloc + } else { + return errors.New("not found") + } } _, err := alloc.flush(db) return err