ethereum/go-ethereum · critical
%T: invalid node: %v (%v)
Error message
%T: invalid node: %v (%v)
What it means
This panic fires in Trie.delete when descending the trie to remove a key and encountering a node type outside the supported set (nil, *shortNode, *fullNode, hashNode, valueNode handled above). The format includes the offending key to aid debugging. Like the insert variant, it indicates the trie contains a structurally invalid node, usually from state corruption or misuse.
Source
Thrown at trie/trie.go:729
case nil:
return false, nil, nil
case hashNode:
// We've hit a part of the trie that isn't loaded yet. Load
// the node and delete from it. This leaves all child nodes on
// the path to the value in the trie.
rn, err := t.resolveAndTrack(n, prefix)
if err != nil {
return false, nil, err
}
dirty, nn, err := t.delete(rn, prefix, key)
if !dirty || err != nil {
return false, rn, err
}
return true, nn, nil
default:
panic(fmt.Sprintf("%T: invalid node: %v (%v)", n, n, key))
}
}
// copyNode deep-copies the supplied node along with its children recursively.
func copyNode(n node) node {
switch n := (n).(type) {
case nil:
return nil
case valueNode:
return valueNode(common.CopyBytes(n))
case *shortNode:
return &shortNode{
flags: n.flags.copy(),
Key: common.CopyBytes(n.Key),
Val: copyNode(n.Val),
}
case *fullNode:View on GitHub (pinned to 6bb0588ad8)
Solutions
- Capture the key from the panic message and check whether the stored value/node at that path is well-formed RLP
- Recreate the trie from the last known-good root and retry the delete
- If it reproduces deterministically, dump the path nodes (t.Get each prefix) and file/inspect where the invalid node was introduced
- Restore state from a snapshot or resync if on-disk state is corrupted
Example fix
// before
t.Delete(key) // panics: '%T: invalid node: %v (%v)'
// after
func safeDelete(t *trie.Trie, key []byte) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("trie delete failed for key %x: %v", key, r)
}
}()
return t.Delete(key)
} Defensive patterns
Strategy: try-catch
Validate before calling
// Confirm the key exists through the read path before mutating
if _, err := t.Get(key); err != nil {
return fmt.Errorf("key %x unreadable, refusing delete: %w", key, err)
} Type guard
func isTraversable(n node) bool {
switch n.(type) {
case nil, valueNode, *shortNode, *fullNode, hashNode:
return true
}
return false
} Try / catch
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("delete %x panicked: %v — likely corrupt state at this path", key, r)
}
}()
err = t.Delete(key) Prevention
- Route deletes through a wrapper that recovers and reports the key
- Keep the trie database attached for the trie's whole lifetime so hashNode children can resolve
- If using pathdb, avoid skipping required flatten/commit steps between mutations
When it happens
Trigger: Calling Trie.Delete (or DeleteWithPath) with a key whose path traverses a child node of unknown type — for example a valueNode occupying an internal (non-leaf) position, which can occur if the trie was assembled from inconsistent node data.
Common situations: Pruning/snapshot bugs leaving partially deleted tries; using a trie after its database was modified externally; forged or truncated test fixtures in state-test harnesses; concurrent access races.
Related errors
- %T: unknown node type
- invalid group depth size
- not implemented
- unexpected node type, %T
- %T: invalid node: %v
AI-assisted analysis of ethereum/go-ethereum@6bb0588ad8 (2026-08-15).
Data as JSON: /api/errors/4095d89e36f36179.
Report an issue: GitHub.