Skip to content

Commit 77e0e83

Browse files
rjl493456442holiman
authored andcommitted
cmd, core, eth, les, light: track deleted nodes (ethereum#25757)
* cmd, core, eth, les, light: track deleted nodes * trie: add docs * trie: address comments * cmd, core, eth, les, light, trie: trie id * trie: add tests * trie, core: updates * trie: fix imports * trie: add utility print-method for nodeset * trie: import err * trie: fix go vet warnings Co-authored-by: Martin Holst Swende <[email protected]>
1 parent 63572ee commit 77e0e83

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+862
-283
lines changed

cmd/geth/dbcmd.go

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ WARNING: This is a low-level operation which may cause database corruption!`,
150150
Action: dbDumpTrie,
151151
Name: "dumptrie",
152152
Usage: "Show the storage key/values of a given storage trie",
153-
ArgsUsage: "<hex-encoded storage trie root> <hex-encoded start (optional)> <int max elements (optional)>",
153+
ArgsUsage: "<hex-encoded state root> <hex-encoded account hash> <hex-encoded storage trie root> <hex-encoded start (optional)> <int max elements (optional)>",
154154
Flags: flags.Merge([]cli.Flag{
155155
utils.SyncModeFlag,
156156
}, utils.NetworkFlags, utils.DatabasePathFlags),
@@ -486,38 +486,49 @@ func dbPut(ctx *cli.Context) error {
486486

487487
// dbDumpTrie shows the key-value slots of a given storage trie
488488
func dbDumpTrie(ctx *cli.Context) error {
489-
if ctx.NArg() < 1 {
489+
if ctx.NArg() < 3 {
490490
return fmt.Errorf("required arguments: %v", ctx.Command.ArgsUsage)
491491
}
492492
stack, _ := makeConfigNode(ctx)
493493
defer stack.Close()
494494

495495
db := utils.MakeChainDatabase(ctx, stack, true)
496496
defer db.Close()
497+
497498
var (
498-
root []byte
499-
start []byte
500-
max = int64(-1)
501-
err error
499+
state []byte
500+
storage []byte
501+
account []byte
502+
start []byte
503+
max = int64(-1)
504+
err error
502505
)
503-
if root, err = hexutil.Decode(ctx.Args().Get(0)); err != nil {
504-
log.Info("Could not decode the root", "error", err)
506+
if state, err = hexutil.Decode(ctx.Args().Get(0)); err != nil {
507+
log.Info("Could not decode the state root", "error", err)
505508
return err
506509
}
507-
stRoot := common.BytesToHash(root)
508-
if ctx.NArg() >= 2 {
509-
if start, err = hexutil.Decode(ctx.Args().Get(1)); err != nil {
510+
if account, err = hexutil.Decode(ctx.Args().Get(1)); err != nil {
511+
log.Info("Could not decode the account hash", "error", err)
512+
return err
513+
}
514+
if storage, err = hexutil.Decode(ctx.Args().Get(2)); err != nil {
515+
log.Info("Could not decode the storage trie root", "error", err)
516+
return err
517+
}
518+
if ctx.NArg() > 3 {
519+
if start, err = hexutil.Decode(ctx.Args().Get(3)); err != nil {
510520
log.Info("Could not decode the seek position", "error", err)
511521
return err
512522
}
513523
}
514-
if ctx.NArg() >= 3 {
515-
if max, err = strconv.ParseInt(ctx.Args().Get(2), 10, 64); err != nil {
524+
if ctx.NArg() > 4 {
525+
if max, err = strconv.ParseInt(ctx.Args().Get(4), 10, 64); err != nil {
516526
log.Info("Could not decode the max count", "error", err)
517527
return err
518528
}
519529
}
520-
theTrie, err := trie.New(common.Hash{}, stRoot, trie.NewDatabase(db))
530+
id := trie.StorageTrieID(common.BytesToHash(state), common.BytesToHash(account), common.BytesToHash(storage))
531+
theTrie, err := trie.New(id, trie.NewDatabase(db))
521532
if err != nil {
522533
return err
523534
}

cmd/geth/snapshot.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ func traverseState(ctx *cli.Context) error {
286286
log.Info("Start traversing the state", "root", root, "number", headBlock.NumberU64())
287287
}
288288
triedb := trie.NewDatabase(chaindb)
289-
t, err := trie.NewStateTrie(common.Hash{}, root, triedb)
289+
t, err := trie.NewStateTrie(trie.StateTrieID(root), triedb)
290290
if err != nil {
291291
log.Error("Failed to open trie", "root", root, "err", err)
292292
return err
@@ -307,7 +307,8 @@ func traverseState(ctx *cli.Context) error {
307307
return err
308308
}
309309
if acc.Root != emptyRoot {
310-
storageTrie, err := trie.NewStateTrie(common.BytesToHash(accIter.Key), acc.Root, triedb)
310+
id := trie.StorageTrieID(root, common.BytesToHash(accIter.Key), acc.Root)
311+
storageTrie, err := trie.NewStateTrie(id, triedb)
311312
if err != nil {
312313
log.Error("Failed to open storage trie", "root", acc.Root, "err", err)
313314
return err
@@ -375,7 +376,7 @@ func traverseRawState(ctx *cli.Context) error {
375376
log.Info("Start traversing the state", "root", root, "number", headBlock.NumberU64())
376377
}
377378
triedb := trie.NewDatabase(chaindb)
378-
t, err := trie.NewStateTrie(common.Hash{}, root, triedb)
379+
t, err := trie.NewStateTrie(trie.StateTrieID(root), triedb)
379380
if err != nil {
380381
log.Error("Failed to open trie", "root", root, "err", err)
381382
return err
@@ -421,7 +422,8 @@ func traverseRawState(ctx *cli.Context) error {
421422
return errors.New("invalid account")
422423
}
423424
if acc.Root != emptyRoot {
424-
storageTrie, err := trie.NewStateTrie(common.BytesToHash(accIter.LeafKey()), acc.Root, triedb)
425+
id := trie.StorageTrieID(root, common.BytesToHash(accIter.LeafKey()), acc.Root)
426+
storageTrie, err := trie.NewStateTrie(id, triedb)
425427
if err != nil {
426428
log.Error("Failed to open storage trie", "root", acc.Root, "err", err)
427429
return errors.New("missing storage trie")

core/blockchain.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ var (
7070
snapshotStorageReadTimer = metrics.NewRegisteredTimer("chain/snapshot/storage/reads", nil)
7171
snapshotCommitTimer = metrics.NewRegisteredTimer("chain/snapshot/commits", nil)
7272

73+
triedbCommitTimer = metrics.NewRegisteredTimer("chain/triedb/commits", nil)
74+
7375
blockInsertTimer = metrics.NewRegisteredTimer("chain/inserts", nil)
7476
blockValidationTimer = metrics.NewRegisteredTimer("chain/validation", nil)
7577
blockExecutionTimer = metrics.NewRegisteredTimer("chain/execution", nil)
@@ -737,10 +739,10 @@ func (bc *BlockChain) SnapSyncCommitHead(hash common.Hash) error {
737739
if block == nil {
738740
return fmt.Errorf("non existent block [%x..]", hash[:4])
739741
}
740-
if _, err := trie.NewStateTrie(common.Hash{}, block.Root(), bc.stateCache.TrieDB()); err != nil {
741-
return err
742+
root := block.Root()
743+
if !bc.HasState(root) {
744+
return fmt.Errorf("non existent state [%x..]", root[:4])
742745
}
743-
744746
// If all checks out, manually set the head block.
745747
if !bc.chainmu.TryLock() {
746748
return errChainStopped
@@ -752,7 +754,7 @@ func (bc *BlockChain) SnapSyncCommitHead(hash common.Hash) error {
752754
// Destroy any existing state snapshot and regenerate it in the background,
753755
// also resuming the normal maintenance of any previously paused snapshot.
754756
if bc.snaps != nil {
755-
bc.snaps.Rebuild(block.Root())
757+
bc.snaps.Rebuild(root)
756758
}
757759
log.Info("Committed new head block", "number", block.Number(), "hash", hash)
758760
return nil
@@ -1750,8 +1752,9 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals, setHead bool)
17501752
accountCommitTimer.Update(statedb.AccountCommits) // Account commits are complete, we can mark them
17511753
storageCommitTimer.Update(statedb.StorageCommits) // Storage commits are complete, we can mark them
17521754
snapshotCommitTimer.Update(statedb.SnapshotCommits) // Snapshot commits are complete, we can mark them
1755+
triedbCommitTimer.Update(statedb.TrieDBCommits) // Triedb commits are complete, we can mark them
17531756

1754-
blockWriteTimer.Update(time.Since(substart) - statedb.AccountCommits - statedb.StorageCommits - statedb.SnapshotCommits)
1757+
blockWriteTimer.Update(time.Since(substart) - statedb.AccountCommits - statedb.StorageCommits - statedb.SnapshotCommits - statedb.TrieDBCommits)
17551758
blockInsertTimer.UpdateSince(start)
17561759

17571760
// Report the import stats before returning the various results

core/state/database.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ type Database interface {
4343
OpenTrie(root common.Hash) (Trie, error)
4444

4545
// OpenStorageTrie opens the storage trie of an account.
46-
OpenStorageTrie(addrHash, root common.Hash) (Trie, error)
46+
OpenStorageTrie(stateRoot common.Hash, addrHash, root common.Hash) (Trie, error)
4747

4848
// CopyTrie returns an independent copy of the given trie.
4949
CopyTrie(Trie) Trie
@@ -148,16 +148,16 @@ type cachingDB struct {
148148

149149
// OpenTrie opens the main account trie at a specific root hash.
150150
func (db *cachingDB) OpenTrie(root common.Hash) (Trie, error) {
151-
tr, err := trie.NewStateTrie(common.Hash{}, root, db.db)
151+
tr, err := trie.NewStateTrie(trie.StateTrieID(root), db.db)
152152
if err != nil {
153153
return nil, err
154154
}
155155
return tr, nil
156156
}
157157

158158
// OpenStorageTrie opens the storage trie of an account.
159-
func (db *cachingDB) OpenStorageTrie(addrHash, root common.Hash) (Trie, error) {
160-
tr, err := trie.NewStateTrie(addrHash, root, db.db)
159+
func (db *cachingDB) OpenStorageTrie(stateRoot common.Hash, addrHash, root common.Hash) (Trie, error) {
160+
tr, err := trie.NewStateTrie(trie.StorageTrieID(stateRoot, addrHash, root), db.db)
161161
if err != nil {
162162
return nil, err
163163
}

core/state/iterator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func (it *NodeIterator) step() error {
109109
if err := rlp.Decode(bytes.NewReader(it.stateIt.LeafBlob()), &account); err != nil {
110110
return err
111111
}
112-
dataTrie, err := it.state.db.OpenStorageTrie(common.BytesToHash(it.stateIt.LeafKey()), account.Root)
112+
dataTrie, err := it.state.db.OpenStorageTrie(it.state.originalRoot, common.BytesToHash(it.stateIt.LeafKey()), account.Root)
113113
if err != nil {
114114
return err
115115
}

core/state/metrics.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ package state
1919
import "github.com/ethereum/go-ethereum/metrics"
2020

2121
var (
22-
accountUpdatedMeter = metrics.NewRegisteredMeter("state/update/account", nil)
23-
storageUpdatedMeter = metrics.NewRegisteredMeter("state/update/storage", nil)
24-
accountDeletedMeter = metrics.NewRegisteredMeter("state/delete/account", nil)
25-
storageDeletedMeter = metrics.NewRegisteredMeter("state/delete/storage", nil)
26-
accountTrieCommittedMeter = metrics.NewRegisteredMeter("state/commit/accountnodes", nil)
27-
storageTriesCommittedMeter = metrics.NewRegisteredMeter("state/commit/storagenodes", nil)
22+
accountUpdatedMeter = metrics.NewRegisteredMeter("state/update/account", nil)
23+
storageUpdatedMeter = metrics.NewRegisteredMeter("state/update/storage", nil)
24+
accountDeletedMeter = metrics.NewRegisteredMeter("state/delete/account", nil)
25+
storageDeletedMeter = metrics.NewRegisteredMeter("state/delete/storage", nil)
26+
accountTrieUpdatedMeter = metrics.NewRegisteredMeter("state/update/accountnodes", nil)
27+
storageTriesUpdatedMeter = metrics.NewRegisteredMeter("state/update/storagenodes", nil)
28+
accountTrieDeletedMeter = metrics.NewRegisteredMeter("state/delete/accountnodes", nil)
29+
storageTriesDeletedMeter = metrics.NewRegisteredMeter("state/delete/storagenodes", nil)
2830
)

core/state/pruner/pruner.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ type Pruner struct {
9393
func NewPruner(db ethdb.Database, config Config) (*Pruner, error) {
9494
headBlock := rawdb.ReadHeadBlock(db)
9595
if headBlock == nil {
96-
return nil, errors.New("Failed to load head block")
96+
return nil, errors.New("failed to load head block")
9797
}
9898
snapconfig := snapshot.Config{
9999
CacheSize: 256,
@@ -427,7 +427,7 @@ func extractGenesis(db ethdb.Database, stateBloom *stateBloom) error {
427427
if genesis == nil {
428428
return errors.New("missing genesis block")
429429
}
430-
t, err := trie.NewStateTrie(common.Hash{}, genesis.Root(), trie.NewDatabase(db))
430+
t, err := trie.NewStateTrie(trie.StateTrieID(genesis.Root()), trie.NewDatabase(db))
431431
if err != nil {
432432
return err
433433
}
@@ -447,7 +447,8 @@ func extractGenesis(db ethdb.Database, stateBloom *stateBloom) error {
447447
return err
448448
}
449449
if acc.Root != emptyRoot {
450-
storageTrie, err := trie.NewStateTrie(common.BytesToHash(accIter.LeafKey()), acc.Root, trie.NewDatabase(db))
450+
id := trie.StorageTrieID(genesis.Root(), common.BytesToHash(accIter.LeafKey()), acc.Root)
451+
storageTrie, err := trie.NewStateTrie(id, trie.NewDatabase(db))
451452
if err != nil {
452453
return err
453454
}

core/state/snapshot/generate.go

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ func (result *proofResult) forEach(callback func(key []byte, val []byte) error)
166166
//
167167
// The proof result will be returned if the range proving is finished, otherwise
168168
// the error will be returned to abort the entire procedure.
169-
func (dl *diskLayer) proveRange(ctx *generatorContext, owner common.Hash, root common.Hash, prefix []byte, kind string, origin []byte, max int, valueConvertFn func([]byte) ([]byte, error)) (*proofResult, error) {
169+
func (dl *diskLayer) proveRange(ctx *generatorContext, trieId *trie.ID, prefix []byte, kind string, origin []byte, max int, valueConvertFn func([]byte) ([]byte, error)) (*proofResult, error) {
170170
var (
171171
keys [][]byte
172172
vals [][]byte
@@ -233,8 +233,9 @@ func (dl *diskLayer) proveRange(ctx *generatorContext, owner common.Hash, root c
233233
}(time.Now())
234234

235235
// The snap state is exhausted, pass the entire key/val set for verification
236+
root := trieId.Root
236237
if origin == nil && !diskMore {
237-
stackTr := trie.NewStackTrieWithOwner(nil, owner)
238+
stackTr := trie.NewStackTrie(nil)
238239
for i, key := range keys {
239240
stackTr.TryUpdate(key, vals[i])
240241
}
@@ -248,7 +249,7 @@ func (dl *diskLayer) proveRange(ctx *generatorContext, owner common.Hash, root c
248249
return &proofResult{keys: keys, vals: vals}, nil
249250
}
250251
// Snap state is chunked, generate edge proofs for verification.
251-
tr, err := trie.New(owner, root, dl.triedb)
252+
tr, err := trie.New(trieId, dl.triedb)
252253
if err != nil {
253254
ctx.stats.Log("Trie missing, state snapshotting paused", dl.root, dl.genMarker)
254255
return nil, errMissingTrie
@@ -313,9 +314,9 @@ type onStateCallback func(key []byte, val []byte, write bool, delete bool) error
313314
// generateRange generates the state segment with particular prefix. Generation can
314315
// either verify the correctness of existing state through range-proof and skip
315316
// generation, or iterate trie to regenerate state on demand.
316-
func (dl *diskLayer) generateRange(ctx *generatorContext, owner common.Hash, root common.Hash, prefix []byte, kind string, origin []byte, max int, onState onStateCallback, valueConvertFn func([]byte) ([]byte, error)) (bool, []byte, error) {
317+
func (dl *diskLayer) generateRange(ctx *generatorContext, trieId *trie.ID, prefix []byte, kind string, origin []byte, max int, onState onStateCallback, valueConvertFn func([]byte) ([]byte, error)) (bool, []byte, error) {
317318
// Use range prover to check the validity of the flat state in the range
318-
result, err := dl.proveRange(ctx, owner, root, prefix, kind, origin, max, valueConvertFn)
319+
result, err := dl.proveRange(ctx, trieId, prefix, kind, origin, max, valueConvertFn)
319320
if err != nil {
320321
return false, nil, err
321322
}
@@ -363,7 +364,7 @@ func (dl *diskLayer) generateRange(ctx *generatorContext, owner common.Hash, roo
363364
if len(result.keys) > 0 {
364365
snapNodeCache = memorydb.New()
365366
snapTrieDb := trie.NewDatabase(snapNodeCache)
366-
snapTrie, _ := trie.New(owner, common.Hash{}, snapTrieDb)
367+
snapTrie := trie.NewEmpty(snapTrieDb)
367368
for i, key := range result.keys {
368369
snapTrie.Update(key, result.vals[i])
369370
}
@@ -377,7 +378,7 @@ func (dl *diskLayer) generateRange(ctx *generatorContext, owner common.Hash, roo
377378
// if it's already opened with some nodes resolved.
378379
tr := result.tr
379380
if tr == nil {
380-
tr, err = trie.New(owner, root, dl.triedb)
381+
tr, err = trie.New(trieId, dl.triedb)
381382
if err != nil {
382383
ctx.stats.Log("Trie missing, state snapshotting paused", dl.root, dl.genMarker)
383384
return false, nil, errMissingTrie
@@ -460,7 +461,7 @@ func (dl *diskLayer) generateRange(ctx *generatorContext, owner common.Hash, roo
460461
} else {
461462
snapAccountTrieReadCounter.Inc((time.Since(start) - internal).Nanoseconds())
462463
}
463-
logger.Debug("Regenerated state range", "root", root, "last", hexutil.Encode(last),
464+
logger.Debug("Regenerated state range", "root", trieId.Root, "last", hexutil.Encode(last),
464465
"count", count, "created", created, "updated", updated, "untouched", untouched, "deleted", deleted)
465466

466467
// If there are either more trie items, or there are more snap items
@@ -511,7 +512,7 @@ func (dl *diskLayer) checkAndFlush(ctx *generatorContext, current []byte) error
511512

512513
// generateStorages generates the missing storage slots of the specific contract.
513514
// It's supposed to restart the generation from the given origin position.
514-
func generateStorages(ctx *generatorContext, dl *diskLayer, account common.Hash, storageRoot common.Hash, storeMarker []byte) error {
515+
func generateStorages(ctx *generatorContext, dl *diskLayer, stateRoot common.Hash, account common.Hash, storageRoot common.Hash, storeMarker []byte) error {
515516
onStorage := func(key []byte, val []byte, write bool, delete bool) error {
516517
defer func(start time.Time) {
517518
snapStorageWriteCounter.Inc(time.Since(start).Nanoseconds())
@@ -540,7 +541,8 @@ func generateStorages(ctx *generatorContext, dl *diskLayer, account common.Hash,
540541
// Loop for re-generating the missing storage slots.
541542
var origin = common.CopyBytes(storeMarker)
542543
for {
543-
exhausted, last, err := dl.generateRange(ctx, account, storageRoot, append(rawdb.SnapshotStoragePrefix, account.Bytes()...), snapStorage, origin, storageCheckRange, onStorage, nil)
544+
id := trie.StorageTrieID(stateRoot, account, storageRoot)
545+
exhausted, last, err := dl.generateRange(ctx, id, append(rawdb.SnapshotStoragePrefix, account.Bytes()...), snapStorage, origin, storageCheckRange, onStorage, nil)
544546
if err != nil {
545547
return err // The procedure it aborted, either by external signal or internal error.
546548
}
@@ -624,7 +626,7 @@ func generateAccounts(ctx *generatorContext, dl *diskLayer, accMarker []byte) er
624626
if accMarker != nil && bytes.Equal(account[:], accMarker) && len(dl.genMarker) > common.HashLength {
625627
storeMarker = dl.genMarker[common.HashLength:]
626628
}
627-
if err := generateStorages(ctx, dl, account, acc.Root, storeMarker); err != nil {
629+
if err := generateStorages(ctx, dl, dl.root, account, acc.Root, storeMarker); err != nil {
628630
return err
629631
}
630632
}
@@ -640,7 +642,8 @@ func generateAccounts(ctx *generatorContext, dl *diskLayer, accMarker []byte) er
640642
}
641643
origin := common.CopyBytes(accMarker)
642644
for {
643-
exhausted, last, err := dl.generateRange(ctx, common.Hash{}, dl.root, rawdb.SnapshotAccountPrefix, snapAccount, origin, accountRange, onAccount, FullAccountRLP)
645+
id := trie.StateTrieID(dl.root)
646+
exhausted, last, err := dl.generateRange(ctx, id, rawdb.SnapshotAccountPrefix, snapAccount, origin, accountRange, onAccount, FullAccountRLP)
644647
if err != nil {
645648
return err // The procedure it aborted, either by external signal or internal error.
646649
}

core/state/snapshot/generate_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ type testHelper struct {
149149
func newHelper() *testHelper {
150150
diskdb := rawdb.NewMemoryDatabase()
151151
triedb := trie.NewDatabase(diskdb)
152-
accTrie, _ := trie.NewStateTrie(common.Hash{}, common.Hash{}, triedb)
152+
accTrie, _ := trie.NewStateTrie(trie.StateTrieID(common.Hash{}), triedb)
153153
return &testHelper{
154154
diskdb: diskdb,
155155
triedb: triedb,
@@ -182,7 +182,8 @@ func (t *testHelper) addSnapStorage(accKey string, keys []string, vals []string)
182182
}
183183

184184
func (t *testHelper) makeStorageTrie(stateRoot, owner common.Hash, keys []string, vals []string, commit bool) []byte {
185-
stTrie, _ := trie.NewStateTrie(owner, common.Hash{}, t.triedb)
185+
id := trie.StorageTrieID(stateRoot, owner, common.Hash{})
186+
stTrie, _ := trie.NewStateTrie(id, t.triedb)
186187
for i, k := range keys {
187188
stTrie.Update([]byte(k), []byte(vals[i]))
188189
}

core/state/state_object.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,9 @@ func (s *stateObject) getTrie(db Database) Trie {
159159
}
160160
if s.trie == nil {
161161
var err error
162-
s.trie, err = db.OpenStorageTrie(s.addrHash, s.data.Root)
162+
s.trie, err = db.OpenStorageTrie(s.db.originalRoot, s.addrHash, s.data.Root)
163163
if err != nil {
164-
s.trie, _ = db.OpenStorageTrie(s.addrHash, common.Hash{})
164+
s.trie, _ = db.OpenStorageTrie(s.db.originalRoot, s.addrHash, common.Hash{})
165165
s.setError(fmt.Errorf("can't create storage trie: %v", err))
166166
}
167167
}

0 commit comments

Comments
 (0)