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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -464,3 +464,27 @@ they are in-crate Rust source preparation: this package still emits only static
and dynamic libraries, not a downstream Cargo `rlib`. Released Swift/Kotlin
interfaces, binary tags and checksums are unchanged. No migration or writer is
activated by creating or reading a session. Do not log observations or plaintext.


### Original-wallet migration status (Rust source preparation)

`BackupReadSession::read_migration_state` observes the candidate VSS status contract
through the original app or LDK credential provider. Each result retains its session
and domain, including the server-reported physical principal/store. Different
principals may share the same textual store ID. A renewed proof can return a changed
mapping; old observations remain unchanged and never authorize a later mutation.

An explicit legacy tuple is distinct from committed `bitkit-migration-v1` metadata.
Unsupported routes, authorization failures, incomplete or conflicting fields,
unknown protocols/epochs and over-limit responses remain errors. The storage POST
has no automatic retry or redirect, a 64 KiB body limit, and a ten-second deadline
covering credentials and body. The inherited credential provider keeps its own
challenge/callback redirect and body policies. Dropping a read does not replay it;
session revocation rejects pending and later results.

This is unpublished-contract, in-crate source preparation: no new UniFFI export,
released native API, server deployment or app activation. A single response cannot
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.
64 changes: 63 additions & 1 deletion src/backup_session.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Read-only, in-memory ownership across app and LDK backup observations.

use crate::{LdkNamespace, LdkVssClient, VssClient, VssError, VssReadReceipt};
use crate::{LdkNamespace, LdkVssClient, MigrationState, VssClient, VssError, VssReadReceipt};
use std::sync::Arc;
use tokio::sync::watch;

Expand Down Expand Up @@ -34,6 +34,27 @@ pub enum BackupReadError {
Read(#[from] VssError),
}

/// Credential/derivation domain, independent of textual store identity.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BackupDomain {
App,
Ldk,
}

/// Server-reported policy bound to one original session and credential domain.
/// Not serializable or Debug; it carries physical storage identity, not authority.
pub struct MigrationObservation {
owner: Arc<watch::Sender<bool>>,
domain: BackupDomain,
state: MigrationState,
}

impl MigrationObservation {
pub fn domain(&self) -> BackupDomain {
self.domain
}
}

/// An observation bound to one native session. Deliberately not serializable or
/// Debug: it can contain a complete plaintext wallet and is not remote authority.
pub struct BackupReadObservation {
Expand Down Expand Up @@ -138,6 +159,47 @@ impl BackupReadSession {
Ok(observation.receipt.as_ref())
}

/// One read of the candidate VSS migration-status contract. Unsupported routes,
/// policy errors and missing/malformed tuples are errors, never legacy absence.
/// The ten-second deadline includes credentials; the storage body is capped at 64 KiB.
pub async fn read_migration_state(
&self,
domain: BackupDomain,
) -> Result<MigrationObservation, BackupReadError> {
let mut cancellation = self.revoked.subscribe();
self.ensure_active()?;
let read = async {
match domain {
BackupDomain::App => self.app.read_migration_state().await,
BackupDomain::Ldk => self.ldk.read_migration_state().await,
}
};
let state = tokio::select! {
biased;
_ = cancellation.changed() => return Err(BackupReadError::Revoked),
result = read => result,
};
self.ensure_active()?;
Ok(MigrationObservation {
owner: Arc::clone(&self.revoked),
domain,
state: state?,
})
}

/// Local observation access only. A future mutation must freshly and atomically
/// bind its authorization to the observed principal/store on an enforcing server.
pub fn migration_state<'a>(
&self,
observation: &'a MigrationObservation,
) -> Result<&'a MigrationState, BackupReadError> {
if !Arc::ptr_eq(&self.revoked, &observation.owner) {
return Err(BackupReadError::DifferentOwner);
}
self.ensure_active()?;
Ok(&observation.state)
}

fn ensure_active(&self) -> Result<(), BackupReadError> {
if *self.revoked.borrow() {
return Err(BackupReadError::Revoked);
Expand Down
Loading
Loading