Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,49 @@ prove all-tuples coherence, deployed principal mapping or old reader/writer
exclusion. Before any mutation an enforcing server must atomically recheck fresh
authorization against the observed original principal/store. Do not serialize
observations, log identities/proofs or treat status as permission to migrate.

### Immutable migration proposal (Rust source preparation)

`BackupReadSession::prepare_migration` consumes references to the original session's
app and LDK status observations, original logical `WALLET` observation and present
app component observations. Components must already be safe to make immutable;
ordinary mutable app keys, including same-key USDT retries that re-encrypt, cannot
be substituted for immutable snapshots. This semantic remains the app's duty.
LDK operational objects are rejected as components, while LDK policy still
participates in the proposal. It preserves the legacy archive's encrypted key/AAD and
exact raw envelope digest; explicit absence is a separate condition. It encrypts
the supplied initial head once with the original app key, without uploading or
replacing any object. The app remains responsible for the head schema and complete,
coherent capture. An opaque wallet value alone cannot prove that completeness.

Every domain must have a positive legacy status. Equal store IDs do not combine
identities: only an exact match of both reported principal and store coalesces app
and LDK into one physical tuple, retaining both domain labels. Duplicate/conflicting
component keys, destructive head/archive collisions, noncanonical UUIDs and inputs
outside the candidate server's activation limits are rejected. The digest uses the
server's UTF-8 tuple/key ordering and exact field encoding, including the encrypted
head and all archive/component conditions. It contains no renewable proof.

Access the frozen result through `prepared_migration`. Another session cannot adopt
it, even with the same seed. Revocation or starting either domain's next status read
invalidates access and preparation from earlier status observations. Failed,
pending and abandoned status reads also invalidate earlier preparation; historical
status remains readable. Repeated access retains the same ciphertext/digest. Calling
preparation again requires a fresh UUID and creates different encrypted content,
not a retry of the old plan. This preparation API has no durable UUID registry;
future dispatch must preserve one exact attempt across uncertain outcomes.

This content is neither persisted authority nor an execution API. Separate status
and object reads cannot prove a coherent physical snapshot or stable authorization
mapping between requests. A future enforcing server must atomically compare fresh
proofs against every original physical tuple and exact envelope condition before
mutation. Lifecycle drains, RN and running-node capabilities, old reader/writer
exclusion, durable uncertain-outcome reconciliation and app restoration remain
unfinished requirements. No activation request, new UniFFI API, release or deployed
server support is introduced here. Never log plaintext, proofs or private identities.

Coalescing does not verify that both credential providers still map to the shared
tuple. The preserved server candidate accepts one proof per unique target; that
alone cannot recheck a second provider which has since remapped elsewhere. Retained
domain labels are necessary, but enforcing that condition remains unresolved before
any future activation.
23 changes: 23 additions & 0 deletions src/backup_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ use crate::{LdkNamespace, LdkVssClient, MigrationState, VssClient, VssError, Vss
use std::sync::Arc;
use tokio::sync::watch;

mod preparation;
pub use preparation::{
MigrationHeadInput, MigrationPreparation, MigrationPreparationError, PreparedMigration,
PreparedMigrationContent, PreparedMigrationHead, PreparedMigrationStore,
};

/// One immutable storage/auth configuration for both native derivation domains.
/// Equal store IDs do not establish equal physical server principals.
pub struct BackupReadConfig {
Expand Down Expand Up @@ -45,6 +51,7 @@ pub enum BackupDomain {
/// Not serializable or Debug; it carries physical storage identity, not authority.
pub struct MigrationObservation {
owner: Arc<watch::Sender<bool>>,
revision: Arc<()>,
domain: BackupDomain,
state: MigrationState,
}
Expand Down Expand Up @@ -78,6 +85,7 @@ pub struct BackupReadSession {
app: VssClient,
ldk: LdkVssClient,
revoked: Arc<watch::Sender<bool>>,
migration_revisions: [watch::Sender<Arc<()>>; 2],
}

impl BackupReadSession {
Expand All @@ -100,6 +108,7 @@ impl BackupReadSession {
app,
ldk,
revoked: Arc::new(watch::channel(false).0),
migration_revisions: std::array::from_fn(|_| watch::channel(Arc::new(())).0),
})
}

Expand Down Expand Up @@ -168,6 +177,10 @@ impl BackupReadSession {
) -> Result<MigrationObservation, BackupReadError> {
let mut cancellation = self.revoked.subscribe();
self.ensure_active()?;
// A pending, failed or abandoned refresh must not leave earlier policy
// observations admissible for a new proposal.
let revision = Arc::new(());
self.migration_revisions[domain.index()].send_replace(Arc::clone(&revision));
let read = async {
match domain {
BackupDomain::App => self.app.read_migration_state().await,
Expand All @@ -182,6 +195,7 @@ impl BackupReadSession {
self.ensure_active()?;
Ok(MigrationObservation {
owner: Arc::clone(&self.revoked),
revision,
domain,
state: state?,
})
Expand All @@ -208,6 +222,15 @@ impl BackupReadSession {
}
}

impl BackupDomain {
fn index(self) -> usize {
match self {
Self::App => 0,
Self::Ldk => 1,
}
}
}

impl Drop for BackupReadSession {
fn drop(&mut self) {
self.revoke();
Expand Down
Loading
Loading