C2 — Carve persistence into seams - #4267
Merged
piotr-roslaniec merged 5 commits intoAug 20, 2026
Merged
Conversation
Engine module (pkg/tbtc/signer/src/engine/persistence.rs, currently 2389
lines) bundles four orthogonal concerns in one file: schema codec,
envelope I/O, key-provider subprocess management, and pending-operation
registry. Spec proposes splitting into four pub(crate) modules under
engine::persistence/:
schema_codec // pure TryFrom + encode/decode + schema-version validation
envelope_io // file I/O, lock, atomic rename, corrupt recovery, backup retention
key_provider // StateKeyProvider trait + EnvKeyProvider + CommandKeyProvider
// + CachedStateKeyProvider decorator (production, 3rd adapter)
// + state_encryption_key_material() thin wrapper
pending_ops // registry + snapshot covering + durable retry
Trait shape verified against current code:
trait StateKeyProvider: Send + Sync {
fn material(&self) -> Result<StateEncryptionKeyMaterial, EngineError>;
fn key_id(&self) -> &str;
}
Per-file corrections: persistence.rs 2389 lines (was 2256 in earlier
draft); wired data flow diagram now shows the load path also calling
state_encryption_key_material() to decrypt the envelope (was missing
entirely from earlier draft); clear_snapshot_covered_operations
moved to pending_ops with correct pub(crate) visibility (it's called
from persist_engine_state_to_storage, not from load); resolve_state_key_provider
function name (not the fictional resolve_state_key_provider_plan);
existing CachedStateKeyProvider added as a real production adapter;
no InMemoryKeyProvider, tests define local StateKeyProvider impls
(TrackingMockKeyProvider + StdArcMockProvider newtype, exposed through
CachedStateKeyProvider at tests.rs:14435+); legacy_plaintext_state_permitted
stays fn (private, only called from envelope_io); pending_build_taproot_tx_operation
(name with -taproot- not -tx_persistence_pending-; single session_id arg);
clear_persistence_pending_operations is cfg(any(test, feature='bench-restart-hook'))
not cfg(test); clear_snapshot_covered_operations parameter is
engine_state: &EngineState (not state); StateKeyProvider plan-state
Type replaced with function name; tests.rs:5787 state_lock_rejects_multi_process_contention
(not state_file_lock_rejects_multi_process_contention at line ~5700).
On-disk format and persisted schema (PersistedEngineState, PersistedSessionState,
PersistedEncryptedEngineStateEnvelope) byte-for-byte stable. TBTC_SIGNER_ABI_*
constants not bumped. Chaossuite test-path pins (scripts/run_phase5_chaos_suite.sh)
preserved.
NOT independent from C5: both specs rewrite the bodies of TryFrom<PersistedSessionState>
for SessionState and TryFrom<&SessionState> for PersistedSessionState
(persistence.rs:1917-2233 and the inverse). Land whichever is simpler
to rebase the other onto, then hand-merge those bodies.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Introduces StateKeyProvider trait + EnvKeyProvider / CommandKeyProvider / CachedStateKeyProvider struct adapters (replacing the prior enum-based dispatch), then splits persistence.rs into 4 pub(crate) submodules: schema_codec, envelope_io, key_provider, pending_ops. mod.rs re-exports so call sites are unchanged. Persisted schemas (PersistedSessionState, PersistedEngineState, PersistedKeyPackage) byte-for-byte stable. TryFrom impl bodies unchanged.
Spec doc corrections (no Rust source touched in this commit): - The StateKeyProvider trait is newly introduced by this change, not relocated from persistence.rs:1050 — that line in the base commit is the StateKeyProviderPlan enum (the old enum-based dispatch this PR replaces). The Risks section now reflects this. - The CachedStateKeyProvider is a new decorator with real process- lifetime caching behavior; the pre-split code re-resolved the key (and re-spawned the key-command subprocess) on every call. The key_provider section now owns this as a deliberate behavior change. - The schema_codec interface keeps the TryFrom impls on the codec types (callers in envelope_io use .try_into()); the function-call signatures shown above the prose are illustrative shorthand, not a separate API surface. The 'no TryFrom' claim is removed. - The Definition of Done no longer claims 'no behavioral change' — the CachedStateKeyProvider caching is a behavior change (reduction in subprocess spawns / KMS contact rate), and that is the only one.
Fixes the CI 'Signer Rust checks' job failure (fmt/clippy gate). No behavior change: reformatting plus mechanical clippy autofixes (option_map_unit_fn -> if let, or_insert_with(default) -> or_default(), identity_op literal simplification, dead struct-update-syntax removal where every field was already specified).
piotr-roslaniec
marked this pull request as ready for review
August 19, 2026 09:22
…4262 log-hygiene fix into envelope_io.rs)
piotr-roslaniec
merged commit Aug 20, 2026
885ad5e
into
extraction/frost-signer-mirror-2026-05-26
19 checks passed
piotr-roslaniec
added a commit
that referenced
this pull request
Aug 20, 2026
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.
C2 — Carve persistence into seams
Targets
origin/extraction/frost-signer-mirror-2026-05-26(the source branch for PR #4005). NOT independent from PR #5 (C5SessionStategrouping) — both rewrite the bodies of the same twoTryFromimpls inpersistence.rs. Sequencing note in both PRs; hand-merge required at theTryFrombodies.This PR ships two commits: a spec/doc commit (
spec(frost-signer): carve persistence into seams) and the implementation commit (feat(frost-signer): carve persistence into seams) that performs the 4-module persistence split described in the spec. The implementation is byte-for-byte stable on the wire (on-disk file format and persisted schemas unchanged), and the full test suite passes; the body of this PR is the design doc + the matching restructuring. Subsequent commits beyond these two are doc-only corrections to the spec doc and the PR description itself.What this PR is
Deepening spec for
pkg/tbtc/signer/src/engine/persistence.rs(currently 2,389 lines). Splits one monolithic file into 4pub(crate)modules underengine::persistence/:The 4 concerns are independently testable but currently require each other in the test environment. The boundary between them is invisible — and the highest-stakes functions in the codebase (AEAD envelope verification, key-provider subprocess management, schema-version migration) are the ones that most need clean per-concern test surfaces.
Trait shape (verified against current code)
Three real production adapters live in
key_provider:EnvKeyProvider— readsTBTC_SIGNER_STATE_ENCRYPTION_KEY_HEXenv.CommandKeyProvider— runs the configuredTBTC_SIGNER_STATE_KEY_COMMAND, with a timeout enforced bystate_key_command_timeout_secs().CachedStateKeyProvider— process-lifetime cache decorator wrapping anyBox<dyn StateKeyProvider>; keyed onkey_id()so a freshmaterial()is only invoked when the provider identity changes. New in this PR — the pre-split code re-resolved the key (and re-spawnedTBTC_SIGNER_STATE_KEY_COMMANDfor the command provider) on everystate_encryption_key_material()call. The caching is a deliberate, beneficial behavior change; see the spec doc'skey_providersection andRiskssubsection for details.No test-only fake type moves into this module. Tests continue to define their own
StateKeyProviderimpls locally (current convention:TrackingMockKeyProvider+StdArcMockProvidernewtype), wrapped throughCachedStateKeyProviderfor cache-coverage tests.Wired data flow
persist_engine_state_to_storage_with_keyis a 2ndpub(crate)envelope_io entry point that takes an already-resolved key — callers that need the key resolved under a specific lock ordering (e.g. round2's durable-marker write, which must resolve the key before writing the marker) call it directly and skip the wrapper's own key resolution.What stays unchanged
PersistedEncryptedEngineStateEnvelope,PersistedEngineState,PersistedSessionState,PersistedKeyPackage— field names, ordering, and serde tags preserved byte-for-byte.TBTC_SIGNER_ABI_*constants are not bumped.STATE_FILE_LOCKmutex and the#[cfg(unix)]/#[cfg(not(unix))]subprocess-group-keying branching are preserved.Sequencing with PR #5 (C5)
Both PRs rewrite the bodies of
TryFrom<PersistedSessionState> for SessionState(persistence.rs:1917-2233) andTryFrom<&SessionState> for PersistedSessionState(persistence.rs:2235 onward). This PR moves those bodies intoschema_codec.rsunchanged in shape; PR #5 reshapes what they project into (flat fields → 6 substructures).Merge order: land whichever is easier to rebase the other onto (either order works structurally), then hand-merge the shared
TryFrombodies. Do not apply both to a worktree in parallel and expect a clean merge.Verification
Spec was verified against current code. Verified facts that the spec encodes (corrections from earlier draft):
persistence.rsis 2,389 lines (not 2,256 as in the earlier draft).StateKeyProvidertrait methods arematerial()andkey_id() -> &str(the earlier draft had fictionalresolve()/key_id() -> String).InMemoryKeyProvider(the earlier draft invented one).clear_snapshot_covered_operationsisfn(private) with parameterengine_state: &EngineState. Called frompersist_engine_state_to_storage(success path), not fromload_engine_state_from_storage. Earlier draft had it on the load path with the wrong caller.legacy_plaintext_state_permittedstaysfn(private); only called insideenvelope_io.pending_build_taproot_tx_operation(singlesession_idarg, name with-taproot-not-tx_persistence_pending-); the earlier draft name was wrong.clear_persistence_pending_operationsiscfg(any(test, feature = "bench-restart-hook")), not justcfg(test).state_lock_rejects_multi_process_contentionlives at tests.rs:5787 (notstate_file_lock_rejects_multi_process_contentionat line ~5700 as in the earlier draft).Files added
Spec doc:
docs/specs/frost-signer-persistence-deepening.md(design doc, ~238 lines)Implementation (omitted from base commit
555f2c514; added by the second commit on this branch):pkg/tbtc/signer/src/engine/persistence/mod.rs(re-exports the four submodules)pkg/tbtc/signer/src/engine/persistence/schema_codec.rs(pure TryFrom + encode/decode + schema-version validation)pkg/tbtc/signer/src/engine/persistence/envelope_io.rs(file I/O, lock, atomic rename, corrupt recovery, backup retention)pkg/tbtc/signer/src/engine/persistence/key_provider.rs(StateKeyProvider trait + EnvKeyProvider + CommandKeyProvider + CachedStateKeyProvider + subprocess machinery)pkg/tbtc/signer/src/engine/persistence/pending_ops.rs(registry + snapshot covering + durable retry)Removed:
pkg/tbtc/signer/src/engine/persistence.rs(2,256 lines replaced by the four submodules above +mod.rs; contents preserved byte-for-byte across the split)Revisited risks (now landed in this PR)
The 4 split modules talk to each other without a circular dependency:
envelope_iocallskey_provider::state_encryption_key_material()for both load and persist paths, and the cross-moduleclear_snapshot_covered_operationscall fromenvelope_io::persist_engine_state_to_storage(success path only) topending_opsis one-directional. The resolution path isenvelope_io → key_providerandenvelope_io → pending_ops;key_providerandpending_opsdo not call back intoenvelope_io. Themod.rsorchestration preserves the pre-split call ordering.