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

  1. Capture the key from the panic message and check whether the stored value/node at that path is well-formed RLP
  2. Recreate the trie from the last known-good root and retry the delete
  3. If it reproduces deterministically, dump the path nodes (t.Get each prefix) and file/inspect where the invalid node was introduced
  4. 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

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


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