Migrate legacy wallet output key ids on startup#7846
Conversation
|
If you're new to commit signing, there are different ways to set it up: Sign commits with
|
There was a problem hiding this comment.
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.
| 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)"); | ||
| } |
There was a problem hiding this comment.
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
- 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.
- 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.
There was a problem hiding this comment.
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.
| 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) | ||
| } |
There was a problem hiding this comment.
The current migration implementation has two performance issues:
Self::index(conn)?loads the entireoutputstable, including large BLOB columns likerangeproofandencrypted_data, into memory. This can lead to high memory usage or OOM for wallets with many outputs.- Individual
UPDATEstatements 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)
})
}There was a problem hiding this comment.
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.
8a368c7 to
ade3401
Compare
|
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:
Local verification still available here: |
8412e7d to
641f339
Compare
641f339 to
4ff9d72
Compare
| 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 { |
There was a problem hiding this comment.
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(|| { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
Summary
Closes #7829 by adding a startup migration path for legacy wallet output key ids.
This change:
Verification
git diff --check origin/development...HEADI could not run the Rust integration test suite locally in this Windows environment because
cargo/rustcare not installed here. The PR includes a focused storage test for the migration behavior.Bounty
Submitting for the $70 bounty on #7829.