From e4fe8bc5979da907ce4e4d607318154124d00834 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Wed, 9 Oct 2024 16:36:37 -0300 Subject: [PATCH 01/18] feat(strata-cli)!: add hash version and optional password (#392) * feat(strata-cli)!: add hash version and optional password * feat(strata-cli): warn user about password strength --- Cargo.lock | 60 ++++++++++++++++++++++++++++ bin/strata-cli/Cargo.toml | 1 + bin/strata-cli/src/cmd/change_pwd.rs | 12 ++++++ bin/strata-cli/src/seed.rs | 24 +++++++++-- bin/strata-cli/src/seed/password.rs | 46 ++++++++++++++++++--- 5 files changed, 135 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c8390bb2..2952ec2e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3492,6 +3492,37 @@ dependencies = [ "syn 2.0.71", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.71", +] + [[package]] name = "derive_more" version = "0.99.18" @@ -4245,6 +4276,17 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fancy-regex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" +dependencies = [ + "bit-set", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + [[package]] name = "fast-float" version = "0.2.0" @@ -12766,6 +12808,7 @@ dependencies = [ "strata-bridge-tx-builder", "terrors", "tokio", + "zxcvbn", ] [[package]] @@ -15491,3 +15534,20 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zxcvbn" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad76e35b00ad53688d6b90c431cabe3cbf51f7a4a154739e04b63004ab1c736c" +dependencies = [ + "chrono", + "derive_builder", + "fancy-regex", + "itertools 0.13.0", + "lazy_static", + "regex", + "time", + "wasm-bindgen", + "web-sys", +] diff --git a/bin/strata-cli/Cargo.toml b/bin/strata-cli/Cargo.toml index 8fd51524f..52255b391 100644 --- a/bin/strata-cli/Cargo.toml +++ b/bin/strata-cli/Cargo.toml @@ -42,6 +42,7 @@ sled = "0.34.7" strata-bridge-tx-builder.workspace = true terrors.workspace = true tokio.workspace = true +zxcvbn = "3.1.0" # sha2 fails to compile on windows with the "asm" feature [target.'cfg(not(target_os = "windows"))'.dependencies] diff --git a/bin/strata-cli/src/cmd/change_pwd.rs b/bin/strata-cli/src/cmd/change_pwd.rs index 4351a69ba..c1c0d24e6 100644 --- a/bin/strata-cli/src/cmd/change_pwd.rs +++ b/bin/strata-cli/src/cmd/change_pwd.rs @@ -1,6 +1,7 @@ use argh::FromArgs; use console::Term; use rand::thread_rng; +use zxcvbn::Score; use crate::seed::{password::Password, EncryptedSeedPersister, Seed}; @@ -12,6 +13,17 @@ pub struct ChangePwdArgs {} pub async fn change_pwd(_args: ChangePwdArgs, seed: Seed, persister: impl EncryptedSeedPersister) { let term = Term::stdout(); let mut new_pw = Password::read(true).unwrap(); + let entropy = new_pw.entropy(); + let _ = term.write_line(format!("Password strength (Overall strength score from 0-4, where anything below 3 is too weak): {}", entropy.score()).as_str()); + if entropy.score() <= Score::Two { + let _ = term.write_line( + entropy + .feedback() + .expect("No feedback") + .to_string() + .as_str(), + ); + } let encrypted_seed = seed.encrypt(&mut new_pw, &mut thread_rng()).unwrap(); persister.save(&encrypted_seed).unwrap(); let _ = term.write_line("Password changed successfully"); diff --git a/bin/strata-cli/src/seed.rs b/bin/strata-cli/src/seed.rs index 8089c0944..00ca8f71f 100644 --- a/bin/strata-cli/src/seed.rs +++ b/bin/strata-cli/src/seed.rs @@ -11,10 +11,11 @@ use bdk_wallet::{ use bip39::{Language, Mnemonic}; use console::Term; use dialoguer::{Confirm, Input}; -use password::{IncorrectPassword, Password}; +use password::{HashVersion, IncorrectPassword, Password}; use rand::{thread_rng, Rng, RngCore}; use sha2::{Digest, Sha256}; use terrors::OneOf; +use zxcvbn::Score; use crate::constants::{AES_NONCE_LEN, AES_TAG_LEN, PW_SALT_LEN, SEED_LEN}; @@ -56,7 +57,10 @@ impl Seed { rng.fill_bytes(&mut buf[..PW_SALT_LEN + AES_NONCE_LEN]); let seed_encryption_key = password - .seed_encryption_key(&buf[..PW_SALT_LEN].try_into().expect("cannot fail")) + .seed_encryption_key( + &buf[..PW_SALT_LEN].try_into().expect("cannot fail"), + HashVersion::V0, + ) .map_err(OneOf::new)?; let (salt_and_nonce, rest) = buf.split_at_mut(PW_SALT_LEN + AES_NONCE_LEN); @@ -111,7 +115,10 @@ impl EncryptedSeed { password: &mut Password, ) -> Result> { let seed_encryption_key = password - .seed_encryption_key(&self.0[..PW_SALT_LEN].try_into().expect("cannot fail")) + .seed_encryption_key( + &self.0[..PW_SALT_LEN].try_into().expect("cannot fail"), + HashVersion::V0, + ) .map_err(OneOf::new)?; let mut cipher = @@ -188,6 +195,17 @@ pub fn load_or_create( }; let mut password = Password::read(true).map_err(OneOf::new)?; + let entropy = password.entropy(); + let _ = term.write_line(format!("Password strength (Overall strength score from 0-4, where anything below 3 is too weak): {}", entropy.score()).as_str()); + if entropy.score() <= Score::Two { + let _ = term.write_line( + entropy + .feedback() + .expect("No feedback") + .to_string() + .as_str(), + ); + } let encrypted_seed = match seed.encrypt(&mut password, &mut thread_rng()) { Ok(es) => es, Err(e) => { diff --git a/bin/strata-cli/src/seed/password.rs b/bin/strata-cli/src/seed/password.rs index e6b51b21b..9880e9ddc 100644 --- a/bin/strata-cli/src/seed/password.rs +++ b/bin/strata-cli/src/seed/password.rs @@ -1,5 +1,6 @@ -use argon2::Argon2; +use argon2::{Algorithm, Argon2, Params, Version}; use dialoguer::Password as InputPassword; +use zxcvbn::{zxcvbn, Entropy}; use super::PW_SALT_LEN; @@ -8,13 +9,35 @@ pub struct Password { seed_encryption_key: Option<[u8; 32]>, } +pub enum HashVersion { + V0, +} + +impl HashVersion { + const fn params(&self) -> (Algorithm, Version, Result) { + match self { + HashVersion::V0 => ( + Algorithm::Argon2id, + Version::V0x13, + // NOTE: This is the OWASP recommended params for Argon2id + // see https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id + Params::new(19_456, 2, 1, Some(32)), + ), + } + } +} + impl Password { pub fn read(new: bool) -> Result { let mut input = InputPassword::new(); if new { input = input - .with_prompt("Create a new password") - .with_confirmation("Confirm password", "Passwords didn't match"); + .with_prompt("Create a new password (leave empty for no password, dangerous!)") + .with_confirmation( + "Confirm password (leave empty for no password, dangerous!)", + "Passwords didn't match", + ) + .allow_empty_password(true); } else { input = input.with_prompt("Enter your password"); } @@ -27,17 +50,30 @@ impl Password { }) } + /// Returns the password entropy. + pub fn entropy(&self) -> Entropy { + zxcvbn(self.inner.as_str(), &[]) + } + pub fn seed_encryption_key( &mut self, salt: &[u8; PW_SALT_LEN], + version: HashVersion, ) -> Result<&[u8; 32], argon2::Error> { match self.seed_encryption_key { Some(ref key) => Ok(key), None => { let mut sek = [0u8; 32]; - Argon2::default().hash_password_into(self.inner.as_bytes(), salt, &mut sek)?; + let (algo, ver, params) = version.params(); + if !self.inner.is_empty() { + Argon2::new(algo, ver, params.expect("valid params")).hash_password_into( + self.inner.as_bytes(), + salt, + &mut sek, + )?; + } self.seed_encryption_key = Some(sek); - self.seed_encryption_key(salt) + self.seed_encryption_key(salt, version) } } } From dcb9a57bc32315da121bd9453cf29e76aa35f871 Mon Sep 17 00:00:00 2001 From: Aaron Feickert <66188213+AaronFeickert@users.noreply.github.com> Date: Thu, 10 Oct 2024 10:00:09 -0500 Subject: [PATCH 02/18] Require secure RNG for seed encryption (#385) Co-authored-by: Jose Storopoli --- bin/strata-cli/src/seed.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bin/strata-cli/src/seed.rs b/bin/strata-cli/src/seed.rs index 00ca8f71f..4fdffd651 100644 --- a/bin/strata-cli/src/seed.rs +++ b/bin/strata-cli/src/seed.rs @@ -12,7 +12,7 @@ use bip39::{Language, Mnemonic}; use console::Term; use dialoguer::{Confirm, Input}; use password::{HashVersion, IncorrectPassword, Password}; -use rand::{thread_rng, Rng, RngCore}; +use rand::{thread_rng, CryptoRng, RngCore}; use sha2::{Digest, Sha256}; use terrors::OneOf; use zxcvbn::Score; @@ -31,8 +31,10 @@ impl BaseWallet { pub struct Seed([u8; SEED_LEN]); impl Seed { - fn gen(rng: &mut impl Rng) -> Self { - Self(rng.gen()) + fn gen(rng: &mut R) -> Self { + let mut bytes = [0u8; SEED_LEN]; + rng.fill_bytes(&mut bytes); + Self(bytes) } pub fn print_mnemonic(&self, language: Language) { @@ -48,10 +50,10 @@ impl Seed { hasher.finalize().into() } - pub fn encrypt( + pub fn encrypt( &self, password: &mut Password, - rng: &mut impl RngCore, + rng: &mut R, ) -> Result> { let mut buf = [0u8; EncryptedSeed::LEN]; rng.fill_bytes(&mut buf[..PW_SALT_LEN + AES_NONCE_LEN]); From 483014dfd721fff10dbec6a6de247dd4a0246941 Mon Sep 17 00:00:00 2001 From: Aaron Feickert <66188213+AaronFeickert@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:46:25 -0500 Subject: [PATCH 03/18] Use `OsRng` consistently --- bin/strata-cli/src/cmd/change_pwd.rs | 4 ++-- bin/strata-cli/src/cmd/faucet.rs | 4 ++-- bin/strata-cli/src/recovery.rs | 5 +++-- bin/strata-cli/src/seed.rs | 6 +++--- bin/strata-client/src/extractor.rs | 17 ++++++++--------- crates/bridge-tx-builder/src/operations.rs | 3 ++- crates/btcio/src/writer/builder.rs | 8 ++++---- crates/crypto/src/lib.rs | 12 +++++------- crates/evmexec/src/el_payload.rs | 4 ++-- crates/evmexec/src/engine.rs | 8 +++----- crates/primitives/src/bridge.rs | 3 ++- crates/primitives/src/hash.rs | 5 ++--- crates/primitives/src/l1.rs | 11 ++++++----- crates/primitives/src/relay/util.rs | 3 ++- crates/proof-impl/btc-blockspace/src/block.rs | 4 ++-- crates/proof-impl/btc-blockspace/src/merkle.rs | 8 +++----- crates/rocksdb-store/src/l1/db.rs | 3 ++- crates/state/src/l1/header_verification.rs | 6 +++--- crates/test-utils/src/bridge.rs | 4 ++-- crates/test-utils/src/lib.rs | 5 ++--- crates/tx-parser/src/filter.rs | 4 ++-- crates/util/shrex/src/lib.rs | 4 ++-- tests/cooperative-bridge-flow.rs | 3 ++- tests/cooperative-bridge-out-flow.rs | 3 ++- 24 files changed, 68 insertions(+), 69 deletions(-) diff --git a/bin/strata-cli/src/cmd/change_pwd.rs b/bin/strata-cli/src/cmd/change_pwd.rs index c1c0d24e6..5de23db38 100644 --- a/bin/strata-cli/src/cmd/change_pwd.rs +++ b/bin/strata-cli/src/cmd/change_pwd.rs @@ -1,6 +1,6 @@ use argh::FromArgs; use console::Term; -use rand::thread_rng; +use rand::rngs::OsRng; use zxcvbn::Score; use crate::seed::{password::Password, EncryptedSeedPersister, Seed}; @@ -24,7 +24,7 @@ pub async fn change_pwd(_args: ChangePwdArgs, seed: Seed, persister: impl Encryp .as_str(), ); } - let encrypted_seed = seed.encrypt(&mut new_pw, &mut thread_rng()).unwrap(); + let encrypted_seed = seed.encrypt(&mut new_pw, &mut OsRng).unwrap(); persister.save(&encrypted_seed).unwrap(); let _ = term.write_line("Password changed successfully"); } diff --git a/bin/strata-cli/src/cmd/faucet.rs b/bin/strata-cli/src/cmd/faucet.rs index 006e1d9b1..fb8bbdcb8 100644 --- a/bin/strata-cli/src/cmd/faucet.rs +++ b/bin/strata-cli/src/cmd/faucet.rs @@ -5,7 +5,7 @@ use argh::FromArgs; use bdk_wallet::{bitcoin::Address, KeychainKind}; use console::Term; use indicatif::ProgressBar; -use rand::{distributions::uniform::SampleRange, thread_rng}; +use rand::{distributions::uniform::SampleRange, rngs::OsRng}; use reqwest::{StatusCode, Url}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -77,7 +77,7 @@ pub async fn faucet(args: FaucetArgs, seed: Seed, settings: Settings) { solution.to_le_bytes(), ) { solution += 1; - if (0..100).sample_single(&mut thread_rng()) == 0 { + if (0..100).sample_single(&mut OsRng) == 0 { pb.set_message(format!("Trying {solution}")); } } diff --git a/bin/strata-cli/src/recovery.rs b/bin/strata-cli/src/recovery.rs index ed72babd5..cc8424f43 100644 --- a/bin/strata-cli/src/recovery.rs +++ b/bin/strata-cli/src/recovery.rs @@ -13,7 +13,7 @@ use bdk_wallet::{ miniscript::{descriptor::DescriptorKeyParseError, Descriptor}, template::DescriptorTemplateOut, }; -use rand::{thread_rng, Rng}; +use rand::{rngs::OsRng, RngCore}; use sha2::{Digest, Sha256}; use terrors::OneOf; use tokio::io::AsyncReadExt; @@ -101,7 +101,8 @@ impl DescriptorRecovery { bytes.extend_from_slice(&net); } - let nonce = Nonce::from(thread_rng().gen::<[u8; 12]>()); + let mut nonce = Nonce::default(); + OsRng.fill_bytes(&mut nonce); // encrypted_bytes | tag (16 bytes) | nonce (12 bytes) self.cipher diff --git a/bin/strata-cli/src/seed.rs b/bin/strata-cli/src/seed.rs index 4fdffd651..1b0154be7 100644 --- a/bin/strata-cli/src/seed.rs +++ b/bin/strata-cli/src/seed.rs @@ -12,7 +12,7 @@ use bip39::{Language, Mnemonic}; use console::Term; use dialoguer::{Confirm, Input}; use password::{HashVersion, IncorrectPassword, Password}; -use rand::{thread_rng, CryptoRng, RngCore}; +use rand::{rngs::OsRng, CryptoRng, RngCore}; use sha2::{Digest, Sha256}; use terrors::OneOf; use zxcvbn::Score; @@ -193,7 +193,7 @@ pub fn load_or_create( } } else { let _ = term.write_line("Creating new wallet"); - Seed::gen(&mut thread_rng()) + Seed::gen(&mut OsRng) }; let mut password = Password::read(true).map_err(OneOf::new)?; @@ -208,7 +208,7 @@ pub fn load_or_create( .as_str(), ); } - let encrypted_seed = match seed.encrypt(&mut password, &mut thread_rng()) { + let encrypted_seed = match seed.encrypt(&mut password, &mut OsRng) { Ok(es) => es, Err(e) => { let narrowed = e.narrow::(); diff --git a/bin/strata-client/src/extractor.rs b/bin/strata-client/src/extractor.rs index 33f26c05d..b89f82a49 100644 --- a/bin/strata-client/src/extractor.rs +++ b/bin/strata-client/src/extractor.rs @@ -189,6 +189,7 @@ mod tests { transaction::Version, ScriptBuf, Sequence, TxIn, TxOut, Witness, }; + use rand::rngs::OsRng; use strata_bridge_tx_builder::prelude::{create_taproot_addr, SpendPath}; use strata_common::logging; use strata_db::traits::L1DataStore; @@ -341,14 +342,14 @@ mod tests { "num_blocks and max_tx_per_block must be at least 1" ); - let random_block = rand::thread_rng().gen_range(1..num_blocks); + let random_block = OsRng.gen_range(1..num_blocks); let mut num_valid_duties = 0; for idx in 0..num_blocks { - let num_txs = rand::thread_rng().gen_range(1..max_txs_per_block); + let num_txs = OsRng.gen_range(1..max_txs_per_block); let known_tx_idx = if idx == random_block { - Some(rand::thread_rng().gen_range(0..num_txs)) + Some(OsRng.gen_range(0..num_txs)) } else { None }; @@ -485,7 +486,7 @@ mod tests { // true => tx invalid // false => script_pubkey in tx output invalid - let tx_invalid: bool = rand::thread_rng().gen_bool(0.5); + let tx_invalid: bool = OsRng.gen_bool(0.5); if tx_invalid { let mut random_tx: Vec = arb.generate(); @@ -575,9 +576,7 @@ mod tests { break; } - let mut rng = rand::thread_rng(); - - let random_assignee = rng.gen_range(0..operators.len()); + let random_assignee = OsRng.gen_range(0..operators.len()); let random_assignee = operators[random_assignee]; let mut dispatched_deposits = vec![]; @@ -590,7 +589,7 @@ mod tests { deposits_table.add_deposits(&tx_ref, &operators, amt); // dispatch about half of the deposits - let should_dispatch = rng.gen_bool(0.5); + let should_dispatch = OsRng.gen_bool(0.5); if should_dispatch.not() { continue; } @@ -619,7 +618,7 @@ mod tests { "some deposits should have been randomly dispatched" ); - let needle_index = rand::thread_rng().gen_range(0..dispatched_deposits.len()); + let needle_index = OsRng.gen_range(0..dispatched_deposits.len()); let needle = dispatched_deposits .get(needle_index) diff --git a/crates/bridge-tx-builder/src/operations.rs b/crates/bridge-tx-builder/src/operations.rs index af5dcfd3f..8ca89e6dd 100644 --- a/crates/bridge-tx-builder/src/operations.rs +++ b/crates/bridge-tx-builder/src/operations.rs @@ -246,6 +246,7 @@ mod tests { key::Keypair, secp256k1::{rand, SecretKey}, }; + use rand::rngs::OsRng; use super::*; @@ -281,7 +282,7 @@ mod tests { "should work if the number of scripts is not an exact power of 2" ); - let secret_key = SecretKey::new(&mut rand::thread_rng()); + let secret_key = SecretKey::new(&mut OsRng); let keypair = Keypair::from_secret_key(SECP256K1, &secret_key); let (x_only_public_key, _) = XOnlyPublicKey::from_keypair(&keypair); diff --git a/crates/btcio/src/writer/builder.rs b/crates/btcio/src/writer/builder.rs index fc49d9cb2..590ccaf1b 100644 --- a/crates/btcio/src/writer/builder.rs +++ b/crates/btcio/src/writer/builder.rs @@ -24,7 +24,7 @@ use bitcoin::{ Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, }; -use rand::RngCore; +use rand::{rngs::OsRng, RngCore}; use strata_state::tx::InscriptionData; use strata_tx_parser::inscription::{BATCH_DATA_TAG, ROLLUP_NAME_TAG, VERSION_TAG}; use thiserror::Error; @@ -402,8 +402,8 @@ pub fn generate_key_pair( secp256k1: &Secp256k1, ) -> Result { let mut rand_bytes = [0; 32]; - rand::thread_rng().fill_bytes(&mut rand_bytes); - Ok(UntweakedKeypair::from_seckey_slice(secp256k1, &rand_bytes)?) + OsRng.fill_bytes(&mut rand_bytes); + Ok(UntweakedKeypair::from_seckey_slice(SECP256K1, &rand_bytes)?) } /// Builds reveal script such that it contains opcodes for verifying the internal key as well as the @@ -465,7 +465,7 @@ fn sign_reveal_transaction( )?; let mut randbytes = [0; 32]; - rand::thread_rng().fill_bytes(&mut randbytes); + OsRng.fill_bytes(&mut randbytes); let signature = secp256k1.sign_schnorr_with_aux_rand( &secp256k1::Message::from_digest_slice(signature_hash.as_byte_array())?, diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs index 9c08729dc..cd955adda 100644 --- a/crates/crypto/src/lib.rs +++ b/crates/crypto/src/lib.rs @@ -36,23 +36,21 @@ pub fn verify_schnorr_sig(sig: &Buf64, msg: &Buf32, pk: &Buf32) -> bool { #[cfg(test)] mod tests { - use rand::Rng; - use secp256k1::{Secp256k1, SecretKey}; + use rand::{rngs::OsRng, Rng}; + use secp256k1::{SecretKey, SECP256K1}; use strata_primitives::buf::Buf32; use super::{sign_schnorr_sig, verify_schnorr_sig}; #[test] fn test_schnorr_signature_pass() { - let secp = Secp256k1::new(); - let mut rng = rand::thread_rng(); - let msg: [u8; 32] = [(); 32].map(|_| rng.gen()); + let msg: [u8; 32] = [(); 32].map(|_| OsRng.gen()); let mut mod_msg = msg; mod_msg.swap(1, 2); - let sk = SecretKey::new(&mut rng); - let (pk, _) = sk.x_only_public_key(&secp); + let sk = SecretKey::new(&mut OsRng); + let (pk, _) = sk.x_only_public_key(SECP256K1); let msg = Buf32::from(msg); let sk = Buf32::from(*sk.as_ref()); diff --git a/crates/evmexec/src/el_payload.rs b/crates/evmexec/src/el_payload.rs index 2d025f977..c374b7e30 100644 --- a/crates/evmexec/src/el_payload.rs +++ b/crates/evmexec/src/el_payload.rs @@ -118,14 +118,14 @@ impl From for ExecutionPayloadV1 { #[cfg(test)] mod tests { use arbitrary::{Arbitrary, Unstructured}; - use rand::RngCore; + use rand::{rngs::OsRng, RngCore}; use super::*; #[test] fn into() { let mut rand_data = vec![0u8; 1024]; - rand::thread_rng().fill_bytes(&mut rand_data); + OsRng.fill_bytes(&mut rand_data); let mut unstructured = Unstructured::new(&rand_data); let el_payload = ElPayload::arbitrary(&mut unstructured).unwrap(); diff --git a/crates/evmexec/src/engine.rs b/crates/evmexec/src/engine.rs index e19e390fb..4079a89c0 100644 --- a/crates/evmexec/src/engine.rs +++ b/crates/evmexec/src/engine.rs @@ -403,7 +403,7 @@ fn to_bridge_withdrawal_intent( #[cfg(test)] mod tests { - use rand::Rng; + use rand::{rngs::OsRng, Rng}; use reth_primitives::{revm_primitives::FixedBytes, Bloom, Bytes, U256}; use reth_rpc_types::{engine::ForkchoiceUpdated, ExecutionPayloadV1}; use strata_eectl::{errors::EngineResult, messages::PayloadEnv}; @@ -419,8 +419,6 @@ mod tests { } fn random_execution_payload_v1() -> ExecutionPayloadV1 { - let mut rng = rand::thread_rng(); - ExecutionPayloadV1 { parent_hash: B256::random(), fee_recipient: Address::random(), @@ -428,10 +426,10 @@ mod tests { receipts_root: B256::random(), logs_bloom: Bloom::random(), prev_randao: B256::random(), - block_number: rng.gen(), + block_number: OsRng.gen(), gas_limit: 200_000u64, gas_used: 10_000u64, - timestamp: rng.gen(), + timestamp: OsRng.gen(), extra_data: Bytes::new(), base_fee_per_gas: U256::from(50), block_hash: B256::random(), diff --git a/crates/primitives/src/bridge.rs b/crates/primitives/src/bridge.rs index 9b7f4caac..0333ac747 100644 --- a/crates/primitives/src/bridge.rs +++ b/crates/primitives/src/bridge.rs @@ -12,6 +12,7 @@ use bitcoin::{ }; use borsh::{BorshDeserialize, BorshSerialize}; use musig2::{errors::KeyAggError, KeyAggContext, NonceSeed, PartialSignature, PubNonce, SecNonce}; +use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; use crate::{ @@ -162,7 +163,7 @@ impl BorshDeserialize for Musig2PartialSig { impl<'a> Arbitrary<'a> for Musig2PartialSig { fn arbitrary(_u: &mut Unstructured<'a>) -> arbitrary::Result { - let secret_key = SecretKey::new(&mut rand::thread_rng()); + let secret_key = SecretKey::new(&mut OsRng); // Create a PartialSignature from the secret key bytes let partial_sig = PartialSignature::from_slice(secret_key.as_ref()) diff --git a/crates/primitives/src/hash.rs b/crates/primitives/src/hash.rs index 3fd1944bf..f97d17fe7 100644 --- a/crates/primitives/src/hash.rs +++ b/crates/primitives/src/hash.rs @@ -40,16 +40,15 @@ pub fn sha256d(buf: &[u8]) -> Buf32 { #[cfg(test)] mod tests { use bitcoin::hashes::{sha256d, Hash}; - use rand::Rng; + use rand::{rngs::OsRng, RngCore}; use super::sha256d; use crate::buf::Buf32; #[test] fn test_sha256d_equivalence() { - let mut rng = rand::thread_rng(); let mut array = [0u8; 32]; - rng.fill(&mut array); + OsRng.fill_bytes(&mut array); let expected = Buf32::from(sha256d::Hash::hash(&array).to_byte_array()); let output = sha256d(&array); diff --git a/crates/primitives/src/l1.rs b/crates/primitives/src/l1.rs index 9df2166a1..1b1a20119 100644 --- a/crates/primitives/src/l1.rs +++ b/crates/primitives/src/l1.rs @@ -19,6 +19,7 @@ use bitcoin::{ Transaction, TxIn, TxOut, Txid, Witness, }; use borsh::{BorshDeserialize, BorshSerialize}; +use rand::rngs::OsRng; use reth_primitives::revm_primitives::FixedBytes; use serde::{de, Deserialize, Deserializer, Serialize}; @@ -839,7 +840,7 @@ impl<'a> Arbitrary<'a> for TaprootSpendPath { }; // Generate a random secret key and derive the internal key - let secret_key = SecretKey::new(&mut rand::thread_rng()); + let secret_key = SecretKey::new(&mut OsRng); let keypair = Keypair::from_secret_key(SECP256K1, &secret_key); let (internal_key, _) = XOnlyPublicKey::from_keypair(&keypair); @@ -890,7 +891,7 @@ mod tests { taproot::{ControlBlock, LeafVersion, TaprootBuilder, TaprootMerkleBranch}, Address, Amount, Network, ScriptBuf, TapNodeHash, TxOut, XOnlyPublicKey, }; - use rand::Rng; + use rand::{rngs::OsRng, Rng}; use secp256k1::{Parity, SECP256K1}; use strata_test_utils::ArbitraryGenerator; @@ -915,7 +916,7 @@ mod tests { let num_possible_networks = possible_networks.len(); - let (secret_key, _) = SECP256K1.generate_keypair(&mut rand::thread_rng()); + let (secret_key, _) = SECP256K1.generate_keypair(&mut OsRng); let keypair = Keypair::from_secret_key(SECP256K1, &secret_key); let (internal_key, _) = XOnlyPublicKey::from_keypair(&keypair); @@ -930,7 +931,7 @@ mod tests { let invalid_network = match network { Network::Bitcoin => { // get one of the testnets - let index = rand::thread_rng().gen_range(1..num_possible_networks); + let index = OsRng.gen_range(1..num_possible_networks); possible_networks[index] } @@ -1189,7 +1190,7 @@ mod tests { let output_key_parity = Parity::Even; // Generate a random internal key - let secret_key = SecretKey::new(&mut rand::thread_rng()); + let secret_key = SecretKey::new(&mut OsRng); let keypair = Keypair::from_secret_key(SECP256K1, &secret_key); let (internal_key, _) = XOnlyPublicKey::from_keypair(&keypair); diff --git a/crates/primitives/src/relay/util.rs b/crates/primitives/src/relay/util.rs index f03dfabfb..f8d53e8d2 100644 --- a/crates/primitives/src/relay/util.rs +++ b/crates/primitives/src/relay/util.rs @@ -137,6 +137,7 @@ pub fn verify_bridge_msg_sig( #[cfg(test)] mod tests { + use rand::rngs::OsRng; use strata_test_utils::ArbitraryGenerator; use super::*; @@ -198,7 +199,7 @@ mod tests { let txid: BitcoinTxid = generator.generate(); let scope = Scope::V0PubNonce(txid); let payload: Musig2PubNonce = generator.generate(); - let keypair = Keypair::new(SECP256K1, &mut rand::thread_rng()); + let keypair = Keypair::new(SECP256K1, &mut OsRng); let msg_signer = MessageSigner::new(0, keypair.secret_key().into()); let signed_message = msg_signer diff --git a/crates/proof-impl/btc-blockspace/src/block.rs b/crates/proof-impl/btc-blockspace/src/block.rs index a703fb462..42b0618a7 100644 --- a/crates/proof-impl/btc-blockspace/src/block.rs +++ b/crates/proof-impl/btc-blockspace/src/block.rs @@ -199,7 +199,7 @@ pub fn check_pow(block: &Header) -> bool { #[cfg(test)] mod tests { use bitcoin::{hashes::Hash, TxMerkleNode, WitnessMerkleNode}; - use rand::Rng; + use rand::{rngs::OsRng, Rng}; use strata_state::{l1::generate_l1_tx, tx::ProtocolOperation}; use strata_test_utils::{bitcoin::get_btc_mainnet_block, ArbitraryGenerator}; @@ -243,7 +243,7 @@ mod tests { // } let parsed_tx: ProtocolOperation = ArbitraryGenerator::new().generate(); - let r = rand::thread_rng().gen_range(1..block.txdata.len()) as u32; + let r = OsRng.gen_range(1..block.txdata.len()) as u32; let l1_tx = generate_l1_tx(&block, r, parsed_tx); assert!(check_witness_commitment_for_l1_tx(&block, &l1_tx)); diff --git a/crates/proof-impl/btc-blockspace/src/merkle.rs b/crates/proof-impl/btc-blockspace/src/merkle.rs index 6b78b7e1b..8773e6ee7 100644 --- a/crates/proof-impl/btc-blockspace/src/merkle.rs +++ b/crates/proof-impl/btc-blockspace/src/merkle.rs @@ -66,21 +66,19 @@ fn merkle_root_r(hashes: &mut [Buf32]) -> Buf32 { #[cfg(test)] mod tests { use bitcoin::{hashes::Hash, TxMerkleNode}; - use rand::Rng; + use rand::{rngs::OsRng, Rng}; use strata_primitives::buf::Buf32; use super::calculate_root; #[test] fn test_merkle_root() { - let mut rng = rand::thread_rng(); - - let n = rng.gen_range(1..1_000); + let n = OsRng.gen_range(1..1_000); let mut btc_hashes = Vec::with_capacity(n); let mut hashes = Vec::with_capacity(n); for _ in 0..n { - let random_bytes: [u8; 32] = rng.gen(); + let random_bytes: [u8; 32] = OsRng.gen(); btc_hashes.push(TxMerkleNode::from_byte_array(random_bytes)); let hash = Buf32::from(random_bytes); hashes.push(hash); diff --git a/crates/rocksdb-store/src/l1/db.rs b/crates/rocksdb-store/src/l1/db.rs index 6d130d203..f359a31e5 100644 --- a/crates/rocksdb-store/src/l1/db.rs +++ b/crates/rocksdb-store/src/l1/db.rs @@ -229,6 +229,7 @@ impl L1DataProvider for L1Db { #[cfg(test)] mod tests { use bitcoin::key::rand::{self, Rng}; + use rand::rngs::OsRng; use strata_primitives::l1::L1TxProof; use strata_state::tx::ProtocolOperation; use strata_test_utils::ArbitraryGenerator; @@ -536,7 +537,7 @@ mod tests { l1_txs.push(l1_tx); } - let offset = rand::thread_rng().gen_range(0..total_num_blocks); + let offset = OsRng.gen_range(0..total_num_blocks); let expected_num_blocks = total_num_blocks - offset; let (actual, latest_idx) = db.get_txs_from(offset as u64).unwrap(); diff --git a/crates/state/src/l1/header_verification.rs b/crates/state/src/l1/header_verification.rs index 9999b718e..3f6b35963 100644 --- a/crates/state/src/l1/header_verification.rs +++ b/crates/state/src/l1/header_verification.rs @@ -269,7 +269,7 @@ pub fn get_difficulty_adjustment_height(idx: u32, start: u32, params: &BtcParams #[cfg(test)] mod tests { use bitcoin::params::MAINNET; - use rand::Rng; + use rand::{rngs::OsRng, Rng}; use strata_test_utils::bitcoin::get_btc_chain; use super::*; @@ -280,7 +280,7 @@ mod tests { // TODO: figure out why passing btc_params to `check_and_update_full` doesn't work let btc_params: BtcParams = MAINNET.clone().into(); let h1 = get_difficulty_adjustment_height(1, chain.start, &btc_params); - let r1 = rand::thread_rng().gen_range(h1..chain.end); + let r1 = OsRng.gen_range(h1..chain.end); let mut verification_state = chain.get_verification_state(r1, &MAINNET.clone().into()); for header_idx in r1..chain.end { @@ -316,7 +316,7 @@ mod tests { #[test] fn test_get_difficulty_adjustment_height() { let start = 0; - let idx = rand::thread_rng().gen_range(1..1000); + let idx = OsRng.gen_range(1..1000); let h = get_difficulty_adjustment_height(idx, start, &MAINNET.clone().into()); assert_eq!(h, MAINNET.difficulty_adjustment_interval() as u32 * idx); } diff --git a/crates/test-utils/src/bridge.rs b/crates/test-utils/src/bridge.rs index adbfbf957..43e0078d5 100644 --- a/crates/test-utils/src/bridge.rs +++ b/crates/test-utils/src/bridge.rs @@ -31,7 +31,7 @@ pub fn generate_keypairs(count: usize) -> (Vec, Vec) { let mut pubkeys_set: HashSet = HashSet::new(); while pubkeys_set.len() != count { - let sk = SecretKey::new(&mut rand::thread_rng()); + let sk = SecretKey::new(&mut OsRng); let keypair = Keypair::from_secret_key(SECP256K1, &sk); let pubkey = PublicKey::from_keypair(&keypair); @@ -198,7 +198,7 @@ pub fn generate_sec_nonce( /// This is used to generate random order for indices in a list (for example, list of pubkeys, /// nonces, etc.) pub fn permute(list: &mut [T]) { - let num_permutations = rand::thread_rng().gen_range(0..list.len()); + let num_permutations = OsRng.gen_range(0..list.len()); generate_permutation(list, num_permutations); } diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index 800b1a7da..9823504a7 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -1,7 +1,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use arbitrary::{Arbitrary, Unstructured}; -use rand::RngCore; +use rand::{rngs::OsRng, RngCore}; pub mod bitcoin; pub mod bridge; @@ -27,9 +27,8 @@ impl ArbitraryGenerator { } pub fn new_with_size(n: usize) -> Self { - let mut rng = rand::thread_rng(); let mut buf = vec![0; n]; - rng.fill_bytes(&mut buf); // 128 wasn't enough + OsRng.fill_bytes(&mut buf); // 128 wasn't enough let off = AtomicUsize::new(0); ArbitraryGenerator { buf, off } } diff --git a/crates/tx-parser/src/filter.rs b/crates/tx-parser/src/filter.rs index 37fe192ff..0822fd49f 100644 --- a/crates/tx-parser/src/filter.rs +++ b/crates/tx-parser/src/filter.rs @@ -105,7 +105,7 @@ mod test { Address, Amount, Block, BlockHash, CompactTarget, Network, ScriptBuf, TapNodeHash, Transaction, TxMerkleNode, TxOut, }; - use rand::RngCore; + use rand::{rngs::OsRng, RngCore}; use strata_btcio::test_utils::{ build_reveal_transaction_test, generate_inscription_script_test, }; @@ -181,7 +181,7 @@ mod test { let secp256k1 = Secp256k1::new(); let mut rand_bytes = [0; 32]; rand::thread_rng().fill_bytes(&mut rand_bytes); - let key_pair = UntweakedKeypair::from_seckey_slice(&secp256k1, &rand_bytes).unwrap(); + let key_pair = UntweakedKeypair::from_seckey_slice(SECP256K1, &rand_bytes).unwrap(); let public_key = XOnlyPublicKey::from_keypair(&key_pair).0; let nodehash: [TapNodeHash; 0] = []; let cb = ControlBlock { diff --git a/crates/util/shrex/src/lib.rs b/crates/util/shrex/src/lib.rs index a48fa51cc..77cebd618 100644 --- a/crates/util/shrex/src/lib.rs +++ b/crates/util/shrex/src/lib.rs @@ -277,7 +277,7 @@ impl From for char { #[cfg(test)] mod tests { - use rand::{thread_rng, Rng}; + use rand::{rngs::OsRng, Rng}; use super::*; @@ -306,7 +306,7 @@ mod tests { #[test] fn e2e() { - let buf: [u8; 32] = thread_rng().gen(); + let buf: [u8; 32] = OsRng.gen(); let string = encode(&buf); let mut debuf = [0u8; 32]; assert!(decode(&string, &mut debuf).is_ok()); diff --git a/tests/cooperative-bridge-flow.rs b/tests/cooperative-bridge-flow.rs index 92fdbe980..624b8555a 100644 --- a/tests/cooperative-bridge-flow.rs +++ b/tests/cooperative-bridge-flow.rs @@ -11,6 +11,7 @@ use bitcoind::{ BitcoinD, }; use common::bridge::{perform_rollup_actions, perform_user_actions, setup, BridgeDuty, User}; +use rand::rngs::OsRng; use strata_bridge_tx_builder::prelude::{ create_taproot_addr, get_aggregated_pubkey, CooperativeWithdrawalInfo, SpendPath, }; @@ -82,7 +83,7 @@ async fn full_flow() { let unspent_utxos_prewithdrawal = user.agent().get_unspent_utxos().await; event!(Level::DEBUG, event = "got unspent utxos from requester before withdrawal", num_unspent_utxos = %unspent_utxos_prewithdrawal.len()); - let assigned_operator_idx = rand::thread_rng().gen_range(0..num_operators) as OperatorIdx; + let assigned_operator_idx = OsRng.gen_range(0..num_operators) as OperatorIdx; event!(Level::INFO, event = "assigning withdrawal", operator_idx = %assigned_operator_idx); let withdrawal_info = diff --git a/tests/cooperative-bridge-out-flow.rs b/tests/cooperative-bridge-out-flow.rs index 53c651363..a982aeb77 100644 --- a/tests/cooperative-bridge-out-flow.rs +++ b/tests/cooperative-bridge-out-flow.rs @@ -11,6 +11,7 @@ use bitcoin::{ }; use bitcoind::BitcoinD; use common::bridge::{setup, BridgeDuty, User, MIN_MINER_REWARD_CONFS}; +use rand::rngs::OsRng; use strata_bridge_tx_builder::prelude::{ create_taproot_addr, create_tx, create_tx_ins, create_tx_outs, get_aggregated_pubkey, CooperativeWithdrawalInfo, SpendPath, BRIDGE_DENOMINATION, @@ -47,7 +48,7 @@ async fn withdrawal_flow() { event!(Level::INFO, event = "created user to initiate withdrawal", user_x_only_pk = ?user_x_only_pk); - let assigned_operator_idx = rand::thread_rng().gen_range(0..num_operators) as OperatorIdx; + let assigned_operator_idx = OsRng.gen_range(0..num_operators) as OperatorIdx; event!(Level::INFO, event = "assigning withdrawal", operator_idx = %assigned_operator_idx); let withdrawal_info = From 7c3e73beb0dbbccc8425e46cccf8c9cfc052108f Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Fri, 11 Oct 2024 17:11:24 -0300 Subject: [PATCH 04/18] fix(strata-cli): better handling of low-entropy passwords warnings (#397) * fix(strata-cli): better warnings of low-entropy pass * refactor(strata-cli): password warnings into a function --- bin/strata-cli/src/cmd/change_pwd.rs | 14 +++----------- bin/strata-cli/src/seed.rs | 16 ++++----------- bin/strata-cli/src/seed/password.rs | 29 ++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/bin/strata-cli/src/cmd/change_pwd.rs b/bin/strata-cli/src/cmd/change_pwd.rs index 5de23db38..634351be3 100644 --- a/bin/strata-cli/src/cmd/change_pwd.rs +++ b/bin/strata-cli/src/cmd/change_pwd.rs @@ -1,7 +1,6 @@ use argh::FromArgs; use console::Term; use rand::rngs::OsRng; -use zxcvbn::Score; use crate::seed::{password::Password, EncryptedSeedPersister, Seed}; @@ -13,16 +12,9 @@ pub struct ChangePwdArgs {} pub async fn change_pwd(_args: ChangePwdArgs, seed: Seed, persister: impl EncryptedSeedPersister) { let term = Term::stdout(); let mut new_pw = Password::read(true).unwrap(); - let entropy = new_pw.entropy(); - let _ = term.write_line(format!("Password strength (Overall strength score from 0-4, where anything below 3 is too weak): {}", entropy.score()).as_str()); - if entropy.score() <= Score::Two { - let _ = term.write_line( - entropy - .feedback() - .expect("No feedback") - .to_string() - .as_str(), - ); + let password_validation: Result<(), String> = new_pw.validate(); + if let Err(feedback) = password_validation { + let _ = term.write_line(&format!("Password is weak. {}", feedback)); } let encrypted_seed = seed.encrypt(&mut new_pw, &mut OsRng).unwrap(); persister.save(&encrypted_seed).unwrap(); diff --git a/bin/strata-cli/src/seed.rs b/bin/strata-cli/src/seed.rs index 1b0154be7..a896c182b 100644 --- a/bin/strata-cli/src/seed.rs +++ b/bin/strata-cli/src/seed.rs @@ -15,7 +15,6 @@ use password::{HashVersion, IncorrectPassword, Password}; use rand::{rngs::OsRng, CryptoRng, RngCore}; use sha2::{Digest, Sha256}; use terrors::OneOf; -use zxcvbn::Score; use crate::constants::{AES_NONCE_LEN, AES_TAG_LEN, PW_SALT_LEN, SEED_LEN}; @@ -197,17 +196,10 @@ pub fn load_or_create( }; let mut password = Password::read(true).map_err(OneOf::new)?; - let entropy = password.entropy(); - let _ = term.write_line(format!("Password strength (Overall strength score from 0-4, where anything below 3 is too weak): {}", entropy.score()).as_str()); - if entropy.score() <= Score::Two { - let _ = term.write_line( - entropy - .feedback() - .expect("No feedback") - .to_string() - .as_str(), - ); - } + let password_validation: Result<(), String> = password.validate(); + if let Err(feedback) = password_validation { + let _ = term.write_line(&format!("Password is weak. {}", feedback)); + }; let encrypted_seed = match seed.encrypt(&mut password, &mut OsRng) { Ok(es) => es, Err(e) => { diff --git a/bin/strata-cli/src/seed/password.rs b/bin/strata-cli/src/seed/password.rs index 9880e9ddc..60bfda349 100644 --- a/bin/strata-cli/src/seed/password.rs +++ b/bin/strata-cli/src/seed/password.rs @@ -77,6 +77,35 @@ impl Password { } } } + + /// Validates the password strength and returns feedback if it's weak. + pub fn validate(&self) -> Result<(), String> { + let entropy = self.entropy(); + if entropy.score() <= zxcvbn::Score::Two { + let feedback = entropy.feedback(); + let feedback_message = if let Some(feedback) = feedback { + feedback + .warning() + .map(|w| w.to_string()) + .unwrap_or_default() + } else { + "".to_string() // empty string is fine + }; + let mut message = String::new(); + + message.push_str(feedback_message.as_str()); + + if let Some(feedback) = feedback { + message.push_str("Suggestions:\n"); + for suggestion in feedback.suggestions() { + message.push_str(&format!("- {suggestion}\n")); + } + } + + return Err(message); + } + Ok(()) + } } #[derive(Debug)] From cdad88278bcf11ca6a8feca94a95669830ff49b4 Mon Sep 17 00:00:00 2001 From: Zk2u Date: Tue, 15 Oct 2024 17:53:28 +0100 Subject: [PATCH 05/18] feat(cli): manual signet fee rates, full scan cmd, disable L2 faucet, refresh -> recover, config file + more (#382) * feat: manual signet fee rates + full scan command * fix: use `u64` for feerates + rename refresh to recover * fix: disable faucet L2 functionality * feat: add network + bridge pubkey settings & use config file from env var * chore: remove all strata logic from faucet * feat: add config command to show config file path * fix: featuregate strata faucet code * chore: rename `l2_http_endpoint` to `strata_endpoint` * fix: patch featuregated to use `strata_endpoint` * fix: remove password requirement from reset cmd * chore: update bridge pubkey * fix: drain command * fix: "bridge-in" and "bridge-out" to "deposit" and "withdraw" * chore: update withdraw cmd * chore: updating comments.+ logs * chore: comments * chore: update bridge strata address * chore: hide endpoints * fix: printing config file location with no config * fix: various fixes, recovery descriptor autocleanup * fix: remove `seed_encryption_key` caching for `Password` * feat: zeroize password * chore: derive `Eq` as well as `PartialEq` on `NetworkType` Co-authored-by: Jose Storopoli * fix(cli): feerate logic revamp * chore(cli): fix clippy --------- Co-authored-by: Jose Storopoli --- Cargo.lock | 1 + bin/strata-cli/Cargo.toml | 4 + bin/strata-cli/src/cmd/balance.rs | 16 ++-- bin/strata-cli/src/cmd/config.rs | 12 +++ .../src/cmd/{bridge_in.rs => deposit.rs} | 49 ++++++---- bin/strata-cli/src/cmd/drain.rs | 24 +++-- bin/strata-cli/src/cmd/faucet.rs | 68 +++++++++++--- bin/strata-cli/src/cmd/mod.rs | 24 +++-- bin/strata-cli/src/cmd/receive.rs | 5 +- .../src/cmd/{refresh.rs => recover.rs} | 41 +++++---- bin/strata-cli/src/cmd/reset.rs | 3 +- bin/strata-cli/src/cmd/scan.rs | 17 ++++ bin/strata-cli/src/cmd/send.rs | 43 ++++----- .../src/cmd/{bridge_out.rs => withdraw.rs} | 18 ++-- bin/strata-cli/src/constants.rs | 20 +++-- bin/strata-cli/src/main.rs | 28 ++++-- bin/strata-cli/src/net_type.rs | 1 + bin/strata-cli/src/recovery.rs | 11 ++- bin/strata-cli/src/seed.rs | 4 +- bin/strata-cli/src/seed/password.rs | 39 +++----- bin/strata-cli/src/settings.rs | 71 +++++++++------ bin/strata-cli/src/signet.rs | 90 +++++++++++++++---- bin/strata-client/src/extractor.rs | 2 +- 23 files changed, 401 insertions(+), 190 deletions(-) create mode 100644 bin/strata-cli/src/cmd/config.rs rename bin/strata-cli/src/cmd/{bridge_in.rs => deposit.rs} (81%) rename bin/strata-cli/src/cmd/{refresh.rs => recover.rs} (66%) create mode 100644 bin/strata-cli/src/cmd/scan.rs rename bin/strata-cli/src/cmd/{bridge_out.rs => withdraw.rs} (77%) diff --git a/Cargo.lock b/Cargo.lock index 2952ec2e8..49922e327 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12808,6 +12808,7 @@ dependencies = [ "strata-bridge-tx-builder", "terrors", "tokio", + "zeroize", "zxcvbn", ] diff --git a/bin/strata-cli/Cargo.toml b/bin/strata-cli/Cargo.toml index 52255b391..8d1f308d4 100644 --- a/bin/strata-cli/Cargo.toml +++ b/bin/strata-cli/Cargo.toml @@ -3,6 +3,9 @@ edition = "2021" name = "strata-cli" version = "0.1.0" +[features] +strata_faucet = [] + [[bin]] name = "strata" path = "src/main.rs" @@ -42,6 +45,7 @@ sled = "0.34.7" strata-bridge-tx-builder.workspace = true terrors.workspace = true tokio.workspace = true +zeroize = { version = "1.8.1", features = ["derive"] } zxcvbn = "3.1.0" # sha2 fails to compile on windows with the "asm" feature diff --git a/bin/strata-cli/src/cmd/balance.rs b/bin/strata-cli/src/cmd/balance.rs index b4609861f..4c63adf52 100644 --- a/bin/strata-cli/src/cmd/balance.rs +++ b/bin/strata-cli/src/cmd/balance.rs @@ -3,10 +3,11 @@ use alloy::{ providers::{Provider, WalletProvider}, }; use argh::FromArgs; +use bdk_wallet::bitcoin::Amount; use console::Term; use crate::{ - constants::NETWORK, + constants::SATS_TO_WEI, net_type::{net_type_or_exit, NetworkType}, seed::Seed, settings::Settings, @@ -28,7 +29,7 @@ pub async fn balance(args: BalanceArgs, seed: Seed, settings: Settings, esplora: let network_type = net_type_or_exit(&args.network_type, &term); if let NetworkType::Signet = network_type { - let mut l1w = SignetWallet::new(&seed, NETWORK).unwrap(); + let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); l1w.sync(&esplora).await.unwrap(); let balance = l1w.balance(); let _ = term.write_line(&format!("Total: {}", balance.total())); @@ -42,11 +43,14 @@ pub async fn balance(args: BalanceArgs, seed: Seed, settings: Settings, esplora: } if let NetworkType::Strata = network_type { - let l2w = StrataWallet::new(&seed, &settings.l2_http_endpoint).unwrap(); + let l2w = StrataWallet::new(&seed, &settings.strata_endpoint).unwrap(); let _ = term.write_line("Getting balance..."); let balance = l2w.get_balance(l2w.default_signer_address()).await.unwrap(); - // 1 BTC = 1 ETH - let balance_in_btc = balance / U256::from(10u64.pow(18)); - let _ = term.write_line(&format!("\nTotal: {} BTC", balance_in_btc)); + let balance = Amount::from_sat( + (balance / U256::from(SATS_TO_WEI)) + .try_into() + .expect("valid amount"), + ); + let _ = term.write_line(&format!("\nTotal: {}", balance)); } } diff --git a/bin/strata-cli/src/cmd/config.rs b/bin/strata-cli/src/cmd/config.rs new file mode 100644 index 000000000..434f1012d --- /dev/null +++ b/bin/strata-cli/src/cmd/config.rs @@ -0,0 +1,12 @@ +use argh::FromArgs; + +use crate::settings::CONFIG_FILE; + +/// Prints the location of the CLI's TOML config file +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "config")] +pub struct ConfigArgs {} + +pub async fn config(_args: ConfigArgs) { + println!("{}", CONFIG_FILE.to_string_lossy()) +} diff --git a/bin/strata-cli/src/cmd/bridge_in.rs b/bin/strata-cli/src/cmd/deposit.rs similarity index 81% rename from bin/strata-cli/src/cmd/bridge_in.rs rename to bin/strata-cli/src/cmd/deposit.rs index f95afd1f8..5eed60e10 100644 --- a/bin/strata-cli/src/cmd/bridge_in.rs +++ b/bin/strata-cli/src/cmd/deposit.rs @@ -16,32 +16,43 @@ use strata_bridge_tx_builder::constants::MAGIC_BYTES; use crate::{ constants::{ - BRIDGE_IN_AMOUNT, L2_BLOCK_TIME, NETWORK, RECOVER_AT_DELAY, RECOVER_DELAY, UNSPENDABLE, + BRIDGE_IN_AMOUNT, RECOVER_AT_DELAY, RECOVER_DELAY, SIGNET_BLOCK_TIME, UNSPENDABLE, }, recovery::DescriptorRecovery, seed::Seed, settings::Settings, - signet::{get_fee_rate, log_fee_rate, EsploraClient, SignetWallet}, + signet::{get_fee_rate, log_fee_rate, print_explorer_url, EsploraClient, SignetWallet}, strata::StrataWallet, taproot::{ExtractP2trPubkey, NotTaprootAddress}, }; -/// Bridge 10 BTC from signet to Strata. If an address is not provided, the wallet's internal +/// Deposit 10 BTC from signet to Strata. If an address is not provided, the wallet's internal /// Strata address will be used. #[derive(FromArgs, PartialEq, Debug)] -#[argh(subcommand, name = "bridge-in")] -pub struct BridgeInArgs { +#[argh(subcommand, name = "deposit")] +pub struct DepositArgs { #[argh(positional)] strata_address: Option, + + /// override signet fee rate in sat/vbyte. must be >=1 + #[argh(option)] + fee_rate: Option, } -pub async fn bridge_in(args: BridgeInArgs, seed: Seed, settings: Settings, esplora: EsploraClient) { +pub async fn deposit( + DepositArgs { + strata_address, + fee_rate, + }: DepositArgs, + seed: Seed, + settings: Settings, + esplora: EsploraClient, +) { let term = Term::stdout(); - let requested_strata_address = args - .strata_address - .map(|a| StrataAddress::from_str(&a).expect("bad strata address")); - let mut l1w = SignetWallet::new(&seed, NETWORK).unwrap(); - let l2w = StrataWallet::new(&seed, &settings.l2_http_endpoint).unwrap(); + let requested_strata_address = + strata_address.map(|a| StrataAddress::from_str(&a).expect("bad strata address")); + let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); + let l2w = StrataWallet::new(&seed, &settings.strata_endpoint).unwrap(); l1w.sync(&esplora).await.unwrap(); let recovery_address = l1w.reveal_next_address(KeychainKind::External).address; @@ -65,11 +76,11 @@ pub async fn bridge_in(args: BridgeInArgs, seed: Seed, settings: Settings, esplo let desc = bridge_in_desc .clone() - .into_wallet_descriptor(l1w.secp_ctx(), NETWORK) + .into_wallet_descriptor(l1w.secp_ctx(), settings.network) .expect("valid descriptor"); let mut temp_wallet = Wallet::create_single(desc.clone()) - .network(NETWORK) + .network(settings.network) .create_wallet_no_persist() .expect("valid wallet"); @@ -90,11 +101,9 @@ pub async fn bridge_in(args: BridgeInArgs, seed: Seed, settings: Settings, esplo style(bridge_in_address.to_string()).yellow() )); - let fee_rate = get_fee_rate(1, &esplora) + let fee_rate = get_fee_rate(fee_rate, &esplora, 1) .await - .expect("should get fee rate") - .expect("should have valid fee rate"); - + .expect("valid fee rate"); log_fee_rate(&term, &fee_rate); const MBL: usize = MAGIC_BYTES.len(); @@ -135,10 +144,12 @@ pub async fn bridge_in(args: BridgeInArgs, seed: Seed, settings: Settings, esplo let pb = ProgressBar::new_spinner().with_message("Broadcasting transaction"); pb.enable_steady_tick(Duration::from_millis(100)); esplora.broadcast(&tx).await.expect("successful broadcast"); - pb.finish_with_message(format!("Transaction {} broadcasted", tx.compute_txid())); + let txid = tx.compute_txid(); + pb.finish_with_message(format!("Transaction {} broadcasted", txid)); + let _ = print_explorer_url(&txid, &term, &settings); let _ = term.write_line(&format!( "Expect transaction confirmation in ~{:?}. Funds will take longer than this to be available on Strata.", - L2_BLOCK_TIME + SIGNET_BLOCK_TIME )); } diff --git a/bin/strata-cli/src/cmd/drain.rs b/bin/strata-cli/src/cmd/drain.rs index 569f0d640..f92955cf7 100644 --- a/bin/strata-cli/src/cmd/drain.rs +++ b/bin/strata-cli/src/cmd/drain.rs @@ -9,10 +9,9 @@ use bdk_wallet::bitcoin::{Address, Amount}; use console::{style, Term}; use crate::{ - constants::NETWORK, seed::Seed, settings::Settings, - signet::{get_fee_rate, log_fee_rate, EsploraClient, SignetWallet}, + signet::{get_fee_rate, log_fee_rate, print_explorer_url, EsploraClient, SignetWallet}, strata::StrataWallet, }; @@ -24,15 +23,21 @@ pub struct DrainArgs { /// a signet address for signet funds to be drained to #[argh(option, short = 's')] signet_address: Option, + /// a Strata address for Strata funds to be drained to #[argh(option, short = 'r')] strata_address: Option, + + /// override signet fee rate in sat/vbyte. must be >=1 + #[argh(option)] + fee_rate: Option, } pub async fn drain( DrainArgs { signet_address, strata_address, + fee_rate, }: DrainArgs, seed: Seed, settings: Settings, @@ -46,14 +51,14 @@ pub async fn drain( let signet_address = signet_address.map(|a| { Address::from_str(&a) .expect("valid signet address") - .require_network(NETWORK) + .require_network(settings.network) .expect("correct network") }); let strata_address = strata_address.map(|a| StrataAddress::from_str(&a).expect("valid Strata address")); if let Some(address) = signet_address { - let mut l1w = SignetWallet::new(&seed, NETWORK).unwrap(); + let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); l1w.sync(&esplora).await.unwrap(); let balance = l1w.balance(); if balance.untrusted_pending > Amount::ZERO { @@ -63,10 +68,14 @@ pub async fn drain( .to_string(), ); } - let fr = get_fee_rate(1, &esplora).await.unwrap().unwrap(); + let fr = get_fee_rate(fee_rate, &esplora, 1) + .await + .expect("valid fee rate"); log_fee_rate(&term, &fr); + let mut psbt = l1w .build_tx() + .drain_wallet() .drain_to(address.script_pubkey()) .fee_rate(fr) .clone() @@ -75,10 +84,11 @@ pub async fn drain( l1w.sign(&mut psbt, Default::default()).unwrap(); let tx = psbt.extract_tx().expect("fully signed tx"); esplora.broadcast(&tx).await.unwrap(); + let _ = print_explorer_url(&tx.compute_txid(), &term, &settings); } if let Some(address) = strata_address { - let l2w = StrataWallet::new(&seed, &settings.l2_http_endpoint).unwrap(); + let l2w = StrataWallet::new(&seed, &settings.strata_endpoint).unwrap(); let balance = l2w.get_balance(l2w.default_signer_address()).await.unwrap(); if balance == U256::ZERO { let _ = term.write_line("No Strata bitcoin to send"); @@ -91,7 +101,7 @@ pub async fn drain( let estimate_tx = l2w .transaction_request() .to(address) - .value(balance) // Use full balance for estimation + .value(U256::from(1)) .max_fee_per_gas(max_fee_per_gas) .max_priority_fee_per_gas(max_priority_fee_per_gas); diff --git a/bin/strata-cli/src/cmd/faucet.rs b/bin/strata-cli/src/cmd/faucet.rs index fb8bbdcb8..8b732c21b 100644 --- a/bin/strata-cli/src/cmd/faucet.rs +++ b/bin/strata-cli/src/cmd/faucet.rs @@ -1,8 +1,12 @@ use std::str::FromStr; +#[cfg(feature = "strata_faucet")] use alloy::{primitives::Address as StrataAddress, providers::WalletProvider}; use argh::FromArgs; -use bdk_wallet::{bitcoin::Address, KeychainKind}; +use bdk_wallet::{ + bitcoin::{Address, Txid}, + KeychainKind, +}; use console::Term; use indicatif::ProgressBar; use rand::{distributions::uniform::SampleRange, rngs::OsRng}; @@ -11,13 +15,15 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use shrex::{encode, Hex}; +#[cfg(feature = "strata_faucet")] use crate::{ - constants::NETWORK, net_type::{net_type_or_exit, NetworkType}, + strata::StrataWallet, +}; +use crate::{ seed::Seed, settings::Settings, - signet::SignetWallet, - strata::StrataWallet, + signet::{print_explorer_url, SignetWallet}, }; /// Request some bitcoin from the faucet @@ -25,9 +31,9 @@ use crate::{ #[argh(subcommand, name = "faucet")] pub struct FaucetArgs { /// either "signet" or "strata" + #[cfg(feature = "strata_faucet")] #[argh(positional)] network_type: String, - /// address that funds will be sent to. defaults to internal wallet #[argh(positional)] address: Option, @@ -44,10 +50,11 @@ pub struct PowChallenge { pub async fn faucet(args: FaucetArgs, seed: Seed, settings: Settings) { let term = Term::stdout(); - let network_type = net_type_or_exit(&args.network_type, &term); - let _ = term.write_line("Fetching challenge from faucet"); + #[cfg(feature = "strata_faucet")] + let network_type = net_type_or_exit(&args.network_type, &term); + let client = reqwest::Client::new(); let base = Url::from_str(&settings.faucet_endpoint).expect("valid url"); let challenge = client @@ -85,9 +92,10 @@ pub async fn faucet(args: FaucetArgs, seed: Seed, settings: Settings) { "✔ Solved challenge after {solution} attempts. Claiming now." )); + #[cfg(feature = "strata_faucet")] let url = match network_type { NetworkType::Signet => { - let mut l1w = SignetWallet::new(&seed, NETWORK).unwrap(); + let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); let address = match args.address { None => { let address_info = l1w.reveal_next_address(KeychainKind::External); @@ -97,7 +105,7 @@ pub async fn faucet(args: FaucetArgs, seed: Seed, settings: Settings) { Some(address) => { let address = Address::from_str(&address).expect("bad address"); address - .require_network(NETWORK) + .require_network(settings.network) .expect("wrong bitcoin network") } }; @@ -111,7 +119,7 @@ pub async fn faucet(args: FaucetArgs, seed: Seed, settings: Settings) { ) } NetworkType::Strata => { - let l2w = StrataWallet::new(&seed, &settings.l2_http_endpoint).unwrap(); + let l2w = StrataWallet::new(&seed, &settings.strata_endpoint).unwrap(); // they said EVMs were advanced 👁️👁️ let address = match args.address { Some(address) => StrataAddress::from_str(&address).expect("bad address"), @@ -126,11 +134,51 @@ pub async fn faucet(args: FaucetArgs, seed: Seed, settings: Settings) { } }; + #[cfg(not(feature = "strata_faucet"))] + let url = { + let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); + let address = match args.address { + None => { + let address_info = l1w.reveal_next_address(KeychainKind::External); + l1w.persist().unwrap(); + address_info.address + } + Some(address) => { + let address = Address::from_str(&address).expect("bad address"); + address + .require_network(settings.network) + .expect("wrong bitcoin network") + } + }; + + let _ = term.write_line(&format!("Claiming to signet address {}", address)); + + format!( + "{base}claim_l1/{}/{}", + encode(&solution.to_le_bytes()), + address + ) + }; + let res = client.get(url).send().await.unwrap(); let status = res.status(); let body = res.text().await.expect("invalid response"); if status == StatusCode::OK { + #[cfg(feature = "strata_faucet")] + if network_type == NetworkType::Signet { + let _ = print_explorer_url( + &Txid::from_str(&body).expect("valid txid"), + &term, + &settings, + ); + } + #[cfg(not(feature = "strata_faucet"))] + let _ = print_explorer_url( + &Txid::from_str(&body).expect("valid txid"), + &term, + &settings, + ); let _ = term.write_line(&format!("Successful. Claimed in transaction {body}")); } else { let _ = term.write_line(&format!("Failed: faucet responded with {status}: {body}")); diff --git a/bin/strata-cli/src/cmd/mod.rs b/bin/strata-cli/src/cmd/mod.rs index d66210df2..3742e9f4d 100644 --- a/bin/strata-cli/src/cmd/mod.rs +++ b/bin/strata-cli/src/cmd/mod.rs @@ -1,27 +1,31 @@ use argh::FromArgs; use backup::BackupArgs; use balance::BalanceArgs; -use bridge_in::BridgeInArgs; -use bridge_out::BridgeOutArgs; use change_pwd::ChangePwdArgs; +use config::ConfigArgs; +use deposit::DepositArgs; use drain::DrainArgs; use faucet::FaucetArgs; use receive::ReceiveArgs; -use refresh::RefreshArgs; +use recover::RecoverArgs; use reset::ResetArgs; +use scan::ScanArgs; use send::SendArgs; +use withdraw::WithdrawArgs; pub mod backup; pub mod balance; -pub mod bridge_in; -pub mod bridge_out; pub mod change_pwd; +pub mod config; +pub mod deposit; pub mod drain; pub mod faucet; pub mod receive; -pub mod refresh; +pub mod recover; pub mod reset; +pub mod scan; pub mod send; +pub mod withdraw; /// A CLI for interacting with Strata and Alpen Labs' bitcoin signet #[derive(FromArgs, PartialEq, Debug)] @@ -33,15 +37,17 @@ pub struct TopLevel { #[derive(FromArgs, PartialEq, Debug)] #[argh(subcommand)] pub enum Commands { - Refresh(RefreshArgs), + Recover(RecoverArgs), Drain(DrainArgs), Balance(BalanceArgs), Backup(BackupArgs), - BridgeIn(BridgeInArgs), - BridgeOut(BridgeOutArgs), + Deposit(DepositArgs), + Withdraw(WithdrawArgs), Faucet(FaucetArgs), Send(SendArgs), Receive(ReceiveArgs), ChangePwd(ChangePwdArgs), Reset(ResetArgs), + Scan(ScanArgs), + Config(ConfigArgs), } diff --git a/bin/strata-cli/src/cmd/receive.rs b/bin/strata-cli/src/cmd/receive.rs index 6018424c1..eac15c791 100644 --- a/bin/strata-cli/src/cmd/receive.rs +++ b/bin/strata-cli/src/cmd/receive.rs @@ -4,7 +4,6 @@ use bdk_wallet::KeychainKind; use console::Term; use crate::{ - constants::NETWORK, net_type::{net_type_or_exit, NetworkType}, seed::Seed, settings::Settings, @@ -27,7 +26,7 @@ pub async fn receive(args: ReceiveArgs, seed: Seed, settings: Settings, esplora: let address = match network_type { NetworkType::Signet => { - let mut l1w = SignetWallet::new(&seed, NETWORK).unwrap(); + let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); let _ = term.write_line("Syncing signet wallet"); l1w.sync(&esplora).await.unwrap(); let _ = term.write_line("Wallet synced"); @@ -36,7 +35,7 @@ pub async fn receive(args: ReceiveArgs, seed: Seed, settings: Settings, esplora: address_info.address.to_string() } NetworkType::Strata => { - let l2w = StrataWallet::new(&seed, &settings.l2_http_endpoint).unwrap(); + let l2w = StrataWallet::new(&seed, &settings.strata_endpoint).unwrap(); l2w.default_signer_address().to_string() } }; diff --git a/bin/strata-cli/src/cmd/refresh.rs b/bin/strata-cli/src/cmd/recover.rs similarity index 66% rename from bin/strata-cli/src/cmd/refresh.rs rename to bin/strata-cli/src/cmd/recover.rs index 2dda2361f..1b7ee0812 100644 --- a/bin/strata-cli/src/cmd/refresh.rs +++ b/bin/strata-cli/src/cmd/recover.rs @@ -6,22 +6,25 @@ use bdk_wallet::{ use console::{style, Term}; use crate::{ - constants::NETWORK, + constants::RECOVERY_DESC_CLEANUP_DELAY, recovery::DescriptorRecovery, seed::Seed, settings::Settings, - signet::{get_fee_rate, EsploraClient, SignetWallet}, + signet::{get_fee_rate, log_fee_rate, EsploraClient, SignetWallet}, }; -/// Runs any background tasks manually. Currently performs recovery of old -/// bridge in transactions +/// Attempt recovery of old deposit transactions #[derive(FromArgs, PartialEq, Debug)] -#[argh(subcommand, name = "refresh")] -pub struct RefreshArgs {} +#[argh(subcommand, name = "recover")] +pub struct RecoverArgs { + /// override signet fee rate in sat/vbyte. must be >=1 + #[argh(option)] + fee_rate: Option, +} -pub async fn refresh(seed: Seed, settings: Settings, esplora: EsploraClient) { +pub async fn recover(args: RecoverArgs, seed: Seed, settings: Settings, esplora: EsploraClient) { let term = Term::stdout(); - let mut l1w = SignetWallet::new(&seed, NETWORK).unwrap(); + let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); l1w.sync(&esplora).await.unwrap(); let _ = term.write_line("Opening descriptor recovery"); @@ -40,19 +43,19 @@ pub async fn refresh(seed: Seed, settings: Settings, esplora: EsploraClient) { return; } - let fee_rate = get_fee_rate(1, &esplora) + let fee_rate = get_fee_rate(args.fee_rate, &esplora, 1) .await - .expect("request should succeed") - .expect("valid target"); + .expect("valid fee rate"); + log_fee_rate(&term, &fee_rate); - for desc in descs { + for (key, desc) in descs { let desc = desc .clone() - .into_wallet_descriptor(l1w.secp_ctx(), NETWORK) + .into_wallet_descriptor(l1w.secp_ctx(), settings.network) .expect("valid descriptor"); let mut recovery_wallet = Wallet::create_single(desc) - .network(NETWORK) + .network(settings.network) .create_wallet_no_persist() .expect("valid wallet"); @@ -64,6 +67,14 @@ pub async fn refresh(seed: Seed, settings: Settings, esplora: EsploraClient) { let needs_recovery = recovery_wallet.balance().confirmed > Amount::ZERO; if !needs_recovery { + assert!(key.len() > 4); + let desc_height = u32::from_be_bytes(unsafe { *(key[..4].as_ptr() as *const [_; 4]) }); + if desc_height + RECOVERY_DESC_CLEANUP_DELAY > current_height { + descriptor_file.remove(key).expect("removal should succeed"); + let _ = term.write_line(&format!( + "removed old, already claimed descriptor due for recovery at {desc_height}" + )); + } continue; } @@ -73,7 +84,7 @@ pub async fn refresh(seed: Seed, settings: Settings, esplora: EsploraClient) { let recover_to = l1w.reveal_next_address(KeychainKind::External).address; let _ = term.write_line(&format!( - "Recovering a bridge-in transaction from address {} to {}", + "Recovering a deposit transaction from address {} to {}", style(address).yellow(), style(&recover_to).yellow() )); diff --git a/bin/strata-cli/src/cmd/reset.rs b/bin/strata-cli/src/cmd/reset.rs index 7372bab82..3020b4d50 100644 --- a/bin/strata-cli/src/cmd/reset.rs +++ b/bin/strata-cli/src/cmd/reset.rs @@ -4,7 +4,8 @@ use dialoguer::Confirm; use crate::{seed::EncryptedSeedPersister, settings::Settings}; -/// Prints a BIP39 mnemonic encoding the internal wallet's seed bytes +/// DANGER: resets the CLI completely, destroying all keys and databases. +/// Keeps config. #[derive(FromArgs, PartialEq, Debug)] #[argh(subcommand, name = "reset")] pub struct ResetArgs { diff --git a/bin/strata-cli/src/cmd/scan.rs b/bin/strata-cli/src/cmd/scan.rs new file mode 100644 index 000000000..1c765f173 --- /dev/null +++ b/bin/strata-cli/src/cmd/scan.rs @@ -0,0 +1,17 @@ +use argh::FromArgs; + +use crate::{ + seed::Seed, + settings::Settings, + signet::{EsploraClient, SignetWallet}, +}; + +/// Performs a full scan of the signet wallet +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "scan")] +pub struct ScanArgs {} + +pub async fn scan(_args: ScanArgs, seed: Seed, settings: Settings, esplora: EsploraClient) { + let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); + l1w.scan(&esplora).await.unwrap(); +} diff --git a/bin/strata-cli/src/cmd/send.rs b/bin/strata-cli/src/cmd/send.rs index f1c70da15..bb37c19fa 100644 --- a/bin/strata-cli/src/cmd/send.rs +++ b/bin/strata-cli/src/cmd/send.rs @@ -7,16 +7,15 @@ use alloy::{ rpc::types::TransactionRequest, }; use argh::FromArgs; -use bdk_wallet::bitcoin::{hashes::Hash, Address, Amount}; +use bdk_wallet::bitcoin::{Address, Amount}; use console::Term; -use shrex::encode; use crate::{ - constants::NETWORK, + constants::SATS_TO_WEI, net_type::{net_type_or_exit, NetworkType}, seed::Seed, settings::Settings, - signet::{get_fee_rate, EsploraClient, SignetWallet}, + signet::{get_fee_rate, log_fee_rate, print_explorer_url, EsploraClient, SignetWallet}, strata::StrataWallet, }; @@ -35,25 +34,29 @@ pub struct SendArgs { /// address to send to #[argh(positional)] address: String, + + /// override signet fee rate in sat/vbyte. must be >=1 + #[argh(option)] + fee_rate: Option, } pub async fn send(args: SendArgs, seed: Seed, settings: Settings, esplora: EsploraClient) { let term = Term::stdout(); let network_type = net_type_or_exit(&args.network_type, &term); - let txid = match network_type { + match network_type { NetworkType::Signet => { let amount = Amount::from_sat(args.amount); let address = Address::from_str(&args.address) .expect("valid address") - .require_network(NETWORK) + .require_network(settings.network) .expect("correct network"); - let mut l1w = SignetWallet::new(&seed, NETWORK).expect("valid wallet"); + let mut l1w = SignetWallet::new(&seed, settings.network).expect("valid wallet"); l1w.sync(&esplora).await.unwrap(); - let fee_rate = get_fee_rate(1, &esplora) + let fee_rate = get_fee_rate(args.fee_rate, &esplora, 1) .await - .expect("valid response") - .expect("valid target"); + .expect("valid fee rate"); + log_fee_rate(&term, &fee_rate); let mut psbt = l1w .build_tx() .add_recipient(address.script_pubkey(), amount) @@ -65,27 +68,25 @@ pub async fn send(args: SendArgs, seed: Seed, settings: Settings, esplora: Esplo .expect("signable psbt"); let tx = psbt.extract_tx().expect("signed tx"); esplora.broadcast(&tx).await.expect("successful broadcast"); - tx.compute_txid().as_raw_hash().to_byte_array() + let _ = print_explorer_url(&tx.compute_txid(), &term, &settings); } NetworkType::Strata => { - let l2w = StrataWallet::new(&seed, &settings.l2_http_endpoint).expect("valid wallet"); + let l2w = StrataWallet::new(&seed, &settings.strata_endpoint).expect("valid wallet"); let address = StrataAddress::from_str(&args.address).expect("valid address"); let tx = TransactionRequest::default() .with_to(address) - // 1 btc == 1 "eth" => 1 sat = 1e10 "wei" - .with_value(U256::from(args.amount * 10u64.pow(10))); - l2w.send_transaction(tx) + .with_value(U256::from(args.amount as u128 * SATS_TO_WEI)); + let res = l2w + .send_transaction(tx) .await - .expect("successful broadcast") - .tx_hash() - .0 + .expect("successful broadcast"); + let _ = term.write_line(&format!("Transaction {} sent", res.tx_hash())); } }; let _ = term.write_line(&format!( - "Sent {} to {} in tx {}", - args.amount, + "Sent {} to {}", + Amount::from_sat(args.amount), args.address, - encode(&txid) )); } diff --git a/bin/strata-cli/src/cmd/bridge_out.rs b/bin/strata-cli/src/cmd/withdraw.rs similarity index 77% rename from bin/strata-cli/src/cmd/bridge_out.rs rename to bin/strata-cli/src/cmd/withdraw.rs index 075912b0b..d5a7b85f9 100644 --- a/bin/strata-cli/src/cmd/bridge_out.rs +++ b/bin/strata-cli/src/cmd/withdraw.rs @@ -10,7 +10,7 @@ use console::Term; use indicatif::ProgressBar; use crate::{ - constants::{BRIDGE_OUT_AMOUNT, NETWORK}, + constants::{BRIDGE_OUT_AMOUNT, SATS_TO_WEI}, seed::Seed, settings::Settings, signet::SignetWallet, @@ -18,25 +18,25 @@ use crate::{ taproot::ExtractP2trPubkey, }; -/// Bridge 10 BTC from Strata to signet +/// Withdraw 10 BTC from Strata to signet #[derive(FromArgs, PartialEq, Debug)] -#[argh(subcommand, name = "bridge-out")] -pub struct BridgeOutArgs { +#[argh(subcommand, name = "withdraw")] +pub struct WithdrawArgs { /// the signet address to send funds to. defaults to a new internal wallet address #[argh(positional)] p2tr_address: Option, } -pub async fn bridge_out(args: BridgeOutArgs, seed: Seed, settings: Settings) { +pub async fn withdraw(args: WithdrawArgs, seed: Seed, settings: Settings) { let address = args.p2tr_address.map(|a| { Address::from_str(&a) .expect("valid address") - .require_network(NETWORK) + .require_network(settings.network) .expect("correct network") }); - let mut l1w = SignetWallet::new(&seed, NETWORK).unwrap(); - let l2w = StrataWallet::new(&seed, &settings.l2_http_endpoint).unwrap(); + let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); + let l2w = StrataWallet::new(&seed, &settings.strata_endpoint).unwrap(); let address = match address { Some(a) => a, @@ -56,7 +56,7 @@ pub async fn bridge_out(args: BridgeOutArgs, seed: Seed, settings: Settings) { let tx = l2w .transaction_request() .with_to(settings.bridge_strata_address) - .with_value(U256::from(BRIDGE_OUT_AMOUNT.to_sat() * 1u64.pow(10))) + .with_value(U256::from(BRIDGE_OUT_AMOUNT.to_sat() as u128 * SATS_TO_WEI)) .input(TransactionInput::new( address .extract_p2tr_pubkey() diff --git a/bin/strata-cli/src/constants.rs b/bin/strata-cli/src/constants.rs index 3b0133dbf..c2c1c8c82 100644 --- a/bin/strata-cli/src/constants.rs +++ b/bin/strata-cli/src/constants.rs @@ -1,5 +1,6 @@ use std::{sync::LazyLock, time::Duration}; +use alloy::consensus::constants::ETH_TO_WEI; use bdk_wallet::bitcoin::{ key::{Parity, Secp256k1}, secp256k1::{PublicKey, SecretKey}, @@ -14,6 +15,8 @@ pub const RECOVER_DELAY: u32 = 1008; /// reorgs that may happen at the recovery height. pub const RECOVER_AT_DELAY: u32 = RECOVER_DELAY + 10; +pub const RECOVERY_DESC_CLEANUP_DELAY: u32 = 100; + /// 10 BTC + 0.01 to cover fees in the following transaction where the operator spends it into the /// federation. pub const BRIDGE_IN_AMOUNT: Amount = Amount::from_sat(1_001_000_000); @@ -21,25 +24,24 @@ pub const BRIDGE_IN_AMOUNT: Amount = Amount::from_sat(1_001_000_000); /// Bridge outs are enforced to be exactly 10 BTC pub const BRIDGE_OUT_AMOUNT: Amount = Amount::from_int_btc(10); +pub const BTC_TO_WEI: u128 = ETH_TO_WEI; +pub const SATS_TO_WEI: u128 = BTC_TO_WEI / 100_000_000; + /// Length of salt used for password hashing pub const PW_SALT_LEN: usize = 16; /// Length of nonce in bytes pub const AES_NONCE_LEN: usize = 12; /// Length of seed in bytes -pub const SEED_LEN: usize = 32; +pub const SEED_LEN: usize = 16; /// AES-256-GCM-SIV tag len pub const AES_TAG_LEN: usize = 16; -pub const NETWORK: Network = Network::Signet; -pub const BRIDGE_STRATA_ADDRESS: &str = "0x000000000000000000000000000000000B121d9E"; -pub const L2_BLOCK_TIME: Duration = Duration::from_secs(30); +pub const DEFAULT_NETWORK: Network = Network::Signet; +pub const BRIDGE_STRATA_ADDRESS: &str = "0x5400000000000000000000000000000000000001"; +pub const SIGNET_BLOCK_TIME: Duration = Duration::from_secs(30); -pub const DEFAULT_ESPLORA: &str = "https://esploraapi.devnet-annapurna.stratabtc.org"; -pub const DEFAULT_L2_HTTP_ENDPOINT: &str = "https://stratareth.devnet-annapurna.stratabtc.org"; -pub const DEFAULT_FAUCET_ENDPOINT: &str = "https://faucet.devnet-annapurna.stratabtc.org"; -// FIXME: CHANGE BELOW!!! pub const BRIDGE_MUSIG2_PUBKEY: &str = - "fbd79b6b8b7fe11bad25ae89a7415221c030978de448775729c3f0a903819dd0"; + "14ced579c6a92533fa68ccc16da93b41073993cfc6cc982320645d8e9a63ee65"; /// A provably unspendable, static public key from predetermined inputs created using method specified in [BIP-341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23) pub static UNSPENDABLE: LazyLock = LazyLock::new(|| { diff --git a/bin/strata-cli/src/main.rs b/bin/strata-cli/src/main.rs index c56af4e7f..4fa421467 100644 --- a/bin/strata-cli/src/main.rs +++ b/bin/strata-cli/src/main.rs @@ -9,9 +9,9 @@ pub mod strata; pub mod taproot; use cmd::{ - backup::backup, balance::balance, bridge_in::bridge_in, bridge_out::bridge_out, - change_pwd::change_pwd, drain::drain, faucet::faucet, receive::receive, refresh::refresh, - reset::reset, send::send, Commands, TopLevel, + backup::backup, balance::balance, change_pwd::change_pwd, config::config, deposit::deposit, + drain::drain, faucet::faucet, receive::receive, recover::recover, reset::reset, scan::scan, + send::send, withdraw::withdraw, Commands, TopLevel, }; #[cfg(target_os = "linux")] use seed::FilePersister; @@ -23,6 +23,12 @@ use signet::{set_data_dir, EsploraClient}; #[tokio::main(flavor = "current_thread")] async fn main() { let TopLevel { cmd } = argh::from_env(); + + if let Commands::Config(args) = cmd { + config(args).await; + return; + } + let settings = Settings::load().unwrap(); #[cfg(not(target_os = "linux"))] @@ -30,22 +36,28 @@ async fn main() { #[cfg(target_os = "linux")] let persister = FilePersister::new(settings.linux_seed_file.clone()); - let seed = seed::load_or_create(&persister).unwrap(); + if let Commands::Reset(args) = cmd { + reset(args, persister, settings).await; + return; + } assert!(set_data_dir(settings.data_dir.clone())); + + let seed = seed::load_or_create(&persister).unwrap(); let esplora = EsploraClient::new(&settings.esplora).expect("valid esplora url"); match cmd { - Commands::Refresh(_) => refresh(seed, settings, esplora).await, + Commands::Recover(args) => recover(args, seed, settings, esplora).await, Commands::Drain(args) => drain(args, seed, settings, esplora).await, Commands::Balance(args) => balance(args, seed, settings, esplora).await, Commands::Backup(args) => backup(args, seed).await, - Commands::BridgeIn(args) => bridge_in(args, seed, settings, esplora).await, - Commands::BridgeOut(args) => bridge_out(args, seed, settings).await, + Commands::Deposit(args) => deposit(args, seed, settings, esplora).await, + Commands::Withdraw(args) => withdraw(args, seed, settings).await, Commands::Faucet(args) => faucet(args, seed, settings).await, Commands::Send(args) => send(args, seed, settings, esplora).await, Commands::Receive(args) => receive(args, seed, settings, esplora).await, - Commands::Reset(args) => reset(args, persister, settings).await, Commands::ChangePwd(args) => change_pwd(args, seed, persister).await, + Commands::Scan(args) => scan(args, seed, settings, esplora).await, + _ => {} } } diff --git a/bin/strata-cli/src/net_type.rs b/bin/strata-cli/src/net_type.rs index c9ab27096..37992c77d 100644 --- a/bin/strata-cli/src/net_type.rs +++ b/bin/strata-cli/src/net_type.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use console::Term; /// Represents a type of network, either Alpen's signet or Strata +#[derive(PartialEq, Eq)] pub enum NetworkType { Signet, Strata, diff --git a/bin/strata-cli/src/recovery.rs b/bin/strata-cli/src/recovery.rs index cc8424f43..3639dce68 100644 --- a/bin/strata-cli/src/recovery.rs +++ b/bin/strata-cli/src/recovery.rs @@ -15,6 +15,7 @@ use bdk_wallet::{ }; use rand::{rngs::OsRng, RngCore}; use sha2::{Digest, Sha256}; +use sled::IVec; use terrors::OneOf; use tokio::io::AsyncReadExt; @@ -128,11 +129,11 @@ impl DescriptorRecovery { pub async fn read_descs_after_block( &mut self, height: u32, - ) -> Result, OneOf> { + ) -> Result, OneOf> { let after_height = self.db.range(height.to_be_bytes()..); let mut descs = vec![]; for desc_entry in after_height { - let mut raw = desc_entry.map_err(OneOf::new)?.1; + let (key, mut raw) = desc_entry.map_err(OneOf::new)?; if raw.len() <= 12 + 16 { return Err(OneOf::new(EntryTooShort { length: raw.len() })); } @@ -199,10 +200,14 @@ impl DescriptorRecovery { networks.insert(network); } - descs.push((desc, keymap, networks)); + descs.push((key, (desc, keymap, networks))); } Ok(descs) } + + pub fn remove>(&self, key: K) -> sled::Result> { + self.db.remove(key) + } } pub type ReadDescsAfterError = ( diff --git a/bin/strata-cli/src/seed.rs b/bin/strata-cli/src/seed.rs index a896c182b..b2b0d659d 100644 --- a/bin/strata-cli/src/seed.rs +++ b/bin/strata-cli/src/seed.rs @@ -69,7 +69,7 @@ impl Seed { seed.copy_from_slice(&self.0); let mut cipher = - Aes256GcmSiv::new_from_slice(seed_encryption_key).expect("should be correct key size"); + Aes256GcmSiv::new_from_slice(&seed_encryption_key).expect("should be correct key size"); let nonce = Nonce::from_slice(&salt_and_nonce[PW_SALT_LEN..]); let tag = cipher .encrypt_in_place_detached(nonce, &[], seed) @@ -123,7 +123,7 @@ impl EncryptedSeed { .map_err(OneOf::new)?; let mut cipher = - Aes256GcmSiv::new_from_slice(seed_encryption_key).expect("should be correct key size"); + Aes256GcmSiv::new_from_slice(&seed_encryption_key).expect("should be correct key size"); let (salt_and_nonce, rest) = self.0.split_at_mut(PW_SALT_LEN + AES_NONCE_LEN); let (seed, tag) = rest.split_at_mut(SEED_LEN); let tag = Tag::from_slice(tag); diff --git a/bin/strata-cli/src/seed/password.rs b/bin/strata-cli/src/seed/password.rs index 60bfda349..f2ddb63fb 100644 --- a/bin/strata-cli/src/seed/password.rs +++ b/bin/strata-cli/src/seed/password.rs @@ -1,12 +1,13 @@ use argon2::{Algorithm, Argon2, Params, Version}; use dialoguer::Password as InputPassword; +use zeroize::ZeroizeOnDrop; use zxcvbn::{zxcvbn, Entropy}; use super::PW_SALT_LEN; +#[derive(ZeroizeOnDrop)] pub struct Password { inner: String, - seed_encryption_key: Option<[u8; 32]>, } pub enum HashVersion { @@ -29,25 +30,21 @@ impl HashVersion { impl Password { pub fn read(new: bool) -> Result { - let mut input = InputPassword::new(); + let mut input = InputPassword::new().allow_empty_password(true); if new { input = input .with_prompt("Create a new password (leave empty for no password, dangerous!)") .with_confirmation( "Confirm password (leave empty for no password, dangerous!)", "Passwords didn't match", - ) - .allow_empty_password(true); + ); } else { input = input.with_prompt("Enter your password"); } let password = input.interact()?; - Ok(Self { - inner: password, - seed_encryption_key: None, - }) + Ok(Self { inner: password }) } /// Returns the password entropy. @@ -59,23 +56,15 @@ impl Password { &mut self, salt: &[u8; PW_SALT_LEN], version: HashVersion, - ) -> Result<&[u8; 32], argon2::Error> { - match self.seed_encryption_key { - Some(ref key) => Ok(key), - None => { - let mut sek = [0u8; 32]; - let (algo, ver, params) = version.params(); - if !self.inner.is_empty() { - Argon2::new(algo, ver, params.expect("valid params")).hash_password_into( - self.inner.as_bytes(), - salt, - &mut sek, - )?; - } - self.seed_encryption_key = Some(sek); - self.seed_encryption_key(salt, version) - } - } + ) -> Result<[u8; 32], argon2::Error> { + let mut sek = [0u8; 32]; + let (algo, ver, params) = version.params(); + Argon2::new(algo, ver, params.expect("valid params")).hash_password_into( + self.inner.as_bytes(), + salt, + &mut sek, + )?; + Ok(sek) } /// Validates the password strength and returns feedback if it's weak. diff --git a/bin/strata-cli/src/settings.rs b/bin/strata-cli/src/settings.rs index 20ad07ba1..cc11c85ca 100644 --- a/bin/strata-cli/src/settings.rs +++ b/bin/strata-cli/src/settings.rs @@ -3,26 +3,27 @@ use std::{ io, path::PathBuf, str::FromStr, + sync::LazyLock, }; use alloy::primitives::Address as StrataAddress; -use bdk_wallet::bitcoin::XOnlyPublicKey; +use bdk_wallet::bitcoin::{Network, XOnlyPublicKey}; use config::Config; use directories::ProjectDirs; use serde::{Deserialize, Serialize}; -use shrex::decode; +use shrex::{decode, Hex}; use terrors::OneOf; -use crate::constants::{ - BRIDGE_MUSIG2_PUBKEY, BRIDGE_STRATA_ADDRESS, DEFAULT_ESPLORA, DEFAULT_FAUCET_ENDPOINT, - DEFAULT_L2_HTTP_ENDPOINT, -}; +use crate::constants::{BRIDGE_MUSIG2_PUBKEY, BRIDGE_STRATA_ADDRESS, DEFAULT_NETWORK}; #[derive(Serialize, Deserialize)] pub struct SettingsFromFile { - pub esplora: Option, - pub l2_http_endpoint: Option, - pub faucet_endpoint: Option, + pub esplora: String, + pub strata_endpoint: String, + pub faucet_endpoint: String, + pub mempool_endpoint: String, + pub bridge_pubkey: Option>, + pub network: Option, } /// Settings struct filled with either config values or @@ -30,46 +31,64 @@ pub struct SettingsFromFile { #[derive(Serialize, Deserialize, Debug)] pub struct Settings { pub esplora: String, - pub l2_http_endpoint: String, + pub strata_endpoint: String, pub data_dir: PathBuf, pub faucet_endpoint: String, pub bridge_musig2_pubkey: XOnlyPublicKey, pub descriptor_db: PathBuf, + pub mempool_endpoint: String, pub bridge_strata_address: StrataAddress, pub linux_seed_file: PathBuf, + pub network: Network, + pub config_file: PathBuf, } +pub static PROJ_DIRS: LazyLock = LazyLock::new(|| { + ProjectDirs::from("io", "alpenlabs", "strata").expect("project dir should be available") +}); + +pub static CONFIG_FILE: LazyLock = + LazyLock::new(|| match std::env::var("CLI_CONFIG").ok() { + Some(path) => PathBuf::from_str(&path).expect("valid config path"), + None => PROJ_DIRS.config_dir().to_owned().join("config.toml"), + }); + impl Settings { pub fn load() -> Result> { - let proj_dirs = ProjectDirs::from("io", "alpenlabs", "strata") - .expect("project dir should be available"); - let config_file = proj_dirs.config_dir().to_owned().join("config.toml"); + let proj_dirs = &PROJ_DIRS; + let config_file = CONFIG_FILE.as_path(); let descriptor_file = proj_dirs.data_dir().to_owned().join("descriptors"); let linux_seed_file = proj_dirs.data_dir().to_owned().join("seed"); + create_dir_all(proj_dirs.config_dir()).map_err(OneOf::new)?; create_dir_all(proj_dirs.data_dir()).map_err(OneOf::new)?; - let _ = File::create_new(&config_file); - let from_file = Config::builder() + + // create config file if not exists + let _ = File::create_new(config_file); + let from_file: SettingsFromFile = Config::builder() .add_source(config::File::from(config_file)) .build() .map_err(OneOf::new)? .try_deserialize::() .map_err(OneOf::new)?; + Ok(Settings { - esplora: from_file.esplora.unwrap_or(DEFAULT_ESPLORA.to_owned()), - l2_http_endpoint: from_file - .l2_http_endpoint - .unwrap_or(DEFAULT_L2_HTTP_ENDPOINT.to_owned()), + esplora: from_file.esplora, + strata_endpoint: from_file.strata_endpoint, data_dir: proj_dirs.data_dir().to_owned(), - faucet_endpoint: from_file - .faucet_endpoint - .unwrap_or(DEFAULT_FAUCET_ENDPOINT.to_owned()), - bridge_musig2_pubkey: XOnlyPublicKey::from_slice(&{ - let mut buf = [0u8; 32]; - decode(BRIDGE_MUSIG2_PUBKEY, &mut buf).expect("valid hex"); - buf + faucet_endpoint: from_file.faucet_endpoint, + mempool_endpoint: from_file.mempool_endpoint, + bridge_musig2_pubkey: XOnlyPublicKey::from_slice(&match from_file.bridge_pubkey { + Some(key) => key.0, + None => { + let mut buf = [0u8; 32]; + decode(BRIDGE_MUSIG2_PUBKEY, &mut buf).expect("valid hex"); + buf + } }) .expect("valid length"), + config_file: CONFIG_FILE.clone(), + network: from_file.network.unwrap_or(DEFAULT_NETWORK), descriptor_db: descriptor_file, bridge_strata_address: StrataAddress::from_str(BRIDGE_STRATA_ADDRESS) .expect("valid strata address"), diff --git a/bin/strata-cli/src/signet.rs b/bin/strata-cli/src/signet.rs index 874125336..579cc4a7e 100644 --- a/bin/strata-cli/src/signet.rs +++ b/bin/strata-cli/src/signet.rs @@ -1,5 +1,6 @@ use std::{ cell::RefCell, + collections::BTreeSet, io, ops::{Deref, DerefMut}, path::{Path, PathBuf}, @@ -12,34 +13,62 @@ use bdk_esplora::{ EsploraAsyncExt, }; use bdk_wallet::{ - bitcoin::{FeeRate, Network}, + bitcoin::{FeeRate, Network, Txid}, rusqlite::{self, Connection}, - ChangeSet, PersistedWallet, WalletPersister, + ChangeSet, KeychainKind, PersistedWallet, WalletPersister, }; use console::{style, Term}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; -use crate::seed::Seed; - -/// Retrieves an estimated fee rate to settle a transaction in `target` blocks -pub async fn get_fee_rate( - target: u16, - esplora_client: &AsyncClient, -) -> Result, esplora_client::Error> { - Ok(esplora_client - .get_fee_estimates() - .await - .map(|frs| frs.get(&target).cloned())? - .and_then(|fr| FeeRate::from_sat_per_vb(fr as u64))) -} +use crate::{seed::Seed, settings::Settings}; pub fn log_fee_rate(term: &Term, fr: &FeeRate) { let _ = term.write_line(&format!( "Using {} as feerate", - style(format!("~{} sat/vb", fr.to_sat_per_vb_ceil())).green(), + style(format!("{} sat/vb", fr.to_sat_per_vb_ceil())).green(), )); } +pub fn print_explorer_url(txid: &Txid, term: &Term, settings: &Settings) -> Result<(), io::Error> { + term.write_line(&format!( + "View transaction at {}", + style(format!("{}/tx/{txid}", settings.mempool_endpoint)).blue() + )) +} + +#[derive(Debug)] +pub enum FeeRateError { + InvalidDueToOverflow, + BelowBroadcastMin, + /// Esplora didn't have a fee for the requested target + FeeMissing, + EsploraError(esplora_client::Error), +} + +pub async fn get_fee_rate( + user_provided: Option, + esplora: &EsploraClient, + target: u16, +) -> Result { + let fee_rate = if let Some(fr) = user_provided { + FeeRate::from_sat_per_vb(fr).ok_or(FeeRateError::InvalidDueToOverflow)? + } else { + esplora + .get_fee_estimates() + .await + .map(|frs| frs.get(&target).cloned()) + .map_err(FeeRateError::EsploraError)? + .and_then(|fr| FeeRate::from_sat_per_vb(fr as u64)) + .ok_or(FeeRateError::FeeMissing)? + }; + + if fee_rate < FeeRate::BROADCAST_MIN { + Err(FeeRateError::BelowBroadcastMin) + } else { + Ok(fee_rate) + } +} + #[derive(Clone)] pub struct EsploraClient(AsyncClient); @@ -138,6 +167,7 @@ impl SignetWallet { ops2.finish(); spks2.finish(); txids2.finish(); + let _ = term.write_line("Persisting updates"); self.apply_update(update) .expect("should be able to connect to db"); self.persist().expect("persist should work"); @@ -145,6 +175,34 @@ impl SignetWallet { Ok(()) } + pub async fn scan( + &mut self, + esplora_client: &AsyncClient, + ) -> Result<(), Box> { + let bar = ProgressBar::new_spinner(); + let bar2 = bar.clone(); + let req = self + .start_full_scan() + .inspect({ + let mut once = BTreeSet::::new(); + move |keychain, spk_i, script| { + if once.insert(keychain) { + bar2.println(format!("\nScanning keychain [{:?}]", keychain)); + } + bar2.println(format!("- idx {spk_i}: {script}")); + } + }) + .build(); + + let update = esplora_client.full_scan(req, 5, 3).await?; + bar.set_message("Persisting updates"); + self.apply_update(update) + .expect("should be able to connect to db"); + self.persist().expect("persist should work"); + bar.finish_with_message("Scan complete"); + Ok(()) + } + pub fn persist(&mut self) -> Result { self.0.persist(&mut Persister) } diff --git a/bin/strata-client/src/extractor.rs b/bin/strata-client/src/extractor.rs index b89f82a49..c50f049f4 100644 --- a/bin/strata-client/src/extractor.rs +++ b/bin/strata-client/src/extractor.rs @@ -21,7 +21,7 @@ use strata_rpc_types::RpcServerError; use strata_state::{bridge_state::DepositState, chain_state::ChainState, tx::ProtocolOperation}; use tracing::{debug, error}; -/// The `vout` corresponding to the bridge-in related Taproot address on the Deposit Request +/// The `vout` corresponding to the deposit related Taproot address on the Deposit Request /// Transaction. /// /// This is always going to be the first [`OutPoint`]. From f37b12a269f967ef20444466a4e7d8fbce20c975 Mon Sep 17 00:00:00 2001 From: Aaron Feickert <66188213+AaronFeickert@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:15:57 -0500 Subject: [PATCH 06/18] Minor trait syntax clarification for the analyzer --- bin/strata-cli/src/recovery.rs | 2 +- bin/strata-cli/src/seed.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/strata-cli/src/recovery.rs b/bin/strata-cli/src/recovery.rs index 3639dce68..2cf301344 100644 --- a/bin/strata-cli/src/recovery.rs +++ b/bin/strata-cli/src/recovery.rs @@ -38,7 +38,7 @@ impl DescriptorRecovery { let db_key = { let mut key = Vec::from(recover_at.to_be_bytes()); // this will actually write the private key inside the descriptor so we hash it - let mut hasher = Sha256::new(); + let mut hasher = ::new(); // this is to appease the analyzer hasher.update(desc_string.as_bytes()); key.extend_from_slice(hasher.finalize().as_ref()); key diff --git a/bin/strata-cli/src/seed.rs b/bin/strata-cli/src/seed.rs index b2b0d659d..42e7ea553 100644 --- a/bin/strata-cli/src/seed.rs +++ b/bin/strata-cli/src/seed.rs @@ -43,7 +43,7 @@ impl Seed { } pub fn descriptor_recovery_key(&self) -> [u8; 32] { - let mut hasher = Sha256::new(); + let mut hasher = ::new(); // this is to appease the analyzer hasher.update(b"alpen labs strata descriptor recovery file 2024"); hasher.update(self.0); hasher.finalize().into() @@ -94,7 +94,7 @@ impl Seed { pub fn strata_wallet(&self) -> EthereumWallet { let l2_private_bytes = { - let mut hasher = Sha256::new(); + let mut hasher = ::new(); // this is to appease the analyzer hasher.update(b"alpen labs strata l2 wallet 2024"); hasher.update(self.0); hasher.finalize() From 727ef28047111e451bf41b00f1635c5df3cd8d53 Mon Sep 17 00:00:00 2001 From: Aaron Feickert <66188213+AaronFeickert@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:37:23 -0500 Subject: [PATCH 07/18] Test seed encryption (#408) --- bin/strata-cli/src/seed.rs | 83 +++++++++++++++++++++++++++++ bin/strata-cli/src/seed/password.rs | 10 +++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/bin/strata-cli/src/seed.rs b/bin/strata-cli/src/seed.rs index 42e7ea553..0316071a1 100644 --- a/bin/strata-cli/src/seed.rs +++ b/bin/strata-cli/src/seed.rs @@ -257,3 +257,86 @@ mod keychain; pub use keychain::*; pub mod password; + +#[cfg(test)] +mod test { + use rand::rngs::OsRng; + + use super::*; + + #[test] + // Test valid seed encryption and decryption + fn seed_encrypt_decrypt() { + let mut password = Password::new(String::from("swordfish")); + let seed = Seed::gen(&mut OsRng); + + let encrypted_seed = seed.encrypt(&mut password, &mut OsRng).unwrap(); + let decrypted_seed = encrypted_seed.decrypt(&mut password).unwrap(); + + assert_eq!(seed.0, decrypted_seed.0); + } + + #[test] + // Using an evil password fails decryption + fn evil_password() { + let mut password = Password::new(String::from("swordfish")); + let mut evil_password = Password::new(String::from("evil")); + let seed = Seed::gen(&mut OsRng); + + let encrypted_seed = seed.encrypt(&mut password, &mut OsRng).unwrap(); + + assert!(encrypted_seed.decrypt(&mut evil_password).is_err()); + } + + #[test] + // Using an evil salt fails decryption + fn evil_salt() { + let mut password = Password::new(String::from("swordfish")); + let seed = Seed::gen(&mut OsRng); + + let mut encrypted_seed = seed.encrypt(&mut password, &mut OsRng).unwrap(); + let index = 0; + encrypted_seed.0[index] = !encrypted_seed.0[index]; + + assert!(encrypted_seed.decrypt(&mut password).is_err()); + } + + #[test] + // Using an evil nonce fails decryption + fn evil_nonce() { + let mut password = Password::new(String::from("swordfish")); + let seed = Seed::gen(&mut OsRng); + + let mut encrypted_seed = seed.encrypt(&mut password, &mut OsRng).unwrap(); + let index = PW_SALT_LEN; + encrypted_seed.0[index] = !encrypted_seed.0[index]; + + assert!(encrypted_seed.decrypt(&mut password).is_err()); + } + + #[test] + // Using an evil seed fails decryption + fn evil_seed() { + let mut password = Password::new(String::from("swordfish")); + let seed = Seed::gen(&mut OsRng); + + let mut encrypted_seed = seed.encrypt(&mut password, &mut OsRng).unwrap(); + let index = PW_SALT_LEN + AES_NONCE_LEN; + encrypted_seed.0[index] = !encrypted_seed.0[index]; + + assert!(encrypted_seed.decrypt(&mut password).is_err()); + } + + #[test] + // Using an evil tag fails decryption + fn evil_tag() { + let mut password = Password::new(String::from("swordfish")); + let seed = Seed::gen(&mut OsRng); + + let mut encrypted_seed = seed.encrypt(&mut password, &mut OsRng).unwrap(); + let index = PW_SALT_LEN + AES_NONCE_LEN + SEED_LEN; + encrypted_seed.0[index] = !encrypted_seed.0[index]; + + assert!(encrypted_seed.decrypt(&mut password).is_err()); + } +} diff --git a/bin/strata-cli/src/seed/password.rs b/bin/strata-cli/src/seed/password.rs index f2ddb63fb..780818827 100644 --- a/bin/strata-cli/src/seed/password.rs +++ b/bin/strata-cli/src/seed/password.rs @@ -29,6 +29,13 @@ impl HashVersion { } impl Password { + /// Constructs a password from a string. (The complexity of the password is not checked.) + pub fn new(password: String) -> Self { + Self { inner: password } + } + + /// Constructs a password from user interaction. (The complexity of the password is not + /// checked.) pub fn read(new: bool) -> Result { let mut input = InputPassword::new().allow_empty_password(true); if new { @@ -44,7 +51,7 @@ impl Password { let password = input.interact()?; - Ok(Self { inner: password }) + Ok(Self::new(password)) } /// Returns the password entropy. @@ -52,6 +59,7 @@ impl Password { zxcvbn(self.inner.as_str(), &[]) } + /// Derives a seed encryption key from a password. pub fn seed_encryption_key( &mut self, salt: &[u8; PW_SALT_LEN], From 3283c2ab8d6c11b084a34666f8fb292fa728457a Mon Sep 17 00:00:00 2001 From: Aaron Feickert <66188213+AaronFeickert@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:17:46 -0500 Subject: [PATCH 08/18] Zeroize seed encryption key (#411) --- bin/strata-cli/src/seed.rs | 8 ++++---- bin/strata-cli/src/seed/password.rs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bin/strata-cli/src/seed.rs b/bin/strata-cli/src/seed.rs index 0316071a1..c3d401c2b 100644 --- a/bin/strata-cli/src/seed.rs +++ b/bin/strata-cli/src/seed.rs @@ -68,8 +68,8 @@ impl Seed { let (seed, _) = rest.split_at_mut(SEED_LEN); seed.copy_from_slice(&self.0); - let mut cipher = - Aes256GcmSiv::new_from_slice(&seed_encryption_key).expect("should be correct key size"); + let mut cipher = Aes256GcmSiv::new_from_slice(seed_encryption_key.as_ref()) + .expect("should be correct key size"); let nonce = Nonce::from_slice(&salt_and_nonce[PW_SALT_LEN..]); let tag = cipher .encrypt_in_place_detached(nonce, &[], seed) @@ -122,8 +122,8 @@ impl EncryptedSeed { ) .map_err(OneOf::new)?; - let mut cipher = - Aes256GcmSiv::new_from_slice(&seed_encryption_key).expect("should be correct key size"); + let mut cipher = Aes256GcmSiv::new_from_slice(seed_encryption_key.as_ref()) + .expect("should be correct key size"); let (salt_and_nonce, rest) = self.0.split_at_mut(PW_SALT_LEN + AES_NONCE_LEN); let (seed, tag) = rest.split_at_mut(SEED_LEN); let tag = Tag::from_slice(tag); diff --git a/bin/strata-cli/src/seed/password.rs b/bin/strata-cli/src/seed/password.rs index 780818827..fb5c09aff 100644 --- a/bin/strata-cli/src/seed/password.rs +++ b/bin/strata-cli/src/seed/password.rs @@ -1,6 +1,6 @@ use argon2::{Algorithm, Argon2, Params, Version}; use dialoguer::Password as InputPassword; -use zeroize::ZeroizeOnDrop; +use zeroize::{ZeroizeOnDrop, Zeroizing}; use zxcvbn::{zxcvbn, Entropy}; use super::PW_SALT_LEN; @@ -64,13 +64,13 @@ impl Password { &mut self, salt: &[u8; PW_SALT_LEN], version: HashVersion, - ) -> Result<[u8; 32], argon2::Error> { - let mut sek = [0u8; 32]; + ) -> Result, argon2::Error> { + let mut sek = Zeroizing::new([0u8; 32]); let (algo, ver, params) = version.params(); Argon2::new(algo, ver, params.expect("valid params")).hash_password_into( self.inner.as_bytes(), salt, - &mut sek, + sek.as_mut(), )?; Ok(sek) } From 6b6f7e5865907439d963d47ce8dd4897805f2caf Mon Sep 17 00:00:00 2001 From: Aaron Feickert <66188213+AaronFeickert@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:18:18 -0500 Subject: [PATCH 09/18] Zeroize seed (#412) --- bin/strata-cli/src/seed.rs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/bin/strata-cli/src/seed.rs b/bin/strata-cli/src/seed.rs index c3d401c2b..eaeef6ff0 100644 --- a/bin/strata-cli/src/seed.rs +++ b/bin/strata-cli/src/seed.rs @@ -15,6 +15,7 @@ use password::{HashVersion, IncorrectPassword, Password}; use rand::{rngs::OsRng, CryptoRng, RngCore}; use sha2::{Digest, Sha256}; use terrors::OneOf; +use zeroize::Zeroizing; use crate::constants::{AES_NONCE_LEN, AES_TAG_LEN, PW_SALT_LEN, SEED_LEN}; @@ -27,25 +28,25 @@ impl BaseWallet { } #[derive(Clone)] -pub struct Seed([u8; SEED_LEN]); +pub struct Seed(Zeroizing<[u8; SEED_LEN]>); impl Seed { fn gen(rng: &mut R) -> Self { - let mut bytes = [0u8; SEED_LEN]; - rng.fill_bytes(&mut bytes); + let mut bytes = Zeroizing::new([0u8; SEED_LEN]); + rng.fill_bytes(bytes.as_mut()); Self(bytes) } pub fn print_mnemonic(&self, language: Language) { let term = Term::stdout(); - let mnemonic = Mnemonic::from_entropy_in(language, &self.0).expect("valid entropy"); + let mnemonic = Mnemonic::from_entropy_in(language, self.0.as_ref()).expect("valid entropy"); let _ = term.write_line(&mnemonic.to_string()); } pub fn descriptor_recovery_key(&self) -> [u8; 32] { let mut hasher = ::new(); // this is to appease the analyzer hasher.update(b"alpen labs strata descriptor recovery file 2024"); - hasher.update(self.0); + hasher.update(self.0.as_slice()); hasher.finalize().into() } @@ -66,7 +67,7 @@ impl Seed { let (salt_and_nonce, rest) = buf.split_at_mut(PW_SALT_LEN + AES_NONCE_LEN); let (seed, _) = rest.split_at_mut(SEED_LEN); - seed.copy_from_slice(&self.0); + seed.copy_from_slice(self.0.as_ref()); let mut cipher = Aes256GcmSiv::new_from_slice(seed_encryption_key.as_ref()) .expect("should be correct key size"); @@ -79,7 +80,7 @@ impl Seed { } pub fn signet_wallet(&self) -> BaseWallet { - let rootpriv = Xpriv::new_master(Network::Signet, &self.0).expect("valid xpriv"); + let rootpriv = Xpriv::new_master(Network::Signet, self.0.as_ref()).expect("valid xpriv"); let base_desc = format!("tr({}/86h/0h/0h", rootpriv); let external_desc = format!("{base_desc}/0/*)"); let internal_desc = format!("{base_desc}/1/*)"); @@ -96,7 +97,7 @@ impl Seed { let l2_private_bytes = { let mut hasher = ::new(); // this is to appease the analyzer hasher.update(b"alpen labs strata l2 wallet 2024"); - hasher.update(self.0); + hasher.update(self.0.as_slice()); hasher.finalize() }; @@ -125,15 +126,18 @@ impl EncryptedSeed { let mut cipher = Aes256GcmSiv::new_from_slice(seed_encryption_key.as_ref()) .expect("should be correct key size"); let (salt_and_nonce, rest) = self.0.split_at_mut(PW_SALT_LEN + AES_NONCE_LEN); - let (seed, tag) = rest.split_at_mut(SEED_LEN); + let (encrypted_seed, tag) = rest.split_at_mut(SEED_LEN); let tag = Tag::from_slice(tag); let nonce = Nonce::from_slice(&salt_and_nonce[PW_SALT_LEN..]); + let mut seed = Zeroizing::new([0u8; SEED_LEN]); + seed.copy_from_slice(encrypted_seed); + cipher - .decrypt_in_place_detached(nonce, &[], seed, tag) + .decrypt_in_place_detached(nonce, &[], seed.as_mut(), tag) .map_err(OneOf::new)?; - Ok(Seed(unsafe { *(seed.as_ptr() as *const [_; SEED_LEN]) })) + Ok(Seed(seed)) } } @@ -186,7 +190,7 @@ pub fn load_or_create( let _ = term.write_line("incorrect entropy length"); continue; } - let mut buf = [0u8; SEED_LEN]; + let mut buf = Zeroizing::new([0u8; SEED_LEN]); buf.copy_from_slice(&entropy); break Seed(buf); } From a01615b866099be7a28c9e9ecfc552a6bbb46920 Mon Sep 17 00:00:00 2001 From: Aaron Feickert <66188213+AaronFeickert@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:04:42 -0500 Subject: [PATCH 10/18] Add sanity test for key generation --- bin/strata-cli/src/seed.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bin/strata-cli/src/seed.rs b/bin/strata-cli/src/seed.rs index eaeef6ff0..d8f00ba93 100644 --- a/bin/strata-cli/src/seed.rs +++ b/bin/strata-cli/src/seed.rs @@ -265,9 +265,25 @@ pub mod password; #[cfg(test)] mod test { use rand::rngs::OsRng; + use sha2::digest::generic_array::GenericArray; use super::*; + #[test] + // Sanity check on private key construction + fn invalid_keys() { + // The key can't be zero + assert!(PrivateKeySigner::from_field_bytes(GenericArray::from_slice(&[0u8; 32])).is_err()); + + // The key can be within the group order + assert!(PrivateKeySigner::from_field_bytes(GenericArray::from_slice(&[1u8; 32])).is_ok()); + + // The key can't exceed the group order + assert!( + PrivateKeySigner::from_field_bytes(GenericArray::from_slice(&[u8::MAX; 32])).is_err() + ); + } + #[test] // Test valid seed encryption and decryption fn seed_encrypt_decrypt() { From 1a2c536925735226f4623de02232268b6aca25f5 Mon Sep 17 00:00:00 2001 From: Aaron Feickert <66188213+AaronFeickert@users.noreply.github.com> Date: Wed, 16 Oct 2024 08:20:25 -0500 Subject: [PATCH 11/18] Improve scalar tests --- bin/strata-cli/src/seed.rs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/bin/strata-cli/src/seed.rs b/bin/strata-cli/src/seed.rs index d8f00ba93..35a303cf9 100644 --- a/bin/strata-cli/src/seed.rs +++ b/bin/strata-cli/src/seed.rs @@ -270,18 +270,36 @@ mod test { use super::*; #[test] - // Sanity check on private key construction - fn invalid_keys() { - // The key can't be zero + // Sanity checks on curve scalar construction, to ensure proper rejection + // This treats zero as invalid (for ECDSA reasons) + fn scalar_sanity_checks() { + // This is the (big-endian) order of the `secp256k1` curve group + // You can find it in, for example, section 2.4.1 of https://www.secg.org/sec2-v2.pdf + let mut order: [u8; 32] = [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C, + 0xD0, 0x36, 0x41, 0x41, + ]; + + // The scalar can't be zero assert!(PrivateKeySigner::from_field_bytes(GenericArray::from_slice(&[0u8; 32])).is_err()); - // The key can be within the group order + // The scalar can be well within the group order assert!(PrivateKeySigner::from_field_bytes(GenericArray::from_slice(&[1u8; 32])).is_ok()); - // The key can't exceed the group order + // The scalar can't equal the group order + assert!(PrivateKeySigner::from_field_bytes(GenericArray::from_slice(&order)).is_err()); + + // The scalar can't exceed the group order + order[31] = 0x42; + assert!(PrivateKeySigner::from_field_bytes(GenericArray::from_slice(&order)).is_err()); assert!( PrivateKeySigner::from_field_bytes(GenericArray::from_slice(&[u8::MAX; 32])).is_err() ); + + // The scalar can be _just_ under the group order + order[31] = 0x40; + assert!(PrivateKeySigner::from_field_bytes(GenericArray::from_slice(&order)).is_ok()); } #[test] From afb6125a383739768159005121be0349a4f7dc37 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 17 Oct 2024 11:23:16 -0300 Subject: [PATCH 12/18] ci: update to bitcoin 28.0 (#404) * ci: update to bitcoin 28.0 * fix(btcio): non-breaking changes in GetBlockChainInfo Shouldn't be a breaking change for us, since we are not using any of the changed stuff. See: https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-28.0.md#updated-rpcs --- .github/workflows/functional.yml | 7 +++--- crates/btcio/src/rpc/client.rs | 6 ++--- crates/btcio/src/rpc/traits.rs | 5 ++-- crates/btcio/src/rpc/types.rs | 43 ++++++++++++++++++++++++++++++++ crates/btcio/src/test_utils.rs | 9 ++----- 5 files changed, 53 insertions(+), 17 deletions(-) diff --git a/.github/workflows/functional.yml b/.github/workflows/functional.yml index c6d9eb6a6..ff868a8d6 100644 --- a/.github/workflows/functional.yml +++ b/.github/workflows/functional.yml @@ -8,7 +8,6 @@ on: env: CARGO_TERM_COLOR: always - jobs: lint: name: Lint test files @@ -55,11 +54,11 @@ jobs: - name: Install bitcoind env: - BITCOIND_VERSION: "27.0" + BITCOIND_VERSION: "28.0" BITCOIND_ARCH: "x86_64-linux-gnu" - SHASUM: "2a6974c5486f528793c79d42694b5987401e4a43c97f62b1383abf35bcee44a8" + SHASUM: "7fe294b02b25b51acb8e8e0a0eb5af6bbafa7cd0c5b0e5fcbb61263104a82fbc" run: | - wget -q "https://bitcoin.org/bin/bitcoin-core-${{ env.BITCOIND_VERSION }}/bitcoin-${{ env.BITCOIND_VERSION }}-${{ env.BITCOIND_ARCH }}.tar.gz" + curl -fsSLO --proto "=https" --tlsv1.2 "https://bitcoincore.org/bin/bitcoin-core-${{ env.BITCOIND_VERSION }}/bitcoin-${{ env.BITCOIND_VERSION }}-${{ env.BITCOIND_ARCH }}.tar.gz" sha256sum -c <<< "$SHASUM bitcoin-${{ env.BITCOIND_VERSION }}-${{ env.BITCOIND_ARCH }}.tar.gz" tar xzf "bitcoin-${{ env.BITCOIND_VERSION }}-${{ env.BITCOIND_ARCH }}.tar.gz" sudo install -m 0755 -t /usr/local/bin bitcoin-${{ env.BITCOIND_VERSION }}/bin/* diff --git a/crates/btcio/src/rpc/client.rs b/crates/btcio/src/rpc/client.rs index a271747d4..6f8f7ff55 100644 --- a/crates/btcio/src/rpc/client.rs +++ b/crates/btcio/src/rpc/client.rs @@ -11,7 +11,7 @@ use bitcoin::{ bip32::Xpriv, consensus::encode::serialize_hex, Address, Block, BlockHash, Network, Transaction, Txid, }; -use bitcoind_json_rpc_types::v26::{GetBlockVerbosityZero, GetBlockchainInfo, GetNewAddress}; +use bitcoind_json_rpc_types::v26::{GetBlockVerbosityZero, GetNewAddress}; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, Client, @@ -28,8 +28,8 @@ use crate::rpc::{ error::{BitcoinRpcError, ClientError}, traits::{Broadcaster, Reader, Signer, Wallet}, types::{ - CreateWallet, GetTransaction, ImportDescriptor, ImportDescriptorResult, ListDescriptors, - ListTransactions, ListUnspent, SignRawTransactionWithWallet, + CreateWallet, GetBlockchainInfo, GetTransaction, ImportDescriptor, ImportDescriptorResult, + ListDescriptors, ListTransactions, ListUnspent, SignRawTransactionWithWallet, }, }; diff --git a/crates/btcio/src/rpc/traits.rs b/crates/btcio/src/rpc/traits.rs index 4468e0f9c..5a05222a9 100644 --- a/crates/btcio/src/rpc/traits.rs +++ b/crates/btcio/src/rpc/traits.rs @@ -1,12 +1,11 @@ use async_trait::async_trait; use bitcoin::{bip32::Xpriv, Address, Block, BlockHash, Network, Transaction, Txid}; -use bitcoind_json_rpc_types::v26::GetBlockchainInfo; use crate::rpc::{ client::ClientResult, types::{ - GetTransaction, ImportDescriptor, ImportDescriptorResult, ListTransactions, ListUnspent, - SignRawTransactionWithWallet, + GetBlockchainInfo, GetTransaction, ImportDescriptor, ImportDescriptorResult, + ListTransactions, ListUnspent, SignRawTransactionWithWallet, }, }; diff --git a/crates/btcio/src/rpc/types.rs b/crates/btcio/src/rpc/types.rs index 317e99cdf..59d4e8692 100644 --- a/crates/btcio/src/rpc/types.rs +++ b/crates/btcio/src/rpc/types.rs @@ -35,6 +35,49 @@ pub enum TransactionCategory { Orphan, } +/// Result of JSON-RPC method `getblockchaininfo`. +/// +/// Method call: `getblockchaininfo` +/// +/// > Returns an object containing various state info regarding blockchain processing. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetBlockchainInfo { + /// Current network name as defined in BIP70 (main, test, signet, regtest). + pub chain: String, + /// The current number of blocks processed in the server. + pub blocks: u64, + /// The current number of headers we have validated. + pub headers: u64, + /// The hash of the currently best block. + #[serde(rename = "bestblockhash")] + pub best_block_hash: String, + /// The current difficulty. + pub difficulty: f64, + /// Median time for the current best block. + #[serde(rename = "mediantime")] + pub median_time: u64, + /// Estimate of verification progress (between 0 and 1). + #[serde(rename = "verificationprogress")] + pub verification_progress: f64, + /// Estimate of whether this node is in Initial Block Download (IBD) mode. + #[serde(rename = "initialblockdownload")] + pub initial_block_download: bool, + /// Total amount of work in active chain, in hexadecimal. + #[serde(rename = "chainwork")] + pub chain_work: String, + /// The estimated size of the block and undo files on disk. + pub size_on_disk: u64, + /// If the blocks are subject to pruning. + pub pruned: bool, + /// Lowest-height complete block stored (only present if pruning is enabled). + #[serde(rename = "pruneheight")] + pub prune_height: Option, + /// Whether automatic pruning is enabled (only present if pruning is enabled). + pub automatic_pruning: Option, + /// The target size used by pruning (only present if automatic pruning is enabled). + pub prune_target_size: Option, +} + /// Models the result of JSON-RPC method `listunspent`. /// /// # Note diff --git a/crates/btcio/src/test_utils.rs b/crates/btcio/src/test_utils.rs index e157f51a3..4b091b7a1 100644 --- a/crates/btcio/src/test_utils.rs +++ b/crates/btcio/src/test_utils.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use async_trait::async_trait; use bitcoin::{ bip32::Xpriv, @@ -8,15 +6,14 @@ use bitcoin::{ taproot::ControlBlock, Address, Amount, Block, BlockHash, Network, ScriptBuf, SignedAmount, Transaction, Txid, Work, }; -use bitcoind_json_rpc_types::v26::GetBlockchainInfo; use strata_state::tx::InscriptionData; use crate::{ rpc::{ traits::{Broadcaster, Reader, Signer, Wallet}, types::{ - GetTransaction, ImportDescriptor, ImportDescriptorResult, ListTransactions, - ListUnspent, SignRawTransactionWithWallet, + GetBlockchainInfo, GetTransaction, ImportDescriptor, ImportDescriptorResult, + ListTransactions, ListUnspent, SignRawTransactionWithWallet, }, ClientResult, }, @@ -94,8 +91,6 @@ impl Reader for TestBitcoinClient { prune_height: None, automatic_pruning: None, prune_target_size: None, - softforks: BTreeMap::new(), - warnings: "".to_string(), }) } From f11f7d5107593a2313a2b59d6203f211b37b0d29 Mon Sep 17 00:00:00 2001 From: Zk2u Date: Thu, 17 Oct 2024 15:43:37 +0100 Subject: [PATCH 13/18] fix(cli): strata drain command (#420) --- bin/strata-cli/src/cmd/drain.rs | 34 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/bin/strata-cli/src/cmd/drain.rs b/bin/strata-cli/src/cmd/drain.rs index f92955cf7..03e582ce6 100644 --- a/bin/strata-cli/src/cmd/drain.rs +++ b/bin/strata-cli/src/cmd/drain.rs @@ -2,13 +2,14 @@ use std::str::FromStr; use alloy::{ primitives::{Address as StrataAddress, U256}, - providers::{utils::Eip1559Estimation, Provider, WalletProvider}, + providers::{Provider, WalletProvider}, }; use argh::FromArgs; use bdk_wallet::bitcoin::{Address, Amount}; use console::{style, Term}; use crate::{ + constants::SATS_TO_WEI, seed::Seed, settings::Settings, signet::{get_fee_rate, log_fee_rate, print_explorer_url, EsploraClient, SignetWallet}, @@ -85,6 +86,7 @@ pub async fn drain( let tx = psbt.extract_tx().expect("fully signed tx"); esplora.broadcast(&tx).await.unwrap(); let _ = print_explorer_url(&tx.compute_txid(), &term, &settings); + let _ = term.write_line(&format!("Drained signet wallet to {}", address,)); } if let Some(address) = strata_address { @@ -93,31 +95,27 @@ pub async fn drain( if balance == U256::ZERO { let _ = term.write_line("No Strata bitcoin to send"); } - let Eip1559Estimation { - max_fee_per_gas, - max_priority_fee_per_gas, - } = l2w.estimate_eip1559_fees(None).await.unwrap(); let estimate_tx = l2w .transaction_request() + .from(l2w.default_signer_address()) .to(address) - .value(U256::from(1)) - .max_fee_per_gas(max_fee_per_gas) - .max_priority_fee_per_gas(max_priority_fee_per_gas); + .value(U256::from(1)); - let gas_limit = l2w.estimate_gas(&estimate_tx).await.unwrap(); + let gas_price = l2w.get_gas_price().await.unwrap(); + let gas_estimate = l2w.estimate_gas(&estimate_tx).await.unwrap(); - let max_gas_fee = gas_limit * max_fee_per_gas; - let max_send_amount = balance.saturating_sub(U256::from(max_gas_fee)); + let total_fee = gas_estimate * gas_price; + let max_send_amount = balance.saturating_sub(U256::from(total_fee)); - let tx = l2w - .transaction_request() - .to(address) - .value(max_send_amount) - .gas_limit(gas_limit) - .max_fee_per_gas(max_fee_per_gas) - .max_priority_fee_per_gas(max_priority_fee_per_gas); + let tx = l2w.transaction_request().to(address).value(max_send_amount); let _ = l2w.send_transaction(tx).await.unwrap(); + + let _ = term.write_line(&format!( + "Drained {} from Strata wallet to {}", + Amount::from_sat((max_send_amount / U256::from(SATS_TO_WEI)).wrapping_to()), + address, + )); } } From 55eac20491a90d02d3c7b031bfcc590613b6384b Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Tue, 22 Oct 2024 08:24:39 -0400 Subject: [PATCH 14/18] refactor(btcio): remove bitcoind_json_rpc_types It does not make sense to have this dep since we're already half-baking our own custom types on top of it and using only a tiny minority of the types as being the ones provided by bitcoind_json_rpc_types. --- Cargo.lock | 13 ---- Cargo.toml | 1 - crates/btcio/Cargo.toml | 1 - crates/btcio/src/rpc/client.rs | 6 +- crates/btcio/src/rpc/types.rs | 127 ++++++++++++++++++++++++++++++--- 5 files changed, 119 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49922e327..316ae9fc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2157,18 +2157,6 @@ dependencies = [ "zip", ] -[[package]] -name = "bitcoind-json-rpc-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80270bb74df085641b0acab8ee00c42eb531cb7ed869bfb0d9ed37f7fb23c230" -dependencies = [ - "bitcoin", - "bitcoin-internals", - "serde", - "serde_json", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -12735,7 +12723,6 @@ dependencies = [ "base64 0.22.1", "bitcoin", "bitcoind", - "bitcoind-json-rpc-types", "borsh", "bytes", "hex", diff --git a/Cargo.toml b/Cargo.toml index b394dab96..a3c1170e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,7 +122,6 @@ base64 = "0.22.1" bincode = "1.3.3" bitcoin = { version = "=0.32.1", features = ["serde"] } bitcoind = { version = "0.36.0", features = ["26_0"] } -bitcoind-json-rpc-types = "0.3.0" borsh = { version = "1.5.0", features = ["derive"] } bytes = "1.6.0" chrono = "0.4.38" diff --git a/crates/btcio/Cargo.toml b/crates/btcio/Cargo.toml index 5edf02ff5..9e846c114 100644 --- a/crates/btcio/Cargo.toml +++ b/crates/btcio/Cargo.toml @@ -17,7 +17,6 @@ anyhow.workspace = true async-trait.workspace = true base64.workspace = true bitcoin.workspace = true -bitcoind-json-rpc-types.workspace = true borsh.workspace = true bytes.workspace = true hex.workspace = true diff --git a/crates/btcio/src/rpc/client.rs b/crates/btcio/src/rpc/client.rs index 6f8f7ff55..4667fee2e 100644 --- a/crates/btcio/src/rpc/client.rs +++ b/crates/btcio/src/rpc/client.rs @@ -11,7 +11,6 @@ use bitcoin::{ bip32::Xpriv, consensus::encode::serialize_hex, Address, Block, BlockHash, Network, Transaction, Txid, }; -use bitcoind_json_rpc_types::v26::{GetBlockVerbosityZero, GetNewAddress}; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, Client, @@ -28,8 +27,9 @@ use crate::rpc::{ error::{BitcoinRpcError, ClientError}, traits::{Broadcaster, Reader, Signer, Wallet}, types::{ - CreateWallet, GetBlockchainInfo, GetTransaction, ImportDescriptor, ImportDescriptorResult, - ListDescriptors, ListTransactions, ListUnspent, SignRawTransactionWithWallet, + CreateWallet, GetBlockVerbosityZero, GetBlockchainInfo, GetNewAddress, GetTransaction, + ImportDescriptor, ImportDescriptorResult, ListDescriptors, ListTransactions, ListUnspent, + SignRawTransactionWithWallet, }, }; diff --git a/crates/btcio/src/rpc/types.rs b/crates/btcio/src/rpc/types.rs index 59d4e8692..fb78060af 100644 --- a/crates/btcio/src/rpc/types.rs +++ b/crates/btcio/src/rpc/types.rs @@ -1,8 +1,9 @@ use bitcoin::{ - absolute::Height, address::NetworkUnchecked, consensus, Address, Amount, BlockHash, - SignedAmount, Transaction, Txid, + absolute::Height, + address::{self, NetworkUnchecked}, + consensus::{self, encode}, + Address, Amount, Block, BlockHash, SignedAmount, Transaction, Txid, }; -use bitcoind_json_rpc_types::v26::GetTransactionDetail; use serde::{ de::{self, IntoDeserializer, Visitor}, Deserialize, Deserializer, Serialize, @@ -78,6 +79,118 @@ pub struct GetBlockchainInfo { pub prune_target_size: Option, } +/// Result of JSON-RPC method `getblock` with verbosity set to 0. +/// +/// A string that is serialized, hex-encoded data for block 'hash'. +/// +/// Method call: `getblock "blockhash" ( verbosity )` +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct GetBlockVerbosityZero(pub String); + +impl GetBlockVerbosityZero { + /// Converts json straight to a [`Block`]. + pub fn block(self) -> Result { + let block: Block = encode::deserialize_hex(&self.0)?; + Ok(block) + } +} + +/// Result of JSON-RPC method `getblock` with verbosity set to 1. +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct GetBlockVerbosityOne { + /// The block hash (same as provided) in RPC call. + pub hash: String, + /// The number of confirmations, or -1 if the block is not on the main chain. + pub confirmations: i32, + /// The block size. + pub size: usize, + /// The block size excluding witness data. + #[serde(rename = "strippedsize")] + pub stripped_size: Option, + /// The block weight as defined in BIP-141. + pub weight: u64, + /// The block height or index. + pub height: usize, + /// The block version. + pub version: i32, + /// The block version formatted in hexadecimal. + #[serde(rename = "versionHex")] + pub version_hex: String, + /// The merkle root + #[serde(rename = "merkleroot")] + pub merkle_root: String, + /// The transaction ids + pub tx: Vec, + /// The block time expressed in UNIX epoch time. + pub time: usize, + /// The median block time expressed in UNIX epoch time. + #[serde(rename = "mediantime")] + pub median_time: Option, + /// The nonce + pub nonce: u32, + /// The bits. + pub bits: String, + /// The difficulty. + pub difficulty: f64, + /// Expected number of hashes required to produce the chain up to this block (in hex). + #[serde(rename = "chainwork")] + pub chain_work: String, + /// The number of transactions in the block. + #[serde(rename = "nTx")] + pub n_tx: u32, + /// The hash of the previous block (if available). + #[serde(rename = "previousblockhash")] + pub previous_block_hash: Option, + /// The hash of the next block (if available). + #[serde(rename = "nextblockhash")] + pub next_block_hash: Option, +} + +/// Result of JSON-RPC method `gettxout`. +/// +/// # Note +/// +/// This assumes that the UTXOs are present in the underlying Bitcoin +/// client's wallet. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetTransactionDetail { + pub address: String, + pub category: GetTransactionDetailCategory, + pub amount: f64, + pub label: Option, + pub vout: u32, + pub fee: Option, + pub abandoned: Option, +} + +/// Enum to represent the category of a transaction. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum GetTransactionDetailCategory { + Send, + Receive, + Generate, + Immature, + Orphan, +} + +/// Result of the JSON-RPC method `getnewaddress`. +/// +/// # Note +/// +/// This assumes that the UTXOs are present in the underlying Bitcoin +/// client's wallet. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetNewAddress(pub String); + +impl GetNewAddress { + /// Converts json straight to a [`Address`]. + pub fn address(self) -> Result, address::ParseError> { + let address = self.0.parse::>()?; + Ok(address) + } +} + /// Models the result of JSON-RPC method `listunspent`. /// /// # Note @@ -88,8 +201,6 @@ pub struct GetBlockchainInfo { /// Careful with the amount field. It is a [`SignedAmount`], hence can be negative. /// Negative amounts for the [`TransactionCategory::Send`], and is positive /// for all other categories. -/// -/// We can upstream this to [`bitcoind_json_rpc_types`]. #[derive(Clone, Debug, PartialEq, Deserialize)] pub struct GetTransaction { /// The signed amount in BTC. @@ -140,8 +251,6 @@ impl GetTransaction { /// /// This assumes that the UTXOs are present in the underlying Bitcoin /// client's wallet. -/// -/// We can upstream this to [`bitcoind_json_rpc_types`]. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct ListUnspent { /// The transaction id. @@ -183,8 +292,6 @@ pub struct ListUnspent { /// Careful with the amount field. It is a [`SignedAmount`], hence can be negative. /// Negative amounts for the [`TransactionCategory::Send`], and is positive /// for all other categories. -/// -/// We can upstream this to [`bitcoind_json_rpc_types`]. #[derive(Clone, Debug, PartialEq, Deserialize)] pub struct ListTransactions { /// The Bitcoin address. @@ -216,8 +323,6 @@ pub struct ListTransactions { /// /// This assumes that the transactions are present in the underlying Bitcoin /// client's wallet. -/// -/// We can upstream this to [`bitcoind_json_rpc_types`]. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct SignRawTransactionWithWallet { /// The Transaction ID. From bbe0138ad11f3e4412f972ab722b0f0fd555599e Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Tue, 22 Oct 2024 15:28:00 -0400 Subject: [PATCH 15/18] chore: bump bitcoin to 0.32.3 (#425) --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 316ae9fc9..459113915 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2056,9 +2056,9 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitcoin" -version = "0.32.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf33434c870e98ecc8608588ccc990c5daba9ba9ad39733dc85fba22c211504" +checksum = "0032b0e8ead7074cda7fc4f034409607e3f03a6f71d66ade8a307f79b4d99e73" dependencies = [ "base58ck", "base64 0.21.7", diff --git a/Cargo.toml b/Cargo.toml index a3c1170e5..72ae1678f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,7 +120,7 @@ argh = "0.1" async-trait = "0.1.80" base64 = "0.22.1" bincode = "1.3.3" -bitcoin = { version = "=0.32.1", features = ["serde"] } +bitcoin = { version = "=0.32.3", features = ["serde"] } bitcoind = { version = "0.36.0", features = ["26_0"] } borsh = { version = "1.5.0", features = ["derive"] } bytes = "1.6.0" From 902045dd32e1e331e2578275963c55e061c1081f Mon Sep 17 00:00:00 2001 From: Zk2u Date: Fri, 25 Oct 2024 11:10:47 +0100 Subject: [PATCH 16/18] fix:(cli) default to broadcast min when esplora doesn't return a fee --- bin/strata-cli/src/signet.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bin/strata-cli/src/signet.rs b/bin/strata-cli/src/signet.rs index 579cc4a7e..755eef6a3 100644 --- a/bin/strata-cli/src/signet.rs +++ b/bin/strata-cli/src/signet.rs @@ -40,8 +40,6 @@ pub fn print_explorer_url(txid: &Txid, term: &Term, settings: &Settings) -> Resu pub enum FeeRateError { InvalidDueToOverflow, BelowBroadcastMin, - /// Esplora didn't have a fee for the requested target - FeeMissing, EsploraError(esplora_client::Error), } @@ -59,7 +57,7 @@ pub async fn get_fee_rate( .map(|frs| frs.get(&target).cloned()) .map_err(FeeRateError::EsploraError)? .and_then(|fr| FeeRate::from_sat_per_vb(fr as u64)) - .ok_or(FeeRateError::FeeMissing)? + .unwrap_or(FeeRate::BROADCAST_MIN) }; if fee_rate < FeeRate::BROADCAST_MIN { From 63f49d2747e323a77976e2edc9db5d8700620d4a Mon Sep 17 00:00:00 2001 From: Zk2u Date: Mon, 28 Oct 2024 12:44:40 +0000 Subject: [PATCH 17/18] feat(cli): ability to use bitcoin core as backend for signet wallet (#423) * feat: bitcoind wallet syncing * feat: feerate + broadcast via `SyncBackend` * feat:(cli) `SignetBackend` * feat:(cli) `spawn_bitcoin_core` wrapper * feat:(cli) trait based `SignetBackend` * refactor:(cli) update `SignetBackend` to not use `&mut Wallet` * doc: fix doclink --------- Co-authored-by: Jose Storopoli --- Cargo.lock | 2 + bin/strata-cli/Cargo.toml | 2 + bin/strata-cli/src/cmd/balance.rs | 9 +- bin/strata-cli/src/cmd/deposit.rs | 18 +- bin/strata-cli/src/cmd/drain.rs | 18 +- bin/strata-cli/src/cmd/faucet.rs | 7 +- bin/strata-cli/src/cmd/receive.rs | 10 +- bin/strata-cli/src/cmd/recover.rs | 27 ++- bin/strata-cli/src/cmd/scan.rs | 13 +- bin/strata-cli/src/cmd/send.rs | 20 +- bin/strata-cli/src/cmd/withdraw.rs | 3 +- bin/strata-cli/src/main.rs | 17 +- bin/strata-cli/src/settings.rs | 40 +++- bin/strata-cli/src/signet.rs | 284 +++++++++-------------- bin/strata-cli/src/signet/backend.rs | 328 +++++++++++++++++++++++++++ bin/strata-cli/src/signet/persist.rs | 63 +++++ 16 files changed, 611 insertions(+), 250 deletions(-) create mode 100644 bin/strata-cli/src/signet/backend.rs create mode 100644 bin/strata-cli/src/signet/persist.rs diff --git a/Cargo.lock b/Cargo.lock index 459113915..8db042f85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12777,6 +12777,8 @@ dependencies = [ "alloy", "argh", "argon2", + "async-trait", + "bdk_bitcoind_rpc", "bdk_esplora", "bdk_wallet", "bip39", diff --git a/bin/strata-cli/Cargo.toml b/bin/strata-cli/Cargo.toml index 8d1f308d4..349568c27 100644 --- a/bin/strata-cli/Cargo.toml +++ b/bin/strata-cli/Cargo.toml @@ -23,6 +23,8 @@ alloy = { version = "0.3.5", features = [ ] } argh.workspace = true argon2 = "0.5.3" +async-trait.workspace = true +bdk_bitcoind_rpc = "0.16.0" bdk_esplora = { version = "0.19.0", features = [ "async-https", "async-https-rustls", diff --git a/bin/strata-cli/src/cmd/balance.rs b/bin/strata-cli/src/cmd/balance.rs index 4c63adf52..1a4c06c5a 100644 --- a/bin/strata-cli/src/cmd/balance.rs +++ b/bin/strata-cli/src/cmd/balance.rs @@ -11,7 +11,7 @@ use crate::{ net_type::{net_type_or_exit, NetworkType}, seed::Seed, settings::Settings, - signet::{EsploraClient, SignetWallet}, + signet::SignetWallet, strata::StrataWallet, }; @@ -24,13 +24,14 @@ pub struct BalanceArgs { network_type: String, } -pub async fn balance(args: BalanceArgs, seed: Seed, settings: Settings, esplora: EsploraClient) { +pub async fn balance(args: BalanceArgs, seed: Seed, settings: Settings) { let term = Term::stdout(); let network_type = net_type_or_exit(&args.network_type, &term); if let NetworkType::Signet = network_type { - let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); - l1w.sync(&esplora).await.unwrap(); + let mut l1w = + SignetWallet::new(&seed, settings.network, settings.signet_backend.clone()).unwrap(); + l1w.sync().await.unwrap(); let balance = l1w.balance(); let _ = term.write_line(&format!("Total: {}", balance.total())); let _ = term.write_line(&format!(" Confirmed: {}", balance.confirmed)); diff --git a/bin/strata-cli/src/cmd/deposit.rs b/bin/strata-cli/src/cmd/deposit.rs index 5eed60e10..e7fe885e6 100644 --- a/bin/strata-cli/src/cmd/deposit.rs +++ b/bin/strata-cli/src/cmd/deposit.rs @@ -21,7 +21,7 @@ use crate::{ recovery::DescriptorRecovery, seed::Seed, settings::Settings, - signet::{get_fee_rate, log_fee_rate, print_explorer_url, EsploraClient, SignetWallet}, + signet::{get_fee_rate, log_fee_rate, print_explorer_url, SignetWallet}, strata::StrataWallet, taproot::{ExtractP2trPubkey, NotTaprootAddress}, }; @@ -46,15 +46,15 @@ pub async fn deposit( }: DepositArgs, seed: Seed, settings: Settings, - esplora: EsploraClient, ) { let term = Term::stdout(); let requested_strata_address = strata_address.map(|a| StrataAddress::from_str(&a).expect("bad strata address")); - let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); + let mut l1w = + SignetWallet::new(&seed, settings.network, settings.signet_backend.clone()).unwrap(); let l2w = StrataWallet::new(&seed, &settings.strata_endpoint).unwrap(); - l1w.sync(&esplora).await.unwrap(); + l1w.sync().await.unwrap(); let recovery_address = l1w.reveal_next_address(KeychainKind::External).address; l1w.persist().unwrap(); @@ -101,9 +101,7 @@ pub async fn deposit( style(bridge_in_address.to_string()).yellow() )); - let fee_rate = get_fee_rate(fee_rate, &esplora, 1) - .await - .expect("valid fee rate"); + let fee_rate = get_fee_rate(fee_rate, settings.signet_backend.as_ref()).await; log_fee_rate(&term, &fee_rate); const MBL: usize = MAGIC_BYTES.len(); @@ -143,7 +141,11 @@ pub async fn deposit( let pb = ProgressBar::new_spinner().with_message("Broadcasting transaction"); pb.enable_steady_tick(Duration::from_millis(100)); - esplora.broadcast(&tx).await.expect("successful broadcast"); + settings + .signet_backend + .broadcast_tx(&tx) + .await + .expect("successful broadcast"); let txid = tx.compute_txid(); pb.finish_with_message(format!("Transaction {} broadcasted", txid)); let _ = print_explorer_url(&txid, &term, &settings); diff --git a/bin/strata-cli/src/cmd/drain.rs b/bin/strata-cli/src/cmd/drain.rs index 03e582ce6..be2f90518 100644 --- a/bin/strata-cli/src/cmd/drain.rs +++ b/bin/strata-cli/src/cmd/drain.rs @@ -12,7 +12,7 @@ use crate::{ constants::SATS_TO_WEI, seed::Seed, settings::Settings, - signet::{get_fee_rate, log_fee_rate, print_explorer_url, EsploraClient, SignetWallet}, + signet::{get_fee_rate, log_fee_rate, print_explorer_url, SignetWallet}, strata::StrataWallet, }; @@ -42,7 +42,6 @@ pub async fn drain( }: DrainArgs, seed: Seed, settings: Settings, - esplora: EsploraClient, ) { let term = Term::stdout(); if strata_address.is_none() && signet_address.is_none() { @@ -59,8 +58,9 @@ pub async fn drain( strata_address.map(|a| StrataAddress::from_str(&a).expect("valid Strata address")); if let Some(address) = signet_address { - let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); - l1w.sync(&esplora).await.unwrap(); + let mut l1w = + SignetWallet::new(&seed, settings.network, settings.signet_backend.clone()).unwrap(); + l1w.sync().await.unwrap(); let balance = l1w.balance(); if balance.untrusted_pending > Amount::ZERO { let _ = term.write_line( @@ -69,22 +69,20 @@ pub async fn drain( .to_string(), ); } - let fr = get_fee_rate(fee_rate, &esplora, 1) - .await - .expect("valid fee rate"); - log_fee_rate(&term, &fr); + let fee_rate = get_fee_rate(fee_rate, settings.signet_backend.as_ref()).await; + log_fee_rate(&term, &fee_rate); let mut psbt = l1w .build_tx() .drain_wallet() .drain_to(address.script_pubkey()) - .fee_rate(fr) + .fee_rate(fee_rate) .clone() .finish() .expect("valid transaction"); l1w.sign(&mut psbt, Default::default()).unwrap(); let tx = psbt.extract_tx().expect("fully signed tx"); - esplora.broadcast(&tx).await.unwrap(); + settings.signet_backend.broadcast_tx(&tx).await.unwrap(); let _ = print_explorer_url(&tx.compute_txid(), &term, &settings); let _ = term.write_line(&format!("Drained signet wallet to {}", address,)); } diff --git a/bin/strata-cli/src/cmd/faucet.rs b/bin/strata-cli/src/cmd/faucet.rs index 8b732c21b..8ba4d3d50 100644 --- a/bin/strata-cli/src/cmd/faucet.rs +++ b/bin/strata-cli/src/cmd/faucet.rs @@ -95,7 +95,9 @@ pub async fn faucet(args: FaucetArgs, seed: Seed, settings: Settings) { #[cfg(feature = "strata_faucet")] let url = match network_type { NetworkType::Signet => { - let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); + let mut l1w = + SignetWallet::new(&seed, settings.network, settings.signet_backend.clone()) + .unwrap(); let address = match args.address { None => { let address_info = l1w.reveal_next_address(KeychainKind::External); @@ -136,7 +138,8 @@ pub async fn faucet(args: FaucetArgs, seed: Seed, settings: Settings) { #[cfg(not(feature = "strata_faucet"))] let url = { - let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); + let mut l1w = + SignetWallet::new(&seed, settings.network, settings.signet_backend.clone()).unwrap(); let address = match args.address { None => { let address_info = l1w.reveal_next_address(KeychainKind::External); diff --git a/bin/strata-cli/src/cmd/receive.rs b/bin/strata-cli/src/cmd/receive.rs index eac15c791..7895b293f 100644 --- a/bin/strata-cli/src/cmd/receive.rs +++ b/bin/strata-cli/src/cmd/receive.rs @@ -7,7 +7,7 @@ use crate::{ net_type::{net_type_or_exit, NetworkType}, seed::Seed, settings::Settings, - signet::{EsploraClient, SignetWallet}, + signet::SignetWallet, strata::StrataWallet, }; @@ -20,15 +20,17 @@ pub struct ReceiveArgs { network_type: String, } -pub async fn receive(args: ReceiveArgs, seed: Seed, settings: Settings, esplora: EsploraClient) { +pub async fn receive(args: ReceiveArgs, seed: Seed, settings: Settings) { let term = Term::stdout(); let network_type = net_type_or_exit(&args.network_type, &term); let address = match network_type { NetworkType::Signet => { - let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); + let mut l1w = + SignetWallet::new(&seed, settings.network, settings.signet_backend.clone()) + .unwrap(); let _ = term.write_line("Syncing signet wallet"); - l1w.sync(&esplora).await.unwrap(); + l1w.sync().await.unwrap(); let _ = term.write_line("Wallet synced"); let address_info = l1w.reveal_next_address(KeychainKind::External); l1w.persist().unwrap(); diff --git a/bin/strata-cli/src/cmd/recover.rs b/bin/strata-cli/src/cmd/recover.rs index 1b7ee0812..eef7621ce 100644 --- a/bin/strata-cli/src/cmd/recover.rs +++ b/bin/strata-cli/src/cmd/recover.rs @@ -1,5 +1,4 @@ use argh::FromArgs; -use bdk_esplora::EsploraAsyncExt; use bdk_wallet::{ bitcoin::Amount, chain::ChainOracle, descriptor::IntoWalletDescriptor, KeychainKind, Wallet, }; @@ -10,7 +9,7 @@ use crate::{ recovery::DescriptorRecovery, seed::Seed, settings::Settings, - signet::{get_fee_rate, log_fee_rate, EsploraClient, SignetWallet}, + signet::{get_fee_rate, log_fee_rate, sync_wallet, SignetWallet}, }; /// Attempt recovery of old deposit transactions @@ -22,10 +21,11 @@ pub struct RecoverArgs { fee_rate: Option, } -pub async fn recover(args: RecoverArgs, seed: Seed, settings: Settings, esplora: EsploraClient) { +pub async fn recover(args: RecoverArgs, seed: Seed, settings: Settings) { let term = Term::stdout(); - let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); - l1w.sync(&esplora).await.unwrap(); + let mut l1w = + SignetWallet::new(&seed, settings.network, settings.signet_backend.clone()).unwrap(); + l1w.sync().await.unwrap(); let _ = term.write_line("Opening descriptor recovery"); let mut descriptor_file = DescriptorRecovery::open(&seed, &settings.descriptor_db) @@ -43,9 +43,7 @@ pub async fn recover(args: RecoverArgs, seed: Seed, settings: Settings, esplora: return; } - let fee_rate = get_fee_rate(args.fee_rate, &esplora, 1) - .await - .expect("valid fee rate"); + let fee_rate = get_fee_rate(args.fee_rate, settings.signet_backend.as_ref()).await; log_fee_rate(&term, &fee_rate); for (key, desc) in descs { @@ -61,9 +59,9 @@ pub async fn recover(args: RecoverArgs, seed: Seed, settings: Settings, esplora: // reveal the address for the wallet so we can sync it let address = recovery_wallet.reveal_next_address(KeychainKind::External); - let req = recovery_wallet.start_sync_with_revealed_spks().build(); - let update = esplora.sync(req, 3).await.unwrap(); - recovery_wallet.apply_update(update).unwrap(); + sync_wallet(&mut recovery_wallet, settings.signet_backend.clone()) + .await + .expect("successful recovery wallet sync"); let needs_recovery = recovery_wallet.balance().confirmed > Amount::ZERO; if !needs_recovery { @@ -103,9 +101,10 @@ pub async fn recover(args: RecoverArgs, seed: Seed, settings: Settings, esplora: .expect("valid sign op"); let tx = psbt.extract_tx().unwrap(); - esplora - .broadcast(&tx) + settings + .signet_backend + .broadcast_tx(&tx) .await - .expect("successful tx broadcast"); + .expect("broadcast to succeed") } } diff --git a/bin/strata-cli/src/cmd/scan.rs b/bin/strata-cli/src/cmd/scan.rs index 1c765f173..51f3a12a1 100644 --- a/bin/strata-cli/src/cmd/scan.rs +++ b/bin/strata-cli/src/cmd/scan.rs @@ -1,17 +1,14 @@ use argh::FromArgs; -use crate::{ - seed::Seed, - settings::Settings, - signet::{EsploraClient, SignetWallet}, -}; +use crate::{seed::Seed, settings::Settings, signet::SignetWallet}; /// Performs a full scan of the signet wallet #[derive(FromArgs, PartialEq, Debug)] #[argh(subcommand, name = "scan")] pub struct ScanArgs {} -pub async fn scan(_args: ScanArgs, seed: Seed, settings: Settings, esplora: EsploraClient) { - let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); - l1w.scan(&esplora).await.unwrap(); +pub async fn scan(_args: ScanArgs, seed: Seed, settings: Settings) { + let mut l1w = + SignetWallet::new(&seed, settings.network, settings.signet_backend.clone()).unwrap(); + l1w.scan().await.unwrap(); } diff --git a/bin/strata-cli/src/cmd/send.rs b/bin/strata-cli/src/cmd/send.rs index bb37c19fa..b48ab3ec1 100644 --- a/bin/strata-cli/src/cmd/send.rs +++ b/bin/strata-cli/src/cmd/send.rs @@ -15,7 +15,7 @@ use crate::{ net_type::{net_type_or_exit, NetworkType}, seed::Seed, settings::Settings, - signet::{get_fee_rate, log_fee_rate, print_explorer_url, EsploraClient, SignetWallet}, + signet::{get_fee_rate, log_fee_rate, print_explorer_url, SignetWallet}, strata::StrataWallet, }; @@ -40,7 +40,7 @@ pub struct SendArgs { fee_rate: Option, } -pub async fn send(args: SendArgs, seed: Seed, settings: Settings, esplora: EsploraClient) { +pub async fn send(args: SendArgs, seed: Seed, settings: Settings) { let term = Term::stdout(); let network_type = net_type_or_exit(&args.network_type, &term); @@ -51,11 +51,11 @@ pub async fn send(args: SendArgs, seed: Seed, settings: Settings, esplora: Esplo .expect("valid address") .require_network(settings.network) .expect("correct network"); - let mut l1w = SignetWallet::new(&seed, settings.network).expect("valid wallet"); - l1w.sync(&esplora).await.unwrap(); - let fee_rate = get_fee_rate(args.fee_rate, &esplora, 1) - .await - .expect("valid fee rate"); + let mut l1w = + SignetWallet::new(&seed, settings.network, settings.signet_backend.clone()) + .expect("valid wallet"); + l1w.sync().await.unwrap(); + let fee_rate = get_fee_rate(args.fee_rate, settings.signet_backend.as_ref()).await; log_fee_rate(&term, &fee_rate); let mut psbt = l1w .build_tx() @@ -67,7 +67,11 @@ pub async fn send(args: SendArgs, seed: Seed, settings: Settings, esplora: Esplo l1w.sign(&mut psbt, Default::default()) .expect("signable psbt"); let tx = psbt.extract_tx().expect("signed tx"); - esplora.broadcast(&tx).await.expect("successful broadcast"); + settings + .signet_backend + .broadcast_tx(&tx) + .await + .expect("successful broadcast"); let _ = print_explorer_url(&tx.compute_txid(), &term, &settings); } NetworkType::Strata => { diff --git a/bin/strata-cli/src/cmd/withdraw.rs b/bin/strata-cli/src/cmd/withdraw.rs index d5a7b85f9..a30c77da6 100644 --- a/bin/strata-cli/src/cmd/withdraw.rs +++ b/bin/strata-cli/src/cmd/withdraw.rs @@ -35,7 +35,8 @@ pub async fn withdraw(args: WithdrawArgs, seed: Seed, settings: Settings) { .expect("correct network") }); - let mut l1w = SignetWallet::new(&seed, settings.network).unwrap(); + let mut l1w = + SignetWallet::new(&seed, settings.network, settings.signet_backend.clone()).unwrap(); let l2w = StrataWallet::new(&seed, &settings.strata_endpoint).unwrap(); let address = match address { diff --git a/bin/strata-cli/src/main.rs b/bin/strata-cli/src/main.rs index 4fa421467..ce0d8ae6c 100644 --- a/bin/strata-cli/src/main.rs +++ b/bin/strata-cli/src/main.rs @@ -18,7 +18,7 @@ use seed::FilePersister; #[cfg(not(target_os = "linux"))] use seed::KeychainPersister; use settings::Settings; -use signet::{set_data_dir, EsploraClient}; +use signet::persist::set_data_dir; #[tokio::main(flavor = "current_thread")] async fn main() { @@ -44,20 +44,19 @@ async fn main() { assert!(set_data_dir(settings.data_dir.clone())); let seed = seed::load_or_create(&persister).unwrap(); - let esplora = EsploraClient::new(&settings.esplora).expect("valid esplora url"); match cmd { - Commands::Recover(args) => recover(args, seed, settings, esplora).await, - Commands::Drain(args) => drain(args, seed, settings, esplora).await, - Commands::Balance(args) => balance(args, seed, settings, esplora).await, + Commands::Recover(args) => recover(args, seed, settings).await, + Commands::Drain(args) => drain(args, seed, settings).await, + Commands::Balance(args) => balance(args, seed, settings).await, Commands::Backup(args) => backup(args, seed).await, - Commands::Deposit(args) => deposit(args, seed, settings, esplora).await, + Commands::Deposit(args) => deposit(args, seed, settings).await, Commands::Withdraw(args) => withdraw(args, seed, settings).await, Commands::Faucet(args) => faucet(args, seed, settings).await, - Commands::Send(args) => send(args, seed, settings, esplora).await, - Commands::Receive(args) => receive(args, seed, settings, esplora).await, + Commands::Send(args) => send(args, seed, settings).await, + Commands::Receive(args) => receive(args, seed, settings).await, Commands::ChangePwd(args) => change_pwd(args, seed, persister).await, - Commands::Scan(args) => scan(args, seed, settings, esplora).await, + Commands::Scan(args) => scan(args, seed, settings).await, _ => {} } } diff --git a/bin/strata-cli/src/settings.rs b/bin/strata-cli/src/settings.rs index cc11c85ca..e80bceb46 100644 --- a/bin/strata-cli/src/settings.rs +++ b/bin/strata-cli/src/settings.rs @@ -3,10 +3,11 @@ use std::{ io, path::PathBuf, str::FromStr, - sync::LazyLock, + sync::{Arc, LazyLock}, }; use alloy::primitives::Address as StrataAddress; +use bdk_bitcoind_rpc::bitcoincore_rpc::{Auth, Client}; use bdk_wallet::bitcoin::{Network, XOnlyPublicKey}; use config::Config; use directories::ProjectDirs; @@ -14,11 +15,18 @@ use serde::{Deserialize, Serialize}; use shrex::{decode, Hex}; use terrors::OneOf; -use crate::constants::{BRIDGE_MUSIG2_PUBKEY, BRIDGE_STRATA_ADDRESS, DEFAULT_NETWORK}; +use crate::{ + constants::{BRIDGE_MUSIG2_PUBKEY, BRIDGE_STRATA_ADDRESS, DEFAULT_NETWORK}, + signet::{backend::SignetBackend, EsploraClient}, +}; #[derive(Serialize, Deserialize)] pub struct SettingsFromFile { - pub esplora: String, + pub esplora: Option, + pub bitcoind_rpc_user: Option, + pub bitcoind_rpc_pw: Option, + pub bitcoind_rpc_cookie: Option, + pub bitcoind_rpc_endpoint: Option, pub strata_endpoint: String, pub faucet_endpoint: String, pub mempool_endpoint: String, @@ -28,9 +36,9 @@ pub struct SettingsFromFile { /// Settings struct filled with either config values or /// opinionated defaults -#[derive(Serialize, Deserialize, Debug)] +#[derive(Debug)] pub struct Settings { - pub esplora: String, + pub esplora: Option, pub strata_endpoint: String, pub data_dir: PathBuf, pub faucet_endpoint: String, @@ -41,6 +49,7 @@ pub struct Settings { pub linux_seed_file: PathBuf, pub network: Network, pub config_file: PathBuf, + pub signet_backend: Arc, } pub static PROJ_DIRS: LazyLock = LazyLock::new(|| { @@ -72,6 +81,26 @@ impl Settings { .try_deserialize::() .map_err(OneOf::new)?; + let sync_backend: Arc = match ( + from_file.esplora.clone(), + from_file.bitcoind_rpc_user, + from_file.bitcoind_rpc_pw, + from_file.bitcoind_rpc_cookie, + from_file.bitcoind_rpc_endpoint, + ) { + (Some(url), None, None, None, None) => { + Arc::new(EsploraClient::new(&url).expect("valid esplora url")) + } + (None, Some(user), Some(pw), None, Some(url)) => Arc::new(Arc::new( + Client::new(&url, Auth::UserPass(user, pw)).expect("valid bitcoin core client"), + )), + (None, None, None, Some(cookie_file), Some(url)) => Arc::new(Arc::new( + Client::new(&url, Auth::CookieFile(cookie_file)) + .expect("valid bitcoin core client"), + )), + _ => panic!("invalid config for signet - configure for esplora or bitcoind"), + }; + Ok(Settings { esplora: from_file.esplora, strata_endpoint: from_file.strata_endpoint, @@ -93,6 +122,7 @@ impl Settings { bridge_strata_address: StrataAddress::from_str(BRIDGE_STRATA_ADDRESS) .expect("valid strata address"), linux_seed_file, + signet_backend: sync_backend, }) } } diff --git a/bin/strata-cli/src/signet.rs b/bin/strata-cli/src/signet.rs index 755eef6a3..6d0e1d3c5 100644 --- a/bin/strata-cli/src/signet.rs +++ b/bin/strata-cli/src/signet.rs @@ -1,24 +1,25 @@ +pub mod backend; +pub mod persist; + use std::{ - cell::RefCell, - collections::BTreeSet, - io, + fmt::Debug, + io::{self}, ops::{Deref, DerefMut}, path::{Path, PathBuf}, - rc::Rc, - sync::OnceLock, + sync::Arc, }; -use bdk_esplora::{ - esplora_client::{self, AsyncClient}, - EsploraAsyncExt, -}; +use backend::{ScanError, SignetBackend, SyncError, WalletUpdate}; +use bdk_esplora::esplora_client::{self, AsyncClient}; use bdk_wallet::{ bitcoin::{FeeRate, Network, Txid}, rusqlite::{self, Connection}, - ChangeSet, KeychainKind, PersistedWallet, WalletPersister, + PersistedWallet, Wallet, }; use console::{style, Term}; -use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; +use persist::Persister; +use terrors::OneOf; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; use crate::{seed::Seed, settings::Settings}; @@ -29,6 +30,20 @@ pub fn log_fee_rate(term: &Term, fr: &FeeRate) { )); } +pub async fn get_fee_rate( + user_provided_sats_per_vb: Option, + signet_backend: &dyn SignetBackend, +) -> FeeRate { + match user_provided_sats_per_vb { + Some(fr) => FeeRate::from_sat_per_vb(fr).expect("valid fee rate"), + None => signet_backend + .get_fee_rate(1) + .await + .expect("valid fee rate") + .unwrap_or(FeeRate::BROADCAST_MIN), + } +} + pub fn print_explorer_url(txid: &Txid, term: &Term, settings: &Settings) -> Result<(), io::Error> { term.write_line(&format!( "View transaction at {}", @@ -36,38 +51,7 @@ pub fn print_explorer_url(txid: &Txid, term: &Term, settings: &Settings) -> Resu )) } -#[derive(Debug)] -pub enum FeeRateError { - InvalidDueToOverflow, - BelowBroadcastMin, - EsploraError(esplora_client::Error), -} - -pub async fn get_fee_rate( - user_provided: Option, - esplora: &EsploraClient, - target: u16, -) -> Result { - let fee_rate = if let Some(fr) = user_provided { - FeeRate::from_sat_per_vb(fr).ok_or(FeeRateError::InvalidDueToOverflow)? - } else { - esplora - .get_fee_estimates() - .await - .map(|frs| frs.get(&target).cloned()) - .map_err(FeeRateError::EsploraError)? - .and_then(|fr| FeeRate::from_sat_per_vb(fr as u64)) - .unwrap_or(FeeRate::BROADCAST_MIN) - }; - - if fee_rate < FeeRate::BROADCAST_MIN { - Err(FeeRateError::BelowBroadcastMin) - } else { - Ok(fee_rate) - } -} - -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct EsploraClient(AsyncClient); impl DerefMut for EsploraClient { @@ -92,9 +76,12 @@ impl EsploraClient { } } -/// A wrapper around BDK's wallet with some custom logic #[derive(Debug)] -pub struct SignetWallet(PersistedWallet); +/// A wrapper around BDK's wallet with some custom logic +pub struct SignetWallet { + wallet: PersistedWallet, + sync_backend: Arc, +} impl SignetWallet { fn db_path(wallet: &str, data_dir: &Path) -> PathBuf { @@ -105,10 +92,15 @@ impl SignetWallet { Connection::open(Self::db_path("default", data_dir)) } - pub fn new(seed: &Seed, network: Network) -> io::Result { + pub fn new( + seed: &Seed, + network: Network, + sync_backend: Arc, + ) -> io::Result { let (load, create) = seed.signet_wallet().split(); - Ok(Self( - load.check_network(network) + Ok(Self { + wallet: load + .check_network(network) .load_wallet(&mut Persister) .expect("should be able to load wallet") .unwrap_or_else(|| { @@ -117,160 +109,98 @@ impl SignetWallet { .create_wallet(&mut Persister) .expect("wallet creation to succeed") }), - )) + sync_backend, + }) } - pub async fn sync( - &mut self, - esplora_client: &AsyncClient, - ) -> Result<(), Box> { - let term = Term::stdout(); - let _ = term.write_line("Syncing wallet..."); - let sty = ProgressStyle::with_template( - "[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}", - ) - .unwrap() - .progress_chars("##-"); - - let bar = MultiProgress::new(); - - let ops = bar.add(ProgressBar::new(1)); - ops.set_style(sty.clone()); - ops.set_message("outpoints"); - let ops2 = ops.clone(); - - let spks = bar.add(ProgressBar::new(1)); - spks.set_style(sty.clone()); - spks.set_message("script public keys"); - let spks2 = spks.clone(); - - let txids = bar.add(ProgressBar::new(1)); - txids.set_style(sty.clone()); - txids.set_message("transactions"); - let txids2 = txids.clone(); - let req = self - .start_sync_with_revealed_spks() - .inspect(move |item, progress| { - let _ = bar.println(format!("{item}")); - ops.set_length(progress.total_outpoints() as u64); - ops.set_position(progress.outpoints_consumed as u64); - spks.set_length(progress.total_spks() as u64); - spks.set_position(progress.spks_consumed as u64); - txids.set_length(progress.total_txids() as u64); - txids.set_length(progress.txids_consumed as u64); - }) - .build(); - - let update = esplora_client.sync(req, 3).await?; - ops2.finish(); - spks2.finish(); - txids2.finish(); - let _ = term.write_line("Persisting updates"); - self.apply_update(update) - .expect("should be able to connect to db"); - self.persist().expect("persist should work"); - let _ = term.write_line("Wallet synced"); + pub async fn sync(&mut self) -> Result<(), OneOf<(SyncError, rusqlite::Error)>> { + sync_wallet(&mut self.wallet, self.sync_backend.clone()).await?; + self.persist().map_err(OneOf::new)?; Ok(()) } - pub async fn scan( - &mut self, - esplora_client: &AsyncClient, - ) -> Result<(), Box> { - let bar = ProgressBar::new_spinner(); - let bar2 = bar.clone(); - let req = self - .start_full_scan() - .inspect({ - let mut once = BTreeSet::::new(); - move |keychain, spk_i, script| { - if once.insert(keychain) { - bar2.println(format!("\nScanning keychain [{:?}]", keychain)); - } - bar2.println(format!("- idx {spk_i}: {script}")); - } - }) - .build(); - - let update = esplora_client.full_scan(req, 5, 3).await?; - bar.set_message("Persisting updates"); - self.apply_update(update) - .expect("should be able to connect to db"); - self.persist().expect("persist should work"); - bar.finish_with_message("Scan complete"); + pub async fn scan(&mut self) -> Result<(), OneOf<(ScanError, rusqlite::Error)>> { + scan_wallet(&mut self.wallet, self.sync_backend.clone()).await?; + self.persist().map_err(OneOf::new)?; Ok(()) } pub fn persist(&mut self) -> Result { - self.0.persist(&mut Persister) + self.wallet.persist(&mut Persister) } } -impl Deref for SignetWallet { - type Target = PersistedWallet; +pub async fn scan_wallet( + wallet: &mut Wallet, + sync_backend: Arc, +) -> Result<(), OneOf<(ScanError, rusqlite::Error)>> { + let req = wallet.start_full_scan(); + let last_cp = wallet.latest_checkpoint(); + let (tx, rx) = unbounded_channel(); - fn deref(&self) -> &Self::Target { - &self.0 - } -} + let handle = tokio::spawn(async move { sync_backend.scan_wallet(req, last_cp, tx).await }); -impl DerefMut for SignetWallet { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } + apply_update_stream(wallet, rx).await; + + handle + .await + .expect("thread to be fine") + .map_err(OneOf::new)?; + + Ok(()) } -/// Wrapper around the built-in rusqlite db that allows [`PersistedWallet`] to be -/// shared across multiple threads by lazily initializing per core connections -/// to the sqlite db and keeping them in local thread storage instead of sharing -/// the connection across cores. -/// -/// WARNING: [`set_data_dir`] **MUST** be called and set before using [`Persister`]. -#[derive(Debug)] -pub struct Persister; +pub async fn sync_wallet( + wallet: &mut Wallet, + sync_backend: Arc, +) -> Result<(), OneOf<(SyncError, rusqlite::Error)>> { + let req = wallet.start_sync_with_revealed_spks(); + let last_cp = wallet.latest_checkpoint(); + let (tx, rx) = unbounded_channel(); -static DATA_DIR: OnceLock = OnceLock::new(); + let handle = tokio::spawn(async move { sync_backend.sync_wallet(req, last_cp, tx).await }); -/// Sets the data directory static for the thread local DB. -/// -/// Must be called before accessing [`Persister`]. -/// -/// Can only be set once - will return whether value was set. -pub fn set_data_dir(data_dir: PathBuf) -> bool { - DATA_DIR.set(data_dir).is_ok() -} + apply_update_stream(wallet, rx).await; + + handle + .await + .expect("thread to be fine") + .map_err(OneOf::new)?; -thread_local! { - static DB: Rc> = RefCell::new(Connection::open(SignetWallet::db_path("default", DATA_DIR.get().expect("data dir to be set"))).unwrap()).into(); + Ok(()) } -impl Persister { - fn db() -> Rc> { - DB.with(|db| db.clone()) +async fn apply_update_stream(wallet: &mut Wallet, mut rx: UnboundedReceiver) { + while let Some(update) = rx.recv().await { + match update { + WalletUpdate::SpkSync(update) => { + wallet.apply_update(update).expect("update to connect") + } + WalletUpdate::SpkScan(update) => { + wallet.apply_update(update).expect("update to connect") + } + WalletUpdate::NewBlock(ev) => { + let height = ev.block_height(); + let connected_to = ev.connected_to(); + wallet + .apply_block_connected_to(&ev.block, height, connected_to) + .expect("block to be added") + } + WalletUpdate::MempoolTxs(txs) => wallet.apply_unconfirmed_txs(txs), + } } } -impl WalletPersister for Persister { - type Error = rusqlite::Error; - - fn initialize(_persister: &mut Self) -> Result { - let db = Self::db(); - let mut db_ref = db.borrow_mut(); - let db_tx = db_ref.transaction()?; - ChangeSet::init_sqlite_tables(&db_tx)?; - let changeset = ChangeSet::from_sqlite(&db_tx)?; - db_tx.commit()?; - Ok(changeset) +impl Deref for SignetWallet { + type Target = PersistedWallet; + + fn deref(&self) -> &Self::Target { + &self.wallet } +} - fn persist( - _persister: &mut Self, - changeset: &bdk_wallet::ChangeSet, - ) -> Result<(), Self::Error> { - let db = Self::db(); - let mut db_ref = db.borrow_mut(); - let db_tx = db_ref.transaction()?; - changeset.persist_to_sqlite(&db_tx)?; - db_tx.commit() +impl DerefMut for SignetWallet { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.wallet } } diff --git a/bin/strata-cli/src/signet/backend.rs b/bin/strata-cli/src/signet/backend.rs new file mode 100644 index 000000000..091131b4e --- /dev/null +++ b/bin/strata-cli/src/signet/backend.rs @@ -0,0 +1,328 @@ +use std::{ + collections::BTreeSet, + fmt::Debug, + marker::Send, + sync::Arc, + time::{Duration, Instant}, +}; + +use async_trait::async_trait; +use bdk_bitcoind_rpc::{ + bitcoincore_rpc::{self, json::EstimateMode, RpcApi}, + BlockEvent, Emitter, +}; +use bdk_esplora::EsploraAsyncExt; +use bdk_wallet::{ + bitcoin::{consensus::encode, Block, FeeRate, Transaction}, + chain::{ + spk_client::{FullScanRequestBuilder, FullScanResult, SyncRequestBuilder, SyncResult}, + CheckPoint, + }, + KeychainKind, +}; +use console::Term; +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; +use terrors::OneOf; +use tokio::sync::mpsc::UnboundedSender; + +use super::EsploraClient; + +macro_rules! boxed_err { + ($name:ident) => { + impl std::ops::Deref for $name { + type Target = BoxedInner; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } + } + + impl From for $name { + fn from(err: BoxedErr) -> Self { + Self(err) + } + } + }; +} + +type BoxedInner = dyn Debug + Send + Sync; +type BoxedErr = Box; + +#[derive(Debug)] +pub struct SyncError(BoxedErr); +boxed_err!(SyncError); + +#[derive(Debug)] +pub struct ScanError(BoxedErr); +boxed_err!(ScanError); + +#[derive(Debug)] +pub struct BroadcastTxError(BoxedErr); +boxed_err!(BroadcastTxError); + +#[derive(Debug)] +pub struct GetFeeRateError(BoxedErr); +boxed_err!(GetFeeRateError); + +pub enum WalletUpdate { + SpkSync(SyncResult), + SpkScan(FullScanResult), + NewBlock(BlockEvent), + MempoolTxs(Vec<(Transaction, u64)>), +} + +pub type UpdateSender = UnboundedSender; + +#[async_trait] +pub trait SignetBackend: Debug + Send + Sync { + async fn sync_wallet( + &self, + req: SyncRequestBuilder<(KeychainKind, u32)>, + last_cp: CheckPoint, + send_update: UpdateSender, + ) -> Result<(), SyncError>; + async fn scan_wallet( + &self, + req: FullScanRequestBuilder, + last_cp: CheckPoint, + send_update: UpdateSender, + ) -> Result<(), ScanError>; + async fn broadcast_tx(&self, tx: &Transaction) -> Result<(), BroadcastTxError>; + async fn get_fee_rate( + &self, + target: u16, + ) -> Result, OneOf<(InvalidFee, GetFeeRateError)>>; +} + +#[async_trait] +impl SignetBackend for EsploraClient { + async fn sync_wallet( + &self, + req: SyncRequestBuilder<(KeychainKind, u32)>, + _last_cp: CheckPoint, + send_update: UpdateSender, + ) -> Result<(), SyncError> { + let term = Term::stdout(); + let _ = term.write_line("Syncing wallet..."); + let sty = ProgressStyle::with_template( + "[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}", + ) + .unwrap() + .progress_chars("##-"); + + let bar = MultiProgress::new(); + + let ops = bar.add(ProgressBar::new(1)); + ops.set_style(sty.clone()); + ops.set_message("outpoints"); + let ops2 = ops.clone(); + + let spks = bar.add(ProgressBar::new(1)); + spks.set_style(sty.clone()); + spks.set_message("script public keys"); + let spks2 = spks.clone(); + + let txids = bar.add(ProgressBar::new(1)); + txids.set_style(sty.clone()); + txids.set_message("transactions"); + let txids2 = txids.clone(); + let req = req + .inspect(move |item, progress| { + let _ = bar.println(format!("{item}")); + ops.set_length(progress.total_outpoints() as u64); + ops.set_position(progress.outpoints_consumed as u64); + spks.set_length(progress.total_spks() as u64); + spks.set_position(progress.spks_consumed as u64); + txids.set_length(progress.total_txids() as u64); + txids.set_length(progress.txids_consumed as u64); + }) + .build(); + + let update = self + .sync(req, 3) + .await + .map_err(|e| Box::new(e) as BoxedErr)?; + ops2.finish(); + spks2.finish(); + txids2.finish(); + let _ = term.write_line("Updating wallet"); + send_update.send(WalletUpdate::SpkSync(update)).unwrap(); + let _ = term.write_line("Wallet synced"); + Ok(()) + } + + async fn scan_wallet( + &self, + req: FullScanRequestBuilder, + _last_cp: CheckPoint, + send_update: UpdateSender, + ) -> Result<(), ScanError> { + let bar = ProgressBar::new_spinner(); + bar.enable_steady_tick(Duration::from_millis(100)); + let bar2 = bar.clone(); + let req = req + .inspect({ + let mut once = BTreeSet::::new(); + move |keychain, spk_i, script| { + if once.insert(keychain) { + bar2.println(format!("\nScanning keychain [{:?}]", keychain)); + } + bar2.println(format!("- idx {spk_i}: {script}")); + } + }) + .build(); + + let update = self + .full_scan(req, 5, 3) + .await + .map_err(|e| Box::new(e) as BoxedErr)?; + bar.set_message("Persisting updates"); + send_update.send(WalletUpdate::SpkScan(update)).unwrap(); + bar.finish_with_message("Scan complete"); + Ok(()) + } + + async fn broadcast_tx(&self, tx: &Transaction) -> Result<(), BroadcastTxError> { + self.broadcast(tx) + .await + .map_err(|e| (Box::new(e) as BoxedErr).into()) + } + + async fn get_fee_rate( + &self, + target: u16, + ) -> Result, OneOf<(InvalidFee, GetFeeRateError)>> { + match self + .get_fee_estimates() + .await + .map_err(|e| GetFeeRateError(Box::new(e) as BoxedErr)) + .map_err(OneOf::new)? + .get(&target) + .cloned() + { + Some(fr) => Ok(Some( + FeeRate::from_sat_per_vb(fr as u64).ok_or(OneOf::new(InvalidFee))?, + )), + None => Ok(None), + } + } +} + +#[async_trait] +impl SignetBackend for Arc { + async fn sync_wallet( + &self, + _req: SyncRequestBuilder<(KeychainKind, u32)>, + last_cp: CheckPoint, + send_update: UpdateSender, + ) -> Result<(), SyncError> { + sync_wallet_with_core(self.clone(), last_cp, false, send_update) + .await + .map_err(|e| (Box::new(e) as BoxedErr).into()) + } + + async fn scan_wallet( + &self, + _req: FullScanRequestBuilder, + last_cp: CheckPoint, + send_update: UpdateSender, + ) -> Result<(), ScanError> { + sync_wallet_with_core(self.clone(), last_cp, true, send_update) + .await + .map_err(|e| (Box::new(e) as BoxedErr).into()) + } + + async fn broadcast_tx(&self, tx: &Transaction) -> Result<(), BroadcastTxError> { + let hex = encode::serialize_hex(tx); + + spawn_bitcoin_core(self.clone(), move |c| c.send_raw_transaction(hex)) + .await + .map_err(|e| BroadcastTxError(Box::new(e) as BoxedErr))?; + Ok(()) + } + + async fn get_fee_rate( + &self, + target: u16, + ) -> Result, OneOf<(InvalidFee, GetFeeRateError)>> { + let res = spawn_bitcoin_core(self.clone(), move |c| { + c.estimate_smart_fee(target, Some(EstimateMode::Conservative)) + }) + .await + .map_err(|e| GetFeeRateError(Box::new(e) as BoxedErr)) + .map_err(OneOf::new)?; + + match res.fee_rate { + Some(per_kw) => Ok(Some( + FeeRate::from_sat_per_vb((per_kw / 1000).to_sat()).ok_or(OneOf::new(InvalidFee))?, + )), + None => Ok(None), + } + } +} + +async fn spawn_bitcoin_core( + client: Arc, + func: F, +) -> Result +where + T: Send + 'static, + F: FnOnce(&bitcoincore_rpc::Client) -> Result + Send + 'static, +{ + let handle = tokio::task::spawn_blocking(move || func(&client)); + handle.await.expect("thread should be fine") +} + +async fn sync_wallet_with_core( + client: Arc, + last_cp: CheckPoint, + should_scan: bool, + send_update: UpdateSender, +) -> Result<(), bitcoincore_rpc::Error> { + let bar = ProgressBar::new_spinner() + .with_style(ProgressStyle::with_template("{spinner} [{elapsed_precise}] {msg}").unwrap()); + bar.enable_steady_tick(Duration::from_millis(100)); + let bar2 = bar.clone(); + + let start_height = match should_scan { + true => 0, + false => last_cp.height(), + }; + + let mut blocks_scanned = 0; + + spawn_bitcoin_core(client.clone(), move |client| { + let mut emitter = Emitter::new(client, last_cp, start_height); + while let Some(ev) = emitter.next_block().unwrap() { + blocks_scanned += 1; + let height = ev.block_height(); + let hash = ev.block_hash(); + let start_apply_block = Instant::now(); + send_update.send(WalletUpdate::NewBlock(ev)).unwrap(); + let elapsed = start_apply_block.elapsed(); + bar2.println(format!( + "Applied block {} at height {} in {:?}", + hash, height, elapsed + )); + bar2.set_message(format!( + "Current height: {}, scanned {} blocks", + height, blocks_scanned + )); + } + bar2.println("Scanning mempool"); + let mempool = emitter.mempool().unwrap(); + let txs_len = mempool.len(); + let apply_start = Instant::now(); + send_update.send(WalletUpdate::MempoolTxs(mempool)).unwrap(); + let elapsed = apply_start.elapsed(); + bar.println(format!( + "Applied {} unconfirmed transactions in {:?}", + txs_len, elapsed + )); + Ok(()) + }) + .await +} + +#[derive(Debug)] +pub struct InvalidFee; diff --git a/bin/strata-cli/src/signet/persist.rs b/bin/strata-cli/src/signet/persist.rs new file mode 100644 index 000000000..3bcbfd719 --- /dev/null +++ b/bin/strata-cli/src/signet/persist.rs @@ -0,0 +1,63 @@ +use std::{cell::RefCell, path::PathBuf, rc::Rc, sync::OnceLock}; + +use bdk_wallet::{ + rusqlite::{self, Connection}, + ChangeSet, WalletPersister, +}; + +use crate::signet::SignetWallet; + +/// Wrapper around the built-in rusqlite db that allows +/// [`PersistedWallet`](crate::signet::PersistedWallet) to be shared across multiple threads by +/// lazily initializing per core connections to the sqlite db and keeping them in local thread +/// storage instead of sharing the connection across cores. +/// +/// WARNING: [`set_data_dir`] **MUST** be called and set before using [`Persister`]. +#[derive(Debug)] +pub struct Persister; + +static DATA_DIR: OnceLock = OnceLock::new(); + +/// Sets the data directory static for the thread local DB. +/// +/// Must be called before accessing [`Persister`]. +/// +/// Can only be set once - will return whether value was set. +pub fn set_data_dir(data_dir: PathBuf) -> bool { + DATA_DIR.set(data_dir).is_ok() +} + +thread_local! { + static DB: Rc> = RefCell::new(Connection::open(SignetWallet::db_path("default", DATA_DIR.get().expect("data dir to be set"))).unwrap()).into(); +} + +impl Persister { + fn db() -> Rc> { + DB.with(|db| db.clone()) + } +} + +impl WalletPersister for Persister { + type Error = rusqlite::Error; + + fn initialize(_persister: &mut Self) -> Result { + let db = Self::db(); + let mut db_ref = db.borrow_mut(); + let db_tx = db_ref.transaction()?; + ChangeSet::init_sqlite_tables(&db_tx)?; + let changeset = ChangeSet::from_sqlite(&db_tx)?; + db_tx.commit()?; + Ok(changeset) + } + + fn persist( + _persister: &mut Self, + changeset: &bdk_wallet::ChangeSet, + ) -> Result<(), Self::Error> { + let db = Self::db(); + let mut db_ref = db.borrow_mut(); + let db_tx = db_ref.transaction()?; + changeset.persist_to_sqlite(&db_tx)?; + db_tx.commit() + } +} From 1fb1e1ca401259b5e5537b475aacb70e6bde5947 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Mon, 28 Oct 2024 11:52:26 -0300 Subject: [PATCH 18/18] chore: manual fixes for releases/v0.1.0 --- Cargo.lock | 11 +++++++++++ crates/btcio/src/writer/builder.rs | 7 +++---- crates/test-utils/src/bridge.rs | 2 +- crates/tx-parser/src/filter.rs | 7 +++---- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8db042f85..9e2e4adad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1912,6 +1912,17 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bdk_bitcoind_rpc" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577233392985869b7b5d9e0eae638c5112c48d5edc07422de0e0726c15144e92" +dependencies = [ + "bdk_core", + "bitcoin", + "bitcoincore-rpc", +] + [[package]] name = "bdk_chain" version = "0.20.0" diff --git a/crates/btcio/src/writer/builder.rs b/crates/btcio/src/writer/builder.rs index 590ccaf1b..8236a33e8 100644 --- a/crates/btcio/src/writer/builder.rs +++ b/crates/btcio/src/writer/builder.rs @@ -14,6 +14,7 @@ use bitcoin::{ script::PushBytesBuf, secp256k1::{ self, constants::SCHNORR_SIGNATURE_SIZE, schnorr::Signature, Secp256k1, XOnlyPublicKey, + SECP256K1, }, sighash::{Prevouts, SighashCache}, taproot::{ @@ -94,7 +95,7 @@ pub fn create_inscription_transactions( ) -> Result<(Transaction, Transaction), InscriptionError> { // Create commit key let secp256k1 = Secp256k1::new(); - let key_pair = generate_key_pair(&secp256k1)?; + let key_pair = generate_key_pair()?; let public_key = XOnlyPublicKey::from_keypair(&key_pair).0; let insc_data = InscriptionData::new(write_intent.to_vec()); @@ -398,9 +399,7 @@ pub fn build_reveal_transaction( Ok(tx) } -pub fn generate_key_pair( - secp256k1: &Secp256k1, -) -> Result { +pub fn generate_key_pair() -> Result { let mut rand_bytes = [0; 32]; OsRng.fill_bytes(&mut rand_bytes); Ok(UntweakedKeypair::from_seckey_slice(SECP256K1, &rand_bytes)?) diff --git a/crates/test-utils/src/bridge.rs b/crates/test-utils/src/bridge.rs index 43e0078d5..2f765e2ce 100644 --- a/crates/test-utils/src/bridge.rs +++ b/crates/test-utils/src/bridge.rs @@ -14,7 +14,7 @@ use bitcoin::{ Address, Amount, Network, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, }; use musig2::{secp256k1::SECP256K1, KeyAggContext, SecNonce}; -use rand::{Rng, RngCore}; +use rand::{rngs::OsRng, Rng, RngCore}; use strata_db::stubs::bridge::StubTxStateDb; use strata_primitives::{ bridge::{OperatorIdx, PublickeyTable, TxSigningData}, diff --git a/crates/tx-parser/src/filter.rs b/crates/tx-parser/src/filter.rs index 0822fd49f..8e83ff0d8 100644 --- a/crates/tx-parser/src/filter.rs +++ b/crates/tx-parser/src/filter.rs @@ -98,14 +98,14 @@ mod test { absolute::{Height, LockTime}, block::{Header, Version as BVersion}, hashes::Hash, - key::{Parity, Secp256k1, UntweakedKeypair}, - secp256k1::XOnlyPublicKey, + key::{Parity, UntweakedKeypair}, + secp256k1::{XOnlyPublicKey, SECP256K1}, taproot::{ControlBlock, LeafVersion, TaprootMerkleBranch}, transaction::Version, Address, Amount, Block, BlockHash, CompactTarget, Network, ScriptBuf, TapNodeHash, Transaction, TxMerkleNode, TxOut, }; - use rand::{rngs::OsRng, RngCore}; + use rand::RngCore; use strata_btcio::test_utils::{ build_reveal_transaction_test, generate_inscription_script_test, }; @@ -178,7 +178,6 @@ mod test { let script = generate_inscription_script_test(inscription_data, &rollup_name, 1).unwrap(); // Create controlblock - let secp256k1 = Secp256k1::new(); let mut rand_bytes = [0; 32]; rand::thread_rng().fill_bytes(&mut rand_bytes); let key_pair = UntweakedKeypair::from_seckey_slice(SECP256K1, &rand_bytes).unwrap();