ethereum/go-ethereum · error

not implemented

Error message

not implemented

What it means

The cleaner type wraps the hash database's node cache so it can be iterated as an ethdb.KeyValueReader-ish interface, but the clean cache is a pure read cache backed by disk; deleting individual keys through it is meaningless (disk is the source of truth). Delete therefore panics to signal an unsupported operation rather than silently doing nothing.

Source

Thrown at triedb/hashdb/database.go:532

		c.db.dirties[node.flushPrev].flushNext = node.flushNext
		c.db.dirties[node.flushNext].flushPrev = node.flushPrev
	}
	// Remove the node from the dirty cache
	delete(c.db.dirties, hash)
	c.db.dirtiesSize -= common.StorageSize(common.HashLength + len(node.node))
	if node.external != nil {
		c.db.childrenSize -= common.StorageSize(len(node.external) * common.HashLength)
	}
	// Move the flushed node into the clean cache to prevent insta-reloads
	if c.db.cleans != nil {
		c.db.cleans.Set(hash[:], rlp)
		memcacheCleanWriteMeter.Mark(int64(len(rlp)))
	}
	return nil
}

func (c *cleaner) Delete(key []byte) error {
	panic("not implemented")
}

// Update inserts the dirty nodes in provided nodeset into database and link the
// account trie with multiple storage tries if necessary.
func (db *Database) Update(root common.Hash, parent common.Hash, block uint64, nodes *trienode.MergedNodeSet) error {
	// Ensure the parent state is present and signal a warning if not.
	if parent != types.EmptyRootHash {
		if blob, _ := db.node(parent); len(blob) == 0 {
			log.Error("parent state is not present")
		}
	}
	db.lock.Lock()
	defer db.lock.Unlock()

	// Insert dirty nodes into the database. In the same tree, it must be
	// ensured that children are inserted first, then parent so that children
	// can be linked with their parent correctly.
	//

View on GitHub (pinned to 6bb0588ad8)

Solutions

  1. Audit the call chain that reaches cleaner.Delete and stop routing write operations to the clean cache
  2. Perform deletions on the real backing store (freezer/LightPeerDatabase), not the cache wrapper
  3. If fork semantics genuinely require it, implement Delete as a cache eviction (c.db.cleans.Set(hash, nil) or equivalent) instead of panicking

Example fix

// before
// generic compaction walking all "databases"
for _, db := range handles { db.Delete(key) } // panics on cleaner

// after
for _, db := range handles {
    if _, readOnly := db.(*hashdb.cleaner); readOnly { continue }
    db.Delete(key)
}
Defensive patterns

Strategy: validation

Validate before calling

// Gate cache-backed handles out of write paths
func isReadOnlyCache(db ethdb.Database) bool {
    return strings.Contains(fmt.Sprintf("%T", db), "cleaner")
}
if !isReadOnlyCache(target) { target.Delete(key) }

Type guard

type deletable interface{ Delete([]byte) error }
func canDelete(db ethdb.Database) bool {
    if _, ok := db.(deletable); !ok { return false }
    return !isReadOnlyCache(db)
}

Prevention

When it happens

Trigger: Calling Delete on the cleaner wrapper exposed by Database (hashdb) — in practice this happens when generic code treats the cache as a general KeyValueStore and invokes delete/compaction paths on it.

Common situations: Fork code that runs compaction (DeleteRange/Delete) against every database handle it can find; accidental reuse of the iterator/cache handle beyond its intended read-only scope; upstream refactors that widened an interface the cleaner implements.

Related errors


AI-assisted analysis of ethereum/go-ethereum@6bb0588ad8 (2026-08-15). Data as JSON: /api/errors/bf4f09a318099600. Report an issue: GitHub.