triedb/pathdb: fix trienode history gap when newly enabled on existing node

When enabling trienode history (--history.trienode=0) on a node that
already has state at block N, repairHistory() returns a fatal error:
"gap between state [#N] and trienode history [#0]"

This happens because the newly created trienode freezer has 0 entries
while state is at block N. The gap check on line 427 treats this as a
data corruption error, but the function's own comments document that
it should "detect and resolve such gaps" for the optional trienode
history.

Fix by detecting the newly-enabled case (empty freezer + non-zero
stateID) and skipping both the gap check and the head truncation.
An empty freezer with head=0 would also fail truncateFromHead because
ohead(0) < nhead(stateID) is outside the valid range.

The trienode history will begin accumulating naturally from the
current state forward, which is the expected behavior documented
in the v1.17.0 release notes.

Fixes ethereum/go-ethereum#33907
This commit is contained in:
YQ AltLayer 2026-03-01 12:20:50 +00:00
parent 723aae2b4e
commit eabbd68c19

View file

@ -419,17 +419,27 @@ func repairHistory(db ethdb.Database, isVerkle bool, readOnly bool, stateID uint
if stateID > head { if stateID > head {
return nil, nil, fmt.Errorf("gap between state [#%d] and state history [#%d]", stateID, head) return nil, nil, fmt.Errorf("gap between state [#%d] and state history [#%d]", stateID, head)
} }
// Track whether trienode history is newly enabled (empty freezer on an
// existing node). In that case, skip gap checks and truncation — history
// will begin accumulating from the current state forward.
trienodeNewlyEnabled := false
if trienodes != nil { if trienodes != nil {
th, err := trienodes.Ancients() th, err := trienodes.Ancients()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if stateID > th { if th == 0 && stateID > 0 {
return nil, nil, fmt.Errorf("gap between state [#%d] and trienode history [#%d]", stateID, th) // Trienode history is newly enabled on an existing node.
} log.Info("Trienode history newly enabled, will start from current state", "state", stateID)
if th != head { trienodeNewlyEnabled = true
log.Info("Histories are not aligned with each other", "state", head, "trienode", th) } else {
head = min(head, th) if stateID > th {
return nil, nil, fmt.Errorf("gap between state [#%d] and trienode history [#%d]", stateID, th)
}
if th != head {
log.Info("Histories are not aligned with each other", "state", head, "trienode", th)
head = min(head, th)
}
} }
} }
head = min(head, stateID) head = min(head, stateID)
@ -449,6 +459,11 @@ func repairHistory(db ethdb.Database, isVerkle bool, readOnly bool, stateID uint
} }
} }
truncate(states, typeStateHistory, head) truncate(states, typeStateHistory, head)
truncate(trienodes, typeTrienodeHistory, head) // Only truncate trienode history if it already has data. A newly enabled
// (empty) trienode freezer would cause truncateFromHead to error because
// ohead(0) < nhead(stateID) falls outside the valid range.
if !trienodeNewlyEnabled {
truncate(trienodes, typeTrienodeHistory, head)
}
return states, trienodes, nil return states, trienodes, nil
} }