From c96609195d60ac078659df85a010e9c42e232a76 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Tue, 18 Feb 2025 00:14:34 +0100 Subject: [PATCH] Implement peristence scaffolding --- Cargo.lock | 68 +++++++++++++++++++ Cargo.toml | 1 + blacklist.txt | 1 - src/subcommand/wallet/restore.rs | 2 +- src/wallet.rs | 110 +++++++++++++++++++++++++++++-- src/wallet/persister.rs | 29 ++++++++ tests/wallet/create.rs | 5 ++ 7 files changed, 207 insertions(+), 9 deletions(-) create mode 100644 src/wallet/persister.rs diff --git a/Cargo.lock b/Cargo.lock index 12e208f3f3..43a98d084f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,18 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -471,12 +483,55 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bdk_chain" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4955734f97b2baed3f36d16ae7c203fdde31ae85391ac44ee3cbcaf0886db5ce" +dependencies = [ + "bdk_core", + "bitcoin", + "miniscript", + "serde", +] + +[[package]] +name = "bdk_core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b545aea1efc090e4f71f1dd5468090d9f54c3de48002064c04895ef811fbe0b2" +dependencies = [ + "bitcoin", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "bdk_wallet" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a13c947be940d32a91b876fc5223a6d839a40bc219496c5c78af74714b1b3f7" +dependencies = [ + "bdk_chain", + "bitcoin", + "miniscript", + "rand_core", + "serde", + "serde_json", +] + [[package]] name = "bech32" version = "0.11.0" @@ -535,6 +590,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" dependencies = [ "base58ck", + "base64 0.21.7", "bech32", "bitcoin-internals 0.3.0", "bitcoin-io", @@ -1536,6 +1592,16 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "serde", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -2260,6 +2326,7 @@ checksum = "5bd3c9608217b0d6fa9c9c8ddd875b85ab72bd4311cfc8db35e1b5a08fc11f4d" dependencies = [ "bech32", "bitcoin", + "serde", ] [[package]] @@ -2546,6 +2613,7 @@ dependencies = [ "axum", "axum-server", "base64 0.22.1", + "bdk_wallet", "bip322", "bip39", "bitcoin", diff --git a/Cargo.toml b/Cargo.toml index 327786ac6b..f192066a0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,6 +100,7 @@ tokio-stream = "0.1.9" tokio-util = {version = "0.7.3", features = ["compat"] } tower-http = { version = "0.6.2", features = ["auth", "compression-br", "compression-gzip", "cors", "set-header"] } urlencoding = "2.1.3" +bdk_wallet = "1.1.0" [dev-dependencies] criterion = "0.5.1" diff --git a/blacklist.txt b/blacklist.txt index b98fe2502c..515322f4cc 100644 --- a/blacklist.txt +++ b/blacklist.txt @@ -102,7 +102,6 @@ wallet::burn::oversize_metadata_requires_no_limit_flag wallet::burn::runic_outputs_are_protected wallet::cardinals::cardinals wallet::cardinals::cardinals_does_not_show_runic_outputs -wallet::create::create wallet::create::create_with_different_name wallet::create::detect_wrong_descriptors wallet::create::seed_phrases_are_twelve_words_long diff --git a/src/subcommand/wallet/restore.rs b/src/subcommand/wallet/restore.rs index a6eaef28e0..dad5e77e3f 100644 --- a/src/subcommand/wallet/restore.rs +++ b/src/subcommand/wallet/restore.rs @@ -69,7 +69,7 @@ impl Restore { Source::Mnemonic => { io::stdin().read_line(&mut buffer)?; let mnemonic = Mnemonic::from_str(&buffer)?; - Wallet::initialize( + Wallet::initialize_old( name, settings, mnemonic.to_seed(self.passphrase.unwrap_or_default()), diff --git a/src/wallet.rs b/src/wallet.rs index 64102a4c31..60541ee9bb 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,6 +1,7 @@ use { super::*, batch::ParentInfo, + bdk_wallet as bdk, bitcoin::{ bip32::{ChildNumber, DerivationPath, Xpriv}, psbt::Psbt, @@ -12,7 +13,9 @@ use { index::entry::Entry, indicatif::{ProgressBar, ProgressStyle}, log::log_enabled, - miniscript::descriptor::{DescriptorSecretKey, DescriptorXKey, Wildcard}, + miniscript::descriptor::{ + Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, Wildcard, + }, redb::{Database, DatabaseError, ReadableTable, RepairSession, StorageError, TableDefinition}, std::sync::Once, transaction_builder::TransactionBuilder, @@ -20,11 +23,13 @@ use { pub mod batch; pub mod entry; +pub mod persister; pub mod transaction_builder; pub mod wallet_constructor; -const SCHEMA_VERSION: u64 = 1; +const SCHEMA_VERSION: u64 = 2; +define_table! { CHANGESET, (), &str } define_table! { RUNE_TO_ETCHING, u128, EtchingEntryValue } define_table! { STATISTICS, u64, u64 } @@ -46,7 +51,7 @@ impl From for u64 { } #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct Descriptor { +pub struct DescriptorJson { pub desc: String, pub timestamp: bitcoincore_rpc::bitcoincore_rpc_json::Timestamp, pub active: bool, @@ -58,7 +63,7 @@ pub struct Descriptor { #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct ListDescriptorsResult { pub wallet_name: String, - pub descriptors: Vec, + pub descriptors: Vec, } #[derive(Debug, PartialEq)] @@ -465,7 +470,10 @@ impl Wallet { }) } - fn check_descriptors(wallet_name: &str, descriptors: Vec) -> Result> { + fn check_descriptors( + wallet_name: &str, + descriptors: Vec, + ) -> Result> { let tr = descriptors .iter() .filter(|descriptor| descriptor.desc.starts_with("tr(")) @@ -486,7 +494,7 @@ impl Wallet { pub(crate) fn initialize_from_descriptors( name: String, settings: &Settings, - descriptors: Vec, + descriptors: Vec, ) -> Result { let client = Self::check_version(settings.bitcoin_rpc_client(Some(name.clone()))?)?; @@ -526,8 +534,66 @@ impl Wallet { seed: [u8; 64], timestamp: bitcoincore_rpc::json::Timestamp, ) -> Result { - panic!("attempt to initialize bitcoin client"); + let database = Wallet::create_database(&name, settings)?; + + let network = settings.chain().network(); + + let secp = Secp256k1::new(); + + let master_private_key = Xpriv::new_master(network, &seed)?; + + let fingerprint = master_private_key.fingerprint(&secp); + + let derivation_path = DerivationPath::master() + .child(ChildNumber::Hardened { index: 86 }) + .child(ChildNumber::Hardened { + index: u32::from(network != Network::Bitcoin), + }) + .child(ChildNumber::Hardened { index: 0 }); + + let derived_private_key = master_private_key.derive_priv(&secp, &derivation_path)?; + + let descriptor = |change: bool| -> Result<( + Descriptor, + BTreeMap, + )> { + let secret_key = DescriptorSecretKey::XPrv(DescriptorXKey { + origin: Some((fingerprint, derivation_path.clone())), + xkey: derived_private_key, + derivation_path: DerivationPath::master().child(ChildNumber::Normal { + index: change.into(), + }), + wildcard: Wildcard::Unhardened, + }); + + let public_key = secret_key.to_public(&secp)?; + + let mut key_map = BTreeMap::new(); + key_map.insert(public_key.clone(), secret_key); + + let descriptor = Descriptor::new_tr(public_key, None)?; + + Ok((descriptor, key_map)) + }; + + let mut persister = persister::Persister(Arc::new(database)); + + let mut wallet = bdk::Wallet::create(descriptor(false)?, descriptor(true)?) + .network(network) + .create_wallet(&mut persister)?; + wallet.persist(&mut persister)?; + + Ok(()) + } + + #[allow(unused_variables, unreachable_code)] + pub(crate) fn initialize_old( + name: String, + settings: &Settings, + seed: [u8; 64], + timestamp: bitcoincore_rpc::json::Timestamp, + ) -> Result { Self::check_version(settings.bitcoin_rpc_client(None)?)?.create_wallet( &name, None, @@ -631,6 +697,36 @@ impl Wallet { ) } + pub(crate) fn create_database(wallet_name: &String, settings: &Settings) -> Result { + let path = settings + .data_dir() + .join("wallets") + .join(format!("{wallet_name}.redb")); + + if let Err(err) = fs::create_dir_all(path.parent().unwrap()) { + bail!( + "failed to create data dir `{}`: {err}", + path.parent().unwrap().display() + ); + } + + let database = Database::builder().create(&path)?; + + let mut tx = database.begin_write()?; + tx.set_quick_repair(true); + + tx.open_table(CHANGESET)?; + + tx.open_table(RUNE_TO_ETCHING)?; + + tx.open_table(STATISTICS)? + .insert(&Statistic::Schema.key(), &SCHEMA_VERSION)?; + + tx.commit()?; + + Ok(database) + } + pub(crate) fn open_database(wallet_name: &String, settings: &Settings) -> Result { let path = settings .data_dir() diff --git a/src/wallet/persister.rs b/src/wallet/persister.rs new file mode 100644 index 0000000000..ca83078bfe --- /dev/null +++ b/src/wallet/persister.rs @@ -0,0 +1,29 @@ +use { + super::*, + bdk::{ChangeSet, WalletPersister}, +}; + +pub(crate) struct Persister(pub(crate) Arc); + +impl WalletPersister for Persister { + type Error = Error; + + fn initialize(persister: &mut Self) -> std::result::Result { + Ok(ChangeSet::default()) + } + + fn persist( + persister: &mut Self, + changeset: &bdk_wallet::ChangeSet, + ) -> std::result::Result<(), Self::Error> { + let wtx = persister.0.begin_write()?; + + wtx + .open_table(CHANGESET)? + .insert((), serde_json::to_string(changeset)?.as_str())?; + + wtx.commit()?; + + Ok(()) + } +} diff --git a/tests/wallet/create.rs b/tests/wallet/create.rs index f34cc1e4e9..fc3ad6e2fa 100644 --- a/tests/wallet/create.rs +++ b/tests/wallet/create.rs @@ -91,3 +91,8 @@ fn create_with_different_name() { assert!(core.wallets().contains("inscription-wallet")); } + +#[test] +fn create_with_same_name_fails() { + todo!() +}