Skip to content

Two FileStorageProvider objects on one dataDir clobber the entire wallet file, money journals included #771

Description

@MastaP

Found while designing the tracked_addresses fix for #766. This is strictly larger than that issue and is not covered by #766, #768, #769 or #770.

The defect

FileStorageProvider holds the whole store as an in-memory snapshot loaded once at connect() (impl/nodejs/storage/FileStorageProvider.ts:70-112), and save() rewrites the entire file on every set() (:218-222, JSON.stringify(this.data)).

So with two provider objects over one dataDir:

B's next set() of any key erases every key A has written since B connected.

Not one key — all of them. Including:

IndexedDBStorageProvider is not exposed to this — every get/set goes to the DB rather than through an in-memory snapshot.

Why it isn't the same bug as #766 item 5

#766 item 5 (tracked_addresses lost update) is a single key with a wholesale write, fixable with a read-merge-write serialized inside one provider instance. That fix is correct and worth having, but it only covers one provider object shared by two Spheres — the normal browser wiring.

This is the other case: two provider objects, where no in-provider chain can help, because each has its own snapshot of the whole file and neither observes the other's writes.

Reach

Any Node consumer that constructs two FileStorageProviders on the same dataDir — the exact shape #766 is trying to make safe ("one Sphere per network, disposable, concurrent instances must not interfere"). docs/MIGRATION-TOKEN-REGISTRY.md:95-100 already notes two providers over one dataDir as an unresolved residual; this is what that residual actually costs.

Options

  1. Re-read before write in save() — merge the on-disk object with the in-memory delta rather than overwriting. Costs a read per write; makes concurrent providers safe by construction.
  2. Enforce one provider per dataDir — a lockfile or a process-level registry, failing loudly on a second open. Cheaper, and arguably the honest contract, but it turns a silent corruption into a startup error a consumer must handle.
  3. Per-key files instead of one JSON blob. Biggest change; removes the class of bug entirely.

Option 2 is the smallest thing that stops data loss; option 1 is the smallest thing that keeps the current API working. Worth deciding before #766 is described as delivered, since "concurrent instances must not interfere" is not true while this stands.


Found 2026-09-02 against main @ 74f26e3. Not yet verified by an executed repro — the mechanism is read from source (connect() snapshot + whole-file save()); a two-provider test would confirm it in a few lines.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions