triedb/pathdb, core: keep root->id mappings after truncation (#32502)

This pull request preserves the root->ID mappings in the path database
even after the associated state histories are truncated, regardless of
whether the truncation occurs at the head or the tail.

The motivation is to support an additional history type, trienode history. 
Since the root->ID mappings are shared between two history instances, 
they must not be removed by either one.

As a consequence, the root->ID mappings remain in the database even
after the corresponding histories are pruned. While these mappings may 
become  dangling, it is safe and cheap to keep them.

Additionally, this pull request enhances validation during historical
reader construction, ensuring that only canonical historical state will be
served.
This commit is contained in:
rjl493456442 2025-08-29 15:43:58 +08:00 committed by GitHub
parent 2a795c14f4
commit 7f78fa6912
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 221 additions and 168 deletions

View file

@ -119,13 +119,6 @@ func WriteStateID(db ethdb.KeyValueWriter, root common.Hash, id uint64) {
}
}
// 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)
}
}
// ReadPersistentStateID retrieves the id of the persistent state from the database.
func ReadPersistentStateID(db ethdb.KeyValueReader) uint64 {
data, _ := db.Get(persistentStateIDKey)

View file

@ -334,7 +334,7 @@ func (db *Database) repairHistory() error {
}
// Truncate the extra state histories above in freezer in case it's not
// aligned with the disk layer. It might happen after a unclean shutdown.
pruned, err := truncateFromHead(db.diskdb, db.stateFreezer, id)
pruned, err := truncateFromHead(db.stateFreezer, id)
if err != nil {
log.Crit("Failed to truncate extra state histories", "err", err)
}
@ -590,7 +590,7 @@ func (db *Database) Recover(root common.Hash) error {
if err := db.diskdb.SyncKeyValue(); err != nil {
return err
}
_, err := truncateFromHead(db.diskdb, db.stateFreezer, dl.stateID())
_, err := truncateFromHead(db.stateFreezer, dl.stateID())
if err != nil {
return err
}
@ -615,14 +615,14 @@ func (db *Database) Recoverable(root common.Hash) bool {
return false
}
// This is a temporary workaround for the unavailability of the freezer in
// dev mode. As a consequence, the Pathdb loses the ability for deep reorg
// dev mode. As a consequence, the database loses the ability for deep reorg
// in certain cases.
// TODO(rjl493456442): Implement the in-memory ancient store.
if db.stateFreezer == nil {
return false
}
// Ensure the requested state is a canonical state and all state
// histories in range [id+1, disklayer.ID] are present and complete.
// histories in range [id+1, dl.ID] are present and complete.
return checkStateHistories(db.stateFreezer, *id+1, dl.stateID()-*id, func(m *meta) error {
if m.parent != root {
return errors.New("unexpected state history")

View file

@ -378,7 +378,7 @@ func (dl *diskLayer) writeStateHistory(diff *diffLayer) (bool, error) {
log.Debug("Skip tail truncation", "persistentID", persistentID, "tailID", tail+1, "headID", diff.stateID(), "limit", limit)
return true, nil
}
pruned, err := truncateFromTail(dl.db.diskdb, dl.db.stateFreezer, newFirst-1)
pruned, err := truncateFromTail(dl.db.stateFreezer, newFirst-1)
if err != nil {
return false, err
}

87
triedb/pathdb/history.go Normal file
View file

@ -0,0 +1,87 @@
// Copyright 2025 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 <http://www.gnu.org/licenses/
package pathdb
import (
"errors"
"fmt"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
)
var (
errHeadTruncationOutOfRange = errors.New("history head truncation out of range")
errTailTruncationOutOfRange = errors.New("history tail truncation out of range")
)
// truncateFromHead removes excess elements from the head of the freezer based
// on the given parameters. It returns the number of items that were removed.
func truncateFromHead(store ethdb.AncientStore, nhead uint64) (int, error) {
ohead, err := store.Ancients()
if err != nil {
return 0, err
}
otail, err := store.Tail()
if err != nil {
return 0, err
}
log.Info("Truncating from head", "ohead", ohead, "tail", otail, "nhead", nhead)
// Ensure that the truncation target falls within the valid range.
if ohead < nhead || nhead < otail {
return 0, fmt.Errorf("%w, tail: %d, head: %d, target: %d", errHeadTruncationOutOfRange, otail, ohead, nhead)
}
// Short circuit if nothing to truncate.
if ohead == nhead {
return 0, nil
}
ohead, err = store.TruncateHead(nhead)
if err != nil {
return 0, err
}
// Associated root->id mappings are left in the database and wait
// for overwriting.
return int(ohead - nhead), nil
}
// truncateFromTail removes excess elements from the end of the freezer based
// on the given parameters. It returns the number of items that were removed.
func truncateFromTail(store ethdb.AncientStore, ntail uint64) (int, error) {
ohead, err := store.Ancients()
if err != nil {
return 0, err
}
otail, err := store.Tail()
if err != nil {
return 0, err
}
// Ensure that the truncation target falls within the valid range.
if otail > ntail || ntail > ohead {
return 0, fmt.Errorf("%w, tail: %d, head: %d, target: %d", errTailTruncationOutOfRange, otail, ohead, ntail)
}
// Short circuit if nothing to truncate.
if otail == ntail {
return 0, nil
}
otail, err = store.TruncateTail(ntail)
if err != nil {
return 0, err
}
// Associated root->id mappings are left in the database.
return int(ntail - otail), nil
}

View file

@ -320,11 +320,12 @@ func (r *historyReader) read(state stateIdentQuery, stateID uint64, lastID uint6
tail, err := r.freezer.Tail()
if err != nil {
return nil, err
}
// stateID == tail is allowed, as the first history object preserved
// is tail+1
} // firstID = tail+1
// stateID+1 == firstID is allowed, as all the subsequent state histories
// are present with no gap inside.
if stateID < tail {
return nil, errors.New("historical state has been pruned")
return nil, fmt.Errorf("historical state has been pruned, first: %d, state: %d", tail+1, stateID)
}
// To serve the request, all state histories from stateID+1 to lastID

View file

@ -24,6 +24,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/internal/testrand"
)
func waitIndexing(db *Database) {
@ -36,11 +37,29 @@ func waitIndexing(db *Database) {
}
}
func checkHistoricState(env *tester, root common.Hash, hr *historyReader) error {
// Short circuit if the historical state is no longer available
if rawdb.ReadStateID(env.db.diskdb, root) == nil {
func stateAvail(id uint64, env *tester) bool {
if env.db.config.StateHistory == 0 {
return true
}
dl := env.db.tree.bottom()
if dl.stateID() <= env.db.config.StateHistory {
return true
}
firstID := dl.stateID() - env.db.config.StateHistory + 1
return id+1 >= firstID
}
func checkHistoricalState(env *tester, root common.Hash, id uint64, hr *historyReader) error {
if !stateAvail(id, env) {
return nil
}
// Short circuit if the historical state is no longer available
if rawdb.ReadStateID(env.db.diskdb, root) == nil {
return fmt.Errorf("state not found %d %x", id, root)
}
var (
dl = env.db.tree.bottom()
stateID = rawdb.ReadStateID(env.db.diskdb, root)
@ -124,22 +143,22 @@ func testHistoryReader(t *testing.T, historyLimit uint64) {
defer func() {
maxDiffLayers = 128
}()
//log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
// log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
env := newTester(t, historyLimit, false, 64, true, "")
defer env.release()
waitIndexing(env.db)
var (
roots = env.roots
dRoot = env.db.tree.bottom().rootHash()
dl = env.db.tree.bottom()
hr = newHistoryReader(env.db.diskdb, env.db.stateFreezer)
)
for _, root := range roots {
if root == dRoot {
for i, root := range roots {
if root == dl.rootHash() {
break
}
if err := checkHistoricState(env, root, hr); err != nil {
if err := checkHistoricalState(env, root, uint64(i+1), hr); err != nil {
t.Fatal(err)
}
}
@ -148,12 +167,41 @@ func testHistoryReader(t *testing.T, historyLimit uint64) {
env.extend(4)
waitIndexing(env.db)
for _, root := range roots {
if root == dRoot {
for i, root := range roots {
if root == dl.rootHash() {
break
}
if err := checkHistoricState(env, root, hr); err != nil {
if err := checkHistoricalState(env, root, uint64(i+1), hr); err != nil {
t.Fatal(err)
}
}
}
func TestHistoricalStateReader(t *testing.T) {
maxDiffLayers = 4
defer func() {
maxDiffLayers = 128
}()
//log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
env := newTester(t, 0, false, 64, true, "")
defer env.release()
waitIndexing(env.db)
// non-canonical state
fakeRoot := testrand.Hash()
rawdb.WriteStateID(env.db.diskdb, fakeRoot, 10)
_, err := env.db.HistoricReader(fakeRoot)
if err == nil {
t.Fatal("expected error")
}
t.Log(err)
// canonical state
realRoot := env.roots[9]
_, err = env.db.HistoricReader(realRoot)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
}

View file

@ -504,6 +504,20 @@ func (h *stateHistory) decode(accountData, storageData, accountIndexes, storageI
return nil
}
// readStateHistoryMeta reads the metadata of state history with the specified id.
func readStateHistoryMeta(reader ethdb.AncientReader, id uint64) (*meta, error) {
data := rawdb.ReadStateHistoryMeta(reader, id)
if len(data) == 0 {
return nil, fmt.Errorf("metadata is not found, %d", id)
}
var m meta
err := m.decode(data)
if err != nil {
return nil, err
}
return &m, nil
}
// readStateHistory reads a single state history records with the specified id.
func readStateHistory(reader ethdb.AncientReader, id uint64) (*stateHistory, error) {
mData, accountIndexes, storageIndexes, accountData, storageData, err := rawdb.ReadStateHistory(reader, id)
@ -568,8 +582,8 @@ func writeStateHistory(writer ethdb.AncientWriter, dl *diffLayer) error {
return nil
}
// checkStateHistories retrieves a batch of meta objects with the specified range
// and performs the callback on each item.
// checkStateHistories retrieves a batch of metadata objects with the specified
// range and performs the callback on each item.
func checkStateHistories(reader ethdb.AncientReader, start, count uint64, check func(*meta) error) error {
for count > 0 {
number := count
@ -594,87 +608,3 @@ func checkStateHistories(reader ethdb.AncientReader, start, count uint64, check
}
return nil
}
// truncateFromHead removes the extra state histories from the head with the given
// parameters. It returns the number of items removed from the head.
func truncateFromHead(db ethdb.Batcher, store ethdb.AncientStore, nhead uint64) (int, error) {
ohead, err := store.Ancients()
if err != nil {
return 0, err
}
otail, err := store.Tail()
if err != nil {
return 0, err
}
// Ensure that the truncation target falls within the specified range.
if ohead < nhead || nhead < otail {
return 0, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", otail, ohead, nhead)
}
// Short circuit if nothing to truncate.
if ohead == nhead {
return 0, nil
}
// Load the meta objects in range [nhead+1, ohead]
blobs, err := rawdb.ReadStateHistoryMetaList(store, nhead+1, ohead-nhead)
if err != nil {
return 0, err
}
batch := db.NewBatch()
for _, blob := range blobs {
var m meta
if err := m.decode(blob); err != nil {
return 0, err
}
rawdb.DeleteStateID(batch, m.root)
}
if err := batch.Write(); err != nil {
return 0, err
}
ohead, err = store.TruncateHead(nhead)
if err != nil {
return 0, err
}
return int(ohead - nhead), nil
}
// truncateFromTail removes the extra state histories from the tail with the given
// parameters. It returns the number of items removed from the tail.
func truncateFromTail(db ethdb.Batcher, store ethdb.AncientStore, ntail uint64) (int, error) {
ohead, err := store.Ancients()
if err != nil {
return 0, err
}
otail, err := store.Tail()
if err != nil {
return 0, err
}
// Ensure that the truncation target falls within the specified range.
if otail > ntail || ntail > ohead {
return 0, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", otail, ohead, ntail)
}
// Short circuit if nothing to truncate.
if otail == ntail {
return 0, nil
}
// Load the meta objects in range [otail+1, ntail]
blobs, err := rawdb.ReadStateHistoryMetaList(store, otail+1, ntail-otail)
if err != nil {
return 0, err
}
batch := db.NewBatch()
for _, blob := range blobs {
var m meta
if err := m.decode(blob); err != nil {
return 0, err
}
rawdb.DeleteStateID(batch, m.root)
}
if err := batch.Write(); err != nil {
return 0, err
}
otail, err = store.TruncateTail(ntail)
if err != nil {
return 0, err
}
return int(ntail - otail), nil
}

View file

@ -18,6 +18,7 @@ package pathdb
import (
"bytes"
"errors"
"fmt"
"reflect"
"testing"
@ -108,7 +109,7 @@ func testEncodeDecodeStateHistory(t *testing.T, rawStorageKey bool) {
}
}
func checkStateHistory(t *testing.T, db ethdb.KeyValueReader, freezer ethdb.AncientReader, id uint64, root common.Hash, exist bool) {
func checkStateHistory(t *testing.T, freezer ethdb.AncientReader, id uint64, exist bool) {
blob := rawdb.ReadStateHistoryMeta(freezer, id)
if exist && len(blob) == 0 {
t.Fatalf("Failed to load trie history, %d", id)
@ -116,25 +117,17 @@ func checkStateHistory(t *testing.T, db ethdb.KeyValueReader, freezer ethdb.Anci
if !exist && len(blob) != 0 {
t.Fatalf("Unexpected trie history, %d", id)
}
if exist && rawdb.ReadStateID(db, root) == nil {
t.Fatalf("Root->ID mapping is not found, %d", id)
}
if !exist && rawdb.ReadStateID(db, root) != nil {
t.Fatalf("Unexpected root->ID mapping, %d", id)
}
}
func checkHistoriesInRange(t *testing.T, db ethdb.KeyValueReader, freezer ethdb.AncientReader, from, to uint64, roots []common.Hash, exist bool) {
for i, j := from, 0; i <= to; i, j = i+1, j+1 {
checkStateHistory(t, db, freezer, i, roots[j], exist)
func checkHistoriesInRange(t *testing.T, freezer ethdb.AncientReader, from, to uint64, exist bool) {
for i := from; i <= to; i = i + 1 {
checkStateHistory(t, freezer, i, exist)
}
}
func TestTruncateHeadStateHistory(t *testing.T) {
var (
roots []common.Hash
hs = makeStateHistories(10)
db = rawdb.NewMemoryDatabase()
freezer, _ = rawdb.NewStateFreezer(t.TempDir(), false, false)
)
defer freezer.Close()
@ -142,27 +135,23 @@ func TestTruncateHeadStateHistory(t *testing.T) {
for i := 0; i < len(hs); i++ {
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
rawdb.WriteStateID(db, hs[i].meta.root, uint64(i+1))
roots = append(roots, hs[i].meta.root)
}
for size := len(hs); size > 0; size-- {
pruned, err := truncateFromHead(db, freezer, uint64(size-1))
pruned, err := truncateFromHead(freezer, uint64(size-1))
if err != nil {
t.Fatalf("Failed to truncate from head %v", err)
}
if pruned != 1 {
t.Error("Unexpected pruned items", "want", 1, "got", pruned)
}
checkHistoriesInRange(t, db, freezer, uint64(size), uint64(10), roots[size-1:], false)
checkHistoriesInRange(t, db, freezer, uint64(1), uint64(size-1), roots[:size-1], true)
checkHistoriesInRange(t, freezer, uint64(size), uint64(10), false)
checkHistoriesInRange(t, freezer, uint64(1), uint64(size-1), true)
}
}
func TestTruncateTailStateHistory(t *testing.T) {
var (
roots []common.Hash
hs = makeStateHistories(10)
db = rawdb.NewMemoryDatabase()
freezer, _ = rawdb.NewStateFreezer(t.TempDir(), false, false)
)
defer freezer.Close()
@ -170,16 +159,14 @@ func TestTruncateTailStateHistory(t *testing.T) {
for i := 0; i < len(hs); i++ {
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
rawdb.WriteStateID(db, hs[i].meta.root, uint64(i+1))
roots = append(roots, hs[i].meta.root)
}
for newTail := 1; newTail < len(hs); newTail++ {
pruned, _ := truncateFromTail(db, freezer, uint64(newTail))
pruned, _ := truncateFromTail(freezer, uint64(newTail))
if pruned != 1 {
t.Error("Unexpected pruned items", "want", 1, "got", pruned)
}
checkHistoriesInRange(t, db, freezer, uint64(1), uint64(newTail), roots[:newTail], false)
checkHistoriesInRange(t, db, freezer, uint64(newTail+1), uint64(10), roots[newTail:], true)
checkHistoriesInRange(t, freezer, uint64(1), uint64(newTail), false)
checkHistoriesInRange(t, freezer, uint64(newTail+1), uint64(10), true)
}
}
@ -191,21 +178,29 @@ func TestTruncateTailStateHistories(t *testing.T) {
minUnpruned uint64
empty bool
}{
// history: id [10]
{
1, 9, 9, 10, false,
limit: 1,
expPruned: 9,
maxPruned: 9, minUnpruned: 10, empty: false,
},
// history: none
{
0, 10, 10, 0 /* no meaning */, true,
limit: 0,
expPruned: 10,
empty: true,
},
// history: id [1:10]
{
10, 0, 0, 1, false,
limit: 10,
expPruned: 0,
maxPruned: 0,
minUnpruned: 1,
},
}
for i, c := range cases {
var (
roots []common.Hash
hs = makeStateHistories(10)
db = rawdb.NewMemoryDatabase()
freezer, _ = rawdb.NewStateFreezer(t.TempDir()+fmt.Sprintf("%d", i), false, false)
)
defer freezer.Close()
@ -213,19 +208,16 @@ func TestTruncateTailStateHistories(t *testing.T) {
for i := 0; i < len(hs); i++ {
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
rawdb.WriteStateID(db, hs[i].meta.root, uint64(i+1))
roots = append(roots, hs[i].meta.root)
}
pruned, _ := truncateFromTail(db, freezer, uint64(10)-c.limit)
pruned, _ := truncateFromTail(freezer, uint64(10)-c.limit)
if pruned != c.expPruned {
t.Error("Unexpected pruned items", "want", c.expPruned, "got", pruned)
}
if c.empty {
checkHistoriesInRange(t, db, freezer, uint64(1), uint64(10), roots, false)
checkHistoriesInRange(t, freezer, uint64(1), uint64(10), false)
} else {
tail := 10 - int(c.limit)
checkHistoriesInRange(t, db, freezer, uint64(1), c.maxPruned, roots[:tail], false)
checkHistoriesInRange(t, db, freezer, c.minUnpruned, uint64(10), roots[tail:], true)
checkHistoriesInRange(t, freezer, uint64(1), c.maxPruned, false)
checkHistoriesInRange(t, freezer, c.minUnpruned, uint64(10), true)
}
}
}
@ -233,7 +225,6 @@ func TestTruncateTailStateHistories(t *testing.T) {
func TestTruncateOutOfRange(t *testing.T) {
var (
hs = makeStateHistories(10)
db = rawdb.NewMemoryDatabase()
freezer, _ = rawdb.NewStateFreezer(t.TempDir(), false, false)
)
defer freezer.Close()
@ -241,9 +232,8 @@ func TestTruncateOutOfRange(t *testing.T) {
for i := 0; i < len(hs); i++ {
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
rawdb.WriteStateID(db, hs[i].meta.root, uint64(i+1))
}
truncateFromTail(db, freezer, uint64(len(hs)/2))
truncateFromTail(freezer, uint64(len(hs)/2))
// Ensure of-out-range truncations are rejected correctly.
head, _ := freezer.Ancients()
@ -255,20 +245,20 @@ func TestTruncateOutOfRange(t *testing.T) {
expErr error
}{
{0, head, nil}, // nothing to delete
{0, head + 1, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", tail, head, head+1)},
{0, tail - 1, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", tail, head, tail-1)},
{0, head + 1, errHeadTruncationOutOfRange},
{0, tail - 1, errHeadTruncationOutOfRange},
{1, tail, nil}, // nothing to delete
{1, head + 1, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", tail, head, head+1)},
{1, tail - 1, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", tail, head, tail-1)},
{1, head + 1, errTailTruncationOutOfRange},
{1, tail - 1, errTailTruncationOutOfRange},
}
for _, c := range cases {
var gotErr error
if c.mode == 0 {
_, gotErr = truncateFromHead(db, freezer, c.target)
_, gotErr = truncateFromHead(freezer, c.target)
} else {
_, gotErr = truncateFromTail(db, freezer, c.target)
_, gotErr = truncateFromTail(freezer, c.target)
}
if !reflect.DeepEqual(gotErr, c.expErr) {
if !errors.Is(gotErr, c.expErr) {
t.Errorf("Unexpected error, want: %v, got: %v", c.expErr, gotErr)
}
}

View file

@ -213,20 +213,24 @@ func (db *Database) HistoricReader(root common.Hash) (*HistoricalStateReader, er
if !db.stateIndexer.inited() {
return nil, errors.New("state histories haven't been fully indexed yet")
}
// States at the current disk layer or above are directly accessible via
// db.StateReader.
// - States at the current disk layer or above are directly accessible
// via `db.StateReader`.
//
// States older than the current disk layer (including the disk layer
// itself) are available through historic state access.
//
// Note: the requested state may refer to a stale historic state that has
// already been pruned. This function does not validate availability, as
// underlying states may be pruned dynamically. Validity is checked during
// each actual state retrieval.
// - States older than the current disk layer (including the disk layer
// itself) are available via `db.HistoricReader`.
id := rawdb.ReadStateID(db.diskdb, root)
if id == nil {
return nil, fmt.Errorf("state %#x is not available", root)
}
// Ensure the requested state is canonical, historical states on side chain
// are not accessible.
meta, err := readStateHistoryMeta(db.stateFreezer, *id+1)
if err != nil {
return nil, err // e.g., the referred state history has been pruned
}
if meta.parent != root {
return nil, fmt.Errorf("state %#x is not canonincal", root)
}
return &HistoricalStateReader{
id: *id,
db: db,