diff --git a/cmd/geth/dbcmd.go b/cmd/geth/dbcmd.go index 0c5ef572df..2ebc1c21b1 100644 --- a/cmd/geth/dbcmd.go +++ b/cmd/geth/dbcmd.go @@ -78,22 +78,21 @@ Remove blockchain and state databases`, dbCompactCmd, dbGetCmd, dbDeleteCmd, - dbDeleteTrieStateCmd, - dbInspectTrieCmd, dbPutCmd, dbGetSlotsCmd, dbDumpFreezerIndex, dbImportCmd, dbExportCmd, dbMetadataCmd, - ancientInspectCmd, - // no legacy stored receipts for bsc - // dbMigrateFreezerCmd, dbCheckStateContentCmd, - dbHbss2PbssCmd, + dbInspectHistoryCmd, + + // only defined in bsc + dbInspectTrieCmd, dbTrieGetCmd, dbTrieDeleteCmd, - dbInspectHistoryCmd, + dbDeleteTrieStateCmd, + ancientInspectCmd, }, } dbInspectCmd = &cli.Command{ @@ -125,19 +124,6 @@ Remove blockchain and state databases`, For each trie node encountered, it checks that the key corresponds to the keccak256(value). If this is not true, this indicates a data corruption.`, } - dbHbss2PbssCmd = &cli.Command{ - Action: hbss2pbss, - Name: "hbss-to-pbss", - ArgsUsage: "", - Flags: []cli.Flag{ - utils.DataDirFlag, - utils.SyncModeFlag, - utils.ForceFlag, - 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.`, - } dbTrieGetCmd = &cli.Command{ Action: dbTrieGet, Name: "trie-get", @@ -1201,112 +1187,6 @@ func showMetaData(ctx *cli.Context) error { return nil } -func hbss2pbss(ctx *cli.Context) error { - if ctx.NArg() > 1 { - return fmt.Errorf("required arguments: %v", ctx.Command.ArgsUsage) - } - - var jobnum uint64 - var err error - if ctx.NArg() == 1 { - jobnum, err = strconv.ParseUint(ctx.Args().Get(0), 10, 64) - if err != nil { - return fmt.Errorf("failed to Parse jobnum, Args[1]: %v, err: %v", ctx.Args().Get(1), err) - } - } else { - // by default - jobnum = 1000 - } - - force := ctx.Bool(utils.ForceFlag.Name) - - stack, _ := makeConfigNode(ctx) - defer stack.Close() - - db := utils.MakeChainDatabase(ctx, stack, false) - db.SyncAncient() - defer db.Close() - - // convert hbss trie node to pbss trie node - var lastStateID uint64 - lastStateID = rawdb.ReadPersistentStateID(db.GetStateStore()) - if lastStateID == 0 || force { - config := triedb.HashDefaults - triedb := triedb.NewDatabase(db, config) - triedb.Cap(0) - log.Info("hbss2pbss triedb", "scheme", triedb.Scheme()) - defer triedb.Close() - - headerHash := rawdb.ReadHeadHeaderHash(db) - blockNumber := rawdb.ReadHeaderNumber(db, headerHash) - if blockNumber == nil { - log.Error("read header number failed.") - return fmt.Errorf("read header number failed") - } - - log.Info("hbss2pbss converting", "HeaderHash: ", headerHash.String(), ", blockNumber: ", *blockNumber) - - var headerBlockHash common.Hash - var trieRootHash common.Hash - - if *blockNumber != math.MaxUint64 { - headerBlockHash = rawdb.ReadCanonicalHash(db, *blockNumber) - if headerBlockHash == (common.Hash{}) { - return errors.New("ReadHeadBlockHash empty hash") - } - blockHeader := rawdb.ReadHeader(db, headerBlockHash, *blockNumber) - trieRootHash = blockHeader.Root - fmt.Println("Canonical Hash: ", headerBlockHash.String(), ", TrieRootHash: ", trieRootHash.String()) - } - if (trieRootHash == common.Hash{}) { - log.Error("Empty root hash") - return errors.New("Empty root hash.") - } - - id := trie.StateTrieID(trieRootHash) - theTrie, err := trie.New(id, triedb) - if err != nil { - log.Error("fail to new trie tree", "err", err, "rootHash", err, trieRootHash.String()) - return err - } - - h2p, err := trie.NewHbss2Pbss(theTrie, triedb, trieRootHash, *blockNumber, jobnum) - if err != nil { - log.Error("fail to new hash2pbss", "err", err, "rootHash", err, trieRootHash.String()) - return err - } - h2p.Run() - } else { - log.Info("Convert hbss to pbss success. Nothing to do.") - } - - lastStateID = rawdb.ReadPersistentStateID(db.GetStateStore()) - - if lastStateID == 0 { - log.Error("Convert hbss to pbss trie node error. The last state id is still 0") - } - - var ancient string - if db.HasSeparateStateStore() { - dirName := filepath.Join(stack.ResolvePath("chaindata"), "state") - ancient = filepath.Join(dirName, "ancient") - } else { - ancient = stack.ResolveAncient("chaindata", ctx.String(utils.AncientFlag.Name)) - } - err = rawdb.ResetStateFreezerTableOffset(ancient, lastStateID) - if err != nil { - log.Error("Reset state freezer table offset failed", "error", err) - return err - } - // prune hbss trie node - err = rawdb.PruneHashTrieNodeInDataBase(db.GetStateStore()) - if err != nil { - log.Error("Prune Hash trie node in database failed", "error", err) - return err - } - return nil -} - func inspectAccount(db *triedb.Database, start uint64, end uint64, address common.Address, raw bool) error { stats, err := db.AccountHistory(address, start, end) if err != nil { diff --git a/cmd/geth/snapshot.go b/cmd/geth/snapshot.go index 01ba9b359d..e8d1440731 100644 --- a/cmd/geth/snapshot.go +++ b/cmd/geth/snapshot.go @@ -27,16 +27,13 @@ import ( "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state/pruner" "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" "github.com/urfave/cli/v2" @@ -80,29 +77,6 @@ geth snapshot verify-state will traverse the whole accounts and storages set based on the specified snapshot and recalculate the root hash of state for verification. In other words, this command does the snapshot to trie conversion. -`, - }, - { - Name: "insecure-prune-all", - Usage: "Prune all trie state data except genesis block, it will break storage for fullnode, only suitable for fast node " + - "who do not need trie storage at all", - ArgsUsage: "", - Action: pruneAllState, - Category: "MISCELLANEOUS COMMANDS", - Flags: []cli.Flag{ - utils.DataDirFlag, - utils.AncientFlag, - }, - Description: ` -will prune all historical trie state data except genesis block. -All trie nodes will be deleted from the database. - -It expects the genesis file as argument. - -WARNING: It's necessary to delete the trie clean cache after the pruning. -If you specify another directory for the trie clean cache via "--cache.trie.journal" -during the use of Geth, please also specify it here for correct deletion. Otherwise -the trie clean cache with default directory will be deleted. `, }, { @@ -235,49 +209,6 @@ func pruneState(ctx *cli.Context) error { return nil } -func pruneAllState(ctx *cli.Context) error { - stack, _ := makeConfigNode(ctx) - defer stack.Close() - - genesisPath := ctx.Args().First() - if len(genesisPath) == 0 { - utils.Fatalf("Must supply path to genesis JSON file") - } - file, err := os.Open(genesisPath) - if err != nil { - utils.Fatalf("Failed to read genesis file: %v", err) - } - defer file.Close() - - g := new(core.Genesis) - if err := json.NewDecoder(file).Decode(g); err != nil { - cfg := gethConfig{ - Eth: ethconfig.Defaults, - Node: defaultNodeConfig(), - Metrics: metrics.DefaultConfig, - } - - // Load config file. - if err := loadConfig(genesisPath, &cfg); err != nil { - utils.Fatalf("%v", err) - } - g = cfg.Eth.Genesis - } - - chaindb := utils.MakeChainDatabase(ctx, stack, false) - defer chaindb.Close() - pruner, err := pruner.NewAllPruner(chaindb) - if err != nil { - log.Error("Failed to open snapshot tree", "err", err) - return err - } - if err = pruner.PruneAll(g); err != nil { - log.Error("Failed to prune state", "err", err) - return err - } - return nil -} - func verifyState(ctx *cli.Context) error { stack, _ := makeConfigNode(ctx) defer stack.Close() diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 87418f1e57..80fb36303a 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -587,28 +587,6 @@ func AncientInspect(db ethdb.Database) error { return nil } -func PruneHashTrieNodeInDataBase(db ethdb.Database) error { - it := db.NewIterator([]byte{}, []byte{}) - defer it.Release() - - 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) - } - default: - continue - } - } - log.Info("Pruning hash-base state trie nodes", "Complete progress", total_num) - return nil -} - type DataType int const ( diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index 5ca8e4220b..2cfa1f4cba 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -27,14 +27,9 @@ import ( "strings" "time" - "github.com/holiman/uint256" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state/snapshot" - "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" @@ -124,109 +119,6 @@ func NewPruner(db ethdb.Database, config Config, triesInMemory uint64) (*Pruner, }, nil } -func NewAllPruner(db ethdb.Database) (*Pruner, error) { - headBlock := rawdb.ReadHeadBlock(db) - if headBlock == nil { - return nil, errors.New("Failed to load head block") - } - return &Pruner{ - db: db, - }, nil -} - -func (p *Pruner) PruneAll(genesis *core.Genesis) error { - return p.pruneAll(p.db, genesis) -} - -func (p *Pruner) pruneAll(maindb ethdb.Database, g *core.Genesis) error { - var pruneDB ethdb.Database - if maindb != nil && maindb.HasSeparateStateStore() { - pruneDB = maindb.GetStateStore() - } else { - pruneDB = maindb - } - var ( - count int - size common.StorageSize - pstart = time.Now() - logged = time.Now() - batch = pruneDB.NewBatch() - iter = pruneDB.NewIterator(nil, nil) - ) - start := time.Now() - for iter.Next() { - key := iter.Key() - if len(key) == common.HashLength { - count += 1 - size += common.StorageSize(len(key) + len(iter.Value())) - batch.Delete(key) - - var eta time.Duration // Realistically will never remain uninited - if done := binary.BigEndian.Uint64(key[:8]); done > 0 { - var ( - left = math.MaxUint64 - binary.BigEndian.Uint64(key[:8]) - speed = done/uint64(time.Since(pstart)/time.Millisecond+1) + 1 // +1s to avoid division by zero - ) - eta = time.Duration(left/speed) * time.Millisecond - } - if time.Since(logged) > 8*time.Second { - log.Info("Pruning state data", "nodes", count, "size", size, - "elapsed", common.PrettyDuration(time.Since(pstart)), "eta", common.PrettyDuration(eta)) - logged = time.Now() - } - // Recreate the iterator after every batch commit in order - // to allow the underlying compactor to delete the entries. - if batch.ValueSize() >= ethdb.IdealBatchSize { - batch.Write() - batch.Reset() - - iter.Release() - iter = pruneDB.NewIterator(nil, key) - } - } - } - if batch.ValueSize() > 0 { - batch.Write() - batch.Reset() - } - iter.Release() - log.Info("Pruned state data", "nodes", count, "size", size, "elapsed", common.PrettyDuration(time.Since(pstart))) - - // Start compactions, will remove the deleted data from the disk immediately. - // Note for small pruning, the compaction is skipped. - if count >= rangeCompactionThreshold { - 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 := pruneDB.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))) - } - statedb, _ := state.New(common.Hash{}, state.NewDatabase(triedb.NewDatabase(maindb, nil), p.snaptree)) - for addr, account := range g.Alloc { - statedb.AddBalance(addr, uint256.MustFromBig(account.Balance), tracing.BalanceChangeUnspecified) - statedb.SetCode(addr, account.Code) - statedb.SetNonce(addr, account.Nonce, tracing.NonceChangeGenesis) - for key, value := range account.Storage { - statedb.SetState(addr, key, value) - } - } - root, _ := statedb.Commit(0, false, false) - statedb.Database().TrieDB().Commit(root, true) - log.Info("State pruning successful", "pruned", size, "elapsed", common.PrettyDuration(time.Since(start))) - return nil -} - func prune(snaptree *snapshot.Tree, root common.Hash, maindb ethdb.Database, stateBloom *stateBloom, bloomPath string, middleStateRoots map[common.Hash]struct{}, start time.Time) error { // Delete all stale trie nodes in the disk. With the help of state bloom // the trie nodes(and codes) belong to the active state will be filtered diff --git a/trie/hbss2pbss.go b/trie/hbss2pbss.go deleted file mode 100644 index 3b86534d3b..0000000000 --- a/trie/hbss2pbss.go +++ /dev/null @@ -1,246 +0,0 @@ -package trie - -import ( - "bytes" - "errors" - "fmt" - "runtime" - "sync" - "sync/atomic" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/trie/trienode" -) - -func (n *fullNode) copy() *fullNode { copy := *n; return © } -func (n *shortNode) copy() *shortNode { copy := *n; return © } - -type Hbss2Pbss struct { - trie *Trie // traverse trie - db Database - blocknum uint64 - root node // root of triedb - stateRootHash common.Hash - concurrentQueue chan struct{} - totalNum uint64 - wg sync.WaitGroup -} - -const ( - DEFAULT_TRIEDBCACHE_SIZE = 1024 * 1024 * 1024 -) - -// NewHbss2Pbss return a hash2Path obj -func NewHbss2Pbss(tr *Trie, db Database, stateRootHash common.Hash, blocknum uint64, jobnum uint64) (*Hbss2Pbss, error) { - if tr == nil { - return nil, errors.New("trie is nil") - } - - if tr.root == nil { - return nil, errors.New("trie root is nil") - } - - ins := &Hbss2Pbss{ - trie: tr, - blocknum: blocknum, - db: db, - stateRootHash: stateRootHash, - root: tr.root, - concurrentQueue: make(chan struct{}, jobnum), - wg: sync.WaitGroup{}, - } - - return ins, nil -} - -func (t *Trie) resloveWithoutTrack(n node, prefix []byte) (node, error) { - if n, ok := n.(hashNode); ok { - blob, err := t.reader.node(prefix, common.BytesToHash(n)) - if err != nil { - return nil, err - } - return mustDecodeNode(n, blob), nil - } - return n, nil -} - -func (h2p *Hbss2Pbss) writeNode(pathKey []byte, n *trienode.Node, owner common.Hash) { - if owner == (common.Hash{}) { - rawdb.WriteAccountTrieNode(h2p.db.Disk().GetStateStore(), pathKey, n.Blob) - log.Debug("WriteNodes account node, ", "path: ", common.Bytes2Hex(pathKey), "Hash: ", n.Hash, "BlobHash: ", crypto.Keccak256Hash(n.Blob)) - } else { - rawdb.WriteStorageTrieNode(h2p.db.Disk().GetStateStore(), owner, pathKey, n.Blob) - log.Debug("WriteNodes storage node, ", "path: ", common.Bytes2Hex(pathKey), "owner: ", owner.String(), "Hash: ", n.Hash, "BlobHash: ", crypto.Keccak256Hash(n.Blob)) - } -} - -// Run statistics, external call -func (h2p *Hbss2Pbss) Run() { - log.Debug("Find Account Trie Tree, rootHash: ", h2p.trie.Hash().String(), "BlockNum: ", h2p.blocknum) - - h2p.ConcurrentTraversal(h2p.trie, h2p.root, []byte{}) - h2p.wg.Wait() - - log.Info("Total", "complete", h2p.totalNum, "go routines Num", runtime.NumGoroutine, "h2p concurrentQueue", len(h2p.concurrentQueue)) - - rawdb.WritePersistentStateID(h2p.db.Disk().GetStateStore(), h2p.blocknum) - rawdb.WriteStateID(h2p.db.Disk().GetStateStore(), h2p.stateRootHash, h2p.blocknum) -} - -func (h2p *Hbss2Pbss) SubConcurrentTraversal(theTrie *Trie, theNode node, path []byte) { - h2p.concurrentQueue <- struct{}{} - h2p.ConcurrentTraversal(theTrie, theNode, path) - <-h2p.concurrentQueue - h2p.wg.Done() -} - -func (h2p *Hbss2Pbss) ConcurrentTraversal(theTrie *Trie, theNode node, path []byte) { - total_num := uint64(0) - // nil node - if theNode == nil { - return - } - - switch current := (theNode).(type) { - case *shortNode: - collapsed := current.copy() - collapsed.Key = hexToCompact(current.Key) - var hash, _ = current.cache() - h2p.writeNode(path, trienode.New(common.BytesToHash(hash), nodeToBytes(collapsed)), theTrie.owner) - - h2p.ConcurrentTraversal(theTrie, current.Val, append(path, current.Key...)) - - case *fullNode: - // copy from trie/Committer (*committer).commit - collapsed := current.copy() - var hash, _ = collapsed.cache() - collapsed.Children = h2p.commitChildren(path, current) - - nodebytes := nodeToBytes(collapsed) - if common.BytesToHash(hash) != common.BytesToHash(crypto.Keccak256(nodebytes)) { - log.Error("Hash is inconsistent, hash: ", common.BytesToHash(hash), "node hash: ", common.BytesToHash(crypto.Keccak256(nodebytes)), "node: ", collapsed.fstring("")) - panic("hash inconsistent.") - } - - h2p.writeNode(path, trienode.New(common.BytesToHash(hash), nodeToBytes(collapsed)), theTrie.owner) - - for idx, child := range current.Children { - if child == nil { - continue - } - childPath := append(path, byte(idx)) - if len(h2p.concurrentQueue)*2 < cap(h2p.concurrentQueue) { - h2p.wg.Add(1) - dst := make([]byte, len(childPath)) - copy(dst, childPath) - go h2p.SubConcurrentTraversal(theTrie, child, dst) - } else { - h2p.ConcurrentTraversal(theTrie, child, childPath) - } - } - case hashNode: - n, err := theTrie.resloveWithoutTrack(current, path) - if err != nil { - log.Error("Resolve HashNode", "error", err, "TrieRoot", theTrie.Hash(), "Path", path) - return - } - h2p.ConcurrentTraversal(theTrie, n, path) - total_num = atomic.AddUint64(&h2p.totalNum, 1) - if total_num%100000 == 0 { - log.Info("Converting ", "Complete progress", total_num, "go routines Num", runtime.NumGoroutine(), "h2p concurrentQueue", len(h2p.concurrentQueue)) - } - return - case valueNode: - if !hasTerm(path) { - log.Info("ValueNode miss path term", "path", common.Bytes2Hex(path)) - break - } - var account types.StateAccount - if err := rlp.Decode(bytes.NewReader(current), &account); err != nil { - // log.Info("Rlp decode account failed.", "err", err) - break - } - if account.Root == (common.Hash{}) || account.Root == types.EmptyRootHash { - // log.Info("Not a storage trie.", "account", common.BytesToHash(path).String()) - break - } - - ownerAddress := common.BytesToHash(hexToCompact(path)) - tr, err := New(StorageTrieID(h2p.stateRootHash, ownerAddress, account.Root), h2p.db) - if err != nil { - log.Error("New Storage trie error", "err", err, "root", account.Root.String(), "owner", ownerAddress.String()) - break - } - log.Debug("Find Contract Trie Tree", "rootHash: ", tr.Hash().String(), "") - h2p.wg.Add(1) - go h2p.SubConcurrentTraversal(tr, tr.root, []byte{}) - default: - panic(errors.New("Invalid node type to traverse.")) - } -} - -// copy from trie/Committer (*committer).commit -func (h2p *Hbss2Pbss) commitChildren(path []byte, n *fullNode) [17]node { - var children [17]node - for i := 0; i < 16; i++ { - child := n.Children[i] - if child == nil { - continue - } - // If it's the hashed child, save the hash value directly. - // Note: it's impossible that the child in range [0, 15] - // is a valueNode. - if hn, ok := child.(hashNode); ok { - children[i] = hn - continue - } - - children[i] = h2p.commit(append(path, byte(i)), child) - } - // For the 17th child, it's possible the type is valuenode. - if n.Children[16] != nil { - children[16] = n.Children[16] - } - return children -} - -// commit collapses a node down into a hash node and returns it. -func (h2p *Hbss2Pbss) commit(path []byte, n node) node { - // if this path is clean, use available cached data - hash, dirty := n.cache() - if hash != nil && !dirty { - return hash - } - // Commit children, then parent, and remove the dirty flag. - switch cn := n.(type) { - case *shortNode: - // Commit child - collapsed := cn.copy() - - // If the child is fullNode, recursively commit, - // otherwise it can only be hashNode or valueNode. - if _, ok := cn.Val.(*fullNode); ok { - collapsed.Val = h2p.commit(append(path, cn.Key...), cn.Val) - } - // The key needs to be copied, since we're adding it to the - // modified nodeset. - collapsed.Key = hexToCompact(cn.Key) - return collapsed - case *fullNode: - hashedKids := h2p.commitChildren(path, cn) - collapsed := cn.copy() - collapsed.Children = hashedKids - - return collapsed - case hashNode: - return cn - default: - // nil, valuenode shouldn't be committed - panic(fmt.Sprintf("%T: invalid node: %v", n, n)) - } -} diff --git a/trie/inspect_trie.go b/trie/inspect_trie.go index 4cc53b0c14..d357419b00 100644 --- a/trie/inspect_trie.go +++ b/trie/inspect_trie.go @@ -32,6 +32,7 @@ type Database interface { } const TopN = 3 +const DEFAULT_TRIEDBCACHE_SIZE = 1024 * 1024 * 1024 type Inspector struct { trie *Trie // traverse trie diff --git a/trie/trie.go b/trie/trie.go index fdb4da9be4..a8072d9b53 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -711,3 +711,14 @@ func (t *Trie) Reset() { t.tracer.reset() t.committed = false } + +func (t *Trie) resloveWithoutTrack(n node, prefix []byte) (node, error) { + if n, ok := n.(hashNode); ok { + blob, err := t.reader.node(prefix, common.BytesToHash(n)) + if err != nil { + return nil, err + } + return mustDecodeNode(n, blob), nil + } + return n, nil +}