cmd/geth: add db check-state-content to verify integrity of trie nodes (#24840)

This PR adds db tooling (geth db check-state-content)  to verify the integrity of trie nodes. It iterates through the 32-byte key space in the database, which is expected to contain RLP-encoded trie nodes, addressed by hash.
This commit is contained in:
Martin Holst Swende 2022-05-17 13:01:46 +02:00 committed by GitHub
parent 381c66caf0
commit e0a9752b96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -35,6 +35,7 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
@ -71,6 +72,7 @@ Remove blockchain and state databases`,
dbExportCmd, dbExportCmd,
dbMetadataCmd, dbMetadataCmd,
dbMigrateFreezerCmd, dbMigrateFreezerCmd,
dbCheckStateContentCmd,
}, },
} }
dbInspectCmd = cli.Command{ dbInspectCmd = cli.Command{
@ -83,6 +85,16 @@ Remove blockchain and state databases`,
Usage: "Inspect the storage size for each type of data in the database", Usage: "Inspect the storage size for each type of data in the database",
Description: `This commands iterates the entire database. If the optional 'prefix' and 'start' arguments are provided, then the iteration is limited to the given subset of data.`, Description: `This commands iterates the entire database. If the optional 'prefix' and 'start' arguments are provided, then the iteration is limited to the given subset of data.`,
} }
dbCheckStateContentCmd = cli.Command{
Action: utils.MigrateFlags(checkStateContent),
Name: "check-state-content",
ArgsUsage: "<start (optional)>",
Flags: utils.GroupFlags(utils.NetworkFlags, utils.DatabasePathFlags),
Usage: "Verify that state data is cryptographically correct",
Description: `This command iterates the entire database for 32-byte keys, looking for rlp-encoded trie nodes.
For each trie node encountered, it checks that the key corresponds to the keccak256(value). If this is not true, this indicates
a data corruption.`,
}
dbStatCmd = cli.Command{ dbStatCmd = cli.Command{
Action: utils.MigrateFlags(dbStats), Action: utils.MigrateFlags(dbStats),
Name: "stats", Name: "stats",
@ -289,6 +301,60 @@ func inspect(ctx *cli.Context) error {
return rawdb.InspectDatabase(db, prefix, start) return rawdb.InspectDatabase(db, prefix, start)
} }
func checkStateContent(ctx *cli.Context) error {
var (
prefix []byte
start []byte
)
if ctx.NArg() > 1 {
return fmt.Errorf("Max 1 argument: %v", ctx.Command.ArgsUsage)
}
if ctx.NArg() > 0 {
if d, err := hexutil.Decode(ctx.Args().First()); err != nil {
return fmt.Errorf("failed to hex-decode 'start': %v", err)
} else {
start = d
}
}
stack, _ := makeConfigNode(ctx)
defer stack.Close()
db := utils.MakeChainDatabase(ctx, stack, true)
defer db.Close()
var (
it = rawdb.NewKeyLengthIterator(db.NewIterator(prefix, start), 32)
hasher = crypto.NewKeccakState()
got = make([]byte, 32)
errs int
count int
startTime = time.Now()
lastLog = time.Now()
)
for it.Next() {
count++
v := it.Value()
k := it.Key()
hasher.Reset()
hasher.Write(v)
hasher.Read(got)
if !bytes.Equal(k, got) {
errs++
fmt.Printf("Error at 0x%x\n", k)
fmt.Printf(" Hash: 0x%x\n", got)
fmt.Printf(" Data: 0x%x\n", v)
}
if time.Since(lastLog) > 8*time.Second {
log.Info("Iterating the database", "at", fmt.Sprintf("%#x", k), "elapsed", common.PrettyDuration(time.Since(startTime)))
lastLog = time.Now()
}
}
if err := it.Error(); err != nil {
return err
}
log.Info("Iterated the state content", "errors", errs, "items", count)
return nil
}
func showLeveldbStats(db ethdb.KeyValueStater) { func showLeveldbStats(db ethdb.KeyValueStater) {
if stats, err := db.Stat("leveldb.stats"); err != nil { if stats, err := db.Stat("leveldb.stats"); err != nil {
log.Warn("Failed to read database stats", "error", err) log.Warn("Failed to read database stats", "error", err)