core/state/snapshot: handle legacy journal (#30802)

This workaround is meant to minimize the possibility for snapshot generation
once the geth node upgrades to new version (specifically #30752 )

In #30752, the journal format in state snapshot is modified by removing
the destruct set. Therefore, the existing old format (version = 0) will be
discarded and all in-memory layers will be lost. Unfortunately, the lost 
in-memory layers can't be recovered by some other approaches, and the 
entire state snapshot will be regenerated (it will last about 2.5 hours).

This pull request introduces a workaround to adopt the legacy journal if
the destruct set contained is empty. Since self-destruction has been
deprecated following the cancun fork, the destruct set is expected to be nil for
layers above the fork block. However, an exception occurs during contract 
deployment: pre-funded accounts may self-destruct, causing accounts with 
non-zero balances to be removed from the state. For example,
https://etherscan.io/tx/0xa087333d83f0cd63b96bdafb686462e1622ce25f40bd499e03efb1051f31fe49).


For nodes with a fully synced state, the legacy journal is likely compatible with
the updated definition, eliminating the need for regeneration. Unfortunately,
nodes performing a full sync of historical chain segments or encountering 
pre-funded account deletions may face incompatibilities, leading to automatic 
snapshot regeneration.
This commit is contained in:
rjl493456442 2024-11-28 11:21:31 +08:00 committed by GitHub
parent e0deac7f6f
commit 8c1a36dad3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 43 additions and 6 deletions

View File

@ -33,9 +33,11 @@ import (
"github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb"
) )
// 0: initial version const (
// 1: destruct flag in diff layer is removed journalV0 uint64 = 0 // initial version
const journalVersion uint64 = 1 journalV1 uint64 = 1 // current version, with destruct flag (in diff layers) removed
journalCurrentVersion = journalV1
)
// journalGenerator is a disk layer entry containing the generator progress marker. // journalGenerator is a disk layer entry containing the generator progress marker.
type journalGenerator struct { type journalGenerator struct {
@ -50,6 +52,11 @@ type journalGenerator struct {
Storage uint64 Storage uint64
} }
// journalDestruct is an account deletion entry in a diffLayer's disk journal.
type journalDestruct struct {
Hash common.Hash
}
// journalAccount is an account entry in a diffLayer's disk journal. // journalAccount is an account entry in a diffLayer's disk journal.
type journalAccount struct { type journalAccount struct {
Hash common.Hash Hash common.Hash
@ -285,8 +292,8 @@ func iterateJournal(db ethdb.KeyValueReader, callback journalCallback) error {
log.Warn("Failed to resolve the journal version", "error", err) log.Warn("Failed to resolve the journal version", "error", err)
return errors.New("failed to resolve journal version") return errors.New("failed to resolve journal version")
} }
if version != journalVersion { if version != journalV0 && version != journalCurrentVersion {
log.Warn("Discarded the snapshot journal with wrong version", "required", journalVersion, "got", version) log.Warn("Discarded journal with wrong version", "required", journalCurrentVersion, "got", version)
return errors.New("wrong journal version") return errors.New("wrong journal version")
} }
// Secondly, resolve the disk layer root, ensure it's continuous // Secondly, resolve the disk layer root, ensure it's continuous
@ -316,6 +323,36 @@ func iterateJournal(db ethdb.KeyValueReader, callback journalCallback) error {
} }
return fmt.Errorf("load diff root: %v", err) return fmt.Errorf("load diff root: %v", err)
} }
// If a legacy journal is detected, decode the destruct set from the stream.
// The destruct set has been deprecated. If the journal contains non-empty
// destruct set, then it is deemed incompatible.
//
// Since self-destruction has been deprecated following the cancun fork,
// the destruct set is expected to be nil for layers above the fork block.
// However, an exception occurs during contract deployment: pre-funded accounts
// may self-destruct, causing accounts with non-zero balances to be removed
// from the state. For example,
// https://etherscan.io/tx/0xa087333d83f0cd63b96bdafb686462e1622ce25f40bd499e03efb1051f31fe49).
//
// For nodes with a fully synced state, the legacy journal is likely compatible
// with the updated definition, eliminating the need for regeneration. Unfortunately,
// nodes performing a full sync of historical chain segments or encountering
// pre-funded account deletions may face incompatibilities, leading to automatic
// snapshot regeneration.
//
// This approach minimizes snapshot regeneration for Geth nodes upgrading from a
// legacy version that are already synced. The workaround can be safely removed
// after the next hard fork.
if version == journalV0 {
var destructs []journalDestruct
if err := r.Decode(&destructs); err != nil {
return fmt.Errorf("load diff destructs: %v", err)
}
if len(destructs) > 0 {
log.Warn("Incompatible legacy journal detected", "version", journalV0)
return fmt.Errorf("incompatible legacy journal detected")
}
}
if err := r.Decode(&accounts); err != nil { if err := r.Decode(&accounts); err != nil {
return fmt.Errorf("load diff accounts: %v", err) return fmt.Errorf("load diff accounts: %v", err)
} }

View File

@ -664,7 +664,7 @@ func (t *Tree) Journal(root common.Hash) (common.Hash, error) {
// Firstly write out the metadata of journal // Firstly write out the metadata of journal
journal := new(bytes.Buffer) journal := new(bytes.Buffer)
if err := rlp.Encode(journal, journalVersion); err != nil { if err := rlp.Encode(journal, journalCurrentVersion); err != nil {
return common.Hash{}, err return common.Hash{}, err
} }
diskroot := t.diskRoot() diskroot := t.diskRoot()