ethereum/go-ethereum · critical
duplicated flush operation
Error message
duplicated flush operation
What it means
pathdb's in-memory buffer flushes dirty nodes to disk asynchronously, and flush marks the buffer as consumed by setting b.done. A second flush on the same buffer means the database attempted to persist two overlapping buffer generations — a lifecycle invariant violation that would risk writing inconsistent state, so it panics immediately.
Source
Thrown at triedb/pathdb/buffer.go:137
return b.layers == 0
}
// full returns an indicator if the size of accumulated content exceeds the
// configured threshold.
func (b *buffer) full() bool {
return b.size() > b.limit
}
// size returns the approximate memory size of the held content.
func (b *buffer) size() uint64 {
return b.states.size + b.nodes.size
}
// flush persists the in-memory dirty trie node into the disk if the configured
// memory threshold is reached. Note, all data must be written atomically.
func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, freezers []ethdb.AncientWriter, progress []byte, nodesCache, statesCache *fastcache.Cache, id uint64, postFlush func()) {
if b.done != nil {
panic("duplicated flush operation")
}
b.done = make(chan struct{}) // allocate the channel for notification
// Schedule the background thread to construct the batch, which usually
// take a few seconds.
go func() {
defer func() {
if postFlush != nil {
postFlush()
}
close(b.done)
}()
// Ensure the target state id is aligned with the internal counter.
head := rawdb.ReadPersistentStateID(db)
if head+b.layers != id {
b.flushErr = fmt.Errorf("buffer layers (%d) cannot be applied on top of persisted state id (%d) to reach requested state id (%d)", b.layers, head, id)
returnView on GitHub (pinned to 6bb0588ad8)
Solutions
- Serialize commits: pathdb commits must go through the single db.commit lock; check for a fork that bypassed it
- Inspect the goroutine stacks in the panic dump to find which two paths both reached flush, and gate the second on the first's completion via the done channel
- Upgrade/patch if this reproduces on stock geth — it is an internal invariant break, not caller error
Example fix
// before // two goroutines both trigger the limit path go db.Commit(...) // flush #1 go db.Commit(...) // flush #2 -> duplicated flush operation // after db.lock.Lock() defer db.lock.Unlock() db.Commit(...) // flush serialized under the db lock
Defensive patterns
Strategy: validation
Validate before calling
// (Internal) callers must ensure single flush per buffer generation
if b.done != nil {
return errors.New("buffer already flushing") // hypothetical guard if you fork flush
} Try / catch
defer func() {
if r := recover(); r != nil {
log.Crit("pathdb double flush", "panic", r) // crash-dump, do not continue serving state
}
}()
triedb.Commit(...) Prevention
- Drive pathdb only through its public Commit/Checkpoint/Flush APIs under one lock
- Never invoke buffer.flush from fork code; it is lifecycle-internal
- If you need concurrent commits, serialize at the triedb level and test with -race
When it happens
Trigger: buffer.flush being invoked twice on the same buffer object — e.g. two concurrent commits hitting the memory limit, or a commit racing with a database close/reopen that re-flushes an already-draining buffer.
Common situations: Concurrent calls to triedb Commit/Flush from multiple goroutines without the outer lock; fork modifications that call flush manually; bugs in the layering that reuse a buffer after checkpointing.
Related errors
- unknown layer type: %T
- not supported
- not implemented
- nil chainID
- error decoding contract deployer hex %s: %v
AI-assisted analysis of ethereum/go-ethereum@6bb0588ad8 (2026-08-15).
Data as JSON: /api/errors/cf9680fc9e8cede7.
Report an issue: GitHub.