Skip to content

[flame_manager] Blind overwrite of flame vec at a height in apply_changes (memory/disk divergence) #15

Description

@GideonBature

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:

  1. 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.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions