Skip to content

Transient account resolved to a larger read only image keeps a stale persisted copy #166

Closed

Description

@erkancamli

Problem

AccountsDB::store says each batch "also touches the opposite backend so stale copies are removed after mode changes", but persisted() (accountsdb/src/lib.rs) only routes an Owned entry to persisted storage when its mode is authoritative.

A Transient account is loaded as a Borrowed image. When materialize resolves it to a ReadOnly image whose data is larger than the borrowed capacity (the data length rounded up to 8 bytes), resize promotes it to Owned in the same write. The entry is then Owned and non authoritative, so it skips the persisted backend and the old Transient image stays in LMDB and the mmap.

Effects on 7c95d6a (tag 0.12.0 has the same code):

  • a program scan returns the key twice, the stale Transient image and the ReadOnly one
  • the stale image stays in the persisted checksum
  • after the ReadOnly mirror is evicted, or after reset() drops volatile state, load returns the old Transient image again

Reproduction

Through the public Engine API: materialize Delegated, then Transient with 1 byte of data, then ReadOnly with 64 bytes; scan, evict, scan. Expected [(key, (ReadOnly, 64))], None, [], got:

left:  ([(key,(Transient,1)), (key,(ReadOnly,64))], Some((Transient,[1])), [(key,(Transient,1))])
right: ([(key,(ReadOnly,64))], None, [])

A unit version in accountsdb/src/tests.rs panics with stale transient image left in persisted storage. Both tests are ready to share.

Expected

Resolving to a ReadOnly image removes the Transient copy from persisted storage regardless of whether the data grew.

Context

In magicblock-validator this is reached when the undelegation path materializes the base chain ReadOnly image for a Transient account (cloner → materialize) and the chain image is larger than the local one, for example after a realloc on the base layer. A minimal fix that passes the accountsdb suite and the engine, keeper and replicator tests here is to also route Owned entries whose DirtyMarkers::MODE is set:

CoWAccount::Owned(_) => {
    entry.1.mode().authoritative() || entry.1.markers().contains(DirtyMarkers::MODE)
}

Routing every Owned entry to persisted storage would also work at the cost of more LMDB writes. Happy to open a PR with the tests for whichever you prefer.

Activity

  1. added theissue type on Sep 30, 2026
  2. bmuddha commented on Oct 1, 2026

    @bmuddha
    Collaborator

    @erkancamli thanks for the report, will be fixed soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions