Skip to content

Commit cb2c7ed

Browse files
committed
Merge #411: fix(rusqlite): fix get_pre_1_wallet_keychains migration helper
c730973 fix(rusqlite): fix get_pre_v1_wallet_keychains migration helper (Steve Myers) Pull request description: ### Description While working on bitcoindevkit/book-of-bdk#141 I found some very rough edges with how I'd implemented the Pre1WalletKeychain struct. I've fixed these and added a new Pre1MigrationError type to cover new possible errors when parsing keychain name and checksum bytes. ### Notes to the reviewers - rename public function to get_pre_v1_wallet_keychains - rename returned structure to PreV1WalletKeychain - added PreV1MigrationError - changed PreV1WalletKeychain::keychain to KeychainKind - changed PreV1WalletKeychain::checksum to String ### Changelog notice Replace changelog notice from #364 to: Add get_pre_v1_wallet_keychains to assist migration from pre-1.0 bdk wallets. ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `just p` before pushing #### Bugfixes: * [ ] This pull request breaks the existing API * [x] I've added tests to reproduce the issue which are now passing * [x] I'm linking the issue being fixed by this PR ACKs for top commit: ValuedMammal: ACK c730973 oleonardolima: tACK c730973 Tree-SHA512: bcc4f51522cb2260a4d393c65091a931d90a5b910906d967d660104be3805677b6afb4976bd23f9f2da209d879d7031f9b11ddb5d1e9b6f8136b344f16cc324d
2 parents 4d4adae + c730973 commit cb2c7ed

File tree

2 files changed

+147
-57
lines changed

2 files changed

+147
-57
lines changed

src/wallet/migration.rs

Lines changed: 146 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,41 +13,70 @@
1313
//! This module provides helper functions and types to assist users in migrating wallet data
1414
//! when upgrading between major versions of the `bdk_wallet` crate.
1515
16-
#[cfg(feature = "rusqlite")]
1716
use crate::rusqlite::{self, Connection};
18-
#[cfg(feature = "rusqlite")]
19-
use alloc::{string::String, string::ToString, vec::Vec};
17+
use crate::KeychainKind::{self, External, Internal};
18+
use alloc::{
19+
string::{FromUtf8Error, String, ToString},
20+
vec::Vec,
21+
};
22+
use core::fmt;
2023

21-
// pre-1.0 sqlite database migration helper functions
22-
23-
/// `Pre1WalletKeychain` represents a structure that holds the keychain details
24+
/// [`PreV1WalletKeychain`] represents a structure that holds the keychain details
2425
/// and metadata required for managing a wallet's keys.
25-
#[cfg(feature = "rusqlite")]
2626
#[derive(Debug)]
27-
pub struct Pre1WalletKeychain {
27+
pub struct PreV1WalletKeychain {
2828
/// The name of the wallet keychains, "External" or "Internal".
29-
pub keychain: String,
29+
pub keychain: KeychainKind,
3030
/// The index of the last derived key in the wallet keychain.
3131
pub last_derivation_index: u32,
3232
/// Checksum of the keychain descriptor, it must match the corresponding post-1.0 bdk wallet
3333
/// descriptor checksum.
34-
pub checksum: Vec<u8>,
34+
pub checksum: String,
35+
}
36+
37+
/// Errors thrown when migrating from a pre-v1.0.0 BDK database.
38+
#[derive(Debug)]
39+
pub enum PreV1MigrationError {
40+
/// A SQLite error
41+
RusqliteError(rusqlite::Error),
42+
/// The keychain name is invalid, it must be "External" or "Internal"
43+
InvalidKeychain(String),
44+
/// The checksum could not be decoded as utf8
45+
InvalidChecksum(FromUtf8Error),
46+
}
47+
48+
impl fmt::Display for PreV1MigrationError {
49+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50+
match self {
51+
PreV1MigrationError::RusqliteError(e) => write!(f, "Rusqlite error: {}", e),
52+
PreV1MigrationError::InvalidKeychain(e) => write!(f, "Invalid keychain path: {}", e),
53+
PreV1MigrationError::InvalidChecksum(e) => write!(f, "Invalid checksum: {}", e),
54+
}
55+
}
56+
}
57+
58+
impl std::error::Error for PreV1MigrationError {}
59+
60+
impl From<rusqlite::Error> for PreV1MigrationError {
61+
fn from(e: rusqlite::Error) -> Self {
62+
PreV1MigrationError::RusqliteError(e)
63+
}
3564
}
3665

37-
/// Retrieves a list of [`Pre1WalletKeychain`] objects from a pre-1.0 bdk SQLite database.
66+
/// Retrieves a list of [`PreV1WalletKeychain`] objects from a pre-v1.0.0 bdk SQLite database.
3867
///
3968
/// This function uses a connection to a pre-1.0 bdk wallet SQLite database to execute a query that
4069
/// retrieves data from two tables (`last_derivation_indices` and `checksums`) and maps the
41-
/// resulting rows to a list of `Pre1WalletKeychain` objects.
42-
#[cfg(feature = "rusqlite")]
43-
pub fn get_pre_1_wallet_keychains(
70+
/// resulting rows to a list of [`PreV1WalletKeychain`] objects.
71+
pub fn get_pre_v1_wallet_keychains(
4472
conn: &mut Connection,
45-
) -> Result<Vec<Pre1WalletKeychain>, rusqlite::Error> {
73+
) -> Result<Vec<PreV1WalletKeychain>, PreV1MigrationError> {
4674
let db_tx = conn.transaction()?;
47-
let mut statement = db_tx.prepare(
48-
"SELECT idx.keychain AS keychain, value, checksum FROM last_derivation_indices AS idx \
75+
let mut statement = db_tx
76+
.prepare(
77+
"SELECT trim(idx.keychain,'\"') AS keychain, value, checksum FROM last_derivation_indices AS idx \
4978
JOIN checksums AS chk ON idx.keychain = chk.keychain",
50-
)?;
79+
)?;
5180
let row_iter = statement.query_map([], |row| {
5281
Ok((
5382
row.get::<_, String>("keychain")?,
@@ -58,8 +87,14 @@ pub fn get_pre_1_wallet_keychains(
5887
let mut keychains = vec![];
5988
for row in row_iter {
6089
let (keychain, value, checksum) = row?;
61-
keychains.push(Pre1WalletKeychain {
62-
keychain: keychain.trim_matches('"').to_string(),
90+
let keychain = match keychain.as_str() {
91+
"External" => Ok(External),
92+
"Internal" => Ok(Internal),
93+
name => Err(PreV1MigrationError::InvalidKeychain(name.to_string())),
94+
}?;
95+
let checksum = String::from_utf8(checksum).map_err(PreV1MigrationError::InvalidChecksum)?;
96+
keychains.push(PreV1WalletKeychain {
97+
keychain,
6398
last_derivation_index: value,
6499
checksum,
65100
})
@@ -69,50 +104,53 @@ pub fn get_pre_1_wallet_keychains(
69104

70105
#[cfg(test)]
71106
mod test {
72-
#[cfg(feature = "rusqlite")]
73107
use crate::rusqlite::{self, Connection};
108+
use crate::KeychainKind::{External, Internal};
109+
110+
const SCHEMA_SQL: &str = "CREATE TABLE last_derivation_indices (keychain TEXT, value INTEGER);
111+
CREATE UNIQUE INDEX idx_indices_keychain ON last_derivation_indices(keychain);
112+
CREATE TABLE checksums (keychain TEXT, checksum BLOB);
113+
CREATE INDEX idx_checksums_keychain ON checksums(keychain);";
114+
115+
fn setup_db() -> Connection {
116+
let conn = Connection::open_in_memory().unwrap();
117+
conn.execute_batch(SCHEMA_SQL).unwrap();
118+
conn
119+
}
120+
121+
fn insert_keychain(
122+
conn: &Connection,
123+
keychain: &str,
124+
value: u32,
125+
checksum: &[u8],
126+
) -> rusqlite::Result<()> {
127+
conn.execute(
128+
"INSERT INTO last_derivation_indices (keychain, value) VALUES (?, ?)",
129+
rusqlite::params![keychain, value],
130+
)?;
131+
conn.execute(
132+
"INSERT INTO checksums (keychain, checksum) VALUES (?, ?)",
133+
rusqlite::params![keychain, checksum],
134+
)?;
135+
Ok(())
136+
}
74137

75-
#[cfg(feature = "rusqlite")]
76138
#[test]
77139
fn test_get_pre_1_wallet_keychains() -> anyhow::Result<()> {
78-
let mut conn = Connection::open_in_memory()?;
79-
let external_checksum = vec![0x01u8, 0x02u8, 0x03u8, 0x04u8];
80-
let internal_checksum = vec![0x05u8, 0x06u8, 0x07u8, 0x08u8];
81-
// Init tables
82-
{
83-
// Create pre-1.0 bdk sqlite schema
84-
conn.execute_batch(
85-
"CREATE TABLE last_derivation_indices (keychain TEXT, value INTEGER);
86-
CREATE UNIQUE INDEX idx_indices_keychain ON last_derivation_indices(keychain);
87-
CREATE TABLE checksums (keychain TEXT, checksum BLOB);
88-
CREATE INDEX idx_checksums_keychain ON checksums(keychain);",
89-
)?;
90-
// Insert test data
91-
conn.execute(
92-
"INSERT INTO last_derivation_indices (keychain, value) VALUES (?, ?)",
93-
rusqlite::params!["\"External\"", 42],
94-
)?;
95-
conn.execute(
96-
"INSERT INTO checksums (keychain, checksum) VALUES (?, ?)",
97-
rusqlite::params!["\"External\"", external_checksum],
98-
)?;
99-
conn.execute(
100-
"INSERT INTO last_derivation_indices (keychain, value) VALUES (?, ?)",
101-
rusqlite::params!["\"Internal\"", 21],
102-
)?;
103-
conn.execute(
104-
"INSERT INTO checksums (keychain, checksum) VALUES (?, ?)",
105-
rusqlite::params!["\"Internal\"", internal_checksum],
106-
)?;
107-
}
140+
let mut conn = setup_db();
141+
let external_checksum = "72k0lrja";
142+
let internal_checksum = "07nwzkz9";
143+
144+
insert_keychain(&conn, "\"External\"", 42, external_checksum.as_bytes())?;
145+
insert_keychain(&conn, "\"Internal\"", 21, internal_checksum.as_bytes())?;
108146

109147
// test with a 2 keychain wallet
110-
let result = super::get_pre_1_wallet_keychains(&mut conn)?;
148+
let result = super::get_pre_v1_wallet_keychains(&mut conn)?;
111149
assert_eq!(result.len(), 2);
112-
assert_eq!(result[0].keychain, "External");
150+
assert_eq!(result[0].keychain, External);
113151
assert_eq!(result[0].last_derivation_index, 42);
114152
assert_eq!(result[0].checksum, external_checksum);
115-
assert_eq!(result[1].keychain, "Internal");
153+
assert_eq!(result[1].keychain, Internal);
116154
assert_eq!(result[1].last_derivation_index, 21);
117155
assert_eq!(result[1].checksum, internal_checksum);
118156
// delete "Internal" descriptor
@@ -127,12 +165,63 @@ mod test {
127165
)?;
128166
}
129167
// test with a 1 keychain wallet
130-
let result = super::get_pre_1_wallet_keychains(&mut conn)?;
168+
let result = super::get_pre_v1_wallet_keychains(&mut conn)?;
131169
assert_eq!(result.len(), 1);
132-
assert_eq!(result[0].keychain, "External");
170+
assert_eq!(result[0].keychain, External);
133171
assert_eq!(result[0].last_derivation_index, 42);
134172
assert_eq!(result[0].checksum, external_checksum);
135173

136174
Ok(())
137175
}
176+
177+
#[test]
178+
fn test_invalid_keychain_name() {
179+
let mut conn = setup_db();
180+
insert_keychain(&conn, "\"InvalidKeychain\"", 42, b"72k0lrja").unwrap();
181+
182+
let result = super::get_pre_v1_wallet_keychains(&mut conn);
183+
assert!(result.is_err());
184+
let err = result.unwrap_err();
185+
assert!(
186+
matches!(err, super::PreV1MigrationError::InvalidKeychain(ref name) if name == "InvalidKeychain"),
187+
"Expected InvalidKeychain error with name 'InvalidKeychain', got: {:?}",
188+
err
189+
);
190+
}
191+
192+
#[test]
193+
fn test_invalid_checksum_utf8() {
194+
let mut conn = setup_db();
195+
insert_keychain(&conn, "\"External\"", 42, &[0xFF, 0xFE, 0xFD]).unwrap();
196+
197+
let result = super::get_pre_v1_wallet_keychains(&mut conn);
198+
assert!(result.is_err());
199+
let err = result.unwrap_err();
200+
assert!(
201+
matches!(err, super::PreV1MigrationError::InvalidChecksum(_)),
202+
"Expected InvalidChecksum error, got: {:?}",
203+
err
204+
);
205+
}
206+
207+
#[test]
208+
fn test_empty_database() -> anyhow::Result<()> {
209+
let mut conn = setup_db();
210+
let result = super::get_pre_v1_wallet_keychains(&mut conn)?;
211+
assert_eq!(result.len(), 0);
212+
Ok(())
213+
}
214+
215+
#[test]
216+
fn test_missing_table() {
217+
let mut conn = Connection::open_in_memory().unwrap();
218+
let result = super::get_pre_v1_wallet_keychains(&mut conn);
219+
assert!(result.is_err());
220+
let err = result.unwrap_err();
221+
assert!(
222+
matches!(err, super::PreV1MigrationError::RusqliteError(_)),
223+
"Expected RusqliteError, got: {:?}",
224+
err
225+
);
226+
}
138227
}

src/wallet/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ pub mod error;
5656
mod event;
5757
pub mod export;
5858
pub mod locked_outpoints;
59+
#[cfg(feature = "rusqlite")]
5960
pub mod migration;
6061
mod params;
6162
mod persisted;

0 commit comments

Comments
 (0)