Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
05f6897
docs(migration): design the v0.9.3 legacy identity import
lklimek Jul 13, 2026
453cd15
feat(migration): import legacy v0.9.3 identities and their keys
lklimek Jul 13, 2026
ce1205a
fix(migration): never let a corrupt vote queue strand identity keys
lklimek Jul 13, 2026
e2c0970
merge: bring the v1.0 parity batch into the legacy identity import
lklimek Jul 13, 2026
b56ebff
fix(migration): honor legacy alias fallback and guard identity-import…
lklimek Jul 13, 2026
5f5614c
fix(migration): column-authoritative alias, per-row identity decode, …
lklimek Jul 13, 2026
4587f25
fix(migration): surface unreadable votes alongside unreadable identities
lklimek Jul 13, 2026
e8b6182
fix(migration): reconcile legacy keys into partially loaded identities
lklimek Jul 13, 2026
b454dba
fix(migration): only reconcile bare identities, never keyed ones
lklimek Jul 13, 2026
08242ec
fix(migration): revert legacy-identity reconcile to safe skip-if-present
lklimek Jul 14, 2026
82d9c10
fix(migration/wallet): QA follow-ups for #885 — resurrection, banners…
lklimek Jul 14, 2026
a25dd84
docs(migration): mark the legacy-identity design as shipped and name …
lklimek Jul 14, 2026
7b43483
build(deps): document the deliberate bincode pin (RUSTSEC-2025-0141)
lklimek Jul 14, 2026
84b448a
fix(shielded): let the Verified badge name the balance it vouches for
lklimek Jul 14, 2026
c507b42
test(migration): extract the legacy-identity fixture into database::t…
lklimek Jul 14, 2026
b83dc03
docs(legacy-import): state precisely what read_identities logs
lklimek Jul 14, 2026
4e89da3
fix(migration): name the Identities screen in the unreadable-identity…
lklimek Jul 14, 2026
3326548
fix(migration): publish a terminal state for every migration failure
lklimek Jul 14, 2026
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
32 changes: 20 additions & 12 deletions docs/ai-design/2026-05-28-migration-tool/notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,19 +203,27 @@ idempotency is confirmed.

### `identity` (DET source file: `src/database/identities.rs`)

- **Source:** `identity` in `data.db`
- **Destination:** `identities.entry_blob` (typed BLOB column) in `platform-wallet-storage`
- **Mapping:** Deserialize DET's stored identity representation → serialize as
bincode-encoded `QualifiedIdentity` with a leading version byte prepended
- **Per-network split:** Yes
- **Gotchas:** Upstream schema uses a leading version byte in `entry_blob` for
forward/backward compatibility — this byte must be present and set correctly, or upstream
deserialization will silently produce garbage. Confirm the byte format with the
platform-wallet-storage author before implementing. This is the highest-risk table in the
migration.
> **SUPERSEDED (2026-07-13).** Design and task breakdown now live in
> `docs/ai-design/2026-07-13-legacy-identity-migration/design.md`. The entry below is kept
> for context; two of its claims are wrong. The destination is **not** upstream's
> `identities.entry_blob` — commit `b14bf32c` moved it to DET's own per-network k/v
> (`det:identity:v1` under `DetScope::Identity`, roster at `det:identity_index:v1`). And the
> version byte needs no agreement with the platform-wallet-storage author: `DetKv::put`
> prepends `kv::SCHEMA_VERSION` automatically, so an importer that goes through
> `AppContext::insert_local_qualified_identity` gets it for free.

- **Source:** `identity` in `data.db` (`is_local = 1 AND data IS NOT NULL`)
- **Destination:** `StoredQualifiedIdentity` in DET's per-network k/v — see the design doc
- **Mapping:** `QualifiedIdentity::from_bytes(row.data)` → restore `status` from its column →
`AppContext::insert_local_qualified_identity(&qi, &wallet_link)`. The `data` BLOB carries
**all identity key material**; there is no second key table in v0.9.3.
- **Per-network split:** Yes — two-value filter (`mainnet` / legacy `dash`)
- **Gotchas:** The real risk is cross-version bincode compatibility of the `data` BLOB, not a
version byte. Every DET-side and dpp-side struct in the blob was verified unchanged between
v0.9.3 and HEAD; only the `bincode` rc.3 → 2.0.1 wire format is unproven, and a golden-blob
test closes it. Still the highest-risk table in the migration.
- **Status:** DONE for new-install path — see commit `b14bf32c` (identities + tokens →
per-network k/v). Migration tool still needs to import legacy rows with the version-byte
contract correct.
per-network k/v). Migration tool still needs to import legacy rows.

---

Expand Down
446 changes: 446 additions & 0 deletions docs/ai-design/2026-07-13-legacy-identity-migration/design.md

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ pub fn migration_running_text(step: MigrationStep) -> &'static str {
MigrationStep::Shielded => "Verifying shielded balance.",
MigrationStep::WalletSeeds => "Moving your wallets into the new vault.",
MigrationStep::WalletMeta => "Updating wallet names.",
MigrationStep::Identities => "Restoring your identities and their keys.",
MigrationStep::Finalize => "Finishing storage update.",
}
}
Expand All @@ -156,6 +157,20 @@ pub fn migration_unreadable_votes_text(count: u32) -> String {
)
}

/// User-facing banner copy for a migration that finished the wallet drain but
/// could not decode `count` identities. Their keys are therefore not loaded, so
/// the sentence names the action that restores them. Kept separate from the
/// scheduled-votes copy because the remedy is different — load an identity, not
/// re-schedule a vote. The previous version's data is never deleted, so the
/// re-import is always possible. Exposed for kittest coverage.
pub fn migration_unreadable_identities_text(count: u32) -> String {
format!(
"Some identities from the previous version could not be read and were not carried over \
({count} in total). Your previous data is untouched. Load these identities again to \
restore their keys."
)
}

/// How long the cold-start readiness gate waits for the wallet backend to wire
/// before it stops retrying silently and surfaces a visible, actionable banner.
///
Expand Down Expand Up @@ -1850,6 +1865,7 @@ mod migration_banner_tests {
MigrationStep::Shielded,
MigrationStep::WalletSeeds,
MigrationStep::WalletMeta,
MigrationStep::Identities,
MigrationStep::Finalize,
] {
let text = migration_running_text(step);
Expand All @@ -1873,6 +1889,7 @@ mod migration_banner_tests {
migration_running_text(MigrationStep::Shielded),
migration_running_text(MigrationStep::WalletSeeds),
migration_running_text(MigrationStep::WalletMeta),
migration_running_text(MigrationStep::Identities),
migration_running_text(MigrationStep::Finalize),
];
let unique: std::collections::HashSet<&str> = labels.iter().copied().collect();
Expand Down
17 changes: 15 additions & 2 deletions src/app/reconcilers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ use super::{
COLD_START_BACKEND_READY_TIMEOUT, COLD_START_STUCK_MESSAGE, MIGRATION_RETRY_ACTION_ID,
MIGRATION_VOTES_ACK_ACTION_ID, SPV_CONNECTING_DESCRIPTION, SPV_CONTINUE_BACKGROUND_ACTION,
SPV_SYNCING_DESCRIPTION, SpvBlockStep, cold_start_backend_wait_timed_out,
migration_running_text, migration_unreadable_votes_text, should_dispatch_cold_start,
spv_block_step,
migration_running_text, migration_unreadable_identities_text, migration_unreadable_votes_text,
should_dispatch_cold_start, spv_block_step,
};

/// Drives platform-level accessibility (AccessKit) activation on the first
Expand Down Expand Up @@ -491,6 +491,19 @@ impl MigrationReconciler {
handle.with_action("Got it", MIGRATION_VOTES_ACK_ACTION_ID);
self.banner_handle = Some(handle);
}
MigrationState::SucceededWithUnreadableIdentities { count } => {
// Same shape as the vote warning: the drain is done and the rows
// are still in the previous version's storage, so no retry action
// — but the keys those identities held are not loaded, so the user
// must be told even if they stepped away.
let handle = MessageBanner::set_global(
ctx,
migration_unreadable_identities_text(count),
MessageType::Warning,
);
handle.disable_auto_dismiss();
self.banner_handle = Some(handle);
}
MigrationState::Failed { error } => {
if error.is_backend_not_ready() {
// Transient: the wallet backend had not finished wiring when
Expand Down
Loading
Loading