Skip to content

Migrate legacy wallet output key ids on startup#7846

Open
Catnap7 wants to merge 1 commit into
tari-project:developmentfrom
Catnap7:codex/migrate-legacy-output-key-ids
Open

Migrate legacy wallet output key ids on startup#7846
Catnap7 wants to merge 1 commit into
tari-project:developmentfrom
Catnap7:codex/migrate-legacy-output-key-ids

Conversation

@Catnap7
Copy link
Copy Markdown

@Catnap7 Catnap7 commented May 22, 2026

Summary

Closes #7829 by adding a startup migration path for legacy wallet output key ids.

This change:

  • adds an output-manager startup hook that schedules legacy key migration without blocking wallet startup
  • adds database methods for finding and updating legacy output key ids in batches
  • converts legacy private keys through the existing key manager path before storing the current key id
  • covers the sqlite migration path with an output manager storage test

Verification

  • git diff --check origin/development...HEAD

I could not run the Rust integration test suite locally in this Windows environment because cargo/rustc are not installed here. The PR includes a focused storage test for the migration behavior.

Bounty

Submitting for the $70 bounty on #7829.

@Catnap7 Catnap7 requested a review from a team as a code owner May 22, 2026 11:35
@github-actions
Copy link
Copy Markdown

⚠️ This PR contains unsigned commits. To get your PR merged, please sign those commits (git rebase --exec 'git commit -S --amend --no-edit -n' @{upstream}) and force push them to this branch (git push --force-with-lease).

If you're new to commit signing, there are different ways to set it up:

Sign commits with gpg

Follow the steps below to set up commit signing with gpg:

  1. Generate a GPG key
  2. Add the GPG key to your GitHub account
  3. Configure git to use your GPG key for commit signing
Sign commits with ssh-agent

Follow the steps below to set up commit signing with ssh-agent:

  1. Generate an SSH key and add it to ssh-agent
  2. Add the SSH key to your GitHub account
  3. Configure git to use your SSH key for commit signing
Sign commits with 1Password

You can also sign commits using 1Password, which lets you sign commits with biometrics without the signing key leaving the local 1Password process.

Learn how to use 1Password to sign your commits.

Watch the demo

@Catnap7 Catnap7 mentioned this pull request May 22, 2026
3 tasks
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements a migration process to convert legacy output key ID strings to the current TariKeyId format within the wallet's output manager service. The changes include adding the migration logic to the database backend and service startup, along with a corresponding test case. Feedback suggests performing the migration asynchronously to avoid blocking service startup and optimizing the SQLite implementation by using transactions and selecting only necessary columns to reduce memory overhead.

Comment on lines +219 to +225
let migrated_count = self
.resources
.db
.migrate_legacy_output_key_ids(&self.resources.key_manager)?;
if migrated_count > 0 {
info!(target: LOG_TARGET, "Migrated {migrated_count} legacy output key id(s)");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The PR description states that this migration is scheduled without blocking wallet startup, but the current implementation is synchronous and awaited within the start method. This will block the OutputManagerService from processing any requests until the migration completes. Consider spawning a background task to perform the migration asynchronously. Note that background tasks should handle panics to ensure continuous operation and prevent silent failures, as per repository guidelines.

        let db = self.resources.db.clone();
        let key_manager = self.resources.key_manager.clone();
        tokio::spawn(async move {
            use futures::FutureExt;
            let result = std::panic::AssertUnwindSafe(async move {
                db.migrate_legacy_output_key_ids(&key_manager)
            }).catch_unwind().await;
            match result {
                Ok(Ok(0)) => {},
                Ok(Ok(migrated_count)) => info!(target: LOG_TARGET, "Migrated {migrated_count} legacy output key id(s)"),
                Ok(Err(e)) => error!(target: LOG_TARGET, "Error migrating legacy output key ids: {e}"),
                Err(_) => error!(target: LOG_TARGET, "Panic occurred during legacy output key id migration"),
            }
        });
References
  1. Background tasks should handle panics to ensure continuous operation and prevent silent failures. Consider using std::panic::AssertUnwindSafe and catch_unwind to log errors and allow the task to restart or continue.
  2. A background task designed to reduce a workload for a synchronous process does not need to eliminate the workload entirely. It is acceptable to leave a non-zero amount of work for the synchronous process.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 8a368c766cc907d6396699caf2e325b7bdc63739: the migration now runs from a spawn_blocking background task so wallet startup can continue, and the task wraps the migration in catch_unwind with explicit logging for success, errors, and panics.

Comment on lines +967 to +993
pub fn migrate_legacy_key_ids<KM: LegacyTransactionKeyManagerInterface>(
conn: &mut SqliteConnection,
key_manager: &KM,
) -> Result<usize, OutputManagerStorageError> {
let mut migrated = 0;
for output in Self::index(conn)? {
let migrated_spending_key =
Self::migrate_key_id_string(&output.spending_key, key_manager, "spending key id")?;
let migrated_script_private_key =
Self::migrate_key_id_string(&output.script_private_key, key_manager, "script private key id")?;

if migrated_spending_key.is_none() && migrated_script_private_key.is_none() {
continue;
}

diesel::update(outputs::table.filter(outputs::id.eq(output.id)))
.set((
outputs::spending_key.eq(migrated_spending_key.unwrap_or(output.spending_key)),
outputs::script_private_key.eq(migrated_script_private_key.unwrap_or(output.script_private_key)),
))
.execute(conn)
.num_rows_affected_or_not_found(1)?;
migrated += 1;
}

Ok(migrated)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The current migration implementation has two performance issues:

  1. Self::index(conn)? loads the entire outputs table, including large BLOB columns like rangeproof and encrypted_data, into memory. This can lead to high memory usage or OOM for wallets with many outputs.
  2. Individual UPDATE statements are executed for each row without a surrounding transaction, which is slow in SQLite due to per-statement disk synchronization.

Consider using a targeted query to select only the necessary columns and wrapping the loop in a transaction.

    pub fn migrate_legacy_key_ids<KM: LegacyTransactionKeyManagerInterface>(
        conn: &mut SqliteConnection,
        key_manager: &KM,
    ) -> Result<usize, OutputManagerStorageError> {
        conn.transaction::<_, OutputManagerStorageError, _>(|conn| {
            let mut migrated = 0;
            let outputs = outputs::table
                .select((outputs::id, outputs::spending_key, outputs::script_private_key))
                .load::<(i32, String, String)>(conn)?;

            for (id, spending_key, script_private_key) in outputs {
                let migrated_spending_key =
                    Self::migrate_key_id_string(&spending_key, key_manager, "spending key id")?;
                let migrated_script_private_key =
                    Self::migrate_key_id_string(&script_private_key, key_manager, "script private key id")?;

                if migrated_spending_key.is_none() && migrated_script_private_key.is_none() {
                    continue;
                }

                diesel::update(outputs::table.filter(outputs::id.eq(id)))
                    .set((
                        outputs::spending_key.eq(migrated_spending_key.unwrap_or(spending_key)),
                        outputs::script_private_key.eq(migrated_script_private_key.unwrap_or(script_private_key)),
                    ))
                    .execute(conn)
                    .num_rows_affected_or_not_found(1)?;
                migrated += 1;
            }

            Ok(migrated)
        })
    }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 8a368c766cc907d6396699caf2e325b7bdc63739: the SQLite migration now selects only id, spending_key, and script_private_key, and performs the row updates inside a Diesel transaction.

@Catnap7 Catnap7 force-pushed the codex/migrate-legacy-output-key-ids branch 2 times, most recently from 8a368c7 to ade3401 Compare May 22, 2026 13:40
@Catnap7
Copy link
Copy Markdown
Author

Catnap7 commented May 22, 2026

Follow-up after the original issue description was restored: I updated this PR so the startup migration now runs the output-table key conversion in batches.

Current shape:

  • startup schedules the migration with tokio::task::spawn_blocking, so wallet startup is not blocked
  • each SQLite batch selects only id, spending_key, and script_private_key
  • each batch updates rows inside a Diesel transaction
  • the batch cursor advances by output id with a batch size of 500
  • the included storage test covers legacy key ids being persisted as current TariKeyId strings and verifies the migration is idempotent

Local verification still available here: git diff --check origin/development...HEAD. I still cannot run the Rust suite in this Windows environment because cargo/rustc are not installed locally.

@Catnap7 Catnap7 force-pushed the codex/migrate-legacy-output-key-ids branch 3 times, most recently from 8412e7d to 641f339 Compare May 22, 2026 17:53
@Catnap7 Catnap7 force-pushed the codex/migrate-legacy-output-key-ids branch from 641f339 to 4ff9d72 Compare May 22, 2026 18:00
Self::migrate_legacy_key_id_batch(conn, key_manager, last_seen_id)?;
total_migrated += migrated;

if batch_size < LEGACY_KEY_ID_MIGRATION_BATCH_SIZE as usize {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this seems wrong?, Should it not be 0?

let key_manager = self.resources.key_manager.clone();

tokio::task::spawn_blocking(move || {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why are you trying to catch a panic,nothing in here should panic

let mut last_seen_id = 0;
let mut total_migrated = 0;

loop {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is going to go through all keys at startup, I think its better to use a search with a like to find only this required to migrate. This way running through the entire db wont be required.

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.

Console wallet key migration

2 participants