core: give the encrypted session store one owner#73
Merged
Conversation
Engine held the age identity and the iroh Node side by side and paired them by hand every time session bytes crossed the boundary: encrypt then publish, fetch then decrypt, spelled out at five sites. Two of them existed twice over — get_plain/get_plain_with and verdict_of/verdict_with were the same code with a different receiver, because tick_once already holds &mut Engine and cannot call a &self method. SessionStore is that pairing, named once. It is two shared references, so the same store is reachable from &self methods and from inside a pass holding &mut Engine — the split that forced the duplication. Both helper pairs collapse to one, and Execution carries one store instead of a separate identity and node. The blob-read distinction is now in the method name rather than a comment. Node offers get_blob (local only) and blob (fetch on miss); picking wrong is silent and expensive: divergence relies on a local miss reading as an incomplete version set, so an all-or-skip merge never unions a partially downloaded fork (DECISIONS §8). local_plaintext and fetch_plaintext say which one they depend on, and local_plaintext documents why it must not be swapped. recipients_fingerprint moves next to the identity it fingerprints; Engine no longer knows how age recipients are spelled. No behaviour change. The store is a concrete type, not a trait: this repo uses named production methods as test seams, not mocks (AGENTS.md), so Action execution still needs a real node — the deduplication is the win here, not the test surface.
Merged
fosskar
added a commit
that referenced
this pull request
Jul 25, 2026
Three internal changes landed since v0.20.0 — no features, nothing breaking — so PATCH per the pre-1.0 rule: #71 cli: single-source the systemd hardening set #72 core: give the pass verdict one definition #73 core: give the encrypted session store one owner #71 is the only one that alters a shipped artifact: the unit written by `ssync service install` now lists the sandbox properties alphabetically. The property set is unchanged and systemd is order-insensitive here, so behaviour is identical. Bumping [workspace.package] version is the whole release: nix/effects.nix tags v0.20.1 and generates notes once nixbot goes green on main.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found by an architecture review of
Engine, validated against the source before any code changed.The problem
Engineheld anAgeIdentityand aNodeside by side and paired them by hand every time session bytes crossed the boundary. Publishing isidentity.encrypt(…)thennode.publish(…); reading isnode.blob(…)thenidentity.decrypt(…). That recipe was spelled out at five sites:get_plain,get_plain_with,merge_with, the publish step ofAction::Import, andAction::WriteFile.Two of them were the same code twice:
Byte-identical modulo the receiver;
verdict_of/verdict_withlikewise, modulo variable names. Neither pair is design —tick_oncealready holds&mut Engineand cannot call a&selfmethod, so every read helper had to exist in both shapes, andexecutetook an 8-fieldExecutionstruct to thread the loose pieces back in.The change
SessionStore(newcrates/ssync-core/src/session_store.rs) is that pairing, named once:It holds two shared references, so the same store is reachable from
&selfmethods and from inside a pass holding&mut Engine. That is what dissolves the duplication: both helper pairs collapse to one, andExecutioncarries onestorefield instead of separateidentityandnode.recipients_fingerprintmoves next to the identity it fingerprints —Engineno longer knows that age recipients are joined with newlines and hashed.Naming the blob-read distinction
Nodeoffersget_blob(local only) andblob(fetch on miss, bounded, for iroh-docs#88). The names do not distinguish them, and picking wrong is silent: divergence relies on a local miss reading as an incomplete version set, which is what makes the merge all-or-skip so a partially downloaded fork is never unioned away (DECISIONS §8). Swapping the two compiles and passes every test.local_plaintextandfetch_plaintextput that dependency in the call, andlocal_plaintextcarries the reason it must not be swapped.Action::WriteFile's two-step fetch-then-decrypt, with two separate error arms, becomes one — its log line is nowmaterialiserather thanfetch/decrypt, which is what actually failed from the operator's point of view.What this does not do
The review suggested this would make
Actionexecution testable against an in-memory store. It does not, and I did not force it. AGENTS.md is explicit that test seams in this repo are named production methods (set_resync_interval,spawn_with_gc,disable_auto_download), not mocks — and a trait here would have exactly one production implementation, which is a hypothetical seam, not a real one.SessionStoreis a concrete type;executestill needs a real node.So the win is deduplication and one owner for ciphertext, not a new test surface. Flagging it because the review claimed more.
Verification
No behaviour change; existing tests cover the paths.
cargo clippy --workspace --all-targets -- -D warnings— cleancargo test --workspace -- --test-threads=1— 255 passed, 0 failednix fmt -- --ci— 0 changedlib.rsno longer pairsidentity.encrypt/decryptwithnode.publish/blob/get_blob; remaining occurrences are test-only (asserting a blob is ciphertext, oradd_blob, which is documented for tests)No version bump: pure refactor.
Stacking note: touches
crates/ssync-core/src/lib.rs, as does #72. Both branch frommain; whichever lands second will need a trivial rebase.