Skip to content

feat: observe owner-bound migration status - #33

Open
ben-kaufman wants to merge 1 commit into
feat/backup-read-sessionfrom
feat/backup-migration-status
Open

ben-kaufman wants to merge 1 commit into
feat/backup-read-sessionfrom
feat/backup-migration-status

Conversation

@ben-kaufman

@ben-kaufman ben-kaufman commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Read candidate VSS migration policy through the original wallet's retained app or LDK credential provider. A valid legacy tuple is distinct from committed migration metadata. Missing routes, authorization failures, unknown policies, malformed or conflicting fields and oversized replies remain errors; they cannot authorize legacy fallback.

Each observation belongs to the original session and credential domain. Credential renewal can report a changed physical principal without changing an older observation. Revocation rejects pending and later results, and abandoning a read schedules no replay. The status storage POST shares the strict receipt transport: no automatic retries or redirects, a 64 KiB response cap and a ten-second deadline including credential acquisition.

Stacked on #32. This is in-crate Rust source preparation for terminal complete-WALLET recovery in synonymdev/bitkit-ios#1121 and synonymdev/bitkit-android#1686. It describes the preserved, unpublished VSS status contract. No server code, homeserver dependency, new UniFFI export, released API, app activation, migration or deployment is included.

Validation

  • Full Rust suite: 58 passed, 0 failed/ignored. Production release build and docs pass; whole-repository formatting and diff checks pass. Final Clippy reports only two warnings in unchanged base test modules.
  • Two independent security/recovery and integration reviews completed with affected-delta rechecks. Corrected a legacy Unicode identity limit, ineffective negative fixtures and credential-renewal dispatch coverage. The initial fixture JWT encoding failure and two introduced test lint findings are retained; final checks pass.
  • Required iOS device/simulator and four Android ABI builds completed in isolated validation version 0.5.27-dev.4. Existing Swift/Kotlin interfaces are byte-identical to the validated parent. Both iOS17 links and the Kotlin consumer compile pass. Android lint XML: 0 errors, 3 verified unchanged dependency warnings, 0 introduced findings.
  • Complete build/compiler/lint diagnostics inspected. Archive CRCs/hashes, 862 arm64 objects per iOS slice, all four Android ABIs, 16 KiB load alignment and matching stripped/debug outputs verified. Existing Python/SDK XML and optional ktlint notices remain; NO-SOURCE tasks are not runtime tests. All six hosted jobs passed on attempt 1: Rust, manifest checks, iOS Bindgen and Android Bindgen. Their synthetic checkout exactly matches published head 435bb2a4. The complete audit covered 8,246 log lines and 14 annotations; hosted Rust passed 58 tests with zero failures/ignored cases. Both uploaded artifacts completed in single transfers and passed SHA-256, nested CRC, interface and native-symbol checks. Hosted iOS has 865 correctly targeted arm64 objects per slice; Android has all four API21/r28c ABIs, 16 KiB alignment and matching debug/release build IDs. All preserve the 93 existing FFI exports. Five conditional Rustup-bootstrap skips and two Android NO-SOURCE tasks are not test passes. Existing workflow/toolchain notices and the two base Clippy findings are retained; there were no CI retries or introduced diagnostics.

QA Notes

  • Explicit legacy and committed records, protected absent archive, LDK-only tuple and exact ciphertext digests; legacy Unicode names versus activation byte limits.
  • Wrong store, absent identities, mixed/unknown policy, duplicate/overflow/truncated wire fields, nested duplicates, 32/33 distinct component boundaries and conflicting digests.
  • Original app/LDK signers and separate physical principals; same-wallet replacement owner rejection; global replacement during held original credentials cannot retarget reads.
  • Expired-to-renewed credentials from the same original signer; storage verifies the actual old/new proof markers, while prior and changed server-mapping observations stay separate.
  • Credential refusal dispatches no storage request; unavailable/error statuses are never legacy; a real redirect target receives zero requests; bounded response and one storage attempt.
  • Revocation and deadlines at held credentials and split body, caller drop followed by one explicit read, full Rust/native/interface/lint/format gates and independent reviews.

These are controlled library fixtures, not authenticated app or deployed-server acceptance. The credential provider retains its own challenge/callback redirect and response-size policies. A parsed status does not prove deployment, all-tuples coherence, fresh authorization or old reader/writer exclusion. Any later mutation requires an enforcing server to atomically check fresh authorization against the original observed principal/store. Terminal complete-WALLET availability remains open.

@ben-kaufman
ben-kaufman requested review from dzdidi and ovitrif November 4, 2026 05:20

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: ✅ Approve

Full review: diff 7 files.

Findings: none.

QA for 435bb2a:

Rust test suite at the head ✅ cargo test ran 58 tests: 58 passed, 0 failed, 0 ignored.
Includes the new migration status decode tests and the session tests for domain binding, credential renewal, revocation, deadlines and global client replacement.
Formatting, Clippy and release build ✅ cargo fmt --check is clean, Clippy adds no warning in changed code, and the release build succeeds.
Clippy reports only the two existing module_inception warnings in src/tests.rs and src/ffi_tests.rs, which this PR does not touch.
Check: no new UniFFI export ✅ Checked by reading the diff: it adds only Rust pub use re-exports in src/lib.rs and no uniffi::export or UniFFI derive.
The Swift and Kotlin bindings stay unchanged.

No device run: this PR adds no UniFFI export and no app-visible behaviour, so nothing reaches Bitkit iOS or Android yet. The migration status read runs only against the library's own test fixtures; no deployed VSS server was tested.


Reviewed by claude-opus-5-5-medium

@talosmachina talosmachina left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No findings. Adds a strict, owner-bound read of the candidate getStoreMigrationState contract through the original app or LDK credential provider, with explicit legacy vs committed policy and fail-closed wire validation. Reviewed 435bb2a, full tier, as the delta on top of #32 (feat/backup-read-session, the stack root).

What I checked, and 7 candidates I ruled out

Read in full: src/migration_status.rs, src/receipt.rs, src/backup_session.rs; the diff of src/lib.rs, README.md and both test files.
Build: skipped here. talos-pr-run could not confirm the author's permission on this repo and fails closed, so this is a static review plus CI's own run. CI Tests ran 58 passed, 0 failed at this head, including the five migration_status::tests cases; Clippy and manifests green; Android and iOS Bindgen still pending when I read them.

Ruled out

  • 200 with an empty body read as legacy: it decodes to a default State whose empty store_id fails the state.store_id != store_id check, so it is InvalidData, not Legacy.
  • Negative or large epoch narrowed by prost: validate_wire reads the raw varint and rejects anything above 1 before State::decode, so a 10-byte negative int64 never reaches the match.
  • Duplicate fields merged by prost: the seen bitmask rejects a repeated non-repeated tag at both levels (top level up to 9, nested up to 2 in its own call), and objects is capped at 32 occurrences.
  • Archive and component disagreeing on existence: an archive with ciphertext_sha256: None sharing a key with an object compares None != Some(..) and fails; equal key with a different digest fails too; archive equal to initial_head is rejected outright.
  • Revocation race in read_migration_state: same shape as the existing read: subscribe before ensure_active, a biased select! on changed(), and a second ensure_active before the result is wrapped. The sender lives in self.revoked, so changed() cannot error out from under the borrow.
  • Deadline not covering credential acquisition: get_headers runs inside request, which post wraps in tokio::time::timeout(TIMEOUT, ..), so the ten seconds include it.
  • getObject behaviour changed by the post refactor: same URL ({base}/getObject), same headers, same limit check and bounded_body; only the decode moved to the caller.

Not checkable from this repo: the server side of getStoreMigrationState. The wire messages are pinned to an unpublished proto, so the request shape and field tags are reviewed for internal consistency only.

Merge confidence: 4/5, no findings, but the PR's code was not built here and the two Bindgen jobs were still pending.

@dzdidi dzdidi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 435bb2a48459a556f3504981c4dc471c21166d02 against base 17c8373419b892ec0eda9070551c8f22f25b5e54. No blocking or non-blocking findings after full source/context review. Exact-head Tests, Clippy, manifest, SwiftPM, iOS Bindgen, and Android Bindgen checks succeeded. Residual risk: unpublished server contract and no deployed-server acceptance evidence.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants