diff --git a/.github/workflows/docker_release.yml b/.github/workflows/docker_release.yml new file mode 100644 index 000000000..36af0903a --- /dev/null +++ b/.github/workflows/docker_release.yml @@ -0,0 +1,41 @@ +name: Push Docker Image + +on: + push: + tags: + - morph-v* + +env: + IMAGE_NAME: go-ethereum + +jobs: + # Push image to GitHub Packages. + push: + runs-on: ubuntu-latest + if: github.event_name == 'push' + + steps: + - uses: actions/checkout@v4 + + - name: Build the Docker image + run: docker build . --file Dockerfile -t "${IMAGE_NAME}" + + - name: Log into registry + run: echo "${{ secrets.PACKAGE_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Push image + run: | + IMAGE_ID=ghcr.io/${{ github.repository }} + + # Change all uppercase to lowercase + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + # Strip git ref prefix from version + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + # Strip "morph-v" prefix from tag name + [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^morph-v//') + echo IMAGE_ID=$IMAGE_ID + echo VERSION=$VERSION + docker tag $IMAGE_NAME $IMAGE_ID:$VERSION + docker tag $IMAGE_NAME $IMAGE_ID:latest + docker push $IMAGE_ID:$VERSION + docker push $IMAGE_ID:latest diff --git a/MakefileEc2.mk b/MakefileEc2.mk index 705b80ecc..fbe847cd4 100644 --- a/MakefileEc2.mk +++ b/MakefileEc2.mk @@ -21,3 +21,34 @@ build-bk-prod-morph-prod-mainnet-to-morph-nccc-geth: aws s3 cp morph-nccc-geth.tar.gz s3://morph-0582-morph-technical-department-mainnet-data/morph-setup/morph-nccc-geth.tar.gz +# build for holesky +build-bk-prod-morph-prod-testnet-to-morph-geth-holesky: + if [ ! -d dist ]; then mkdir -p dist; fi + $(GORUN) build/ci.go install ./cmd/geth + cp build/bin/geth dist/ + tar -czvf morph-geth.tar.gz dist + aws s3 cp morph-geth.tar.gz s3://morph-0582-morph-technical-department-testnet-data/testnet/holesky/morph-setup/morph-geth.tar.gz + +build-bk-prod-morph-prod-testnet-to-morph-nccc-geth-holesky: + if [ ! -d dist ]; then mkdir -p dist; fi + $(GORUN) build/ci.go install ./cmd/geth + @echo "Done building." + cp build/bin/geth dist/ + tar -czvf morph-nccc-geth.tar.gz dist + aws s3 cp morph-nccc-geth.tar.gz s3://morph-0582-morph-technical-department-testnet-data/testnet/holesky/morph-setup/morph-nccc-geth.tar.gz + +build-bk-test-morph-test-qanet-to-morph-geth-qanet: + if [ ! -d dist ]; then mkdir -p dist; fi + $(GORUN) build/ci.go install ./cmd/geth + @echo "Done building." + cp build/bin/geth dist/ + tar -czvf morph-geth.tar.gz dist + aws s3 cp morph-geth.tar.gz s3://morph-7637-morph-technical-department-qanet-data/morph-setup/morph-geth.tar.gz + +build-bk-test-morph-test-qanet-to-morph-nccc-geth-qanet: + if [ ! -d dist ]; then mkdir -p dist; fi + $(GORUN) build/ci.go install ./cmd/geth + @echo "Done building." + cp build/bin/geth dist/ + tar -czvf morph-nccc-geth.tar.gz dist + aws s3 cp morph-nccc-geth.tar.gz s3://morph-7637-morph-technical-department-qanet-data/morph-setup/morph-nccc-geth.tar.gz \ No newline at end of file diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 0c9f1ae4c..bfc6ed9d1 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -50,6 +50,7 @@ var ( ArgsUsage: "", Flags: []cli.Flag{ utils.DataDirFlag, + utils.StateSchemeFlag, }, Category: "BLOCKCHAIN COMMANDS", Description: ` @@ -190,7 +191,7 @@ This command dumps out the state for a given block (or latest, if none provided) // the zero'd block (i.e. genesis) or will fail hard if it can't succeed. func initGenesis(ctx *cli.Context) error { // Make sure we have a valid genesis JSON - genesisPath := ctx.Args().First() + genesisPath := ctx.GlobalString(utils.DataDirFlag.Name) if len(genesisPath) == 0 { utils.Fatalf("Must supply path to genesis JSON file") } diff --git a/cmd/geth/dbcmd.go b/cmd/geth/dbcmd.go index 0ccb0cc18..96611c892 100644 --- a/cmd/geth/dbcmd.go +++ b/cmd/geth/dbcmd.go @@ -70,6 +70,7 @@ Remove blockchain and state databases`, dbDumpFreezerIndex, dbImportCmd, dbExportCmd, + dbHbss2PbssCmd, }, } dbInspectCmd = cli.Command{ @@ -254,6 +255,18 @@ WARNING: This is a low-level operation which may cause database corruption!`, }, Description: "Exports the specified chain data to an RLP encoded stream, optionally gzip-compressed.", } + dbHbss2PbssCmd = cli.Command{ + Action: hbss2pbss, + Name: "hbss-to-pbss", + ArgsUsage: "", + Flags: []cli.Flag{ + utils.DataDirFlag, + utils.SyncModeFlag, + utils.AncientFlag, + }, + Usage: "Convert Hash-Base to Path-Base trie node.", + Description: `This command iterates the entire trie node database and convert the hash-base node to path-base node.`, + } ) func removeDB(ctx *cli.Context) error { @@ -706,3 +719,38 @@ func exportChaindata(ctx *cli.Context) error { db := utils.MakeChainDatabase(ctx, stack, true) return utils.ExportChaindata(ctx.Args().Get(1), kind, exporter(db), stop) } + +func hbss2pbss(ctx *cli.Context) error { + stack, config := makeConfigNode(ctx) + defer stack.Close() + + chaindb := utils.MakeChainDatabase(ctx, stack, false) + h2p, err := trie.NewHbss2Pbss(chaindb, stack.ResolvePath(""), stack.ResolvePath(config.Eth.TrieCleanCacheJournal)) + if err != nil { + return err + } + + if ctx.NArg() > 0 { + log.Error("Too many arguments given") + return errors.New("too many arguments") + } + + lastStateID := rawdb.ReadPersistentStateID(chaindb) + if lastStateID == 0 { + h2p.Run() + } + + // prune hbss trie node + err = rawdb.PruneHashTrieNodeInDataBase(chaindb) + if err != nil { + log.Error("Prune Hash trie node in database failed", "error", err) + return err + } + err = h2p.Compact() + if err != nil { + log.Error("Compact trie node failed", "error", err) + return err + } + + return nil +} diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 20198b992..a85780237 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -158,6 +158,8 @@ var ( utils.GpoIgnoreGasPriceFlag, configFileFlag, utils.CatalystFlag, + utils.StateSchemeFlag, + utils.PathDBSyncFlag, } rpcFlags = []cli.Flag{ diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index be4804157..215ff5796 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -57,6 +57,8 @@ var AppHelpFlagGroups = []flags.FlagGroup{ utils.IdentityFlag, utils.LightKDFFlag, utils.WhitelistFlag, + utils.StateSchemeFlag, + utils.PathDBSyncFlag, }, }, { diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 0f79394c4..3692a669e 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -75,6 +75,7 @@ import ( "github.com/morph-l2/go-ethereum/params" "github.com/morph-l2/go-ethereum/rollup/tracing" "github.com/morph-l2/go-ethereum/rpc" + "github.com/morph-l2/go-ethereum/trie" ) func init() { @@ -851,6 +852,16 @@ var ( Name: "rpc.getlogs.maxrange", Usage: "Limit max fetched block range for `eth_getLogs` method", } + + StateSchemeFlag = &cli.StringFlag{ + Name: "state.scheme", + Usage: "Scheme to use for storing zktrie state ('hash' or 'path')", + } + + PathDBSyncFlag = cli.BoolFlag{ + Name: "pathdb.sync", + Usage: "Sync flush nodes cache to disk in path schema", + } ) // MakeDataDir retrieves the currently requested data directory, terminating @@ -1718,6 +1729,19 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { cfg.EthDiscoveryURLs = SplitAndTrim(urls) } } + if ctx.GlobalIsSet(PathDBSyncFlag.Name) { + cfg.PathSyncFlush = true + } + + scheme, err := ParseCLIAndConfigStateScheme(ctx.String(StateSchemeFlag.Name), cfg.StateScheme) + if err != nil { + Fatalf("%v", err) + } + cfg.StateScheme = scheme + if cfg.StateScheme == rawdb.PathScheme { + trie.GenesisStateInPathZkTrie = true + } + // Override any default configs for hard coded networks. switch { case ctx.GlobalBool(MainnetFlag.Name): @@ -1765,6 +1789,11 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { // disable prefetch log.Info("Prefetch disabled") cfg.NoPrefetch = true + + // cheked for path zktrie + if cfg.StateScheme == rawdb.PathScheme && !cfg.Genesis.Config.Morph.ZktrieEnabled() { + log.Crit("Must cooperate with zktrie enable to use --state.scheme=path") + } case ctx.GlobalBool(MorphHoleskyFlag.Name): if !ctx.GlobalIsSet(NetworkIdFlag.Name) { cfg.NetworkId = 2810 @@ -1780,6 +1809,11 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { // disable prefetch log.Info("Prefetch disabled") cfg.NoPrefetch = true + + // cheked for path zktrie + if cfg.StateScheme == rawdb.PathScheme && !cfg.Genesis.Config.Morph.ZktrieEnabled() { + log.Crit("Must cooperate with zktrie enable to use --state.scheme=path") + } case ctx.GlobalBool(DeveloperFlag.Name): if !ctx.GlobalIsSet(NetworkIdFlag.Name) { cfg.NetworkId = 1337 @@ -2147,3 +2181,22 @@ func MigrateFlags(action func(ctx *cli.Context) error) func(*cli.Context) error return action(ctx) } } + +// ParseCLIAndConfigStateScheme parses state scheme in CLI and config. +func ParseCLIAndConfigStateScheme(cliScheme, cfgScheme string) (string, error) { + if cliScheme == "" { + if cfgScheme != "" { + log.Info("Use config state scheme", "config", cfgScheme) + } + return cfgScheme, nil + } + + if !rawdb.ValidateStateScheme(cliScheme) { + return "", fmt.Errorf("invalid state scheme in CLI: %s", cliScheme) + } + if cfgScheme == "" || cliScheme == cfgScheme { + log.Info("Use CLI state scheme", "CLI", cliScheme) + return cliScheme, nil + } + return "", fmt.Errorf("incompatible state scheme, CLI: %s, config: %s", cliScheme, cfgScheme) +} diff --git a/common/types.go b/common/types.go index d438a1649..7035c5941 100644 --- a/common/types.go +++ b/common/types.go @@ -226,6 +226,11 @@ func IsHexAddress(s string) bool { return len(s) == 2*AddressLength && isHex(s) } +// Cmp compares two addresses. +func (a Address) Cmp(other Address) int { + return bytes.Compare(a[:], other[:]) +} + // Bytes gets the string representation of the underlying address. func (a Address) Bytes() []byte { return a[:] } @@ -433,3 +438,62 @@ func (ma *MixedcaseAddress) ValidChecksum() bool { func (ma *MixedcaseAddress) Original() string { return ma.original } + +func ReverseBytes(b []byte) []byte { + o := make([]byte, len(b)) + for i := range b { + o[len(b)-1-i] = b[i] + } + return o +} + +func bitReverseForNibble(b byte) byte { + switch b { + case 0: + return 0 + case 1: + return 8 + case 2: + return 4 + case 3: + return 12 + case 4: + return 2 + case 5: + return 10 + case 6: + return 6 + case 7: + return 14 + case 8: + return 1 + case 9: + return 9 + case 10: + return 5 + case 11: + return 13 + case 12: + return 3 + case 13: + return 11 + case 14: + return 7 + case 15: + return 15 + default: + panic("unexpected input") + } +} + +func BitReverse(inp []byte) (out []byte) { + + l := len(inp) + out = make([]byte, l) + + for i, b := range inp { + out[l-i-1] = bitReverseForNibble(b&15)<<4 + bitReverseForNibble(b>>4) + } + + return +} diff --git a/core/blockchain.go b/core/blockchain.go index f23faaf43..8ab8cc119 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -45,6 +45,8 @@ import ( "github.com/morph-l2/go-ethereum/metrics" "github.com/morph-l2/go-ethereum/params" "github.com/morph-l2/go-ethereum/trie" + "github.com/morph-l2/go-ethereum/triedb/hashdb" + "github.com/morph-l2/go-ethereum/triedb/pathdb" ) var ( @@ -135,6 +137,10 @@ type CacheConfig struct { Preimages bool // Whether to store preimage of trie key to the disk SnapshotWait bool // Wait for snapshot construction on startup. TODO(karalabe): This is a dirty hack for testing, nuke it + + PathSyncFlush bool // Whether sync flush the trienodebuffer of pathdb to disk. + JournalFilePath string + StateScheme string // Scheme used to store ethereum states and zktrit nodes on top } // defaultCacheConfig are the default caching values if none are specified by the @@ -147,6 +153,33 @@ var defaultCacheConfig = &CacheConfig{ SnapshotWait: true, } +// triedbConfig derives the configures for trie database. +func (c *CacheConfig) triedbConfig(zktrie bool) *trie.Config { + config := &trie.Config{ + Cache: c.TrieCleanLimit, + Journal: c.TrieCleanJournal, + Preimages: c.Preimages, + Zktrie: zktrie, + PathZkTrie: zktrie && c.StateScheme == rawdb.PathScheme, + } + if config.PathZkTrie { + config.PathDB = &pathdb.Config{ + SyncFlush: c.PathSyncFlush, + CleanCacheSize: c.TrieCleanLimit * 1024 * 1024, + DirtyCacheSize: c.TrieCleanLimit * 1024 * 1024, + JournalFilePath: c.JournalFilePath, + } + } else { + if config.Zktrie { + config.HashDB = &hashdb.Config{ + Cache: c.TrieCleanLimit, + Journal: c.TrieCleanJournal, + } + } + } + return config +} + // BlockChain represents the canonical chain given a database with a genesis // block. The Blockchain manages chain imports, reverts, chain reorganisations. // @@ -240,16 +273,11 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par } bc := &BlockChain{ - chainConfig: chainConfig, - cacheConfig: cacheConfig, - db: db, - triegc: prque.New(nil), - stateCache: state.NewDatabaseWithConfig(db, &trie.Config{ - Cache: cacheConfig.TrieCleanLimit, - Journal: cacheConfig.TrieCleanJournal, - Preimages: cacheConfig.Preimages, - Zktrie: chainConfig.Morph.ZktrieEnabled(), - }), + chainConfig: chainConfig, + cacheConfig: cacheConfig, + db: db, + triegc: prque.New(nil), + stateCache: state.NewDatabaseWithConfig(db, cacheConfig.triedbConfig(chainConfig.Morph.ZktrieEnabled())), quit: make(chan struct{}), chainmu: syncx.NewClosableMutex(), shouldPreserve: shouldPreserve, @@ -809,43 +837,50 @@ func (bc *BlockChain) Stop() { } } - // Ensure the state of a recent block is also stored to disk before exiting. - // We're writing three different states to catch different restart scenarios: - // - HEAD: So we don't need to reprocess any blocks in the general case - // - HEAD-1: So we don't do large reorgs if our HEAD becomes an uncle - // - HEAD-127: So we have a hard limit on the number of blocks reexecuted - if !bc.cacheConfig.TrieDirtyDisabled { - triedb := bc.stateCache.TrieDB() - - for _, offset := range []uint64{0, 1, TriesInMemory - 1} { - if number := bc.CurrentBlock().NumberU64(); number > offset { - recent := bc.GetBlockByNumber(number - offset) - - log.Info("Writing cached state to disk", "block", recent.Number(), "hash", recent.Hash(), "root", recent.Root()) - if err := triedb.Commit(recent.Root(), true, nil); err != nil { + if bc.stateCache.TrieDB().Scheme() == rawdb.PathScheme { + // Ensure that the in-memory trie nodes are journaled to disk properly. + if err := bc.stateCache.TrieDB().Journal(bc.CurrentBlock().Root()); err != nil { + log.Info("Failed to journal in-memory trie nodes", "err", err) + } + } else { + // Ensure the state of a recent block is also stored to disk before exiting. + // We're writing three different states to catch different restart scenarios: + // - HEAD: So we don't need to reprocess any blocks in the general case + // - HEAD-1: So we don't do large reorgs if our HEAD becomes an uncle + // - HEAD-127: So we have a hard limit on the number of blocks reexecuted + if !bc.cacheConfig.TrieDirtyDisabled { + triedb := bc.stateCache.TrieDB() + + for _, offset := range []uint64{0, 1, TriesInMemory - 1} { + if number := bc.CurrentBlock().NumberU64(); number > offset { + recent := bc.GetBlockByNumber(number - offset) + + log.Info("Writing cached state to disk", "block", recent.Number(), "hash", recent.Hash(), "root", recent.Root()) + if err := triedb.Commit(recent.Root(), true, nil); err != nil { + log.Error("Failed to commit recent state trie", "err", err) + } + } + } + if snapBase != (common.Hash{}) { + log.Info("Writing snapshot state to disk", "root", snapBase) + if err := triedb.Commit(snapBase, true, nil); err != nil { log.Error("Failed to commit recent state trie", "err", err) } } - } - if snapBase != (common.Hash{}) { - log.Info("Writing snapshot state to disk", "root", snapBase) - if err := triedb.Commit(snapBase, true, nil); err != nil { - log.Error("Failed to commit recent state trie", "err", err) + for !bc.triegc.Empty() { + triedb.Dereference(bc.triegc.PopItem().(common.Hash)) + } + if size, _ := triedb.Size(); size != 0 { + log.Error("Dangling trie nodes after full cleanup") } } - for !bc.triegc.Empty() { - triedb.Dereference(bc.triegc.PopItem().(common.Hash)) - } - if size, _ := triedb.Size(); size != 0 { - log.Error("Dangling trie nodes after full cleanup") + // Ensure all live cached entries be saved into disk, so that we can skip + // cache warmup when node restarts. + if bc.cacheConfig.TrieCleanJournal != "" { + triedb := bc.stateCache.TrieDB() + triedb.SaveCache(bc.cacheConfig.TrieCleanJournal) } } - // Ensure all live cached entries be saved into disk, so that we can skip - // cache warmup when node restarts. - if bc.cacheConfig.TrieCleanJournal != "" { - triedb := bc.stateCache.TrieDB() - triedb.SaveCache(bc.cacheConfig.TrieCleanJournal) - } log.Info("Blockchain stopped") } diff --git a/core/blockchain_l2.go b/core/blockchain_l2.go index 0bbdb1a2a..c6e8d82a8 100644 --- a/core/blockchain_l2.go +++ b/core/blockchain_l2.go @@ -107,6 +107,10 @@ func (bc *BlockChain) writeBlockStateWithoutHead(block *types.Block, receipts [] if err := blockBatch.Write(); err != nil { log.Crit("Failed to write block into disk", "err", err) } + + current := block.NumberU64() + origin := state.GetOriginRoot() + // Commit all cached state changes into underlying memory database. root, err := state.Commit(bc.chainConfig.IsEIP158(block.Number())) if err != nil { @@ -116,13 +120,17 @@ func (bc *BlockChain) writeBlockStateWithoutHead(block *types.Block, receipts [] triedb := bc.stateCache.TrieDB() // If we're running an archive node, always flush if bc.cacheConfig.TrieDirtyDisabled { + if triedb.Scheme() == rawdb.PathScheme { + // If node is running in path mode, skip explicit gc operation + // which is unnecessary in this mode. + return triedb.CommitState(root, origin, current, false) + } return triedb.Commit(root, false, nil) } // Full but not archive node, do proper garbage collection triedb.Reference(root, common.Hash{}) // metadata reference to keep trie alive bc.triegc.Push(root, -int64(block.NumberU64())) - current := block.NumberU64() // Flush limits are not considered for the first TriesInMemory blocks. if current <= TriesInMemory { return nil diff --git a/core/genesis.go b/core/genesis.go index b0e168f66..98daf7264 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -201,28 +201,38 @@ func SetupGenesisBlockWithOverride(db ethdb.Database, genesis *Genesis, override if storedcfg == nil { log.Warn("Found genesis block without chain config") } else { - trieCfg = &trie.Config{Zktrie: storedcfg.Morph.ZktrieEnabled()} + trieCfg = &trie.Config{Zktrie: storedcfg.Morph.ZktrieEnabled(), PathZkTrie: trie.GenesisStateInPathZkTrie} } } else { - trieCfg = &trie.Config{Zktrie: genesis.Config.Morph.ZktrieEnabled()} + trieCfg = &trie.Config{Zktrie: genesis.Config.Morph.ZktrieEnabled(), PathZkTrie: trie.GenesisStateInPathZkTrie} } - if _, err := state.New(header.Root, state.NewDatabaseWithConfig(db, trieCfg), nil); err != nil { - log.Error("failed to new state in SetupGenesisBlockWithOverride", "header root", header.Root.String(), "error", err) - if genesis == nil { - genesis = DefaultGenesisBlock() - } - // Ensure the stored genesis matches with the given one. - hash := genesis.ToBlock(nil).Hash() - if hash != stored { - return genesis.Config, hash, &GenesisMismatchError{stored, hash} - } - block, err := genesis.Commit(db) - if err != nil { - return genesis.Config, hash, err + var inited bool = false + // new states already overide genesis states. + if trieCfg.PathZkTrie && rawdb.ExistsStateID(db, header.Root) { + inited = true + } + + if !inited { + if _, err := state.New(header.Root, state.NewDatabaseWithConfig(db, trieCfg), nil); err != nil { + log.Error("failed to new state in SetupGenesisBlockWithOverride", "header root", header.Root.String(), "error", err) + if genesis == nil { + genesis = DefaultGenesisBlock() + } + // Ensure the stored genesis matches with the given one. + hash := genesis.ToBlock(nil).Hash() + if hash != stored { + return genesis.Config, hash, &GenesisMismatchError{stored, hash} + } + + block, err := genesis.Commit(db) + if err != nil { + return genesis.Config, hash, err + } + return genesis.Config, block.Hash(), nil } - return genesis.Config, block.Hash(), nil } + // Check whether the genesis block is already written. if genesis != nil { hash := genesis.ToBlock(nil).Hash() @@ -252,7 +262,6 @@ func SetupGenesisBlockWithOverride(db ethdb.Database, genesis *Genesis, override storedcfg.Morph.MaxTxPerBlock = storedcfg.Scroll.MaxTxPerBlock storedcfg.Morph.MaxTxPayloadBytesPerBlock = storedcfg.Scroll.MaxTxPayloadBytesPerBlock storedcfg.Morph.FeeVaultAddress = storedcfg.Scroll.FeeVaultAddress - } // Special case: don't change the existing config of a non-mainnet chain if no new // config is supplied. These chains would get AllProtocolChanges (and a compat error) @@ -296,12 +305,14 @@ func (g *Genesis) configOrDefault(ghash common.Hash) *params.ChainConfig { // ToBlock creates the genesis block and writes state of a genesis specification // to the given database (or discards it if nil). func (g *Genesis) ToBlock(db ethdb.Database) *types.Block { + toDiskDb := true if db == nil { + toDiskDb = false db = rawdb.NewMemoryDatabase() } var trieCfg *trie.Config if g.Config != nil { - trieCfg = &trie.Config{Zktrie: g.Config.Morph.ZktrieEnabled()} + trieCfg = &trie.Config{Zktrie: g.Config.Morph.ZktrieEnabled(), PathZkTrie: trie.GenesisStateInPathZkTrie} } statedb, err := state.New(common.Hash{}, state.NewDatabaseWithConfig(db, trieCfg), nil) if err != nil { @@ -344,7 +355,11 @@ func (g *Genesis) ToBlock(db ethdb.Database) *types.Block { } } statedb.Commit(false) - statedb.Database().TrieDB().Commit(root, true, nil) + if toDiskDb { + statedb.Database().TrieDB().CommitGenesis(root) + } else { + statedb.Database().TrieDB().Commit(root, true, nil) + } return types.NewBlock(head, nil, nil, nil, trie.NewStackTrie(nil)) } diff --git a/core/rawdb/accessors_state.go b/core/rawdb/accessors_state.go index 4812cc245..b06fe9898 100644 --- a/core/rawdb/accessors_state.go +++ b/core/rawdb/accessors_state.go @@ -17,6 +17,8 @@ package rawdb import ( + "encoding/binary" + "github.com/morph-l2/go-ethereum/common" "github.com/morph-l2/go-ethereum/ethdb" "github.com/morph-l2/go-ethereum/log" @@ -81,6 +83,11 @@ func ReadTrieNode(db ethdb.KeyValueReader, hash common.Hash) []byte { return data } +// ReadTrieNode retrieves the trie node of the provided hash. +func ReadTrieNodeByKey(db ethdb.KeyValueReader, key []byte) ([]byte, error) { + return db.Get(key) +} + // WriteTrieNode writes the provided trie node database. func WriteTrieNode(db ethdb.KeyValueWriter, hash common.Hash, node []byte) { if err := db.Put(hash.Bytes(), node); err != nil { @@ -88,9 +95,137 @@ func WriteTrieNode(db ethdb.KeyValueWriter, hash common.Hash, node []byte) { } } +// WriteTrieNodeByKey writes the provided trie node into database. +func WriteTrieNodeByKey(db ethdb.KeyValueWriter, path []byte, node []byte) { + if err := db.Put(path, node); err != nil { + log.Crit("Failed to store trie node", "err", err) + } +} + // DeleteTrieNode deletes the specified trie node from the database. func DeleteTrieNode(db ethdb.KeyValueWriter, hash common.Hash) { if err := db.Delete(hash.Bytes()); err != nil { log.Crit("Failed to delete trie node", "err", err) } } + +// ExistsAccountTrieNode checks the presence of the account trie node with the +// specified node path, regardless of the node hash. +func ExistsAccountTrieNode(db ethdb.KeyValueReader, path []byte) bool { + has, err := db.Has(accountTrieNodeKey(path)) + if err != nil { + return false + } + return has +} + +// WriteAccountTrieNode writes the provided account trie node into database. +func WriteAccountTrieNode(db ethdb.KeyValueWriter, path []byte, node []byte) { + if err := db.Put(accountTrieNodeKey(path), node); err != nil { + log.Crit("Failed to store account trie node", "err", err) + } +} + +// DeleteAccountTrieNode deletes the specified account trie node from the database. +func DeleteAccountTrieNode(db ethdb.KeyValueWriter, path []byte) { + if err := db.Delete(accountTrieNodeKey(path)); err != nil { + log.Crit("Failed to delete account trie node", "err", err) + } +} + +// ExistsStorageTrieNode checks the presence of the storage trie node with the +// specified account hash and node path, regardless of the node hash. +func ExistsStorageTrieNode(db ethdb.KeyValueReader, accountHash common.Hash, path []byte) bool { + has, err := db.Has(storageTrieNodeKey(accountHash, path)) + if err != nil { + return false + } + return has +} + +// WriteStorageTrieNode writes the provided storage trie node into database. +func WriteStorageTrieNode(db ethdb.KeyValueWriter, accountHash common.Hash, path []byte, node []byte) { + if err := db.Put(storageTrieNodeKey(accountHash, path), node); err != nil { + log.Crit("Failed to store storage trie node", "err", err) + } +} + +// DeleteStorageTrieNode deletes the specified storage trie node from the database. +func DeleteStorageTrieNode(db ethdb.KeyValueWriter, accountHash common.Hash, path []byte) { + if err := db.Delete(storageTrieNodeKey(accountHash, path)); err != nil { + log.Crit("Failed to delete storage trie node", "err", err) + } +} + +// ReadTrieJournal retrieves the serialized in-memory trie nodes of layers saved at +// the last shutdown. +func ReadTrieJournal(db ethdb.KeyValueReader) []byte { + data, _ := db.Get(trieJournalKey) + return data +} + +// WriteTrieJournal stores the serialized in-memory trie nodes of layers to save at +// shutdown. +func WriteTrieJournal(db ethdb.KeyValueWriter, journal []byte) { + if err := db.Put(trieJournalKey, journal); err != nil { + log.Crit("Failed to store tries journal", "err", err) + } +} + +// DeleteTrieJournal deletes the serialized in-memory trie nodes of layers saved at +// the last shutdown. +func DeleteTrieJournal(db ethdb.KeyValueWriter) { + if err := db.Delete(trieJournalKey); err != nil { + log.Crit("Failed to remove tries journal", "err", err) + } +} + +// ReadPersistentStateID retrieves the id of the persistent state from the database. +func ReadPersistentStateID(db ethdb.KeyValueReader) uint64 { + data, _ := db.Get(persistentStateIDKey) + if len(data) != 8 { + return 0 + } + return binary.BigEndian.Uint64(data) +} + +// WritePersistentStateID stores the id of the persistent state into database. +func WritePersistentStateID(db ethdb.KeyValueWriter, number uint64) { + if err := db.Put(persistentStateIDKey, encodeBlockNumber(number)); err != nil { + log.Crit("Failed to store the persistent state ID", "err", err) + } +} + +// ReadStateID retrieves the state id with the provided state root. +func ReadStateID(db ethdb.KeyValueReader, root common.Hash) *uint64 { + data, err := db.Get(stateIDKey(root)) + if err != nil || len(data) == 0 { + return nil + } + number := binary.BigEndian.Uint64(data) + return &number +} + +// WriteStateID writes the provided state lookup to database. +func WriteStateID(db ethdb.KeyValueWriter, root common.Hash, id uint64) { + var buff [8]byte + binary.BigEndian.PutUint64(buff[:], id) + if err := db.Put(stateIDKey(root), buff[:]); err != nil { + log.Crit("Failed to store state ID", "err", err) + } +} + +// DeleteStateID deletes the specified state lookup from the database. +func DeleteStateID(db ethdb.KeyValueWriter, root common.Hash) { + if err := db.Delete(stateIDKey(root)); err != nil { + log.Crit("Failed to delete state ID", "err", err) + } +} + +func ExistsStateID(db ethdb.KeyValueReader, root common.Hash) bool { + has, err := db.Has(stateIDKey(root)) + if err != nil { + return false + } + return has +} diff --git a/core/rawdb/accessors_trie.go b/core/rawdb/accessors_trie.go new file mode 100644 index 000000000..fe7322a43 --- /dev/null +++ b/core/rawdb/accessors_trie.go @@ -0,0 +1,181 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see + +package rawdb + +import ( + "bytes" + "fmt" + + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/ethdb" + "github.com/morph-l2/go-ethereum/log" + + zktrie "github.com/scroll-tech/zktrie/trie" + zkt "github.com/scroll-tech/zktrie/types" +) + +// HashScheme is the legacy hash-based state scheme with which trie nodes are +// stored in the disk with node hash as the database key. The advantage of this +// scheme is that different versions of trie nodes can be stored in disk, which +// is very beneficial for constructing archive nodes. The drawback is it will +// store different trie nodes on the same path to different locations on the disk +// with no data locality, and it's unfriendly for designing state pruning. +// +// Now this scheme is still kept for backward compatibility, and it will be used +// for archive node and some other tries(e.g. light trie). +const HashScheme = "hash" + +// PathScheme is the new path-based state scheme with which trie nodes are stored +// in the disk with node path as the database key. This scheme will only store one +// version of state data in the disk, which means that the state pruning operation +// is native. At the same time, this scheme will put adjacent trie nodes in the same +// area of the disk with good data locality property. But this scheme needs to rely +// on extra state diffs to survive deep reorg. +const PathScheme = "path" + +// ReadAccountTrieNode retrieves the account trie node and the associated node +// hash with the specified node path. +func ReadAccountTrieNode(db ethdb.KeyValueReader, path []byte) ([]byte, common.Hash) { + data, err := db.Get(accountTrieNodeKey(path)) + if err != nil { + return nil, common.Hash{} + } + + n, err := zktrie.NewNodeFromBytes(data) + if err != nil { + return nil, common.Hash{} + } + + zkHash, err := n.NodeHash() + if err != nil { + return nil, common.Hash{} + } + + return data, common.BytesToHash(zkHash.Bytes()) +} + +// IsLegacyTrieNode reports whether a provided database entry is a legacy trie +// node. The characteristics of legacy trie node are: +// - the key length is 32 bytes +// - the key is the hash of val +func IsLegacyTrieNode(key []byte, val []byte) bool { + if len(key) != common.HashLength { + return false + } + + n, err := zktrie.NewNodeFromBytes(val) + if err != nil { + return false + } + + zkHash, err := n.NodeHash() + if err != nil { + return false + } + + hash := common.BytesToHash(common.BitReverse(zkHash[:])) + return bytes.Equal(key[:], hash[:]) +} + +// HasAccountTrieNode checks the presence of the account trie node with the +// specified node path, regardless of the node hash. +func HasAccountTrieNode(db ethdb.KeyValueReader, path []byte) bool { + has, err := db.Has(accountTrieNodeKey(path)) + if err != nil { + return false + } + return has +} + +// HasLegacyTrieNode checks if the trie node with the provided hash is present in db. +func HasLegacyTrieNode(db ethdb.KeyValueReader, hash common.Hash) bool { + ok, _ := db.Has(hash.Bytes()) + return ok +} + +// ReadStateScheme reads the state scheme of persistent state, or none +// if the state is not present in database. +func ReadStateScheme(db ethdb.Database) string { + // Check if state in path-based scheme is present. + if HasAccountTrieNode(db, zkt.TrieRootPathKey[:]) { + return PathScheme + } + // The root node might be deleted during the initial snap sync, check + // the persistent state id then. + if id := ReadPersistentStateID(db); id != 0 { + return PathScheme + } + + // In a hash-based scheme, the genesis state is consistently stored + // on the disk. To assess the scheme of the persistent state, it + // suffices to inspect the scheme of the genesis state. + header := ReadHeader(db, ReadCanonicalHash(db, 0), 0) + if header == nil { + return "" // empty datadir + } + + if !HasLegacyTrieNode(db, header.Root) { + return "" // no state in disk + } + return HashScheme +} + +// ValidateStateScheme used to check state scheme whether is valid. +// Valid state scheme: hash and path. +func ValidateStateScheme(stateScheme string) bool { + if stateScheme == HashScheme || stateScheme == PathScheme { + return true + } + return false +} + +// ParseStateScheme checks if the specified state scheme is compatible with +// the stored state. +// +// - If the provided scheme is none, use the scheme consistent with persistent +// state, or fallback to path-based scheme if state is empty. +// +// - If the provided scheme is hash, use hash-based scheme or error out if not +// compatible with persistent state scheme. +// +// - If the provided scheme is path: use path-based scheme or error out if not +// compatible with persistent state scheme. +func ParseStateScheme(provided string, disk ethdb.Database) (string, error) { + // If state scheme is not specified, use the scheme consistent + // with persistent state, or fallback to hash mode if database + // is empty. + stored := ReadStateScheme(disk) + if provided == "" { + if stored == "" { + log.Info("State scheme set to default", "scheme", "hash") + return HashScheme, nil // use default scheme for empty database + } + log.Info("State scheme set to already existing disk db", "scheme", stored) + return stored, nil // reuse scheme of persistent scheme + } + // If state scheme is specified, ensure it's valid. + if provided != HashScheme && provided != PathScheme { + return "", fmt.Errorf("invalid state scheme %s", provided) + } + // If state scheme is specified, ensure it's compatible with + // persistent state. + if stored == "" || provided == stored { + log.Info("State scheme set by user", "scheme", provided) + return provided, nil + } + return "", fmt.Errorf("incompatible state scheme, stored: %s, user provided: %s", stored, provided) +} diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 01e339d34..475a61183 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -468,3 +468,31 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { return nil } + +func PruneHashTrieNodeInDataBase(db ethdb.Database) error { + it := db.NewIterator([]byte{}, []byte{}) + defer it.Release() + + start := time.Now() + logged := time.Now() + total_num := 0 + for it.Next() { + var key = it.Key() + switch { + case IsLegacyTrieNode(key, it.Value()): + db.Delete(key) + total_num++ + if total_num%100000 == 0 { + log.Info("Pruning hash-base state trie nodes", "Complete progress: ", total_num, "elapsed", common.PrettyDuration(time.Since(start))) + } + default: + if time.Since(logged) > 8*time.Second { + log.Info("Pruning hash-base state trie nodes", "Complete progress: ", total_num, "elapsed", common.PrettyDuration(time.Since(start))) + logged = time.Now() + } + continue + } + } + log.Info("Pruning hash-base state trie nodes", "Complete progress", total_num, "elapsed", common.PrettyDuration(time.Since(start))) + return nil +} diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 8b7ffd07a..60e319dbd 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -25,6 +25,7 @@ import ( leveldb "github.com/syndtr/goleveldb/leveldb/errors" "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/crypto" "github.com/morph-l2/go-ethereum/ethdb/memorydb" "github.com/morph-l2/go-ethereum/metrics" ) @@ -43,6 +44,9 @@ var ( // headFastBlockKey tracks the latest known incomplete block's hash during fast sync. headFastBlockKey = []byte("LastFast") + // persistentStateIDKey tracks the id of latest stored state(for path-based only). + persistentStateIDKey = []byte("LastStateID") + // lastPivotKey tracks the last pivot block used by fast sync (to reenable on sethead). lastPivotKey = []byte("LastPivot") @@ -70,6 +74,9 @@ var ( // skeletonSyncStatusKey tracks the skeleton sync status across restarts. skeletonSyncStatusKey = []byte("SkeletonSyncStatus") + // trieJournalKey tracks the in-memory trie node layers across restarts. + trieJournalKey = []byte("TrieJournal") + // txIndexTailKey tracks the oldest block whose transactions have been indexed. txIndexTailKey = []byte("TransactionIndexTail") @@ -122,6 +129,11 @@ var ( rollupBatchSignaturePrefix = []byte("R-bs") rollupBatchL1DataFeePrefix = []byte("R-df") rollupBatchHeadBatchHasFeeKey = []byte("R-hbf") + + // Path-based storage scheme of merkle patricia trie. + TrieNodeAccountPrefix = []byte("A") // trieNodeAccountPrefix + hexPath -> trie node + TrieNodeStoragePrefix = []byte("O") // trieNodeStoragePrefix + accountHash + hexPath -> trie node + stateIDPrefix = []byte("L") // stateIDPrefix + state root -> state id ) const ( @@ -276,6 +288,10 @@ func isNotFoundErr(err error) bool { return errors.Is(err, leveldb.ErrNotFound) || errors.Is(err, memorydb.ErrMemorydbNotFound) } +func IsNotFoundErr(err error) bool { + return isNotFoundErr(err) +} + // SkippedTransactionKey = skippedTransactionPrefix + tx hash func SkippedTransactionKey(txHash common.Hash) []byte { return append(skippedTransactionPrefix, txHash.Bytes()...) @@ -310,3 +326,31 @@ func RollupBatchSignatureSignerKey(batchHash common.Hash, signer common.Address) func RollupBatchL1DataFeeKey(batchIndex uint64) []byte { return append(rollupBatchL1DataFeePrefix, encodeBigEndian(batchIndex)...) } + +// accountTrieNodeKey = trieNodeAccountPrefix + nodePath. +func accountTrieNodeKey(path []byte) []byte { + return append(TrieNodeAccountPrefix, path...) +} + +// storageTrieNodeKey = trieNodeStoragePrefix + Keccak256Hash(accountHash + nodePath). +func storageTrieNodeKey(accountHash common.Hash, path []byte) []byte { + buf := make([]byte, len(TrieNodeStoragePrefix)+common.HashLength) + n := copy(buf, TrieNodeStoragePrefix) + h := crypto.Keccak256Hash(append(accountHash.Bytes(), path...)).Bytes() + copy(buf[n:], h) + return buf +} + +// stateIDKey = stateIDPrefix + root (32 bytes) +func stateIDKey(root common.Hash) []byte { + return append(stateIDPrefix, root.Bytes()...) +} + +func CompactStorageTrieNodeKey(key []byte) []byte { + if key[0] == TrieNodeStoragePrefix[0] && len(key) > 33 { + h := crypto.Keccak256Hash(key[1:]).Bytes() + newKey := append(TrieNodeStoragePrefix, h...) + return newKey + } + return key +} diff --git a/core/state/database.go b/core/state/database.go index a9c78e121..9f2fe896d 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -28,6 +28,7 @@ import ( "github.com/morph-l2/go-ethereum/core/types" "github.com/morph-l2/go-ethereum/ethdb" "github.com/morph-l2/go-ethereum/trie" + zkt "github.com/scroll-tech/zktrie/types" ) const ( @@ -44,7 +45,7 @@ type Database interface { OpenTrie(root common.Hash) (Trie, error) // OpenStorageTrie opens the storage trie of an account. - OpenStorageTrie(addrHash, root common.Hash) (Trie, error) + OpenStorageTrie(addrHash, root, origin common.Hash) (Trie, error) // CopyTrie returns an independent copy of the given trie. CopyTrie(Trie) Trie @@ -121,6 +122,7 @@ func NewDatabaseWithConfig(db ethdb.Database, config *trie.Config) Database { csc, _ := lru.New(codeSizeCacheSize) return &cachingDB{ zktrie: config != nil && config.Zktrie, + pathZkTrie: config != nil && config.Zktrie && config.PathZkTrie, db: trie.NewDatabaseWithConfig(db, config), codeSizeCache: csc, codeCache: lru2.NewSizeConstrainedLRU(codeCacheSize), @@ -132,12 +134,21 @@ type cachingDB struct { codeSizeCache *lru.Cache codeCache *lru2.SizeConstrainedLRU zktrie bool + pathZkTrie bool } // OpenTrie opens the main account trie at a specific root hash. func (db *cachingDB) OpenTrie(root common.Hash) (Trie, error) { + if db.pathZkTrie { + tr, err := trie.NewPathZkTrie(root, root, db.db, rawdb.TrieNodeAccountPrefix) + if err != nil { + return nil, err + } + return tr, nil + } + if db.zktrie { - tr, err := trie.NewZkTrie(root, trie.NewZktrieDatabaseFromTriedb(db.db)) + tr, err := trie.NewZkTrie(root, db.db) if err != nil { return nil, err } @@ -151,9 +162,18 @@ func (db *cachingDB) OpenTrie(root common.Hash) (Trie, error) { } // OpenStorageTrie opens the storage trie of an account. -func (db *cachingDB) OpenStorageTrie(addrHash, root common.Hash) (Trie, error) { +func (db *cachingDB) OpenStorageTrie(addrHash, root, origin common.Hash) (Trie, error) { + if db.pathZkTrie { + prefix := append(rawdb.TrieNodeStoragePrefix, zkt.ReverseByteOrder(addrHash.Bytes())...) + tr, err := trie.NewPathZkTrie(root, origin, db.db, prefix) + if err != nil { + return nil, err + } + return tr, nil + } + if db.zktrie { - tr, err := trie.NewZkTrie(root, trie.NewZktrieDatabaseFromTriedb(db.db)) + tr, err := trie.NewZkTrie(root, db.db) if err != nil { return nil, err } @@ -173,6 +193,8 @@ func (db *cachingDB) CopyTrie(t Trie) Trie { return t.Copy() case *trie.ZkTrie: return t.Copy() + case *trie.PathZkTrie: + return t.Copy() default: panic(fmt.Errorf("unknown trie type %T", t)) } diff --git a/core/state/iterator.go b/core/state/iterator.go index 6ad0d37bf..0c28af74f 100644 --- a/core/state/iterator.go +++ b/core/state/iterator.go @@ -109,7 +109,7 @@ func (it *NodeIterator) step() error { if err := rlp.Decode(bytes.NewReader(it.stateIt.LeafBlob()), &account); err != nil { return err } - dataTrie, err := it.state.db.OpenStorageTrie(common.BytesToHash(it.stateIt.LeafKey()), account.Root) + dataTrie, err := it.state.db.OpenStorageTrie(common.BytesToHash(it.stateIt.LeafKey()), account.Root, common.Hash{}) if err != nil { return err } diff --git a/core/state/pruner/zk-pruner.go b/core/state/pruner/zk-pruner.go index c72433969..bff037860 100644 --- a/core/state/pruner/zk-pruner.go +++ b/core/state/pruner/zk-pruner.go @@ -47,7 +47,7 @@ func NewZKPruner(chaindb ethdb.Database, bloomSize uint64, datadir, trieCachePat headBlock := rawdb.ReadHeadBlock(chaindb) root := headBlock.Root() log.Info("current head block", "block number", headBlock.NumberU64(), "root", root.Hex()) - zkTrie, err := trie.NewZkTrie(root, trie.NewZktrieDatabaseFromTriedb(stateCache.TrieDB())) + zkTrie, err := trie.NewZkTrie(root, stateCache.TrieDB()) if err != nil { return nil, err } @@ -249,9 +249,9 @@ func (p *ZKPruner) extractTrieNodes(root *common.Hash, accountTrie bool) { } } - p.stateBloom.Put(trie.BitReverse(nodeHash[:]), nil) + p.stateBloom.Put(common.BitReverse(nodeHash[:]), nil) case zktrie.NodeTypeBranch_0, zktrie.NodeTypeBranch_1, zktrie.NodeTypeBranch_2, zktrie.NodeTypeBranch_3, zktrie.DBEntryTypeRoot: - p.stateBloom.Put(trie.BitReverse(nodeHash[:]), nil) + p.stateBloom.Put(common.BitReverse(nodeHash[:]), nil) case zktrie.NodeTypeEmpty, zktrie.NodeTypeLeaf, zktrie.NodeTypeParent: panic("encounter unsupported deprecated node type") default: diff --git a/core/state/state_object.go b/core/state/state_object.go index 073791214..60a6f104b 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -29,6 +29,8 @@ import ( "github.com/morph-l2/go-ethereum/crypto/codehash" "github.com/morph-l2/go-ethereum/metrics" "github.com/morph-l2/go-ethereum/rlp" + + zkt "github.com/scroll-tech/zktrie/types" ) var emptyPoseidonCodeHash = codehash.EmptyPoseidonCodeHash.Bytes() @@ -66,10 +68,11 @@ func (s Storage) Copy() Storage { // Account values can be accessed and modified through the object. // Finally, call CommitTrie to write the modified storage trie into a database. type stateObject struct { - address common.Address - addrHash common.Hash // hash of ethereum address of the account - data types.StateAccount - db *StateDB + address common.Address + addrHash common.Hash // hash of ethereum address of the account + addrPoseidonHash common.Hash // hash of ethereum address of the account + data types.StateAccount + db *StateDB // DB error. // State objects are used by the consensus core and VM which are @@ -114,14 +117,20 @@ func newObject(db *StateDB, address common.Address, data types.StateAccount) *st if data.Root == (common.Hash{}) { data.Root = db.db.TrieDB().EmptyRoot() } + var addrPoseidonHash common.Hash + if db.db.TrieDB().IsPathZkTrie() { + addressKey, _ := zkt.ToSecureKey(address.Bytes()) + addrPoseidonHash = common.BigToHash(addressKey) + } return &stateObject{ - db: db, - address: address, - addrHash: crypto.Keccak256Hash(address[:]), - data: data, - originStorage: make(Storage), - pendingStorage: make(Storage), - dirtyStorage: make(Storage), + db: db, + address: address, + addrHash: crypto.Keccak256Hash(address[:]), + addrPoseidonHash: addrPoseidonHash, + data: data, + originStorage: make(Storage), + pendingStorage: make(Storage), + dirtyStorage: make(Storage), } } @@ -163,9 +172,13 @@ func (s *stateObject) getTrie(db Database) Trie { } if s.trie == nil { var err error - s.trie, err = db.OpenStorageTrie(s.addrHash, s.data.Root) + addrHash := s.addrHash + if db.TrieDB().IsPathZkTrie() { + addrHash = s.addrPoseidonHash + } + s.trie, err = db.OpenStorageTrie(addrHash, s.data.Root, s.db.originalRoot) if err != nil { - s.trie, _ = db.OpenStorageTrie(s.addrHash, common.Hash{}) + s.trie, _ = db.OpenStorageTrie(addrHash, common.Hash{}, s.db.originalRoot) s.setError(fmt.Errorf("can't create storage trie: %v", err)) } } diff --git a/core/state/state_prove.go b/core/state/state_prove.go index aba0981ba..06672cfe6 100644 --- a/core/state/state_prove.go +++ b/core/state/state_prove.go @@ -37,7 +37,7 @@ func (t ZktrieProofTracer) Available() bool { // NewProofTracer is not in Db interface and used explictily for reading proof in storage trie (not updated by the dirty value) func (s *StateDB) NewProofTracer(trieS Trie) ZktrieProofTracer { - if s.IsZktrie() { + if s.IsZkTrie() { zkTrie := trieS.(*zktrie.ZkTrie) if zkTrie == nil { panic("unexpected trie type for zktrie") @@ -52,10 +52,19 @@ func (s *StateDB) GetStorageTrieForProof(addr common.Address) (Trie, error) { // try the trie in stateObject first, else we would create one stateObject := s.getStateObject(addr) + + addrHash := crypto.Keccak256Hash(addr[:]) + if s.IsPathZkTrie() { + k, err := zkt.ToSecureKey(addr.Bytes()) + if err != nil { + return nil, fmt.Errorf("can't create storage trie on ToSecureKey %s: %v ", addr.Hex(), err) + } + addrHash = common.BigToHash(k) + } + if stateObject == nil { // still return a empty trie - addrHash := crypto.Keccak256Hash(addr[:]) - dummy_trie, _ := s.db.OpenStorageTrie(addrHash, common.Hash{}) + dummy_trie, _ := s.db.OpenStorageTrie(addrHash, common.Hash{}, common.Hash{}) return dummy_trie, nil } @@ -63,7 +72,7 @@ func (s *StateDB) GetStorageTrieForProof(addr common.Address) (Trie, error) { var err error if trie == nil { // use a new, temporary trie - trie, err = s.db.OpenStorageTrie(stateObject.addrHash, stateObject.data.Root) + trie, err = s.db.OpenStorageTrie(addrHash, stateObject.data.Root, s.originalRoot) if err != nil { return nil, fmt.Errorf("can't create storage trie on root %s: %v ", stateObject.data.Root, err) } @@ -78,7 +87,7 @@ func (s *StateDB) GetSecureTrieProof(trieProve TrieProve, key common.Hash) ([][] var proof proofList var err error - if s.IsZktrie() { + if s.IsZkTrie() { key_s, _ := zkt.ToSecureKeyBytes(key.Bytes()) err = trieProve.Prove(key_s.Bytes(), 0, &proof) } else { diff --git a/core/state/statedb.go b/core/state/statedb.go index 2a18c9d35..cd893223c 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -156,6 +156,10 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) return sdb, nil } +func (s *StateDB) GetOriginRoot() common.Hash { + return s.originalRoot +} + // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. @@ -189,8 +193,12 @@ func (s *StateDB) Error() error { return s.dbErr } -func (s *StateDB) IsZktrie() bool { - return s.db.TrieDB().Zktrie +func (s *StateDB) IsZkTrie() bool { + return s.db.TrieDB().IsZkTrie() +} + +func (s *StateDB) IsPathZkTrie() bool { + return s.db.TrieDB().IsPathZkTrie() } func (s *StateDB) AddLog(log *types.Log) { @@ -329,9 +337,9 @@ func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { // GetProof returns the Merkle proof for a given account. func (s *StateDB) GetProof(addr common.Address) ([][]byte, error) { - if s.IsZktrie() { - addr_s, _ := zkt.ToSecureKeyBytes(addr.Bytes()) - return s.GetProofByHash(common.BytesToHash(addr_s.Bytes())) + if s.IsZkTrie() { + addressKey, _ := zkt.ToSecureKeyBytes(addr.Bytes()) + return s.GetProofByHash(common.BytesToHash(addressKey.Bytes())) } return s.GetProofByHash(crypto.Keccak256Hash(addr.Bytes())) } @@ -607,7 +615,7 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject { if len(enc) == 0 { return nil } - if s.IsZktrie() { + if s.IsZkTrie() { data, err = types.UnmarshalStateAccount(enc) } else { data = new(types.StateAccount) @@ -724,6 +732,7 @@ func (s *StateDB) Copy() *StateDB { preimages: make(map[common.Hash][]byte, len(s.preimages)), journal: newJournal(), hasher: crypto.NewKeccakState(), + originalRoot: s.originalRoot, } // Copy the dirty states, logs, and preimages for addr := range s.journal.dirties { diff --git a/core/state_transition.go b/core/state_transition.go index b51dcc375..60aad1d2b 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -368,12 +368,18 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { return nil, err } if st.gas < gas { - return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gas, gas) + // Allow L1 message transactions to be included in the block but fail during execution, + // instead of rejecting them outright at this point. + if st.msg.IsL1MessageTx() { + gas = st.gas + } else { + return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gas, gas) + } } st.gas -= gas // Check clause 6 - if msg.Value().Sign() > 0 && !st.evm.Context.CanTransfer(st.state, msg.From(), msg.Value()) { + if msg.Value().Sign() > 0 && !msg.IsL1MessageTx() && !st.evm.Context.CanTransfer(st.state, msg.From(), msg.Value()) { return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From().Hex()) } diff --git a/eth/backend.go b/eth/backend.go index 986ca6740..dfc6874b1 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -121,6 +121,11 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { } config.TrieDirtyCache = 0 } + + if config.JournalFileName == "" { + config.JournalFileName = ethconfig.Defaults.JournalFileName + } + log.Info("Allocated trie memory caches", "clean", common.StorageSize(config.TrieCleanCache)*1024*1024, "dirty", common.StorageSize(config.TrieDirtyCache)*1024*1024) // Assemble the Ethereum object @@ -132,6 +137,12 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { if _, ok := genesisErr.(*params.ConfigCompatError); genesisErr != nil && !ok { return nil, genesisErr } + + config.StateScheme, err = rawdb.ParseStateScheme(config.StateScheme, chainDb) + if err != nil { + return nil, err + } + log.Info("Initialised chain configuration", "config", chainConfig) if err := pruner.RecoverPruning(stack.ResolvePath(""), chainDb, stack.ResolvePath(config.TrieCleanCacheJournal)); err != nil { @@ -186,6 +197,9 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { TrieTimeLimit: config.TrieTimeout, SnapshotLimit: config.SnapshotCache, Preimages: config.Preimages, + PathSyncFlush: config.PathSyncFlush, + StateScheme: config.StateScheme, + JournalFilePath: stack.ResolvePath(config.JournalFileName), } ) // TODO (MariusVanDerWijden) get rid of shouldPreserve in a follow-up PR diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 13a92cd55..f93483e0c 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -90,6 +90,8 @@ var Defaults = Config{ GPO: FullNodeGPO, RPCTxFeeCap: 1, // 1 ether MaxBlockRange: -1, // Default unconfigured value: no block range limit for backward compatibility + JournalFileName: "trie.journal", + PathSyncFlush: true, } func init() { @@ -211,6 +213,14 @@ type Config struct { // Max block range for eth_getLogs api method MaxBlockRange int64 + + // Path schema journal file name + JournalFileName string + + // State scheme used to store ethereum state and merkle trie nodes on top + PathSyncFlush bool + + StateScheme string `toml:",omitempty"` // State scheme used to store zktrie state nodes on top } // CreateConsensusEngine creates a consensus engine for the given chain config. diff --git a/go.mod b/go.mod index 996ac7e8a..2d5211474 100644 --- a/go.mod +++ b/go.mod @@ -59,11 +59,11 @@ require ( github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 github.com/tyler-smith/go-bip39 v1.1.0 golang.org/x/crypto v0.21.0 - golang.org/x/sync v0.5.0 + golang.org/x/sync v0.6.0 golang.org/x/sys v0.18.0 golang.org/x/text v0.14.0 golang.org/x/time v0.3.0 - golang.org/x/tools v0.15.0 + golang.org/x/tools v0.18.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6 @@ -117,7 +117,7 @@ require ( github.com/supranational/blst v0.3.11 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect - golang.org/x/mod v0.14.0 // indirect + golang.org/x/mod v0.15.0 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/term v0.18.0 // indirect golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect @@ -126,3 +126,5 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) + +replace github.com/scroll-tech/zktrie v0.8.4 => github.com/ryanmorphl2/zktrie v1.9.3 diff --git a/go.sum b/go.sum index fcb0ccc45..835d0f221 100644 --- a/go.sum +++ b/go.sum @@ -432,8 +432,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/scroll-tech/zktrie v0.8.4 h1:UagmnZ4Z3ITCk+aUq9NQZJNAwnWl4gSxsLb2Nl7IgRE= -github.com/scroll-tech/zktrie v0.8.4/go.mod h1:XvNo7vAk8yxNyTjBDj5WIiFzYW4bx/gJ78+NK6Zn6Uk= +github.com/ryanmorphl2/zktrie v1.9.3 h1:fea9GBXT7j+r6jncsVyvBgmmnYV/XzmFNvU/g0JtxFI= +github.com/ryanmorphl2/zktrie v1.9.3/go.mod h1:XvNo7vAk8yxNyTjBDj5WIiFzYW4bx/gJ78+NK6Zn6Uk= github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= github.com/segmentio/kafka-go v0.2.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -526,8 +526,8 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -566,8 +566,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -663,8 +663,8 @@ golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= -golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/les/server_requests.go b/les/server_requests.go index ac04e9320..888681b59 100644 --- a/les/server_requests.go +++ b/les/server_requests.go @@ -428,7 +428,7 @@ func handleGetProofs(msg Decoder) (serveRequestFn, uint64, uint64, error) { p.bumpInvalid() continue } - trie, err = statedb.OpenStorageTrie(common.BytesToHash(request.AccKey), account.Root) + trie, err = statedb.OpenStorageTrie(common.BytesToHash(request.AccKey), account.Root, root) if trie == nil || err != nil { p.Log().Warn("Failed to open storage trie for proof", "block", header.Number, "hash", header.Hash(), "account", common.BytesToHash(request.AccKey), "root", account.Root, "err", err) continue diff --git a/light/trie.go b/light/trie.go index 17698b4c3..c62be6827 100644 --- a/light/trie.go +++ b/light/trie.go @@ -51,7 +51,7 @@ func (db *odrDatabase) OpenTrie(root common.Hash) (state.Trie, error) { return &odrTrie{db: db, id: db.id}, nil } -func (db *odrDatabase) OpenStorageTrie(addrHash, root common.Hash) (state.Trie, error) { +func (db *odrDatabase) OpenStorageTrie(addrHash, root, origin common.Hash) (state.Trie, error) { return &odrTrie{db: db, id: StorageTrieID(db.id, addrHash, root)}, nil } diff --git a/rollup/tracing/tracing.go b/rollup/tracing/tracing.go index ae7336e2e..acea543aa 100644 --- a/rollup/tracing/tracing.go +++ b/rollup/tracing/tracing.go @@ -48,6 +48,9 @@ func NewTracerWrapper() *TracerWrapper { // CreateTraceEnvAndGetBlockTrace wraps the whole block tracing logic for a block func (tw *TracerWrapper) CreateTraceEnvAndGetBlockTrace(chainConfig *params.ChainConfig, chainContext core.ChainContext, engine consensus.Engine, chaindb ethdb.Database, statedb *state.StateDB, parent *types.Block, block *types.Block, commitAfterApply bool) (*types.BlockTrace, error) { + if statedb.IsPathZkTrie() { + return nil, fmt.Errorf("@Todo, unimplement, block=%d", block.NumberU64()) + } traceEnv, err := CreateTraceEnv(chainConfig, chainContext, engine, chaindb, statedb, parent, block, commitAfterApply) if err != nil { return nil, err @@ -386,6 +389,13 @@ func (env *TraceEnv) getTxResult(state *state.StateDB, index int, block *types.B // merge required proof data proofAccounts := structLogger.UpdatedAccounts() proofAccounts[vmenv.FeeRecipient()] = struct{}{} + // add from/to address if it does not exist + if _, ok := proofAccounts[from]; !ok { + proofAccounts[from] = struct{}{} + } + if _, ok := proofAccounts[*to]; !ok { + proofAccounts[*to] = struct{}{} + } for addr := range proofAccounts { addrStr := addr.String() diff --git a/trie/database.go b/trie/database.go index 16e9c6bbc..ac93d782c 100644 --- a/trie/database.go +++ b/trie/database.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "io" + "math/big" "reflect" "runtime" "sync" @@ -33,6 +34,10 @@ import ( "github.com/morph-l2/go-ethereum/log" "github.com/morph-l2/go-ethereum/metrics" "github.com/morph-l2/go-ethereum/rlp" + "github.com/morph-l2/go-ethereum/triedb/hashdb" + "github.com/morph-l2/go-ethereum/triedb/pathdb" + zktrie "github.com/scroll-tech/zktrie/trie" + zkt "github.com/scroll-tech/zktrie/types" ) var ( @@ -71,7 +76,8 @@ type Database struct { diskdb ethdb.KeyValueStore // Persistent storage for matured trie nodes // zktrie related stuff - Zktrie bool + Zktrie bool + PathZkTrie bool // TODO: It's a quick&dirty implementation. FIXME later. rawDirties KvMap @@ -93,6 +99,8 @@ type Database struct { preimages *preimageStore // The store for caching preimages lock sync.RWMutex + + backend backend // The backend for managing trie nodes } // rawNode is a simple binary blob used to differentiate between collapsed trie @@ -278,10 +286,48 @@ func expandNode(hash hashNode, n node) node { // Config defines all necessary options for database. type Config struct { - Cache int // Memory allowance (MB) to use for caching trie nodes in memory - Journal string // Journal of clean cache to survive node restarts - Preimages bool // Flag whether the preimage of trie key is recorded - Zktrie bool // use zktrie + Cache int // Memory allowance (MB) to use for caching trie nodes in memory + Journal string // Journal of clean cache to survive node restarts + Preimages bool // Flag whether the preimage of trie key is recorded + Zktrie bool // use zktrie + PathZkTrie bool // use path zktrie + + HashDB *hashdb.Config // Configs for hash-based scheme + PathDB *pathdb.Config // Configs for experimental path-based scheme +} + +var GenesisStateInPathZkTrie = false + +// Reader wraps the Node method of a backing trie reader. +type Reader interface { + // Node retrieves the trie node blob with the provided trie identifier, + // node path and the corresponding node hash. No error will be returned + // if the node is not found. + Node(path []byte) ([]byte, error) +} + +// backend defines the methods needed to access/update trie nodes in different +// state scheme. +type backend interface { + // Scheme returns the identifier of used storage scheme. + Scheme() string + + // Size returns the current storage size of the memory cache in front of the + // persistent database layer. + Size() (common.StorageSize, common.StorageSize, common.StorageSize) + + // Commit writes all relevant trie nodes belonging to the specified state + // to disk. Report specifies whether logs will be displayed in info level. + CommitState(root common.Hash, parentRoot common.Hash, blockNumber uint64, report bool) error + + // Commit write custom nodes belong genesis states, only onece + CommitGenesis(root common.Hash) error + + // Close closes the trie database backend and releases all held resources. + Close() error + + // Put nodes to database + Put(k, v []byte) error } // NewDatabase creates a new trie database to store ephemeral trie content before @@ -291,6 +337,10 @@ func NewDatabase(diskdb ethdb.KeyValueStore) *Database { return NewDatabaseWithConfig(diskdb, nil) } +func NewZkDatabase(diskdb ethdb.KeyValueStore) *Database { + return NewDatabaseWithConfig(diskdb, &Config{Zktrie: true}) +} + // NewDatabaseWithConfig creates a new trie database to store ephemeral trie content // before its written out to disk or garbage collected. It also acts as a read cache // for nodes loaded from disk. @@ -315,6 +365,21 @@ func NewDatabaseWithConfig(diskdb ethdb.KeyValueStore, config *Config) *Database }}, rawDirties: make(KvMap), preimages: preimage, + Zktrie: config != nil && config.Zktrie, + PathZkTrie: config != nil && config.Zktrie && config.PathZkTrie, + } + if config.HashDB != nil && config.PathDB != nil { + log.Crit("Both 'hash' and 'path' mode are configured") + } + + if db.PathZkTrie { + log.Info("Using pathdb for zktrie backend") + db.backend = pathdb.New(diskdb, config.PathDB) + } else { + if db.Zktrie { + log.Info("Using hashdb for zktrie backend") + db.backend = hashdb.NewZkDatabaseWithConfig(diskdb, config.HashDB) + } } return db } @@ -437,6 +502,10 @@ func (db *Database) Node(hash common.Hash) ([]byte, error) { // This method is extremely expensive and should only be used to validate internal // states in test code. func (db *Database) Nodes() []common.Hash { + if db.backend != nil { + panic("Database not support Nodes()") + } + db.lock.RLock() defer db.lock.RUnlock() @@ -454,6 +523,15 @@ func (db *Database) Nodes() []common.Hash { // and external node(e.g. storage trie root), all internal trie nodes // are referenced together by database itself. func (db *Database) Reference(child common.Hash, parent common.Hash) { + if db.backend != nil { + zdb, ok := db.backend.(*hashdb.ZktrieDatabase) + if !ok { + return + } + zdb.Reference(child, parent) + return + } + db.lock.Lock() defer db.lock.Unlock() @@ -483,6 +561,16 @@ func (db *Database) reference(child common.Hash, parent common.Hash) { // Dereference removes an existing reference from a root node. func (db *Database) Dereference(root common.Hash) { + if db.backend != nil { + zdb, ok := db.backend.(*hashdb.ZktrieDatabase) + if !ok { + log.Error("Databse not support dereference") + return + } + zdb.Dereference(root) + return + } + // Sanity check to ensure that the meta-root is not removed if root == (common.Hash{}) { log.Error("Attempted to dereference the trie cache meta root") @@ -562,6 +650,17 @@ func (db *Database) dereference(child common.Hash, parent common.Hash) { // Note, this method is a non-synchronized mutator. It is unsafe to call this // concurrently with other mutators. func (db *Database) Cap(limit common.StorageSize) error { + if db.backend != nil { + zdb, ok := db.backend.(*hashdb.ZktrieDatabase) + if !ok { + return errors.New("not supported") + } + if db.preimages != nil { + db.preimages.commit(false) + } + return zdb.Cap(limit) + } + // Create a database batch to flush persistent data out. It is important that // outside code doesn't see an inconsistent state (referenced data removed from // memory cache during commit but not yet in persistent storage). This is ensured @@ -640,6 +739,13 @@ func (db *Database) Cap(limit common.StorageSize) error { return nil } +func (db *Database) CommitGenesis(root common.Hash) error { + if db.backend != nil { + db.backend.CommitGenesis(root) + } + return db.Commit(root, true, nil) +} + // Commit iterates over all the children of a particular node, writes them out // to disk, forcefully tearing down all references in both directions. As a side // effect, all pre-images accumulated up to this point are also written. @@ -647,6 +753,14 @@ func (db *Database) Cap(limit common.StorageSize) error { // Note, this method is a non-synchronized mutator. It is unsafe to call this // concurrently with other mutators. func (db *Database) Commit(node common.Hash, report bool, callback func(common.Hash)) error { + if db.backend != nil { + zdb, ok := db.backend.(*hashdb.ZktrieDatabase) + if !ok { + return errors.New("not supported") + } + return zdb.CommitState(node, common.Hash{}, 0, report) + } + // Create a database batch to flush persistent data out. It is important that // outside code doesn't see an inconsistent state (referenced data removed from // memory cache during commit but not yet in persistent storage). This is ensured @@ -835,12 +949,29 @@ func (db *Database) saveCache(dir string, threads int) error { // SaveCache atomically saves fast cache data to the given dir using all // available CPU cores. func (db *Database) SaveCache(dir string) error { + if db.backend != nil { + zdb, ok := db.backend.(*hashdb.ZktrieDatabase) + if !ok { + return errors.New("not supported") + } + return zdb.SaveCache(dir) + } + return db.saveCache(dir, runtime.GOMAXPROCS(0)) } // SaveCachePeriodically atomically saves fast cache data to the given dir with // the specified interval. All dump operation will only use a single CPU core. func (db *Database) SaveCachePeriodically(dir string, interval time.Duration, stopCh <-chan struct{}) { + if db.backend != nil { + zdb, ok := db.backend.(*hashdb.ZktrieDatabase) + if !ok { + return + } + zdb.SaveCachePeriodically(dir, interval, stopCh) + return + } + ticker := time.NewTicker(interval) defer ticker.Stop() @@ -856,10 +987,138 @@ func (db *Database) SaveCachePeriodically(dir string, interval time.Duration, st // EmptyRoot indicate what root is for an empty trie, it depends on its underlying implement (zktrie or common trie) func (db *Database) EmptyRoot() common.Hash { - if db.Zktrie { return common.Hash{} } else { return emptyRoot } } + +// Reader returns a reader for accessing all trie nodes with provided state root. +// An error will be returned if the requested state is not available. +func (db *Database) Reader(blockRoot common.Hash) (Reader, error) { + switch b := db.backend.(type) { + case *hashdb.ZktrieDatabase: + return b.Reader(blockRoot) + case *pathdb.Database: + return b.Reader(blockRoot) + } + return nil, errors.New("unknown backend") +} + +func (db *Database) CommitState(root common.Hash, parentRoot common.Hash, blockNumber uint64, report bool) error { + if db.backend != nil { + if db.preimages != nil { + db.preimages.commit(true) + } + return db.backend.CommitState(root, parentRoot, blockNumber, report) + } + + return errors.New("not supported") +} + +// Scheme returns the node scheme used in the database. +func (db *Database) Scheme() string { + if db.backend != nil { + return db.backend.Scheme() + } + return rawdb.HashScheme +} + +// Close flushes the dangling preimages to disk and closes the trie database. +// It is meant to be called when closing the blockchain object, so that all +// resources held can be released correctly. +func (db *Database) Close() error { + if db.preimages != nil { + db.preimages.commit(true) + } + + if db.backend != nil { + return db.backend.Close() + } + return nil +} + +// Preimage retrieves a cached trie node pre-image from memory. If it cannot be +// found cached, the method queries the persistent database for the content. +func (db *Database) Preimage(hash common.Hash) []byte { + if db.preimages == nil { + return nil + } + return db.preimages.preimage(hash) +} + +// Journal commits an entire diff hierarchy to disk into a single journal entry. +// This is meant to be used during shutdown to persist the snapshot without +// flattening everything down (bad for reorgs). It's only supported by path-based +// database and will return an error for others. +func (db *Database) Journal(root common.Hash) error { + if db.backend != nil { + pdb, ok := db.backend.(*pathdb.Database) + if !ok { + return errors.New("backend [Journal] not supported") + } + return pdb.Journal(root) + } + return nil +} + +func (db *Database) IsZkTrie() bool { return db.Zktrie } +func (db *Database) IsPathZkTrie() bool { return db.Zktrie && db.PathZkTrie } + +// ZkTrie database interface +func (db *Database) UpdatePreimage(preimage []byte, hashField *big.Int) { + if db.backend != nil { + if db.preimages != nil { + // we must copy the input key + preimages := make(map[common.Hash][]byte) + preimages[common.BytesToHash(hashField.Bytes())] = common.CopyBytes(preimage) + db.preimages.insertPreimage(preimages) + } + } +} + +func (db *Database) Put(k, v []byte) error { + if db.backend != nil { + return db.backend.Put(k, v) + } + return nil +} + +func (db *Database) Get(key []byte) ([]byte, error) { + if db.backend != nil { + zdb, ok := db.backend.(*hashdb.ZktrieDatabase) + if !ok { + return nil, errors.New("backend [Get] not supported") + } + return zdb.Get(key) + } + return nil, nil +} + +func (db *Database) GetFrom(root, key []byte) ([]byte, error) { + if db.backend != nil { + pdb, ok := db.backend.(*pathdb.Database) + if !ok { + return nil, errors.New("backend [GetFrom] not supported") + } + + key := rawdb.CompactStorageTrieNodeKey(key[:]) + r := common.BytesToHash(zkt.ReverseByteOrder(root[:])) + reader, _ := pdb.Reader(r) + if reader != nil { + n, err := reader.Node(key) + if err != nil { + return nil, err + } + + if n == nil { + return nil, zktrie.ErrKeyNotFound + } + return n, nil + } + + return nil, errors.New("reader is nil") + } + return nil, nil +} diff --git a/trie/hbss2pbss.go b/trie/hbss2pbss.go new file mode 100644 index 000000000..9700a7b80 --- /dev/null +++ b/trie/hbss2pbss.go @@ -0,0 +1,194 @@ +package trie + +import ( + "bytes" + "errors" + "fmt" + "os" + "sync" + "time" + + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/core/rawdb" + "github.com/morph-l2/go-ethereum/core/types" + "github.com/morph-l2/go-ethereum/ethdb" + "github.com/morph-l2/go-ethereum/log" + zktrie "github.com/scroll-tech/zktrie/trie" + zkt "github.com/scroll-tech/zktrie/types" +) + +type Hbss2Pbss struct { + zkTrie *ZkTrie + db ethdb.Database + wg *sync.WaitGroup + datadir string + trieCachePath string + errChan chan error + headerBlock *types.Block +} + +func NewHbss2Pbss(chaindb ethdb.Database, datadir, trieCachePath string) (*Hbss2Pbss, error) { + stateCache := NewDatabaseWithConfig(chaindb, &Config{ + Zktrie: true, + }) + + headBlock := rawdb.ReadHeadBlock(chaindb) + root := headBlock.Root() + log.Info("Hbss2pbss converting", "block number", headBlock.NumberU64(), "root", root.Hex(), "hash", headBlock.Header().Hash().Hex()) + zkTrie, err := NewZkTrie(root, stateCache) + if err != nil { + return nil, err + } + + return &Hbss2Pbss{ + db: chaindb, + zkTrie: zkTrie, + datadir: datadir, + trieCachePath: trieCachePath, + errChan: make(chan error), + wg: &sync.WaitGroup{}, + headerBlock: headBlock, + }, nil +} + +func (h2p *Hbss2Pbss) Run() error { + if _, err := os.Stat(h2p.trieCachePath); os.IsNotExist(err) { + log.Warn("The clean trie cache is not found.") + } else { + os.RemoveAll(h2p.trieCachePath) + log.Info("Deleted trie clean cache", "path", h2p.trieCachePath) + } + + // Write genesis in new state db + err := h2p.handleGenesis() + if err != nil { + return err + } + + // Convert hbss state db to new state db + root, err := h2p.zkTrie.Tree().Root() + if err != nil { + return err + } + start := time.Now() + go func() { + defer func() { + close(h2p.errChan) + }() + + h2p.concurrentTraversal(root, []bool{}, common.Hash{}) + h2p.wg.Wait() + }() + + for err := range h2p.errChan { // wait until p.errChan is closed, or an error occurs + if err != nil { + return err + } + } + log.Info("Hbss2Pbss complete", "elapsed", common.PrettyDuration(time.Since(start))) + + rawdb.WritePersistentStateID(h2p.db, h2p.headerBlock.NumberU64()) + rawdb.WriteStateID(h2p.db, h2p.headerBlock.Root(), h2p.headerBlock.NumberU64()) + + return nil +} + +func (h2p *Hbss2Pbss) handleGenesis() error { + genesisHash := rawdb.ReadCanonicalHash(h2p.db, 0) + if genesisHash == (common.Hash{}) { + return errors.New("missing genesis hash") + } + genesis := rawdb.ReadBlock(h2p.db, genesisHash, 0) + if genesis == nil { + return errors.New("missing genesis block") + } + genesisRoot := genesis.Root() + + log.Info("Hbss2Pbss converting genesis", "root", genesisRoot.Hex()) + + h2p.concurrentTraversal(zkt.NewHashFromBytes(genesisRoot[:]), []bool{}, common.Hash{}) + + // Mark genesis root state + rawdb.WriteStateID(h2p.db, genesisRoot, 0) + + return nil +} + +func (h2p *Hbss2Pbss) Compact() error { + cstart := time.Now() + for b := 0x00; b <= 0xf0; b += 0x10 { + var ( + start = []byte{byte(b)} + end = []byte{byte(b + 0x10)} + ) + if b == 0xf0 { + end = nil + } + log.Info("Compacting database", "range", fmt.Sprintf("%#x-%#x", start, end), "elapsed", common.PrettyDuration(time.Since(cstart))) + if err := h2p.db.Compact(start, end); err != nil { + log.Error("Database compaction failed", "error", err) + return err + } + } + log.Info("Database compaction finished", "elapsed", common.PrettyDuration(time.Since(cstart))) + + return nil +} + +func (h2p *Hbss2Pbss) writeNode(pathKey []bool, n *zktrie.Node, owner common.Hash) { + if owner == (common.Hash{}) { + rawdb.WriteAccountTrieNode(h2p.db, zkt.PathToKey(pathKey)[:], n.CanonicalValue()) + log.Debug("WriteNodes account trie node", "type", n.Type, "path", zkt.PathToString(pathKey)) + } else { + rawdb.WriteStorageTrieNode(h2p.db, owner, zkt.PathToKey(pathKey)[:], n.CanonicalValue()) + log.Debug("WriteNodes storage trie node", "owner", owner.Hex(), "type", n.Type, "path", zkt.PathToString(pathKey)) + } +} + +func (h2p *Hbss2Pbss) concurrentTraversal(nodeHash *zkt.Hash, path []bool, owner common.Hash) error { + n, err := h2p.zkTrie.Tree().GetNode(nodeHash) + if err != nil { + return err + } + + switch n.Type { + case zktrie.NodeTypeEmpty, zktrie.NodeTypeEmpty_New: + h2p.writeNode(path, n, owner) + return nil + case zktrie.NodeTypeLeaf_New: + h2p.writeNode(path, n, owner) + + if bytes.Equal(owner[:], common.Hash{}.Bytes()) { + data, err := types.UnmarshalStateAccount(n.Data()) + if err != nil { + h2p.errChan <- err + return err + } + + if data.CodeSize > 0 { + if !bytes.Equal(data.Root[:], common.Hash{}.Bytes()) { + h2p.concurrentTraversal(zkt.NewHashFromBytes(data.Root[:]), []bool{}, common.BytesToHash(n.NodeKey[:])) + } + } + } + + return nil + case zktrie.NodeTypeBranch_0, zktrie.NodeTypeBranch_1, zktrie.NodeTypeBranch_2, zktrie.NodeTypeBranch_3: + h2p.writeNode(path, n, owner) + + leftErr := h2p.concurrentTraversal(n.ChildL, append(path, false), owner) + if leftErr != nil { + h2p.errChan <- leftErr + return leftErr + } + rightErr := h2p.concurrentTraversal(n.ChildR, append(path, true), owner) + if rightErr != nil { + h2p.errChan <- rightErr + return rightErr + } + default: + return errors.New(fmt.Sprint("unexpected node type", n.Type)) + } + + return nil +} diff --git a/trie/path_zk_trie.go b/trie/path_zk_trie.go new file mode 100644 index 000000000..a0981a687 --- /dev/null +++ b/trie/path_zk_trie.go @@ -0,0 +1,230 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package trie + +import ( + "fmt" + + "github.com/scroll-tech/zktrie/pathtrie" + zkt "github.com/scroll-tech/zktrie/types" + + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/core/types" + "github.com/morph-l2/go-ethereum/crypto/poseidon" + "github.com/morph-l2/go-ethereum/ethdb" + "github.com/morph-l2/go-ethereum/log" +) + +var magicHashPathTrie []byte = []byte("THIS IS THE MAGIC INDEX FOR PATH ZKTRIE") + +// wrap zktrie for trie interface +type PathZkTrie struct { + *pathtrie.ZkTrie + db *Database +} + +func init() { + zkt.InitHashScheme(poseidon.HashFixedWithDomain) +} + +// NewPathZkTrie creates a trie +// NewPathZkTrie bypasses all the buffer mechanism in *Database, it directly uses the +// underlying diskdb +func NewPathZkTrie(root common.Hash, origin common.Hash, db *Database, prefix []byte) (*PathZkTrie, error) { + tr, err := pathtrie.NewZkTrieWithPrefix(*zkt.NewByte32FromBytes(root.Bytes()), *zkt.NewByte32FromBytes(origin.Bytes()), db, prefix) + if err != nil { + return nil, err + } + return &PathZkTrie{tr, db}, nil +} + +// Get returns the value for key stored in the trie. +// The value bytes must not be modified by the caller. +func (t *PathZkTrie) Get(key []byte) []byte { + sanityCheckByte32Key(key) + res, err := t.TryGet(key) + if err != nil { + log.Error(fmt.Sprintf("Unhandled trie error: %v", err)) + } + return res +} + +// TryUpdateAccount will abstract the write of an account to the +// secure trie. +func (t *PathZkTrie) TryUpdateAccount(key []byte, acc *types.StateAccount) error { + sanityCheckByte32Key(key) + value, flag := acc.MarshalFields() + return t.ZkTrie.TryUpdate(key, flag, value) +} + +// Update associates key with value in the trie. Subsequent calls to +// Get will return value. If value has length zero, any existing value +// is deleted from the trie and calls to Get will return nil. +// +// The value bytes must not be modified by the caller while they are +// stored in the trie. +func (t *PathZkTrie) Update(key, value []byte) { + if err := t.TryUpdate(key, value); err != nil { + log.Error(fmt.Sprintf("Unhandled trie error: %v", err)) + } +} + +// NOTE: value is restricted to length of bytes32. +// we override the underlying zktrie's TryUpdate method +func (t *PathZkTrie) TryUpdate(key, value []byte) error { + sanityCheckByte32Key(key) + return t.ZkTrie.TryUpdate(key, 1, []zkt.Byte32{*zkt.NewByte32FromBytes(value)}) +} + +// Delete removes any existing value for key from the trie. +func (t *PathZkTrie) Delete(key []byte) { + sanityCheckByte32Key(key) + if err := t.TryDelete(key); err != nil { + log.Error(fmt.Sprintf("Unhandled trie error: %v", err)) + } +} + +// GetKey returns the preimage of a hashed key that was +// previously used to store a value. +func (t *PathZkTrie) GetKey(kHashBytes []byte) []byte { + // TODO: use a kv cache in memory + k, err := zkt.NewBigIntFromHashBytes(kHashBytes) + if err != nil { + log.Error(fmt.Sprintf("Unhandled trie error: %v", err)) + } + if t.db.preimages != nil { + return t.db.preimages.preimage(common.BytesToHash(k.Bytes())) + } + return nil +} + +// Commit writes all nodes and the secure hash pre-images to the trie's database. +// Nodes are stored with their sha3 hash as the key. +// +// Committing flushes nodes from memory. Subsequent Get calls will load nodes +// from the database. +func (t *PathZkTrie) Commit(LeafCallback) (common.Hash, int, error) { + if err := t.ZkTrie.Commit(); err != nil { + return common.Hash{}, 0, err + } + // in current implmentation, every update of trie already writes into database + // so Commmit does nothing + return t.Hash(), 0, nil +} + +// Hash returns the root hash of SecureBinaryTrie. It does not write to the +// database and can be used even if the trie doesn't have one. +func (t *PathZkTrie) Hash() common.Hash { + var hash common.Hash + hash.SetBytes(t.ZkTrie.Hash()) + return hash +} + +// Copy returns a copy of SecureBinaryTrie. +func (t *PathZkTrie) Copy() *PathZkTrie { + return &PathZkTrie{t.ZkTrie.Copy(), t.db} +} + +// NodeIterator returns an iterator that returns nodes of the underlying trie. Iteration +// starts at the key after the given start key. +func (t *PathZkTrie) NodeIterator(start []byte) NodeIterator { + /// FIXME + panic("not implemented") +} + +// hashKey returns the hash of key as an ephemeral buffer. +// The caller must not hold onto the return value because it will become +// invalid on the next call to hashKey or secKey. +/*func (t *ZkTrie) hashKey(key []byte) []byte { + if len(key) != 32 { + panic("non byte32 input to hashKey") + } + low16 := new(big.Int).SetBytes(key[:16]) + high16 := new(big.Int).SetBytes(key[16:]) + hash, err := poseidon.Hash([]*big.Int{low16, high16}) + if err != nil { + panic(err) + } + return hash.Bytes() +} +*/ + +// Prove constructs a merkle proof for key. The result contains all encoded nodes +// on the path to the value at key. The value itself is also included in the last +// node and can be retrieved by verifying the proof. +// +// If the trie does not contain a value for key, the returned proof contains all +// nodes of the longest existing prefix of the key (at least the root node), ending +// with the node that proves the absence of the key. +func (t *PathZkTrie) Prove(key []byte, fromLevel uint, proofDb ethdb.KeyValueWriter) error { + err := t.ZkTrie.Prove(key, fromLevel, func(n *pathtrie.Node) error { + nodeHash, err := n.NodeHash() + if err != nil { + return err + } + + if n.Type == pathtrie.NodeTypeLeaf_New { + preImage := t.GetKey(n.NodeKey.Bytes()) + if len(preImage) > 0 { + n.KeyPreimage = &zkt.Byte32{} + copy(n.KeyPreimage[:], preImage) + //return fmt.Errorf("key preimage not found for [%x] ref %x", n.NodeKey.Bytes(), k.Bytes()) + } + } + return proofDb.Put(nodeHash[:], n.Value()) + }) + if err != nil { + return err + } + + // we put this special kv pair in db so we can distinguish the type and + // make suitable Proof + return proofDb.Put(magicHashPathTrie, pathtrie.ProofMagicBytes()) +} + +// VerifyProof checks merkle proofs. The given proof must contain the value for +// key in a trie with the given root hash. VerifyProof returns an error if the +// proof contains invalid trie nodes or the wrong value. +func VerifyProofSMT2(rootHash common.Hash, key []byte, proofDb ethdb.KeyValueReader) (value []byte, err error) { + h := zkt.NewHashFromBytes(rootHash.Bytes()) + k, err := zkt.ToSecureKey(key) + if err != nil { + return nil, err + } + + proof, n, err := pathtrie.BuildZkTrieProof(h, k, len(key)*8, func(key *zkt.Hash) (*pathtrie.Node, error) { + buf, _ := proofDb.Get(key[:]) + if buf == nil { + return nil, pathtrie.ErrKeyNotFound + } + n, err := pathtrie.NewNodeFromBytes(buf) + return n, err + }) + + if err != nil { + // do not contain the key + return nil, err + } else if !proof.Existence { + return nil, nil + } + + if pathtrie.VerifyProofZkTrie(h, proof, n) { + return n.Data(), nil + } else { + return nil, fmt.Errorf("bad proof node %v", proof) + } +} diff --git a/trie/proof.go b/trie/proof.go index b89b01e0c..60a0d854e 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -110,6 +110,11 @@ func VerifyProof(rootHash common.Hash, key []byte, proofDb ethdb.KeyValueReader) return VerifyProofSMT(rootHash, key, proofDb) } + // test the type of proof (for path trie or SMT) + if buf, _ := proofDb.Get(magicHashPathTrie); buf != nil { + return VerifyProofSMT2(rootHash, key, proofDb) + } + key = keybytesToHex(key) wantHash := rootHash for i := 0; ; i++ { diff --git a/trie/zk_trie.go b/trie/zk_trie.go index 2d01fcc33..b46a6dda2 100644 --- a/trie/zk_trie.go +++ b/trie/zk_trie.go @@ -34,7 +34,7 @@ var magicHash []byte = []byte("THIS IS THE MAGIC INDEX FOR ZKTRIE") // wrap zktrie for trie interface type ZkTrie struct { *zktrie.ZkTrie - db *ZktrieDatabase + db *Database } func init() { @@ -50,7 +50,7 @@ func sanityCheckByte32Key(b []byte) { // NewZkTrie creates a trie // NewZkTrie bypasses all the buffer mechanism in *Database, it directly uses the // underlying diskdb -func NewZkTrie(root common.Hash, db *ZktrieDatabase) (*ZkTrie, error) { +func NewZkTrie(root common.Hash, db *Database) (*ZkTrie, error) { tr, err := zktrie.NewZkTrie(*zkt.NewByte32FromBytes(root.Bytes()), db) if err != nil { return nil, err @@ -112,8 +112,8 @@ func (t *ZkTrie) GetKey(kHashBytes []byte) []byte { if err != nil { log.Error(fmt.Sprintf("Unhandled trie error: %v", err)) } - if t.db.db.preimages != nil { - return t.db.db.preimages.preimage(common.BytesToHash(k.Bytes())) + if t.db.preimages != nil { + return t.db.preimages.preimage(common.BytesToHash(k.Bytes())) } return nil } diff --git a/trie/zk_trie_proof_test.go b/trie/zk_trie_proof_test.go index 5b23ae7b8..1d42f60a2 100644 --- a/trie/zk_trie_proof_test.go +++ b/trie/zk_trie_proof_test.go @@ -63,7 +63,7 @@ func verifyValue(proveVal []byte, vPreimage []byte) bool { } func TestSMTOneElementProof(t *testing.T) { - tr, _ := NewZkTrie(common.Hash{}, NewZktrieDatabase((memorydb.New()))) + tr, _ := NewZkTrie(common.Hash{}, NewZkDatabase((memorydb.New()))) mt := &zkTrieImplTestWrapper{tr.Tree()} err := mt.UpdateWord( zkt.NewByte32FromBytesPaddingZero(bytes.Repeat([]byte("k"), 32)), @@ -148,7 +148,7 @@ func TestSMTBadProof(t *testing.T) { // Tests that missing keys can also be proven. The test explicitly uses a single // entry trie and checks for missing keys both before and after the single entry. func TestSMTMissingKeyProof(t *testing.T) { - tr, _ := NewZkTrie(common.Hash{}, NewZktrieDatabase((memorydb.New()))) + tr, _ := NewZkTrie(common.Hash{}, NewZkDatabase((memorydb.New()))) mt := &zkTrieImplTestWrapper{tr.Tree()} err := mt.UpdateWord( zkt.NewByte32FromBytesPaddingZero(bytes.Repeat([]byte("k"), 32)), @@ -180,7 +180,7 @@ func TestSMTMissingKeyProof(t *testing.T) { } func randomZktrie(t *testing.T, n int) (*ZkTrie, map[string]*kv) { - tr, err := NewZkTrie(common.Hash{}, NewZktrieDatabase((memorydb.New()))) + tr, err := NewZkTrie(common.Hash{}, NewZkDatabase((memorydb.New()))) if err != nil { panic(err) } @@ -210,7 +210,7 @@ func randomZktrie(t *testing.T, n int) (*ZkTrie, map[string]*kv) { // Tests that new "proof trace" feature func TestProofWithDeletion(t *testing.T) { - tr, _ := NewZkTrie(common.Hash{}, NewZktrieDatabase((memorydb.New()))) + tr, _ := NewZkTrie(common.Hash{}, NewZkDatabase((memorydb.New()))) mt := &zkTrieImplTestWrapper{tr.Tree()} key1 := bytes.Repeat([]byte("l"), 32) key2 := bytes.Repeat([]byte("m"), 32) diff --git a/trie/zk_trie_test.go b/trie/zk_trie_test.go index f93e067dd..0c413c818 100644 --- a/trie/zk_trie_test.go +++ b/trie/zk_trie_test.go @@ -30,6 +30,7 @@ import ( zkt "github.com/scroll-tech/zktrie/types" "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/core/rawdb" "github.com/morph-l2/go-ethereum/ethdb/leveldb" "github.com/morph-l2/go-ethereum/ethdb/memorydb" ) @@ -37,19 +38,16 @@ import ( func newEmptyZkTrie() *ZkTrie { trie, _ := NewZkTrie( common.Hash{}, - &ZktrieDatabase{ - db: NewDatabaseWithConfig(memorydb.New(), - &Config{Preimages: true}), - prefix: []byte{}, - }, + NewDatabaseWithConfig(memorydb.New(), + &Config{Preimages: true, Zktrie: true}), ) return trie } // makeTestSecureTrie creates a large enough secure trie for testing. -func makeTestZkTrie() (*ZktrieDatabase, *ZkTrie, map[string][]byte) { +func makeTestZkTrie() (*Database, *ZkTrie, map[string][]byte) { // Create an empty trie - triedb := NewZktrieDatabase(memorydb.New()) + triedb := NewZkDatabase(memorydb.New()) trie, _ := NewZkTrie(common.Hash{}, triedb) // Fill it with some arbitrary data @@ -173,9 +171,9 @@ const benchElemCountZk = 10000 func BenchmarkZkTrieGet(b *testing.B) { _, tmpdb := tempDBZK(b) - zkTrie, _ := NewZkTrie(common.Hash{}, NewZktrieDatabaseFromTriedb(tmpdb)) + zkTrie, _ := NewZkTrie(common.Hash{}, tmpdb) defer func() { - ldb := zkTrie.db.db.diskdb.(*leveldb.Database) + ldb := zkTrie.db.diskdb.(*leveldb.Database) ldb.Close() os.RemoveAll(ldb.Path()) }() @@ -188,7 +186,7 @@ func BenchmarkZkTrieGet(b *testing.B) { assert.NoError(b, err) } - zkTrie.db.db.Commit(common.Hash{}, true, nil) + zkTrie.db.Commit(common.Hash{}, true, nil) b.ResetTimer() for i := 0; i < b.N; i++ { binary.LittleEndian.PutUint64(k, uint64(i)) @@ -200,9 +198,9 @@ func BenchmarkZkTrieGet(b *testing.B) { func BenchmarkZkTrieUpdate(b *testing.B) { _, tmpdb := tempDBZK(b) - zkTrie, _ := NewZkTrie(common.Hash{}, NewZktrieDatabaseFromTriedb(tmpdb)) + zkTrie, _ := NewZkTrie(common.Hash{}, tmpdb) defer func() { - ldb := zkTrie.db.db.diskdb.(*leveldb.Database) + ldb := zkTrie.db.diskdb.(*leveldb.Database) ldb.Close() os.RemoveAll(ldb.Path()) }() @@ -219,7 +217,7 @@ func BenchmarkZkTrieUpdate(b *testing.B) { binary.LittleEndian.PutUint64(k, benchElemCountZk/2) //zkTrie.Commit(nil) - zkTrie.db.db.Commit(common.Hash{}, true, nil) + zkTrie.db.Commit(common.Hash{}, true, nil) b.ResetTimer() for i := 0; i < b.N; i++ { binary.LittleEndian.PutUint64(k, uint64(i)) @@ -264,3 +262,76 @@ func TestZkTrieDelete(t *testing.T) { assert.Equal(t, hashes[i].Hex(), hash.Hex()) } } + +func TestMorphAccountTrie(t *testing.T) { + // dir := "/data/morph/ethereum/geth/chaindata" + dir := "" + if len(dir) == 0 { + return + } + + db, err := rawdb.NewLevelDBDatabase(dir, 128, 1024, "", false) + if err != nil { + t.Fatalf("error opening database at %v: %v", dir, err) + } + + stored := rawdb.ReadCanonicalHash(db, 0) + header := rawdb.ReadHeader(db, stored, 0) + + println("stored:", stored.Hex()) + println("header:", header.Root.Hex()) + + headBlock := rawdb.ReadHeadBlock(db) + println("headBlock:", headBlock.NumberU64()) + root := headBlock.Root() + println("head state root:", root.Hex()) + + _, diskroot := rawdb.ReadAccountTrieNode(db, zkt.TrieRootPathKey[:]) + // diskroot = types.TrieRootHash(diskroot) + println("test state root:", diskroot.Hex()) + + // trieDb := NewDatabaseWithConfig(db, &Config{Zktrie: true, MorphZkTrie: true, PathDB: pathdb.Defaults}) + // varientTrie, _ := varienttrie.NewZkTrieWithPrefix(*zkt.NewByte32FromBytes(root.Bytes()), trieDb, rawdb.TrieNodeAccountPrefix) + + // calroot, _ := varientTrie.Tree().Root() + // println("recalculate state root:", calroot.Hex()) + + // var buffer bytes.Buffer + // err = varientTrie.Tree().GraphViz(&buffer, nil) + // assert.NoError(t, err) +} + +func TestZkSecureKey(t *testing.T) { + addr1 := common.HexToAddress("0x5300000000000000000000000000000000000004").Bytes() + addr2 := common.HexToAddress("0xc0D3c0D3c0D3c0d3c0d3C0d3C0d3c0D3C0D30004").Bytes() + addr3 := common.HexToAddress("0x523bff68043C818e9b449dd3Bee8ecCfa85D7E50").Bytes() + addr4 := common.HexToAddress("0x803DcE4D3f4Ae2e17AF6C51343040dEe320C149D").Bytes() + addr5 := common.HexToAddress("0x530000000000000000000000000000000000000D").Bytes() + addr6 := common.HexToAddress("0x530000000000000000000000000000000000000b").Bytes() + + k, _ := zkt.ToSecureKey(addr1) + hk := zkt.NewHashFromBigInt(k) + ck := common.BigToHash(k) + assert.Equal(t, hk.Hex(), "20e9fb498ff9c35246d527da24aa1710d2cc9b055ecf9a95a8a2a11d3d836cdf") + assert.Equal(t, ck.Hex(), "0x20e9fb498ff9c35246d527da24aa1710d2cc9b055ecf9a95a8a2a11d3d836cdf") + + k, _ = zkt.ToSecureKey(addr2) + hk = zkt.NewHashFromBigInt(k) + assert.Equal(t, hk.Hex(), "22e3957366ea5ad008a980d4bd48e10b72472a7838a7187d04b926fe80fd9c06") + + k, _ = zkt.ToSecureKey(addr3) + hk = zkt.NewHashFromBigInt(k) + assert.Equal(t, hk.Hex(), "0accc4c88536715cc403dbec52286fc5a0ee01e7e0f7257a2aa84f57b706b0e0") + + k, _ = zkt.ToSecureKey(addr4) + hk = zkt.NewHashFromBigInt(k) + assert.Equal(t, hk.Hex(), "1616e66d32ff1b5ad90c5e7db84045202dc719261bf13b0bb543873dcc135cc6") + + k, _ = zkt.ToSecureKey(addr5) + hk = zkt.NewHashFromBigInt(k) + assert.Equal(t, hk.Hex(), "19fd1d3fef5e6662c505f44512137d7f0fd6d5637856b4abd05a6438e2f07a7f") + + k, _ = zkt.ToSecureKey(addr6) + hk = zkt.NewHashFromBigInt(k) + assert.Equal(t, hk.Hex(), "0c3730cadca93736aec00976a03becc8b73c60233161bc87e213e5fdf3fe4c85") +} diff --git a/triedb/hashdb/metrics.go b/triedb/hashdb/metrics.go new file mode 100644 index 000000000..7ccb428d8 --- /dev/null +++ b/triedb/hashdb/metrics.go @@ -0,0 +1,29 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see + +package hashdb + +import "github.com/morph-l2/go-ethereum/metrics" + +var ( + memcacheCleanHitMeter = metrics.NewRegisteredMeter("trie/memcache/clean/hit", nil) + memcacheCleanMissMeter = metrics.NewRegisteredMeter("trie/memcache/clean/miss", nil) + memcacheCleanReadMeter = metrics.NewRegisteredMeter("trie/memcache/clean/read", nil) + memcacheCleanWriteMeter = metrics.NewRegisteredMeter("trie/memcache/clean/write", nil) + + memcacheCommitTimeTimer = metrics.NewRegisteredResettingTimer("trie/memcache/commit/time", nil) + memcacheCommitNodesMeter = metrics.NewRegisteredMeter("trie/memcache/commit/nodes", nil) +) diff --git a/triedb/hashdb/zk_trie_database.go b/triedb/hashdb/zk_trie_database.go new file mode 100644 index 000000000..ba9af28ee --- /dev/null +++ b/triedb/hashdb/zk_trie_database.go @@ -0,0 +1,230 @@ +package hashdb + +import ( + "runtime" + "sync" + "time" + + "github.com/VictoriaMetrics/fastcache" + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/core/rawdb" + "github.com/morph-l2/go-ethereum/ethdb" + "github.com/morph-l2/go-ethereum/log" + "github.com/morph-l2/go-ethereum/triedb/types" + + zktrie "github.com/scroll-tech/zktrie/trie" +) + +// Config defines all necessary options for database. +type Config struct { + Cache int // Memory allowance (MB) to use for caching trie nodes in memory + Journal string // Journal of clean cache to survive node restarts +} + +// Defaults is the default setting for database if it's not specified. +// Notably, clean cache is disabled explicitly, +var Defaults = &Config{ + // Explicitly set clean cache size to 0 to avoid creating fastcache, + // otherwise database must be closed when it's no longer needed to + // prevent memory leak. + Cache: 0, +} + +type ZktrieDatabase struct { + diskdb ethdb.KeyValueStore // Persistent storage for matured trie nodes + cleans *fastcache.Cache // GC friendly memory cache of clean node RLPs + prefix []byte + dirties types.KvMap + + lock sync.RWMutex + dirtiesSize common.StorageSize // Storage size of the dirty node cache (exc. metadata) +} + +func NewZkDatabaseWithConfig(diskdb ethdb.KeyValueStore, config *Config) *ZktrieDatabase { + if config == nil { + config = Defaults + } + + var cleans *fastcache.Cache + if config != nil && config.Cache > 0 { + if config.Journal == "" { + cleans = fastcache.New(config.Cache * 1024 * 1024) + } else { + cleans = fastcache.LoadFromFileOrNew(config.Journal, config.Cache*1024*1024) + } + } + + return &ZktrieDatabase{ + diskdb: diskdb, + cleans: cleans, + dirties: make(types.KvMap), + } +} + +func (db *ZktrieDatabase) Scheme() string { return rawdb.HashScheme } + +func (db *ZktrieDatabase) Size() (common.StorageSize, common.StorageSize, common.StorageSize) { + db.lock.RLock() + defer db.lock.RUnlock() + + // db.dirtiesSize only contains the useful data in the cache, but when reporting + // the total memory consumption, the maintenance metadata is also needed to be + // counted. + var metadataSize = common.StorageSize(len(db.dirties)) + return 0, 0, db.dirtiesSize + metadataSize +} + +func (db *ZktrieDatabase) CommitState(root common.Hash, parentRoot common.Hash, blockNumber uint64, report bool) error { + beforeDirtyCount, beforeDirtySize := len(db.dirties), db.dirtiesSize + + start := time.Now() + if err := db.commitAllDirties(); err != nil { + return err + } + memcacheCommitTimeTimer.Update(time.Since(start)) + memcacheCommitNodesMeter.Mark(int64(beforeDirtyCount - len(db.dirties))) + + logger := log.Debug + if report { + logger = log.Info + } + logger( + "Persisted trie from memory database", + "nodes", beforeDirtyCount-len(db.dirties), + "size", beforeDirtySize-db.dirtiesSize, + "time", time.Since(start), + "livenodes", len(db.dirties), + "livesize", db.dirtiesSize, + ) + return nil +} + +func (db *ZktrieDatabase) CommitGenesis(root common.Hash) error { + return db.CommitState(root, common.Hash{}, 0, true) +} + +func (db *ZktrieDatabase) commitAllDirties() error { + batch := db.diskdb.NewBatch() + + db.lock.Lock() + for _, v := range db.dirties { + batch.Put(v.K, v.V) + } + for k := range db.dirties { + delete(db.dirties, k) + } + db.lock.Unlock() + + if err := batch.Write(); err != nil { + return err + } + + batch.Reset() + return nil +} + +func (db *ZktrieDatabase) Close() error { return nil } +func (db *ZktrieDatabase) Cap(_ common.StorageSize) error { return nil } +func (db *ZktrieDatabase) Reference(_ common.Hash, _ common.Hash) {} +func (db *ZktrieDatabase) Dereference(_ common.Hash) {} + +func (db *ZktrieDatabase) Node(hash common.Hash) ([]byte, error) { + panic("ZktrieDatabase not implement Node()") +} + +// Put saves a key:value into the Storage +func (db *ZktrieDatabase) Put(k, v []byte) error { + k = common.BitReverse(k) + + db.lock.Lock() + db.dirties.Put(k, v) + db.lock.Unlock() + + if db.cleans != nil { + db.cleans.Set(k[:], v) + memcacheCleanMissMeter.Mark(1) + memcacheCleanWriteMeter.Mark(int64(len(v))) + } + return nil +} + +// Get retrieves a value from a key in the Storage +func (db *ZktrieDatabase) Get(key []byte) ([]byte, error) { + key = common.BitReverse(key[:]) + + db.lock.RLock() + value, ok := db.dirties.Get(key) + db.lock.RUnlock() + if ok { + return value, nil + } + + if db.cleans != nil { + if enc := db.cleans.Get(nil, key); enc != nil { + memcacheCleanHitMeter.Mark(1) + memcacheCleanReadMeter.Mark(int64(len(enc))) + return enc, nil + } + } + + v, err := db.diskdb.Get(key) + if rawdb.IsNotFoundErr(err) { + return nil, zktrie.ErrKeyNotFound + } + if err != nil && db.cleans != nil { + db.cleans.Set(key[:], v) + memcacheCleanMissMeter.Mark(1) + memcacheCleanWriteMeter.Mark(int64(len(v))) + } + return v, err +} + +// saveCache saves clean state cache to given directory path +// using specified CPU cores. +func (db *ZktrieDatabase) saveCache(dir string, threads int) error { + if db.cleans == nil { + return nil + } + log.Info("Writing clean trie cache to disk", "path", dir, "threads", threads) + + start := time.Now() + err := db.cleans.SaveToFileConcurrent(dir, threads) + if err != nil { + log.Error("Failed to persist clean trie cache", "error", err) + return err + } + log.Info("Persisted the clean trie cache", "path", dir, "elapsed", common.PrettyDuration(time.Since(start))) + return nil +} + +// SaveCache atomically saves fast cache data to the given dir using all +// available CPU cores. +func (db *ZktrieDatabase) SaveCache(dir string) error { + return db.saveCache(dir, runtime.GOMAXPROCS(0)) +} + +// SaveCachePeriodically atomically saves fast cache data to the given dir with +// the specified interval. All dump operation will only use a single CPU core. +func (db *ZktrieDatabase) SaveCachePeriodically(dir string, interval time.Duration, stopCh <-chan struct{}) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + db.saveCache(dir, 1) + case <-stopCh: + return + } + } +} + +func (db *ZktrieDatabase) Reader(root common.Hash) (*zkReader, error) { + return &zkReader{db: db}, nil +} + +type zkReader struct{ db *ZktrieDatabase } + +func (z zkReader) Node(path []byte) ([]byte, error) { + return z.db.Get(path) +} diff --git a/triedb/pathdb/asyncnodebuffer.go b/triedb/pathdb/asyncnodebuffer.go new file mode 100644 index 000000000..ec0f1220b --- /dev/null +++ b/triedb/pathdb/asyncnodebuffer.go @@ -0,0 +1,354 @@ +package pathdb + +import ( + "bytes" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/VictoriaMetrics/fastcache" + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/core/rawdb" + "github.com/morph-l2/go-ethereum/ethdb" + "github.com/morph-l2/go-ethereum/log" + dbtypes "github.com/morph-l2/go-ethereum/triedb/types" +) + +var _ trienodebuffer = &asyncnodebuffer{} + +// asyncnodebuffer implement trienodebuffer interface, and async the nodecache +// to disk. +type asyncnodebuffer struct { + mux sync.RWMutex + current *nodecache + background *nodecache + isFlushing atomic.Bool + stopFlushing atomic.Bool +} + +// newAsyncNodeBuffer initializes the async node buffer with the provided nodes. +func newAsyncNodeBuffer(limit int, nodes dbtypes.KvMap, layers uint64) *asyncnodebuffer { + if nodes == nil { + nodes = make(dbtypes.KvMap) + } + var size uint64 + for _, v := range nodes { + size += uint64(len(v.K) + len(v.V)) + } + + return &asyncnodebuffer{ + current: newNodeCache(uint64(limit), size, nodes, layers), + background: newNodeCache(uint64(limit), 0, make(dbtypes.KvMap), 0), + } +} + +// node retrieves the trie node with given node info. +func (a *asyncnodebuffer) node(path []byte) ([]byte, bool) { + a.mux.RLock() + defer a.mux.RUnlock() + + node := a.current.node(path) + if node == nil { + node = a.background.node(path) + } + return node, node != nil +} + +// commit merges the dirty nodes into the nodebuffer. This operation won't take +// the ownership of the nodes map which belongs to the bottom-most diff layer. +// It will just hold the node references from the given map which are safe to +// copy. +func (a *asyncnodebuffer) commit(nodes dbtypes.KvMap) trienodebuffer { + a.mux.Lock() + defer a.mux.Unlock() + + err := a.current.commit(nodes) + if err != nil { + log.Crit("[BUG] Failed to commit nodes to asyncnodebuffer", "error", err) + } + return a +} + +// setSize is unsupported in asyncnodebuffer, due to the double buffer, blocking will occur. +func (a *asyncnodebuffer) setSize(size int, db ethdb.KeyValueStore, clean *fastcache.Cache, id uint64) error { + return errors.New("not supported") +} + +// reset cleans up the disk cache. +func (a *asyncnodebuffer) reset() { + a.mux.Lock() + defer a.mux.Unlock() + + a.current.reset() + a.background.reset() +} + +// empty returns an indicator if nodebuffer contains any state transition inside. +func (a *asyncnodebuffer) empty() bool { + a.mux.RLock() + defer a.mux.RUnlock() + + return a.current.empty() && a.background.empty() +} + +// flush persists the in-memory dirty trie node into the disk if the configured +// memory threshold is reached. Note, all data must be written atomically. +func (a *asyncnodebuffer) flush(db ethdb.KeyValueStore, clean *fastcache.Cache, id uint64, force bool) error { + a.mux.Lock() + defer a.mux.Unlock() + + if a.stopFlushing.Load() { + return nil + } + + if force { + for { + if atomic.LoadUint64(&a.background.immutable) == 1 { + time.Sleep(time.Duration(DefaultBackgroundFlushInterval) * time.Second) + log.Info("Waiting background memory table flushed into disk for forcing flush node buffer") + continue + } + atomic.StoreUint64(&a.current.immutable, 1) + return a.current.flush(db, clean, id) + } + } + + if a.current.size < a.current.limit { + return nil + } + + // background flush doing + if atomic.LoadUint64(&a.background.immutable) == 1 { + return nil + } + + atomic.StoreUint64(&a.current.immutable, 1) + a.current, a.background = a.background, a.current + + a.isFlushing.Store(true) + go func(persistID uint64) { + defer a.isFlushing.Store(false) + for { + err := a.background.flush(db, clean, persistID) + if err == nil { + log.Debug("Succeed to flush background nodecache to disk", "state_id", persistID) + return + } + log.Error("Failed to flush background nodecache to disk", "state_id", persistID, "error", err) + } + }(id) + return nil +} + +func (a *asyncnodebuffer) waitAndStopFlushing() { + a.stopFlushing.Store(true) + for a.isFlushing.Load() { + time.Sleep(time.Second) + log.Warn("Waiting background memory table flushed into disk") + } +} + +func (a *asyncnodebuffer) getAllNodes() dbtypes.KvMap { + a.mux.Lock() + defer a.mux.Unlock() + + cached, err := a.current.merge(a.background) + if err != nil { + log.Crit("[BUG] Failed to merge node cache under revert async node buffer", "error", err) + } + return cached.nodes +} + +func (a *asyncnodebuffer) getLayers() uint64 { + a.mux.RLock() + defer a.mux.RUnlock() + + return a.current.layers + a.background.layers +} + +func (a *asyncnodebuffer) getSize() (uint64, uint64) { + a.mux.RLock() + defer a.mux.RUnlock() + + return a.current.size, a.background.size +} + +type nodecache struct { + layers uint64 // The number of diff layers aggregated inside + size uint64 // The size of aggregated writes + limit uint64 // The maximum memory allowance in bytes + nodes dbtypes.KvMap // The dirty node set, mapped by owner and path + immutable uint64 // The flag equal 1, flush nodes to disk background +} + +func newNodeCache(limit, size uint64, nodes dbtypes.KvMap, layers uint64) *nodecache { + return &nodecache{ + layers: layers, + size: size, + limit: limit, + nodes: nodes, + immutable: 0, + } +} + +func (nc *nodecache) node(path []byte) []byte { + n, ok := nc.nodes.Get(path) + if ok { + return n + } + return nil +} + +func (nc *nodecache) commit(nodes dbtypes.KvMap) error { + if atomic.LoadUint64(&nc.immutable) == 1 { + return errWriteImmutable + } + + var ( + delta int64 + overwrite int64 + overwriteSize int64 + ) + + for _, v := range nodes { + current, exist := nc.nodes.Get(v.K) + if !exist { + nc.nodes.Put(v.K, v.V) + delta += int64(len(v.K) + len(v.V)) + + continue + } + + if !bytes.Equal(current, v.V) { + delta += int64(len(v.V) - len(current)) + overwrite++ + overwriteSize += int64(len(v.V) + len(v.K)) + } + + nc.nodes.Put(v.K, v.V) + } + + nc.updateSize(delta) + nc.layers++ + gcNodesMeter.Mark(overwrite) + gcBytesMeter.Mark(overwriteSize) + return nil +} + +func (nc *nodecache) updateSize(delta int64) { + size := int64(nc.size) + delta + if size >= 0 { + nc.size = uint64(size) + return + } + s := nc.size + nc.size = 0 + log.Error("Invalid pathdb buffer size", "prev", common.StorageSize(s), "delta", common.StorageSize(delta)) +} + +func (nc *nodecache) reset() { + atomic.StoreUint64(&nc.immutable, 0) + nc.layers = 0 + nc.size = 0 + nc.nodes = make(dbtypes.KvMap) +} + +func (nc *nodecache) empty() bool { + return nc.layers == 0 +} + +func (nc *nodecache) flush(db ethdb.KeyValueStore, clean *fastcache.Cache, id uint64) error { + if atomic.LoadUint64(&nc.immutable) != 1 { + return errFlushMutable + } + + // Ensure the target state id is aligned with the internal counter. + head := rawdb.ReadPersistentStateID(db) + if head+nc.layers != id { + return fmt.Errorf("buffer layers (%d) cannot be applied on top of persisted state id (%d) to reach requested state id (%d)", nc.layers, head, id) + } + var ( + start = time.Now() + batch = db.NewBatchWithSize(int(float64(nc.size) * DefaultBatchRedundancyRate)) + ) + nodes := writeNodes(batch, nc.nodes, clean) + rawdb.WritePersistentStateID(batch, id) + + // Flush all mutations in a single batch + size := batch.ValueSize() + if err := batch.Write(); err != nil { + return err + } + commitBytesMeter.Mark(int64(size)) + commitNodesMeter.Mark(int64(nodes)) + commitTimeTimer.UpdateSince(start) + log.Debug("Persisted pathdb nodes", "nodes", len(nc.nodes), "bytes", common.StorageSize(size), "elapsed", common.PrettyDuration(time.Since(start))) + nc.reset() + return nil +} + +func (nc *nodecache) merge(nc1 *nodecache) (*nodecache, error) { + if nc == nil && nc1 == nil { + return nil, nil + } + if nc == nil || nc.empty() { + res := copyNodeCache(nc1) + atomic.StoreUint64(&res.immutable, 0) + return res, nil + } + if nc1 == nil || nc1.empty() { + res := copyNodeCache(nc) + atomic.StoreUint64(&res.immutable, 0) + return res, nil + } + if atomic.LoadUint64(&nc.immutable) == atomic.LoadUint64(&nc1.immutable) { + return nil, errIncompatibleMerge + } + + var ( + immutable *nodecache + mutable *nodecache + res = &nodecache{} + ) + if atomic.LoadUint64(&nc.immutable) == 1 { + immutable = nc + mutable = nc1 + } else { + immutable = nc1 + mutable = nc + } + res.size = immutable.size + mutable.size + res.layers = immutable.layers + mutable.layers + res.limit = immutable.limit + res.nodes = make(dbtypes.KvMap) + for _, v := range immutable.nodes { + res.nodes.Put(v.K, v.V) + } + + for _, v := range mutable.nodes { + res.nodes.Put(v.K, v.V) + } + + return res, nil +} + +func copyNodeCache(n *nodecache) *nodecache { + if n == nil { + return nil + } + nc := &nodecache{ + layers: n.layers, + size: n.size, + limit: n.limit, + immutable: atomic.LoadUint64(&n.immutable), + nodes: make(dbtypes.KvMap), + } + + for _, v := range n.nodes { + nc.nodes.Put(v.K, v.V) + } + + return nc +} diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go new file mode 100644 index 000000000..19c5f161f --- /dev/null +++ b/triedb/pathdb/database.go @@ -0,0 +1,385 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package pathdb + +import ( + "fmt" + "io" + "os" + "sort" + "strconv" + "sync" + + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/core/rawdb" + "github.com/morph-l2/go-ethereum/core/types" + "github.com/morph-l2/go-ethereum/ethdb" + "github.com/morph-l2/go-ethereum/log" + "github.com/morph-l2/go-ethereum/params" + dbtypes "github.com/morph-l2/go-ethereum/triedb/types" +) + +const ( + // maxDiffLayers is the maximum diff layers allowed in the layer tree. + maxDiffLayers = 128 + + // defaultCleanSize is the default memory allowance of clean cache. + defaultCleanSize = 16 * 1024 * 1024 + + // MaxDirtyBufferSize is the maximum memory allowance of node buffer. + // Too large nodebuffer will cause the system to pause for a long + // time when write happens. Also, the largest batch that pebble can + // support is 4GB, node will panic if batch size exceeds this limit. + MaxDirtyBufferSize = 256 * 1024 * 1024 + + // DefaultDirtyBufferSize is the default memory allowance of node buffer + // that aggregates the writes from above until it's flushed into the + // disk. It's meant to be used once the initial sync is finished. + // Do not increase the buffer size arbitrarily, otherwise the system + // pause time will increase when the database writes happen. + DefaultDirtyBufferSize = 64 * 1024 * 1024 + + // DefaultBackgroundFlushInterval defines the default the wait interval + // that background node cache flush disk. + DefaultBackgroundFlushInterval = 3 + + // DefaultBatchRedundancyRate defines the batch size, compatible write + // size calculation is inaccurate + DefaultBatchRedundancyRate = 1.1 +) + +type JournalType int + +const ( + JournalKVType JournalType = iota + JournalFileType +) + +// layer is the interface implemented by all state layers which includes some +// public methods and some additional methods for internal usage. +type layer interface { + // Node retrieves the trie node with the node info. An error will be returned + // if the read operation exits abnormally. For example, if the layer is already + // stale, or the associated state is regarded as corrupted. Notably, no error + // will be returned if the requested node is not found in database. + Node(path []byte) ([]byte, error) + + // rootHash returns the root hash for which this layer was made. + rootHash() common.Hash + + // stateID returns the associated state id of layer. + stateID() uint64 + + // parentLayer returns the subsequent layer of it, or nil if the disk was reached. + parentLayer() layer + + // update creates a new layer on top of the existing layer diff tree with + // the provided dirty trie nodes along with the state change set. + // + // Note, the maps are retained by the method to avoid copying everything. + update(root common.Hash, id uint64, block uint64, nodes dbtypes.KvMap) *diffLayer + + // journal commits an entire diff hierarchy to disk into a single journal entry. + // This is meant to be used during shutdown to persist the layer without + // flattening everything down (bad for reorgs). + journal(w io.Writer, journalType JournalType) error +} + +// Config contains the settings for database. +type Config struct { + SyncFlush bool // Flag of trienodebuffer sync flush cache to disk + StateHistory uint64 // Number of recent blocks to maintain state history for + CleanCacheSize int // Maximum memory allowance (in bytes) for caching clean nodes + DirtyCacheSize int // Maximum memory allowance (in bytes) for caching dirty nodes + ReadOnly bool // Flag whether the database is opened in read only mode. + NoTries bool + JournalFilePath string + JournalFile bool +} + +// sanitize checks the provided user configurations and changes anything that's +// unreasonable or unworkable. +func (c *Config) sanitize() *Config { + conf := *c + if conf.DirtyCacheSize > MaxDirtyBufferSize { + log.Warn("Sanitizing invalid node buffer size", "provided", common.StorageSize(conf.DirtyCacheSize), "updated", common.StorageSize(MaxDirtyBufferSize)) + conf.DirtyCacheSize = MaxDirtyBufferSize + } + return &conf +} + +// Defaults contains default settings for Ethereum mainnet. +var Defaults = &Config{ + StateHistory: params.FullImmutabilityThreshold, + CleanCacheSize: defaultCleanSize, + DirtyCacheSize: DefaultDirtyBufferSize, +} + +// ReadOnly is the config in order to open database in read only mode. +var ReadOnly = &Config{ReadOnly: true} + +// Database is a multiple-layered structure for maintaining in-memory trie nodes. +// It consists of one persistent base layer backed by a key-value store, on top +// of which arbitrarily many in-memory diff layers are stacked. The memory diffs +// can form a tree with branching, but the disk layer is singleton and common to +// all. If a reorg goes deeper than the disk layer, a batch of reverse diffs can +// be applied to rollback. The deepest reorg that can be handled depends on the +// amount of state histories tracked in the disk. +// +// At most one readable and writable database can be opened at the same time in +// the whole system which ensures that only one database writer can operate disk +// state. Unexpected open operations can cause the system to panic. +type Database struct { + // readOnly is the flag whether the mutation is allowed to be applied. + // It will be set automatically when the database is journaled during + // the shutdown to reject all following unexpected mutations. + readOnly bool // Flag if database is opened in read only mode + bufferSize int // Memory allowance (in bytes) for caching dirty nodes + config *Config // Configuration for database + diskdb ethdb.KeyValueStore // Persistent storage for matured trie nodes + tree *layerTree // The group for all known layers + lock sync.RWMutex // Lock to prevent mutations from happening at the same time + dirties dbtypes.KvMap +} + +// New attempts to load an already existing layer from a persistent key-value +// store (with a number of memory layers from a journal). If the journal is not +// matched with the base persistent layer, all the recorded diff layers are discarded. +func New(diskdb ethdb.KeyValueStore, config *Config) *Database { + if config == nil { + config = Defaults + } + config = config.sanitize() + db := &Database{ + readOnly: config.ReadOnly, + bufferSize: config.DirtyCacheSize, + config: config, + diskdb: diskdb, + dirties: make(dbtypes.KvMap), + } + // Construct the layer tree by resolving the in-disk singleton state + // and in-memory layer journal. + db.tree = newLayerTree(db.loadLayers()) + + return db +} + +// Reader retrieves a layer belonging to the given state root. +func (db *Database) Reader(root common.Hash) (layer, error) { + l := db.tree.get(root) + if l == nil { + return nil, fmt.Errorf("state %#x is not available", root) + } + return l, nil +} + +func (db *Database) CommitGenesis(root common.Hash) error { + log.Info("pathdb write genesis state to disk", "root", root.Hex()) + batch := db.diskdb.NewBatch() + for _, v := range db.dirties { + batch.Put(v.K, v.V) + } + for k := range db.dirties { + delete(db.dirties, k) + } + if err := batch.Write(); err != nil { + return err + } + batch.Reset() + + // Update stateID + rawdb.WriteStateID(db.diskdb, root, 0) + return nil +} + +// Commit traverses downwards the layer tree from a specified layer with the +// provided state root and all the layers below are flattened downwards. It +// can be used alone and mostly for test purposes. +func (db *Database) CommitState(root common.Hash, parentRoot common.Hash, blockNumber uint64, report bool) error { + // Hold the lock to prevent concurrent mutations. + db.lock.Lock() + defer db.lock.Unlock() + + // Short circuit if the mutation is not allowed. + if err := db.modifyAllowed(); err != nil { + return err + } + + // only 1 entity, state have no changes + // some block maybe has no txns, so state do not change + if root == parentRoot && len(db.dirties) == 1 { + return nil + } + + if err := db.tree.add(root, parentRoot, blockNumber, db.dirties); err != nil { + db.dirties = make(dbtypes.KvMap) + return err + } + db.dirties = make(dbtypes.KvMap) + + // Keep 128 diff layers in the memory, persistent layer is 129th. + // - head layer is paired with HEAD state + // - head-1 layer is paired with HEAD-1 state + // - head-127 layer(bottom-most diff layer) is paired with HEAD-127 state + // - head-128 layer(disk layer) is paired with HEAD-128 state + return db.tree.cap(root, maxDiffLayers) +} + +// Close closes the trie database and the held freezer. +func (db *Database) Close() error { + db.lock.Lock() + defer db.lock.Unlock() + + // Set the database to read-only mode to prevent all + // following mutations. + db.readOnly = true + + // Release the memory held by clean cache. + db.tree.bottom().resetCache() + + return nil +} + +// Size returns the current storage size of the memory cache in front of the +// persistent database layer. +func (db *Database) Size() (diffs common.StorageSize, nodes common.StorageSize, immutableNodes common.StorageSize) { + db.tree.forEach(func(layer layer) { + if diff, ok := layer.(*diffLayer); ok { + diffs += common.StorageSize(diff.memory) + } + if disk, ok := layer.(*diskLayer); ok { + nodes, immutableNodes = disk.size() + } + }) + return diffs, nodes, immutableNodes +} + +// Initialized returns an indicator if the state data is already +// initialized in path-based scheme. +func (db *Database) Initialized(genesisRoot common.Hash) bool { + var inited bool + db.tree.forEach(func(layer layer) { + if layer.rootHash() != types.EmptyRootHash { + inited = true + } + }) + + return inited +} + +// SetBufferSize sets the node buffer size to the provided value(in bytes). +func (db *Database) SetBufferSize(size int) error { + db.lock.Lock() + defer db.lock.Unlock() + + if size > MaxDirtyBufferSize { + log.Info("Capped node buffer size", "provided", common.StorageSize(size), "adjusted", common.StorageSize(MaxDirtyBufferSize)) + size = MaxDirtyBufferSize + } + db.bufferSize = size + return db.tree.bottom().setBufferSize(db.bufferSize) +} + +// Scheme returns the node scheme used in the database. +func (db *Database) Scheme() string { + return rawdb.PathScheme +} + +// Head return the top non-fork difflayer/disklayer root hash for rewinding. +func (db *Database) Head() common.Hash { + db.lock.Lock() + defer db.lock.Unlock() + return db.tree.front() +} + +// modifyAllowed returns the indicator if mutation is allowed. This function +// assumes the db.lock is already held. +func (db *Database) modifyAllowed() error { + if db.readOnly { + return errDatabaseReadOnly + } + return nil +} + +// GetAllRootHash returns all diffLayer and diskLayer root hash +func (db *Database) GetAllRootHash() [][]string { + db.lock.Lock() + defer db.lock.Unlock() + + data := make([][]string, 0, len(db.tree.layers)) + for _, v := range db.tree.layers { + if dl, ok := v.(*diffLayer); ok { + data = append(data, []string{fmt.Sprintf("%d", dl.block), dl.rootHash().String()}) + } + } + sort.Slice(data, func(i, j int) bool { + block1, _ := strconv.Atoi(data[i][0]) + block2, _ := strconv.Atoi(data[j][0]) + return block1 > block2 + }) + + data = append(data, []string{"-1", db.tree.bottom().rootHash().String()}) + return data +} + +// DetermineJournalTypeForWriter is used when persisting the journal. It determines JournalType based on the config passed in by the Config. +func (db *Database) DetermineJournalTypeForWriter() JournalType { + if db.config.JournalFile { + return JournalFileType + } else { + return JournalKVType + } +} + +// DetermineJournalTypeForReader is used when loading the journal. It loads based on whether JournalKV or JournalFile currently exists. +func (db *Database) DetermineJournalTypeForReader() JournalType { + if journal := rawdb.ReadTrieJournal(db.diskdb); len(journal) != 0 { + return JournalKVType + } + + if fileInfo, stateErr := os.Stat(db.config.JournalFilePath); stateErr == nil && !fileInfo.IsDir() { + return JournalFileType + } + + return JournalKVType +} + +func (db *Database) DeleteTrieJournal(writer ethdb.KeyValueWriter) error { + // To prevent any remnants of old journals after converting from JournalKV to JournalFile or vice versa, all deletions must be completed. + rawdb.DeleteTrieJournal(writer) + + // delete from journal file, may not exist + filePath := db.config.JournalFilePath + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return nil + } + errRemove := os.Remove(filePath) + if errRemove != nil { + log.Crit("Failed to remove tries journal", "journal path", filePath, "err", errRemove) + } + return nil +} + +// zk-trie put dirties +func (db *Database) Put(k, v []byte) error { + db.lock.Lock() + defer db.lock.Unlock() + + key := rawdb.CompactStorageTrieNodeKey(k[:]) + db.dirties.Put(key, v) + return nil +} diff --git a/triedb/pathdb/difflayer.go b/triedb/pathdb/difflayer.go new file mode 100644 index 000000000..36878b4f6 --- /dev/null +++ b/triedb/pathdb/difflayer.go @@ -0,0 +1,187 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package pathdb + +import ( + "fmt" + "sync" + + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/log" + dbtypes "github.com/morph-l2/go-ethereum/triedb/types" +) + +// diffLayer represents a collection of modifications made to the in-memory tries +// along with associated state changes after running a block on top. +// +// The goal of a diff layer is to act as a journal, tracking recent modifications +// made to the state, that have not yet graduated into a semi-immutable state. +type diffLayer struct { + // Immutables + root common.Hash // Root hash to which this layer diff belongs to + id uint64 // Corresponding state id + block uint64 // Associated block number + nodes dbtypes.KvMap // Cached trie nodes indexed by owner and path + memory uint64 // Approximate guess as to how much memory we use + + // mutables + origin *diskLayer // The current difflayer corresponds to the underlying disklayer and is updated during cap. + parent layer // Parent layer modified by this one, never nil, **can be changed** + lock sync.RWMutex // Lock used to protect parent +} + +// newDiffLayer creates a new diff layer on top of an existing layer. +func newDiffLayer(parent layer, root common.Hash, id uint64, block uint64, nodes dbtypes.KvMap) *diffLayer { + var ( + size int64 + count int + ) + dl := &diffLayer{ + root: root, + id: id, + block: block, + nodes: nodes, + parent: parent, + } + + switch l := parent.(type) { + case *diskLayer: + dl.origin = l + case *diffLayer: + dl.origin = l.originDiskLayer() + default: + panic("unknown parent type") + } + + for _, v := range nodes { + dl.memory += uint64(len(v.K) + len(v.V)) + size += int64(len(v.K) + len(v.V)) + count += 1 + } + + dirtyWriteMeter.Mark(size) + diffLayerNodesMeter.Mark(int64(count)) + diffLayerBytesMeter.Mark(int64(dl.memory)) + log.Debug("Created new diff layer", "id", id, "block", block, "nodes", count, "size", common.StorageSize(dl.memory), "root", dl.root) + return dl +} + +func (dl *diffLayer) originDiskLayer() *diskLayer { + dl.lock.RLock() + defer dl.lock.RUnlock() + return dl.origin +} + +func (dl *diffLayer) updateOriginDiskLayer(persistLayer *diskLayer) { + dl.lock.Lock() + defer dl.lock.Unlock() + dl.origin = persistLayer +} + +// rootHash implements the layer interface, returning the root hash of +// corresponding state. +func (dl *diffLayer) rootHash() common.Hash { + return dl.root +} + +// stateID implements the layer interface, returning the state id of the layer. +func (dl *diffLayer) stateID() uint64 { + return dl.id +} + +// parentLayer implements the layer interface, returning the subsequent +// layer of the diff layer. +func (dl *diffLayer) parentLayer() layer { + dl.lock.RLock() + defer dl.lock.RUnlock() + + return dl.parent +} + +// node retrieves the node with provided node information. It's the internal +// version of Node function with additional accessed layer tracked. No error +// will be returned if node is not found. +func (dl *diffLayer) node(path []byte, depth int) ([]byte, error) { + // Hold the lock, ensure the parent won't be changed during the + // state accessing. + dl.lock.RLock() + defer dl.lock.RUnlock() + + // If the trie node is known locally, return it + n, ok := dl.nodes.Get(path) + if ok { + return n, nil + } + // Trie node unknown to this layer, resolve from parent + if diff, ok := dl.parent.(*diffLayer); ok { + return diff.node(path, depth+1) + } + // Failed to resolve through diff layers, fallback to disk layer + return dl.parent.Node(path) +} + +// Node implements the layer interface, retrieving the trie node blob with the +// provided node information. No error will be returned if the node is not found. +func (dl *diffLayer) Node(path []byte) ([]byte, error) { + return dl.node(path, 0) +} + +// update implements the layer interface, creating a new layer on top of the +// existing layer tree with the specified data items. +func (dl *diffLayer) update(root common.Hash, id uint64, block uint64, nodes dbtypes.KvMap) *diffLayer { + return newDiffLayer(dl, root, id, block, nodes) +} + +// persist flushes the diff layer and all its parent layers to disk layer. +func (dl *diffLayer) persist(force bool) (layer, error) { + if parent, ok := dl.parentLayer().(*diffLayer); ok { + // Hold the lock to prevent any read operation until the new + // parent is linked correctly. + dl.lock.Lock() + + // The merging of diff layers starts at the bottom-most layer, + // therefore we recurse down here, flattening on the way up + // (diffToDisk). + result, err := parent.persist(force) + if err != nil { + dl.lock.Unlock() + return nil, err + } + dl.parent = result + dl.lock.Unlock() + } + return diffToDisk(dl, force) +} + +// diffToDisk merges a bottom-most diff into the persistent disk layer underneath +// it. The method will panic if called onto a non-bottom-most diff layer. +func diffToDisk(layer *diffLayer, force bool) (layer, error) { + disk, ok := layer.parentLayer().(*diskLayer) + if !ok { + panic(fmt.Sprintf("unknown layer type: %T", layer.parentLayer())) + } + return disk.commit(layer, force) +} + +func (dl *diffLayer) reset() { + // Hold the lock, ensure the parent won't be changed during the + // state accessing. + dl.lock.RLock() + defer dl.lock.RUnlock() + + dl.nodes = make(dbtypes.KvMap) +} diff --git a/triedb/pathdb/disklayer.go b/triedb/pathdb/disklayer.go new file mode 100644 index 000000000..2c600dacb --- /dev/null +++ b/triedb/pathdb/disklayer.go @@ -0,0 +1,263 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package pathdb + +import ( + "sync" + + "github.com/VictoriaMetrics/fastcache" + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/core/rawdb" + "github.com/morph-l2/go-ethereum/ethdb" + "github.com/morph-l2/go-ethereum/log" + dbtypes "github.com/morph-l2/go-ethereum/triedb/types" +) + +// trienodebuffer is a collection of modified trie nodes to aggregate the disk +// write. The content of the trienodebuffer must be checked before diving into +// disk (since it basically is not-yet-written data). +type trienodebuffer interface { + // node retrieves the trie node with given node info. + node(path []byte) ([]byte, bool) + + // commit merges the dirty nodes into the trienodebuffer. This operation won't take + // the ownership of the nodes map which belongs to the bottom-most diff layer. + // It will just hold the node references from the given map which are safe to + // copy. + commit(nodes dbtypes.KvMap) trienodebuffer + + // flush persists the in-memory dirty trie node into the disk if the configured + // memory threshold is reached. Note, all data must be written atomically. + flush(db ethdb.KeyValueStore, clean *fastcache.Cache, id uint64, force bool) error + + // setSize sets the buffer size to the provided number, and invokes a flush + // operation if the current memory usage exceeds the new limit. + setSize(size int, db ethdb.KeyValueStore, clean *fastcache.Cache, id uint64) error + + // reset cleans up the disk cache. + reset() + + // empty returns an indicator if trienodebuffer contains any state transition inside. + empty() bool + + // getSize return the trienodebuffer used size. + getSize() (uint64, uint64) + + // getAllNodes return all the trie nodes are cached in trienodebuffer. + getAllNodes() dbtypes.KvMap + + // getLayers return the size of cached difflayers. + getLayers() uint64 + + // waitAndStopFlushing will block unit writing the trie nodes of trienodebuffer to disk. + waitAndStopFlushing() +} + +func NewTrieNodeBuffer(sync bool, limit int, nodes dbtypes.KvMap, layers uint64) trienodebuffer { + if sync { + log.Info("New sync node buffer", "limit", common.StorageSize(limit), "layers", layers) + return newNodeBuffer(limit, nodes, layers) + } + log.Info("New async node buffer", "limit", common.StorageSize(limit), "layers", layers) + return newAsyncNodeBuffer(limit, nodes, layers) +} + +// diskLayer is a low level persistent layer built on top of a key-value store. +type diskLayer struct { + root common.Hash // Immutable, root hash to which this layer was made for + id uint64 // Immutable, corresponding state id + db *Database // Path-based trie database + cleans *fastcache.Cache // GC friendly memory cache of clean node RLPs + buffer trienodebuffer // Node buffer to aggregate writes + stale bool // Signals that the layer became stale (state progressed) + lock sync.RWMutex // Lock used to protect stale flag +} + +// newDiskLayer creates a new disk layer based on the passing arguments. +func newDiskLayer(root common.Hash, id uint64, db *Database, cleans *fastcache.Cache, buffer trienodebuffer) *diskLayer { + // Initialize a clean cache if the memory allowance is not zero + // or reuse the provided cache if it is not nil (inherited from + // the original disk layer). + if cleans == nil && db.config.CleanCacheSize != 0 { + cleans = fastcache.New(db.config.CleanCacheSize) + } + return &diskLayer{ + root: root, + id: id, + db: db, + cleans: cleans, + buffer: buffer, + } +} + +// rootHash implements the layer interface, returning root hash of corresponding state. +func (dl *diskLayer) rootHash() common.Hash { + return dl.root +} + +// stateID implements the layer interface, returning the state id of disk layer. +func (dl *diskLayer) stateID() uint64 { + return dl.id +} + +// parentLayer implements the layer interface, returning nil as there's no layer +// below the disk. +func (dl *diskLayer) parentLayer() layer { + return nil +} + +// isStale return whether this layer has become stale (was flattened across) or if +// it's still live. +func (dl *diskLayer) isStale() bool { + dl.lock.RLock() + defer dl.lock.RUnlock() + + return dl.stale +} + +// markStale sets the stale flag as true. +func (dl *diskLayer) markStale() { + dl.lock.Lock() + defer dl.lock.Unlock() + + if dl.stale { + panic("triedb disk layer is stale") // we've committed into the same base from two children, boom + } + dl.stale = true +} + +// Node implements the layer interface, retrieving the trie node with the +// provided node info. No error will be returned if the node is not found. +func (dl *diskLayer) Node(path []byte) ([]byte, error) { + dl.lock.RLock() + defer dl.lock.RUnlock() + + if dl.stale { + return nil, errSnapshotStale + } + // Try to retrieve the trie node from the not-yet-written + // node buffer first. Note the buffer is lock free since + // it's impossible to mutate the buffer before tagging the + // layer as stale. + n, found := dl.buffer.node(path) + if found { + dirtyHitMeter.Mark(1) + dirtyReadMeter.Mark(int64(len(n))) + return n, nil + } + dirtyMissMeter.Mark(1) + + // Try to retrieve the trie node from the clean memory cache + key := path + if dl.cleans != nil { + if blob := dl.cleans.Get(nil, key); len(blob) > 0 { + cleanHitMeter.Mark(1) + cleanReadMeter.Mark(int64(len(blob))) + return blob, nil + } + cleanMissMeter.Mark(1) + } + + // Try to retrieve the trie node from the disk. + n, err := rawdb.ReadTrieNodeByKey(dl.db.diskdb, path) + if err == nil { + if dl.cleans != nil && len(n) > 0 { + dl.cleans.Set(key, n) + cleanWriteMeter.Mark(int64(len(n))) + } + return n, nil + } + return nil, err +} + +// update implements the layer interface, returning a new diff layer on top +// with the given state set. +func (dl *diskLayer) update(root common.Hash, id uint64, block uint64, nodes dbtypes.KvMap) *diffLayer { + return newDiffLayer(dl, root, id, block, nodes) +} + +// commit merges the given bottom-most diff layer into the node buffer +// and returns a newly constructed disk layer. Note the current disk +// layer must be tagged as stale first to prevent re-access. +func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) { + dl.lock.Lock() + defer dl.lock.Unlock() + + // Construct and store the state history first. If crash happens after storing + // the state history but without flushing the corresponding states(journal), + // the stored state history will be truncated from head in the next restart. + + // Mark the diskLayer as stale before applying any mutations on top. + dl.stale = true + + // Store the root->id lookup afterwards. All stored lookups are identified + // by the **unique** state root. It's impossible that in the same chain + // blocks are not adjacent but have the same root. + if dl.id == 0 { + rawdb.WriteStateID(dl.db.diskdb, dl.root, 0) + } + rawdb.WriteStateID(dl.db.diskdb, bottom.rootHash(), bottom.stateID()) + + // Construct a new disk layer by merging the nodes from the provided diff + // layer, and flush the content in disk layer if there are too many nodes + // cached. The clean cache is inherited from the original disk layer. + + ndl := newDiskLayer(bottom.root, bottom.stateID(), dl.db, dl.cleans, dl.buffer.commit(bottom.nodes)) + + if err := ndl.buffer.flush(ndl.db.diskdb, ndl.cleans, ndl.id, force); err != nil { + return nil, err + } + + return ndl, nil +} + +// setBufferSize sets the trie node buffer size to the provided value. +func (dl *diskLayer) setBufferSize(size int) error { + dl.lock.RLock() + defer dl.lock.RUnlock() + + if dl.stale { + return errSnapshotStale + } + return dl.buffer.setSize(size, dl.db.diskdb, dl.cleans, dl.id) +} + +// size returns the approximate size of cached nodes in the disk layer. +func (dl *diskLayer) size() (common.StorageSize, common.StorageSize) { + dl.lock.RLock() + defer dl.lock.RUnlock() + + if dl.stale { + return 0, 0 + } + dirtyNodes, dirtyimmutableNodes := dl.buffer.getSize() + return common.StorageSize(dirtyNodes), common.StorageSize(dirtyimmutableNodes) +} + +// resetCache releases the memory held by clean cache to prevent memory leak. +func (dl *diskLayer) resetCache() { + dl.lock.RLock() + defer dl.lock.RUnlock() + + // Stale disk layer loses the ownership of clean cache. + if dl.stale { + return + } + if dl.cleans != nil { + dl.cleans.Reset() + } +} diff --git a/triedb/pathdb/errors.go b/triedb/pathdb/errors.go new file mode 100644 index 000000000..c62b6b0ab --- /dev/null +++ b/triedb/pathdb/errors.go @@ -0,0 +1,43 @@ +// Copyright 2023 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see + +package pathdb + +import ( + "errors" +) + +var ( + // errDatabaseReadOnly is returned if the database is opened in read only mode + // to prevent any mutation. + errDatabaseReadOnly = errors.New("read only") + + // errSnapshotStale is returned from data accessors if the underlying layer + // layer had been invalidated due to the chain progressing forward far enough + // to not maintain the layer's original state. + errSnapshotStale = errors.New("layer stale") + + // errWriteImmutable is returned if write to background immutable nodecache + // under asyncnodebuffer + errWriteImmutable = errors.New("write immutable nodecache") + + // errFlushMutable is returned if flush the background mutable nodecache + // to disk, under asyncnodebuffer + errFlushMutable = errors.New("flush mutable nodecache") + + // errIncompatibleMerge is returned when merge node cache occurs error. + errIncompatibleMerge = errors.New("incompatible nodecache merge") +) diff --git a/triedb/pathdb/journal.go b/triedb/pathdb/journal.go new file mode 100644 index 000000000..512ec5c6c --- /dev/null +++ b/triedb/pathdb/journal.go @@ -0,0 +1,520 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package pathdb + +import ( + "bytes" + "crypto/sha256" + "errors" + "fmt" + "io" + "io/fs" + "os" + "time" + + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/core/rawdb" + "github.com/morph-l2/go-ethereum/core/types" + "github.com/morph-l2/go-ethereum/ethdb" + "github.com/morph-l2/go-ethereum/log" + "github.com/morph-l2/go-ethereum/rlp" + + dbtypes "github.com/morph-l2/go-ethereum/triedb/types" + zkt "github.com/scroll-tech/zktrie/types" +) + +var ( + errMissJournal = errors.New("journal not found") + errMissVersion = errors.New("version not found") + errUnexpectedVersion = errors.New("unexpected journal version") + errMissDiskRoot = errors.New("disk layer root not found") + errUnmatchedJournal = errors.New("unmatched journal") +) + +const journalVersion uint64 = 0 + +// journalNode represents a trie node persisted in the journal. +type journalNode struct { + Path []byte // Path of the node in the trie + Blob []byte // RLP-encoded trie node blob, nil means the node is deleted +} + +type JournalWriter interface { + io.Writer + + Close() + Size() uint64 +} + +type JournalReader interface { + io.Reader + Close() +} + +type JournalFileWriter struct { + file *os.File +} + +type JournalFileReader struct { + file *os.File +} + +type JournalKVWriter struct { + journalBuf bytes.Buffer + diskdb ethdb.KeyValueStore +} + +type JournalKVReader struct { + journalBuf *bytes.Buffer +} + +// Write appends b directly to the encoder output. +func (fw *JournalFileWriter) Write(b []byte) (int, error) { + return fw.file.Write(b) +} + +func (fw *JournalFileWriter) Close() { + fw.file.Close() +} + +func (fw *JournalFileWriter) Size() uint64 { + if fw.file == nil { + return 0 + } + fileInfo, err := fw.file.Stat() + if err != nil { + log.Crit("Failed to stat journal", "err", err) + } + return uint64(fileInfo.Size()) +} + +func (kw *JournalKVWriter) Write(b []byte) (int, error) { + return kw.journalBuf.Write(b) +} + +func (kw *JournalKVWriter) Close() { + rawdb.WriteTrieJournal(kw.diskdb, kw.journalBuf.Bytes()) + kw.journalBuf.Reset() +} + +func (kw *JournalKVWriter) Size() uint64 { + return uint64(kw.journalBuf.Len()) +} + +func (fr *JournalFileReader) Read(p []byte) (n int, err error) { + return fr.file.Read(p) +} + +func (fr *JournalFileReader) Close() { + fr.file.Close() +} + +func (kr *JournalKVReader) Read(p []byte) (n int, err error) { + return kr.journalBuf.Read(p) +} + +func (kr *JournalKVReader) Close() { +} + +func newJournalWriter(file string, db ethdb.KeyValueStore, journalType JournalType) JournalWriter { + if journalType == JournalKVType { + log.Info("New journal writer for journal kv") + return &JournalKVWriter{ + diskdb: db, + } + } else { + log.Info("New journal writer for journal file", "path", file) + fd, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return nil + } + return &JournalFileWriter{ + file: fd, + } + } +} + +func newJournalReader(file string, db ethdb.KeyValueStore, journalType JournalType) (JournalReader, error) { + if journalType == JournalKVType { + log.Info("New journal reader for journal kv") + journal := rawdb.ReadTrieJournal(db) + if len(journal) == 0 { + return nil, errMissJournal + } + return &JournalKVReader{ + journalBuf: bytes.NewBuffer(journal), + }, nil + } else { + log.Info("New journal reader for journal file", "path", file) + fd, err := os.Open(file) + if errors.Is(err, fs.ErrNotExist) { + return nil, errMissJournal + } + if err != nil { + return nil, err + } + return &JournalFileReader{ + file: fd, + }, nil + } +} + +// loadJournal tries to parse the layer journal from the disk. +func (db *Database) loadJournal(diskRoot common.Hash) (layer, error) { + start := time.Now() + journalTypeForReader := db.DetermineJournalTypeForReader() + reader, err := newJournalReader(db.config.JournalFilePath, db.diskdb, journalTypeForReader) + + if err != nil { + return nil, err + } + if reader != nil { + defer reader.Close() + } + r := rlp.NewStream(reader, 0) + + // Firstly, resolve the first element as the journal version + version, err := r.Uint64() + if err != nil { + return nil, errMissVersion + } + if version != journalVersion { + return nil, fmt.Errorf("%w want %d got %d", errUnexpectedVersion, journalVersion, version) + } + // Secondly, resolve the disk layer root, ensure it's continuous + // with disk layer. Note now we can ensure it's the layer journal + // correct version, so we expect everything can be resolved properly. + var root common.Hash + if err := r.Decode(&root); err != nil { + return nil, errMissDiskRoot + } + // The journal is not matched with persistent state, discard them. + // It can happen that geth crashes without persisting the journal. + if !bytes.Equal(root.Bytes(), diskRoot.Bytes()) { + return nil, fmt.Errorf("%w want %x got %x", errUnmatchedJournal, root, diskRoot) + } + // Load the disk layer from the journal + base, err := db.loadDiskLayer(r, journalTypeForReader) + if err != nil { + return nil, err + } + // Load all the diff layers from the journal + head, err := db.loadDiffLayer(base, r, journalTypeForReader) + if err != nil { + return nil, err + } + log.Info("Loaded layer journal", "diskroot", diskRoot, "diffhead", head.rootHash(), "elapsed", common.PrettyDuration(time.Since(start))) + return head, nil +} + +// loadLayers loads a pre-existing state layer backed by a key-value store. +func (db *Database) loadLayers() layer { + // Retrieve the root node of persistent state. + _, root := rawdb.ReadAccountTrieNode(db.diskdb, zkt.TrieRootPathKey[:]) + + // Load the layers by resolving the journal + head, err := db.loadJournal(root) + if err == nil { + return head + } + // journal is not matched(or missing) with the persistent state, discard + // it. Display log for discarding journal, but try to avoid showing + // useless information when the db is created from scratch. + if !(root == types.EmptyRootHash && errors.Is(err, errMissJournal)) { + log.Info("Failed to load journal, discard it", "err", err) + } + // Return single layer with persistent state. + return newDiskLayer(root, rawdb.ReadPersistentStateID(db.diskdb), db, nil, NewTrieNodeBuffer(db.config.SyncFlush, db.bufferSize, nil, 0)) +} + +// loadDiskLayer reads the binary blob from the layer journal, reconstructing +// a new disk layer on it. +func (db *Database) loadDiskLayer(r *rlp.Stream, journalTypeForReader JournalType) (layer, error) { + // Resolve disk layer root + var ( + root common.Hash + journalBuf *rlp.Stream + journalEncodedBuff []byte + ) + if journalTypeForReader == JournalFileType { + if err := r.Decode(&journalEncodedBuff); err != nil { + return nil, fmt.Errorf("load disk journal: %v", err) + } + journalBuf = rlp.NewStream(bytes.NewReader(journalEncodedBuff), 0) + } else { + journalBuf = r + } + + if err := journalBuf.Decode(&root); err != nil { + return nil, fmt.Errorf("load disk root: %v", err) + } + // Resolve the state id of disk layer, it can be different + // with the persistent id tracked in disk, the id distance + // is the number of transitions aggregated in disk layer. + var id uint64 + if err := journalBuf.Decode(&id); err != nil { + return nil, fmt.Errorf("load state id: %v", err) + } + stored := rawdb.ReadPersistentStateID(db.diskdb) + if stored > id { + return nil, fmt.Errorf("invalid state id: stored %d resolved %d", stored, id) + } + // Resolve nodes cached in node buffer + var encoded []journalNode + if err := journalBuf.Decode(&encoded); err != nil { + return nil, fmt.Errorf("load disk nodes: %v", err) + } + nodes := make(dbtypes.KvMap) + for _, entry := range encoded { + nodes.Put(entry.Path, entry.Blob) + } + + if journalTypeForReader == JournalFileType { + var shaSum [32]byte + if err := r.Decode(&shaSum); err != nil { + return nil, fmt.Errorf("load shasum: %v", err) + } + + expectSum := sha256.Sum256(journalEncodedBuff) + if shaSum != expectSum { + return nil, fmt.Errorf("expect shaSum: %v, real:%v", expectSum, shaSum) + } + } + + // Calculate the internal state transitions by id difference. + base := newDiskLayer(root, id, db, nil, NewTrieNodeBuffer(db.config.SyncFlush, db.bufferSize, nodes, id-stored)) + + return base, nil +} + +// loadDiffLayer reads the next sections of a layer journal, reconstructing a new +// diff and verifying that it can be linked to the requested parent. +func (db *Database) loadDiffLayer(parent layer, r *rlp.Stream, journalTypeForReader JournalType) (layer, error) { + // Read the next diff journal entry + var ( + root common.Hash + journalBuf *rlp.Stream + journalEncodedBuff []byte + ) + if journalTypeForReader == JournalFileType { + if err := r.Decode(&journalEncodedBuff); err != nil { + // The first read may fail with EOF, marking the end of the journal + if err == io.EOF { + return parent, nil + } + return nil, fmt.Errorf("load disk journal buffer: %v", err) + } + journalBuf = rlp.NewStream(bytes.NewReader(journalEncodedBuff), 0) + } else { + journalBuf = r + } + + if err := journalBuf.Decode(&root); err != nil { + // The first read may fail with EOF, marking the end of the journal + if err == io.EOF { + return parent, nil + } + return nil, fmt.Errorf("load diff root: %v", err) + } + var block uint64 + if err := journalBuf.Decode(&block); err != nil { + return nil, fmt.Errorf("load block number: %v", err) + } + // Read in-memory trie nodes from journal + var encoded []journalNode + if err := journalBuf.Decode(&encoded); err != nil { + return nil, fmt.Errorf("load diff nodes: %v", err) + } + nodes := make(dbtypes.KvMap) + for _, entry := range encoded { + nodes.Put(entry.Path, entry.Blob) + } + + if journalTypeForReader == JournalFileType { + var shaSum [32]byte + if err := r.Decode(&shaSum); err != nil { + return nil, fmt.Errorf("load shasum: %v", err) + } + + expectSum := sha256.Sum256(journalEncodedBuff) + if shaSum != expectSum { + return nil, fmt.Errorf("expect shaSum: %v, real:%v", expectSum, shaSum) + } + } + + log.Debug("Loaded diff layer journal", "root", root, "parent", parent.rootHash(), "id", parent.stateID()+1, "block", block, "nodes", len(nodes)) + // add cache first + l := newDiffLayer(parent, root, parent.stateID()+1, block, nodes) + return db.loadDiffLayer(l, r, journalTypeForReader) +} + +// journal implements the layer interface, marshaling the un-flushed trie nodes +// along with layer metadata into provided byte buffer. +func (dl *diskLayer) journal(w io.Writer, journalType JournalType) error { + dl.lock.RLock() + defer dl.lock.RUnlock() + + // Create a buffer to store encoded data + journalBuf := new(bytes.Buffer) + + // Ensure the layer didn't get stale + if dl.stale { + return errSnapshotStale + } + // Step one, write the disk root into the journal. + if err := rlp.Encode(journalBuf, dl.root); err != nil { + return err + } + // Step two, write the corresponding state id into the journal + if err := rlp.Encode(journalBuf, dl.id); err != nil { + return err + } + // Step three, write all unwritten nodes into the journal + bufferNodes := dl.buffer.getAllNodes() + nodes := make([]journalNode, 0, len(bufferNodes)) + for _, v := range bufferNodes { + entry := journalNode{Path: v.K, Blob: v.V} + nodes = append(nodes, entry) + } + if err := rlp.Encode(journalBuf, nodes); err != nil { + return err + } + + // Store the journal buf into w and calculate checksum + if journalType == JournalFileType { + shasum := sha256.Sum256(journalBuf.Bytes()) + if err := rlp.Encode(w, journalBuf.Bytes()); err != nil { + return err + } + if err := rlp.Encode(w, shasum); err != nil { + return err + } + } else { + if _, err := w.Write(journalBuf.Bytes()); err != nil { + return err + } + } + + log.Info("Journaled pathdb disk layer", "root", dl.root, "nodes", len(bufferNodes)) + return nil +} + +// journal implements the layer interface, writing the memory layer contents +// into a buffer to be stored in the database as the layer journal. +func (dl *diffLayer) journal(w io.Writer, journalType JournalType) error { + dl.lock.RLock() + defer dl.lock.RUnlock() + + // journal the parent first + if err := dl.parent.journal(w, journalType); err != nil { + return err + } + // Create a buffer to store encoded data + journalBuf := new(bytes.Buffer) + // Everything below was journaled, persist this layer too + if err := rlp.Encode(journalBuf, dl.root); err != nil { + return err + } + if err := rlp.Encode(journalBuf, dl.block); err != nil { + return err + } + // Write the accumulated trie nodes into buffer + nodes := make([]journalNode, 0, len(dl.nodes)) + for _, v := range dl.nodes { + entry := journalNode{Path: v.K, Blob: v.V} + nodes = append(nodes, entry) + } + if err := rlp.Encode(journalBuf, nodes); err != nil { + return err + } + + // Store the journal buf into w and calculate checksum + if journalType == JournalFileType { + shasum := sha256.Sum256(journalBuf.Bytes()) + if err := rlp.Encode(w, journalBuf.Bytes()); err != nil { + return err + } + if err := rlp.Encode(w, shasum); err != nil { + return err + } + } else { + if _, err := w.Write(journalBuf.Bytes()); err != nil { + return err + } + } + + log.Info("Journaled pathdb diff layer", "root", dl.root, "parent", dl.parent.rootHash(), "id", dl.stateID(), "block", dl.block, "nodes", len(dl.nodes)) + return nil +} + +// Journal commits an entire diff hierarchy to disk into a single journal entry. +// This is meant to be used during shutdown to persist the layer without +// flattening everything down (bad for reorgs). And this function will mark the +// database as read-only to prevent all following mutation to disk. +func (db *Database) Journal(root common.Hash) error { + // Run the journaling + db.lock.Lock() + defer db.lock.Unlock() + + // Retrieve the head layer to journal from. + l := db.tree.get(root) + if l == nil { + return fmt.Errorf("triedb layer [%#x] missing", root) + } + disk := db.tree.bottom() + if l, ok := l.(*diffLayer); ok { + log.Info("Persisting dirty state to disk", "head", l.block, "root", root, "layers", l.id-disk.id+disk.buffer.getLayers()) + } else { // disk layer only on noop runs (likely) or deep reorgs (unlikely) + log.Info("Persisting dirty state to disk", "root", root, "layers", disk.buffer.getLayers()) + } + start := time.Now() + + // wait and stop the flush trienodebuffer, for asyncnodebuffer need fixed diskroot + disk.buffer.waitAndStopFlushing() + // Short circuit if the database is in read only mode. + if db.readOnly { + return errDatabaseReadOnly + } + // Firstly write out the metadata of journal + db.DeleteTrieJournal(db.diskdb) + journal := newJournalWriter(db.config.JournalFilePath, db.diskdb, db.DetermineJournalTypeForWriter()) + defer journal.Close() + + if err := rlp.Encode(journal, journalVersion); err != nil { + return err + } + // The stored state in disk might be empty, convert the + // root to emptyRoot in this case. + _, diskRoot := rawdb.ReadAccountTrieNode(db.diskdb, zkt.TrieRootPathKey[:]) + + // Secondly write out the state root in disk, ensure all layers + // on top are continuous with disk. + if err := rlp.Encode(journal, diskRoot); err != nil { + return err + } + // Finally write out the journal of each layer in reverse order. + if err := l.journal(journal, db.DetermineJournalTypeForWriter()); err != nil { + return err + } + // Store the journal into the database and return + journalSize := journal.Size() + + // Set the db in read only mode to reject all following mutations + db.readOnly = true + log.Info("Persisted dirty state to disk", "size", common.StorageSize(journalSize), "elapsed", common.PrettyDuration(time.Since(start))) + return nil +} diff --git a/triedb/pathdb/layertree.go b/triedb/pathdb/layertree.go new file mode 100644 index 000000000..ef0ce9f44 --- /dev/null +++ b/triedb/pathdb/layertree.go @@ -0,0 +1,282 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see + +package pathdb + +import ( + "errors" + "fmt" + "sync" + + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/log" + dbtypes "github.com/morph-l2/go-ethereum/triedb/types" +) + +// layerTree is a group of state layers identified by the state root. +// This structure defines a few basic operations for manipulating +// state layers linked with each other in a tree structure. It's +// thread-safe to use. However, callers need to ensure the thread-safety +// of the referenced layer by themselves. +type layerTree struct { + lock sync.RWMutex + layers map[common.Hash]layer +} + +// newLayerTree constructs the layerTree with the given head layer. +func newLayerTree(head layer) *layerTree { + tree := new(layerTree) + tree.reset(head) + return tree +} + +// reset initializes the layerTree by the given head layer. +// All the ancestors will be iterated out and linked in the tree. +func (tree *layerTree) reset(head layer) { + tree.lock.Lock() + defer tree.lock.Unlock() + + var layers = make(map[common.Hash]layer) + for head != nil { + layers[head.rootHash()] = head + head = head.parentLayer() + } + tree.layers = layers +} + +// get retrieves a layer belonging to the given state root. +func (tree *layerTree) get(root common.Hash) layer { + tree.lock.RLock() + defer tree.lock.RUnlock() + + return tree.layers[root] +} + +// forEach iterates the stored layers inside and applies the +// given callback on them. +func (tree *layerTree) forEach(onLayer func(layer)) { + tree.lock.RLock() + defer tree.lock.RUnlock() + + for _, layer := range tree.layers { + onLayer(layer) + } +} + +// len returns the number of layers cached. +func (tree *layerTree) len() int { + tree.lock.RLock() + defer tree.lock.RUnlock() + + return len(tree.layers) +} + +// add inserts a new layer into the tree if it can be linked to an existing old parent. +func (tree *layerTree) add(root common.Hash, parentRoot common.Hash, block uint64, nodes dbtypes.KvMap) error { + // Reject noop updates to avoid self-loops. This is a special case that can + // happen for clique networks and proof-of-stake networks where empty blocks + // don't modify the state (0 block subsidy). + // + // Although we could silently ignore this internally, it should be the caller's + // responsibility to avoid even attempting to insert such a layer. + if root == parentRoot { + return errors.New("layer cycle") + } + if tree.get(root) != nil { + log.Info("Skip add repeated difflayer", "root", root.String(), "block_id", block) + return nil + } + parent := tree.get(parentRoot) + if parent == nil { + return fmt.Errorf("triedb parent [%#x] layer missing", parentRoot) + } + l := parent.update(root, parent.stateID()+1, block, nodes) + + tree.lock.Lock() + tree.layers[l.rootHash()] = l + tree.lock.Unlock() + return nil +} + +// cap traverses downwards the diff tree until the number of allowed diff layers +// are crossed. All diffs beyond the permitted number are flattened downwards. +func (tree *layerTree) cap(root common.Hash, layers int) error { + // Retrieve the head layer to cap from + l := tree.get(root) + if l == nil { + return fmt.Errorf("triedb layer [%#x] missing", root) + } + diff, ok := l.(*diffLayer) + if !ok { + return fmt.Errorf("triedb layer [%#x] is disk layer", root) + } + tree.lock.Lock() + defer tree.lock.Unlock() + + // If full commit was requested, flatten the diffs and merge onto disk + if layers == 0 { + base, err := diff.persist(true) + if err != nil { + return err + } + for _, ly := range tree.layers { + if dl, ok := ly.(*diffLayer); ok { + dl.reset() + log.Debug("Cleanup difflayer hash cache due to cap all", "diff_root", dl.root.String(), "diff_block_number", dl.block) + } + } + // Replace the entire layer tree with the flat base + tree.layers = map[common.Hash]layer{base.rootHash(): base} + log.Debug("Cap all difflayers to disklayer", "disk_root", base.rootHash().String()) + return nil + } + // Dive until we run out of layers or reach the persistent database + for i := 0; i < layers-1; i++ { + // If we still have diff layers below, continue down + if parent, ok := diff.parentLayer().(*diffLayer); ok { + diff = parent + } else { + // Diff stack too shallow, return without modifications + return nil + } + } + var persisted *diskLayer + // We're out of layers, flatten anything below, stopping if it's the disk or if + // the memory limit is not yet exceeded. + switch parent := diff.parentLayer().(type) { + case *diskLayer: + return nil + + case *diffLayer: + // Hold the lock to prevent any read operations until the new + // parent is linked correctly. + diff.lock.Lock() + + base, err := parent.persist(false) + if err != nil { + diff.lock.Unlock() + return err + } + tree.layers[base.rootHash()] = base + diff.parent = base + + diff.lock.Unlock() + persisted = base.(*diskLayer) + + default: + panic(fmt.Sprintf("unknown data layer in triedb: %T", parent)) + } + // Remove any layer that is stale or links into a stale layer + children := make(map[common.Hash][]common.Hash) + for root, layer := range tree.layers { + if dl, ok := layer.(*diffLayer); ok { + parent := dl.parentLayer().rootHash() + children[parent] = append(children[parent], root) + } + } + var remove func(root common.Hash) + remove = func(root common.Hash) { + delete(tree.layers, root) + for _, child := range children[root] { + remove(child) + } + delete(children, root) + } + for root, layer := range tree.layers { + if dl, ok := layer.(*diskLayer); ok && dl.isStale() { + remove(root) + log.Debug("Remove stale the disklayer", "disk_root", dl.root.String()) + } + } + + if persisted != nil { + var updateOriginFunc func(root common.Hash) + updateOriginFunc = func(root common.Hash) { + if diff, ok := tree.layers[root].(*diffLayer); ok { + diff.updateOriginDiskLayer(persisted) + } + for _, child := range children[root] { + updateOriginFunc(child) + } + } + updateOriginFunc(persisted.root) + } + + return nil +} + +// bottom returns the bottom-most disk layer in this tree. +func (tree *layerTree) bottom() *diskLayer { + tree.lock.RLock() + defer tree.lock.RUnlock() + + if len(tree.layers) == 0 { + return nil // Shouldn't happen, empty tree + } + // pick a random one as the entry point + var current layer + for _, layer := range tree.layers { + current = layer + break + } + for current.parentLayer() != nil { + current = current.parentLayer() + } + return current.(*diskLayer) +} + +// front return the top non-fork difflayer/disklayer root hash for rewinding. +func (tree *layerTree) front() common.Hash { + tree.lock.RLock() + defer tree.lock.RUnlock() + + chain := make(map[common.Hash][]common.Hash) + var base common.Hash + for _, layer := range tree.layers { + switch dl := layer.(type) { + case *diskLayer: + if dl.stale { + log.Info("pathdb top disklayer is stale") + return base + } + base = dl.rootHash() + case *diffLayer: + if _, ok := chain[dl.parentLayer().rootHash()]; !ok { + chain[dl.parentLayer().rootHash()] = make([]common.Hash, 0) + } + chain[dl.parentLayer().rootHash()] = append(chain[dl.parentLayer().rootHash()], dl.rootHash()) + default: + log.Crit("unsupported layer type") + } + } + if (base == common.Hash{}) { + log.Info("pathdb top difflayer is empty") + return base + } + parent := base + for { + children, ok := chain[parent] + if !ok { + log.Info("pathdb top difflayer", "root", parent) + return parent + } + if len(children) != 1 { + log.Info("pathdb top difflayer is forked", "common ancestor root", parent) + return parent + } + parent = children[0] + } +} diff --git a/triedb/pathdb/metrics.go b/triedb/pathdb/metrics.go new file mode 100644 index 000000000..90cb1c2f0 --- /dev/null +++ b/triedb/pathdb/metrics.go @@ -0,0 +1,47 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see + +package pathdb + +import "github.com/morph-l2/go-ethereum/metrics" + +var ( + cleanHitMeter = metrics.NewRegisteredMeter("pathdb/clean/hit", nil) + cleanMissMeter = metrics.NewRegisteredMeter("pathdb/clean/miss", nil) + cleanReadMeter = metrics.NewRegisteredMeter("pathdb/clean/read", nil) + cleanWriteMeter = metrics.NewRegisteredMeter("pathdb/clean/write", nil) + + dirtyHitMeter = metrics.NewRegisteredMeter("pathdb/dirty/hit", nil) + dirtyMissMeter = metrics.NewRegisteredMeter("pathdb/dirty/miss", nil) + dirtyReadMeter = metrics.NewRegisteredMeter("pathdb/dirty/read", nil) + dirtyWriteMeter = metrics.NewRegisteredMeter("pathdb/dirty/write", nil) + + commitTimeTimer = metrics.NewRegisteredTimer("pathdb/commit/time", nil) + commitNodesMeter = metrics.NewRegisteredMeter("pathdb/commit/nodes", nil) + commitBytesMeter = metrics.NewRegisteredMeter("pathdb/commit/bytes", nil) + + gcNodesMeter = metrics.NewRegisteredMeter("pathdb/gc/nodes", nil) + gcBytesMeter = metrics.NewRegisteredMeter("pathdb/gc/bytes", nil) + + diffLayerBytesMeter = metrics.NewRegisteredMeter("pathdb/diff/bytes", nil) + diffLayerNodesMeter = metrics.NewRegisteredMeter("pathdb/diff/nodes", nil) + + diffHashCacheHitMeter = metrics.NewRegisteredMeter("pathdb/difflayer/hashcache/hit", nil) + diffHashCacheReadMeter = metrics.NewRegisteredMeter("pathdb/difflayer/hashcache/read", nil) + diffHashCacheMissMeter = metrics.NewRegisteredMeter("pathdb/difflayer/hashcache/miss", nil) + diffHashCacheSlowPathMeter = metrics.NewRegisteredMeter("pathdb/difflayer/hashcache/slowpath", nil) + diffHashCacheLengthGauge = metrics.NewRegisteredGauge("pathdb/difflayer/hashcache/size", nil) +) diff --git a/triedb/pathdb/nodebuffer.go b/triedb/pathdb/nodebuffer.go new file mode 100644 index 000000000..abec62bb2 --- /dev/null +++ b/triedb/pathdb/nodebuffer.go @@ -0,0 +1,205 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package pathdb + +import ( + "bytes" + "fmt" + "time" + + "github.com/VictoriaMetrics/fastcache" + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/core/rawdb" + "github.com/morph-l2/go-ethereum/ethdb" + "github.com/morph-l2/go-ethereum/log" + dbtypes "github.com/morph-l2/go-ethereum/triedb/types" +) + +var _ trienodebuffer = &nodebuffer{} + +// nodebuffer is a collection of modified trie nodes to aggregate the disk +// write. The content of the nodebuffer must be checked before diving into +// disk (since it basically is not-yet-written data). +type nodebuffer struct { + layers uint64 // The number of diff layers aggregated inside + size uint64 // The size of aggregated writes + limit uint64 // The maximum memory allowance in bytes + nodes dbtypes.KvMap // The dirty node set, mapped by owner and path +} + +// newNodeBuffer initializes the node buffer with the provided nodes. +func newNodeBuffer(limit int, nodes dbtypes.KvMap, layers uint64) *nodebuffer { + if nodes == nil { + nodes = make(dbtypes.KvMap) + } + var size uint64 + for _, v := range nodes { + size += uint64(len(v.K) + len(v.V)) + } + return &nodebuffer{ + layers: layers, + nodes: nodes, + size: size, + limit: uint64(limit), + } +} + +// node retrieves the trie node with given node info. +func (b *nodebuffer) node(path []byte) ([]byte, bool) { + n, ok := b.nodes.Get(path) + if !ok { + return nil, false + } + + return n, true +} + +// commit merges the dirty nodes into the nodebuffer. This operation won't take +// the ownership of the nodes map which belongs to the bottom-most diff layer. +// It will just hold the node references from the given map which are safe to +// copy. +func (b *nodebuffer) commit(nodes dbtypes.KvMap) trienodebuffer { + var ( + delta int64 + overwrite int64 + overwriteSize int64 + ) + + for _, v := range nodes { + current, exist := b.nodes.Get(v.K) + if !exist { + b.nodes.Put(v.K, v.V) + delta += int64(len(v.K) + len(v.V)) + + continue + } + + if !bytes.Equal(current, v.V) { + delta += int64(len(v.V) - len(current)) + overwrite++ + overwriteSize += int64(len(v.V) + len(v.K)) + } + + b.nodes.Put(v.K, v.V) + } + + b.updateSize(delta) + b.layers++ + gcNodesMeter.Mark(overwrite) + gcBytesMeter.Mark(overwriteSize) + return b +} + +// updateSize updates the total cache size by the given delta. +func (b *nodebuffer) updateSize(delta int64) { + size := int64(b.size) + delta + if size >= 0 { + b.size = uint64(size) + return + } + s := b.size + b.size = 0 + log.Error("Invalid pathdb buffer size", "prev", common.StorageSize(s), "delta", common.StorageSize(delta)) +} + +// reset cleans up the disk cache. +func (b *nodebuffer) reset() { + b.layers = 0 + b.size = 0 + b.nodes = make(dbtypes.KvMap) +} + +// empty returns an indicator if nodebuffer contains any state transition inside. +func (b *nodebuffer) empty() bool { + return b.layers == 0 +} + +// setSize sets the buffer size to the provided number, and invokes a flush +// operation if the current memory usage exceeds the new limit. +func (b *nodebuffer) setSize(size int, db ethdb.KeyValueStore, clean *fastcache.Cache, id uint64) error { + b.limit = uint64(size) + return b.flush(db, clean, id, false) +} + +// flush persists the in-memory dirty trie node into the disk if the configured +// memory threshold is reached. Note, all data must be written atomically. +func (b *nodebuffer) flush(db ethdb.KeyValueStore, clean *fastcache.Cache, id uint64, force bool) error { + if b.size <= b.limit && !force { + return nil + } + // Ensure the target state id is aligned with the internal counter. + head := rawdb.ReadPersistentStateID(db) + if head+b.layers != id { + return fmt.Errorf("buffer layers (%d) cannot be applied on top of persisted state id (%d) to reach requested state id (%d)", b.layers, head, id) + } + var ( + start = time.Now() + // Although the calculation of b.size has been as accurate as possible, + // some omissions were still found during testing and code review, but + // we are still not sure if it is completely accurate. For better protection, + // some redundancy is added here. + batch = db.NewBatchWithSize(int(float64(b.size) * DefaultBatchRedundancyRate)) + ) + nodes := writeNodes(batch, b.nodes, clean) + rawdb.WritePersistentStateID(batch, id) + + // Flush all mutations in a single batch + size := batch.ValueSize() + if err := batch.Write(); err != nil { + return err + } + commitBytesMeter.Mark(int64(size)) + commitNodesMeter.Mark(int64(nodes)) + commitTimeTimer.UpdateSince(start) + log.Debug("Persisted pathdb nodes", "nodes", len(b.nodes), "bytes", common.StorageSize(size), "elapsed", common.PrettyDuration(time.Since(start))) + b.reset() + return nil +} + +func (b *nodebuffer) waitAndStopFlushing() {} + +// writeNodes writes the trie nodes into the provided database batch. +// Note this function will also inject all the newly written nodes +// into clean cache. +func writeNodes(batch ethdb.Batch, nodes dbtypes.KvMap, clean *fastcache.Cache) (total int) { + for _, v := range nodes { + rawdb.WriteTrieNodeByKey(batch, v.K, v.V) + + if clean != nil { + clean.Set(v.K, v.V) + } + + total += 1 + } + + return total +} + +// getSize return the nodebuffer used size. +func (b *nodebuffer) getSize() (uint64, uint64) { + return b.size, 0 +} + +// getAllNodes return all the trie nodes are cached in nodebuffer. +func (b *nodebuffer) getAllNodes() dbtypes.KvMap { + return b.nodes +} + +// getLayers return the size of cached difflayers. +func (b *nodebuffer) getLayers() uint64 { + return b.layers +} diff --git a/triedb/types/database_types.go b/triedb/types/database_types.go new file mode 100644 index 000000000..4322b2216 --- /dev/null +++ b/triedb/types/database_types.go @@ -0,0 +1,56 @@ +package types + +import ( + "bytes" + "crypto/sha256" + "errors" +) + +// ErrNotFound is used by the implementations of the interface db.Storage for +// when a key is not found in the storage +var ErrNotFound = errors.New("key not found") + +// KV contains a key (K) and a value (V) +type KV struct { + K []byte + V []byte +} + +// KvMap is a key-value map between a sha256 byte array hash, and a KV struct +type KvMap map[[sha256.Size]byte]KV + +// Get retreives the value respective to a key from the KvMap +func (m KvMap) Get(k []byte) ([]byte, bool) { + v, ok := m[sha256.Sum256(k)] + return v.V, ok +} + +// Put stores a key and a value in the KvMap +func (m KvMap) Put(k, v []byte) { + m[sha256.Sum256(k)] = KV{k, v} +} + +// Copy returns a map for internal nodes. +func (m KvMap) Copy() KvMap { + nodes := make(KvMap) + for _, v := range m { + nodes.Put(v.K, v.V) + } + return nodes +} + +// Concat concatenates arrays of bytes +func Concat(vs ...[]byte) []byte { + var b bytes.Buffer + for _, v := range vs { + b.Write(v) + } + return b.Bytes() +} + +// Clone clones a byte array into a new byte array +func Clone(b0 []byte) []byte { + b1 := make([]byte, len(b0)) + copy(b1, b0) + return b1 +}