Summary
In FlameManager::apply_changes, the newly-computed flames for an account are inserted into the in-memory flame set with flame_set_mut.insert(new_projector_height, flames.to_owned()). HashMap::insert silently overwrites any pre-existing vec at that height. The displaced entries are not removed from disk. On a re-org, re-apply, or any path where new_projector_height already has flames, this diverges the in-memory state from the on-disk state.
Affected file(s)
src/inscriptive/flame_manager/flame_manager.rs
Location
src/inscriptive/flame_manager/flame_manager.rs (~line 425):
// 5.2.x insert the new flames at the new projector height
flame_set_mut.insert(new_projector_height, flames.to_owned()); // <-- blind overwrite
This sits inside the per-account funding loop in apply_changes. The on-disk writes for the same flames happen just below (~lines 438-450) using the key [8-byte LE height][4-byte LE flame_index].
Root cause / analysis
The flame set is HashMap<AccountKey, HashMap<ProjectorHeight, Vec<(FlameIndex, Flame)>>>. When apply_changes runs for an affected account, it computes a fresh flame set covering the gap and writes it at new_projector_height. If new_projector_height already exists in the map (e.g. the same batch height is being re-applied after a failed/aborted prior attempt, or a re-org lands on the same projector height), the old Vec<(FlameIndex, Flame)> is dropped from memory but its sled keys remain on disk.
Two related observations that compound this:
-
Expired-flame pruning uses <= (flame_manager.rs:264, 329): a flame created at projector_expiry_height is treated as already expired. Combined with the overwrite, an account re-funded at new_projector_height == projector_expiry_height is immediately eligible for pruning on the next tick while its disk entries linger.
-
The prune loop clones the whole flame map to iterate while mutating (flame_manager.rs:327: for (projector_height, flames) in account_flame_set_mut.clone().iter()). The clone hides the fact that the subsequent insert is non-idempotent w.r.t. existing keys.
Impact
- In-memory / on-disk divergence: orphaned flame keys accumulate on disk, never referenced from memory. A restart reloads them (via the construction-time iteration), re-populating the flame set with entries that were "overwritten."
- Possible double-counting of flame value after a restart, since the reloaded set includes both the new and the orphaned-old flames at overlapping heights.
- For a funding/collateralization mechanism, divergence here undermines the invariant that the flame set covers the account's target value.
Summary
In
FlameManager::apply_changes, the newly-computed flames for an account are inserted into the in-memory flame set withflame_set_mut.insert(new_projector_height, flames.to_owned()).HashMap::insertsilently overwrites any pre-existing vec at that height. The displaced entries are not removed from disk. On a re-org, re-apply, or any path wherenew_projector_heightalready has flames, this diverges the in-memory state from the on-disk state.Affected file(s)
src/inscriptive/flame_manager/flame_manager.rsLocation
src/inscriptive/flame_manager/flame_manager.rs(~line 425):This sits inside the per-account funding loop in
apply_changes. The on-disk writes for the same flames happen just below (~lines 438-450) using the key[8-byte LE height][4-byte LE flame_index].Root cause / analysis
The flame set is
HashMap<AccountKey, HashMap<ProjectorHeight, Vec<(FlameIndex, Flame)>>>. Whenapply_changesruns for an affected account, it computes a fresh flame set covering the gap and writes it atnew_projector_height. Ifnew_projector_heightalready exists in the map (e.g. the same batch height is being re-applied after a failed/aborted prior attempt, or a re-org lands on the same projector height), the oldVec<(FlameIndex, Flame)>is dropped from memory but its sled keys remain on disk.Two related observations that compound this:
Expired-flame pruning uses
<=(flame_manager.rs:264, 329): a flame created atprojector_expiry_heightis treated as already expired. Combined with the overwrite, an account re-funded atnew_projector_height == projector_expiry_heightis immediately eligible for pruning on the next tick while its disk entries linger.The prune loop clones the whole flame map to iterate while mutating (
flame_manager.rs:327:for (projector_height, flames) in account_flame_set_mut.clone().iter()). The clone hides the fact that the subsequentinsertis non-idempotent w.r.t. existing keys.Impact