diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index dbf20f9..e5ea119 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -19,6 +19,10 @@ jobs: - esplora-reqwest - compiler - compact_filters + - reserves + - reserves,electrum + - reserves,esplora-ureq + - reserves,compact_filters steps: - name: Checkout uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8191e71..6924377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Replace `wallet bump_fee` command `--send_all` with new `--shrink` option +- Add 'reserve' feature to enable proof of reserve ## [0.3.0] diff --git a/Cargo.lock b/Cargo.lock index aa9ec94..fe68a9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "0.7.15" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] @@ -145,6 +145,7 @@ dependencies = [ "base64 0.11.0", "bdk", "bdk-macros", + "bdk-reserves", "clap", "dirs-next", "env_logger", @@ -167,6 +168,18 @@ dependencies = [ "syn", ] +[[package]] +name = "bdk-reserves" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef9f931b65337ac7149c9af6b191f94996d729778a487a9639b79a97423893e" +dependencies = [ + "base64 0.11.0", + "bdk", + "bitcoinconsensus", + "log", +] + [[package]] name = "bech32" version = "0.8.1" @@ -214,6 +227,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoinconsensus" +version = "0.19.0-3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a8aa43b5cd02f856cb126a9af819e77b8910fdd74dd1407be649f2f5fe3a1b5" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "bitcoincore-rpc" version = "0.14.0" @@ -246,9 +269,9 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "bitvec" -version = "0.19.5" +version = "0.19.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" +checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33" dependencies = [ "funty", "radium", @@ -303,9 +326,9 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.71" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" dependencies = [ "jobserver", ] @@ -333,9 +356,9 @@ checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" [[package]] name = "clang-sys" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10612c0ec0e0a1ff0e97980647cb058a6e7aedb913d01d009c406b8b7d0b26ee" +checksum = "fa66045b9cb23c2e9c1520732030608b02ee07e5cfaa5a521ec15ded7fa24c90" dependencies = [ "glob", "libc", @@ -359,9 +382,9 @@ dependencies = [ [[package]] name = "clipboard-win" -version = "4.2.1" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4ea1881992efc993e4dc50a324cdbd03216e41bdc8385720ff47efc9bd2ca8" +checksum = "3db8340083d28acb43451166543b98c838299b7e0863621be53a338adceea0ed" dependencies = [ "error-code", "str-buf", @@ -752,9 +775,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "h2" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c06815895acec637cd6ed6e9662c935b866d20a106f8361892893a7d9234964" +checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55" dependencies = [ "bytes", "fnv", @@ -816,9 +839,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" dependencies = [ "bytes", "http", @@ -833,9 +856,9 @@ checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" [[package]] name = "httpdate" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "humantime" @@ -848,9 +871,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.13" +version = "0.14.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d1cfb9e4f68655fa04c01f59edb405b6074a0f7118ea881e5026e4a1cd8593" +checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c" dependencies = [ "bytes", "futures-channel", @@ -969,15 +992,15 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.104" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce" +checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" [[package]] name = "libloading" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0" +checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52" dependencies = [ "cfg-if", "winapi 0.3.9", @@ -1169,9 +1192,9 @@ checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" [[package]] name = "openssl" -version = "0.10.36" +version = "0.10.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" dependencies = [ "bitflags", "cfg-if", @@ -1189,9 +1212,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-sys" -version = "0.9.67" +version = "0.9.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058" +checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73" dependencies = [ "autocfg", "cc", @@ -1261,15 +1284,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb" +checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" [[package]] name = "ppv-lite86" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" [[package]] name = "proc-macro-error" @@ -1309,9 +1332,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.30" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" dependencies = [ "unicode-xid", ] @@ -1449,9 +1472,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.4.6" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ "aho-corasick", "memchr", @@ -1692,9 +1715,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.68" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19" dependencies = [ "itoa", "ryu", @@ -1831,9 +1854,9 @@ checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" [[package]] name = "syn" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" +checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" dependencies = [ "proc-macro2", "quote", @@ -1928,9 +1951,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" dependencies = [ "tinyvec_macros", ] @@ -1943,9 +1966,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" +checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" dependencies = [ "autocfg", "bytes", @@ -1980,9 +2003,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" dependencies = [ "bytes", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index cf1cd3b..1afea38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ dirs-next = { version = "2.0", optional = true } env_logger = { version = "0.7", optional = true } clap = { version = "2.33", optional = true } regex = {version = "1", optional = true } +bdk-reserves = { version = "0.13", optional = true} [features] default = ["cli", "repl"] @@ -38,6 +39,7 @@ esplora-reqwest = ["esplora", "bdk/use-esplora-reqwest"] compiler = ["bdk/compiler"] compact_filters = ["bdk/compact_filters"] rpc = ["bdk/rpc"] +reserves = ["bdk-reserves"] [[bin]] name = "bdk-cli" diff --git a/src/bdk_cli.rs b/src/bdk_cli.rs index 4c39ff9..53bbb01 100644 --- a/src/bdk_cli.rs +++ b/src/bdk_cli.rs @@ -407,6 +407,22 @@ fn handle_command(cli_opts: CliOpts, network: Network) -> Result // rl.save_history("history.txt").unwrap(); "Exiting REPL".to_string() } + #[cfg(all(feature = "reserves", feature = "electrum"))] + CliSubCommand::ExternalReserves { + message, + addresses, + psbt, + electrum_opts, + } => { + let result = bdk_cli::handle_ext_reserves_subcommand( + network, + message, + addresses, + psbt, + electrum_opts, + )?; + serde_json::to_string_pretty(&result)? + } }; Ok(result) } diff --git a/src/lib.rs b/src/lib.rs index 248f22e..af0058c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -122,6 +122,8 @@ use crate::OfflineWalletSubCommand::*; feature = "rpc" ))] use crate::OnlineWalletSubCommand::*; +#[cfg(all(feature = "reserves", feature = "electrum"))] +use bdk::bitcoin::blockdata::transaction::TxOut; use bdk::bitcoin::consensus::encode::{deserialize, serialize, serialize_hex}; #[cfg(any( feature = "electrum", @@ -145,6 +147,8 @@ use bdk::database::BatchDatabase; use bdk::descriptor::Segwitv0; #[cfg(feature = "compiler")] use bdk::descriptor::{Descriptor, Legacy, Miniscript}; +#[cfg(all(feature = "reserves", feature = "electrum"))] +use bdk::electrum_client::{Client, ElectrumApi}; use bdk::keys::bip39::{Language, Mnemonic, MnemonicType}; use bdk::keys::DescriptorKey::Secret; use bdk::keys::KeyError::{InvalidNetwork, Message}; @@ -156,6 +160,16 @@ use bdk::wallet::AddressIndex; use bdk::Error; use bdk::SignOptions; use bdk::{FeeRate, KeychainKind, Wallet}; +#[cfg(all(feature = "reserves", feature = "electrum"))] +use bdk_reserves::reserves::verify_proof; +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "compact_filters", + feature = "rpc" +))] +#[cfg(feature = "reserves")] +use bdk_reserves::reserves::ProofOfReserves; /// Global options /// @@ -308,6 +322,22 @@ pub enum CliSubCommand { #[structopt(flatten)] wallet_opts: WalletOpts, }, + /// Proof of reserves external sub-commands + #[cfg(all(feature = "reserves", feature = "electrum"))] + #[structopt(long_about = "Proof of reserves external verification")] + ExternalReserves { + /// Sets the challenge message with which the proof was produced + #[structopt(name = "MESSAGE", required = true, index = 1)] + message: String, + /// Sets the addresses for which the proof was produced + #[structopt(name = "ADDRESSES", required = true, index = 2)] + addresses: Vec, + /// Sets the proof in form of a PSBT to verify + #[structopt(name = "PSBT", required = true, index = 3)] + psbt: String, + #[structopt(flatten)] + electrum_opts: ElectrumOpts, + }, } /// Wallet sub-commands @@ -740,6 +770,9 @@ pub enum OfflineWalletSubCommand { /// Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor #[structopt(name = "HEIGHT", long = "assume_height")] assume_height: Option, + /// Whether the signer should trust the witness_utxo, if the non_witness_utxo hasn’t been provided + #[structopt(name = "WITNESS", long = "trust_witness_utxo")] + trust_witness_utxo: Option, }, /// Extracts a raw transaction from a PSBT ExtractPsbt { @@ -755,6 +788,9 @@ pub enum OfflineWalletSubCommand { /// Assume the blockchain has reached a specific height #[structopt(name = "HEIGHT", long = "assume_height")] assume_height: Option, + /// Whether the signer should trust the witness_utxo, if the non_witness_utxo hasn’t been provided + #[structopt(name = "WITNESS", long = "trust_witness_utxo")] + trust_witness_utxo: Option, }, /// Combines multiple PSBTs into one CombinePsbt { @@ -809,6 +845,23 @@ pub enum OnlineWalletSubCommand { )] tx: Option, }, + /// Produce a proof of reserves + #[cfg(feature = "reserves")] + ProduceProof { + /// Sets the message + #[structopt(name = "MESSAGE", long = "message")] + msg: Option, + }, + /// Verify a proof of reserves for our wallet + #[cfg(feature = "reserves")] + VerifyProof { + /// Sets the PSBT to verify + #[structopt(name = "BASE64_PSBT", long = "psbt")] + psbt: Option, + /// Sets the message to verify + #[structopt(name = "MESSAGE", long = "message")] + msg: Option, + }, } fn parse_recipient(s: &str) -> Result<(Script, u64), String> { @@ -971,11 +1024,14 @@ where Sign { psbt, assume_height, + trust_witness_utxo, } => { - let psbt = base64::decode(&psbt).unwrap(); - let mut psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); + let psbt = base64::decode(&psbt) + .map_err(|e| Error::Generic(format!("Base64 decode error: {:?}", e)))?; + let mut psbt: PartiallySignedTransaction = deserialize(&psbt)?; let signopt = SignOptions { assume_height, + trust_witness_utxo: trust_witness_utxo.unwrap_or(false), ..Default::default() }; let finalized = wallet.sign(&mut psbt, signopt)?; @@ -995,12 +1051,14 @@ where FinalizePsbt { psbt, assume_height, + trust_witness_utxo, } => { let psbt = base64::decode(&psbt).unwrap(); let mut psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); let signopt = SignOptions { assume_height, + trust_witness_utxo: trust_witness_utxo.unwrap_or(false), ..Default::default() }; let finalized = wallet.finalize_psbt(&mut psbt, signopt)?; @@ -1076,6 +1134,47 @@ where let txid = maybe_await!(wallet.broadcast(tx))?; Ok(json!({ "txid": txid })) } + #[cfg(feature = "reserves")] + ProduceProof { msg } => { + let message = if let Some(msg) = msg { + msg + } else { + panic!("Missing `message` option") + }; + + let mut psbt = maybe_await!(wallet.create_proof(&message))?; + + let _finalized = wallet.sign( + &mut psbt, + SignOptions { + trust_witness_utxo: true, + ..Default::default() + }, + )?; + + let psbt_ser = serialize(&psbt); + let psbt_b64 = base64::encode(&psbt_ser); + + Ok(json!({ "psbt": psbt , "psbt_base64" : psbt_b64})) + } + #[cfg(feature = "reserves")] + VerifyProof { psbt, msg } => { + let psbt = if let Some(psbt) = psbt { + let psbt = base64::decode(&psbt).unwrap(); + let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); + psbt + } else { + panic!("Missing `psbt` option") + }; + let message = if let Some(msg) = msg { + msg + } else { + panic!("Missing `message` option") + }; + + let spendable = maybe_await!(wallet.verify_proof(&psbt, &message))?; + Ok(json!({ "spendable": spendable })) + } } } @@ -1219,6 +1318,72 @@ pub fn handle_compile_subcommand( Ok(json!({"descriptor": descriptor.to_string()})) } +/// Proof of reserves verification sub-command +/// +/// Proof of reserves options are described in [`CliSubCommand::ExternalReserves`]. +#[cfg(all(feature = "reserves", feature = "electrum"))] +pub fn handle_ext_reserves_subcommand( + network: Network, + message: String, + addresses: Vec, + psbt: String, + electrum_opts: ElectrumOpts, +) -> Result { + let psbt = base64::decode(&psbt) + .map_err(|e| Error::Generic(format!("Base64 decode error: {:?}", e)))?; + let psbt: PartiallySignedTransaction = deserialize(&psbt)?; + let client = Client::new(&electrum_opts.server)?; + + let outpoints_per_addr = addresses + .iter() + .map(|address| { + let address = Address::from_str(&address) + .map_err(|e| Error::Generic(format!("Invalid address: {:?}", e)))?; + get_outpoints_for_address(address, &client) + }) + .collect::>, Error>>()?; + let outpoints_combined = outpoints_per_addr + .iter() + .fold(Vec::new(), |mut outpoints, outs| { + outpoints.append(&mut outs.clone()); + outpoints + }); + + let spendable = verify_proof(&psbt, &message, outpoints_combined, network)?; + + Ok(json!({ "spendable": spendable })) +} + +#[cfg(all(feature = "reserves", feature = "electrum"))] +pub fn get_outpoints_for_address( + address: Address, + client: &Client, +) -> Result, Error> { + let unspents = client + .script_list_unspent(&address.script_pubkey()) + .map_err(Error::Electrum)?; + + unspents + .iter() + .map(|utxo| { + let tx = match client.transaction_get(&utxo.tx_hash) { + Ok(tx) => tx, + Err(e) => { + return Err(e).map_err(Error::Electrum); + } + }; + + Ok(( + OutPoint { + txid: utxo.tx_hash, + vout: utxo.tx_pos as u32, + }, + tx.output[utxo.tx_pos].clone(), + )) + }) + .collect() +} + #[cfg(test)] mod test { use super::{CliOpts, WalletOpts}; @@ -1231,6 +1396,10 @@ mod test { #[cfg(feature = "esplora")] use crate::EsploraOpts; use crate::OfflineWalletSubCommand::{BumpFee, CreateTx, GetNewAddress}; + #[cfg(all(feature = "reserves", feature = "compact_filters"))] + use crate::OnlineWalletSubCommand::ProduceProof; + #[cfg(all(feature = "reserves", feature = "esplora-ureq"))] + use crate::OnlineWalletSubCommand::VerifyProof; #[cfg(any( feature = "electrum", feature = "esplora", @@ -1242,12 +1411,22 @@ mod test { use crate::ProxyOpts; #[cfg(feature = "rpc")] use crate::RpcOpts; + #[cfg(all(feature = "reserves", feature = "electrum"))] + use crate::{handle_ext_reserves_subcommand, handle_online_wallet_subcommand}; use crate::{handle_key_subcommand, CliSubCommand, KeySubCommand, WalletSubCommand}; - use bdk::bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey}; + #[cfg(all(feature = "reserves", feature = "electrum"))] + use bdk::bitcoin::{consensus::Encodable, util::psbt::PartiallySignedTransaction}; use bdk::bitcoin::{Address, Network, OutPoint}; use bdk::miniscript::bitcoin::network::constants::Network::Testnet; - use std::str::FromStr; + #[cfg(all(feature = "reserves", feature = "electrum"))] + use bdk::{ + blockchain::{noop_progress, ElectrumBlockchain}, + database::MemoryDatabase, + electrum_client::Client, + Wallet, + }; + use std::str::{self, FromStr}; use structopt::StructOpt; #[test] @@ -1936,4 +2115,292 @@ mod test { &"sh(wsh(thresh(3,pk(Alice),s:pk(Bob),s:pk(Carol),sdv:older(2))))#l4qaawgv" ); } + + #[cfg(all(feature = "reserves", feature = "compact_filters"))] + #[test] + fn test_parse_produce_proof() { + let message = "Those coins belong to Satoshi Nakamoto"; + let cli_args = vec![ + "bdk-cli", + "--network", + "bitcoin", + "wallet", + "--descriptor", + "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)", + "produce_proof", + "--message", + message.clone(), + ]; + + let cli_opts = CliOpts::from_iter(&cli_args); + + let expected_cli_opts = CliOpts { + network: Network::Bitcoin, + subcommand: CliSubCommand::Wallet { + wallet_opts: WalletOpts { + wallet: "main".to_string(), + verbose: false, + descriptor: "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)" + .to_string(), + change_descriptor: None, + compactfilter_opts: CompactFilterOpts { + address: vec!["127.0.0.1:18444".to_string()], + conn_count: 4, + skip_blocks: 0, + }, + proxy_opts: ProxyOpts { + proxy: None, + proxy_auth: None, + retries: 5, + }, + }, + subcommand: WalletSubCommand::OnlineWalletSubCommand(ProduceProof { + msg: Some(message.to_string()), + }), + }, + }; + + assert_eq!(expected_cli_opts, cli_opts); + } + + #[cfg(all(feature = "reserves", feature = "esplora-ureq"))] + #[test] + fn test_parse_verify_proof_internal() { + let psbt = r#"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"#; + let message = "Those coins belong to Satoshi Nakamoto"; + let cli_args = vec![ + "bdk-cli", + "--network", + "bitcoin", + "wallet", + "--descriptor", + "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)", + "verify_proof", + "--psbt", + psbt.clone(), + "--message", + message.clone(), + ]; + + let cli_opts = CliOpts::from_iter(&cli_args); + + let expected_cli_opts = CliOpts { + network: Network::Bitcoin, + subcommand: CliSubCommand::Wallet { + wallet_opts: WalletOpts { + wallet: "main".to_string(), + verbose: false, + descriptor: "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)" + .to_string(), + change_descriptor: None, + esplora_opts: EsploraOpts { + server: "https://blockstream.info/testnet/api/".to_string(), + read_timeout: 5, + write_timeout: 5, + stop_gap: 10, + }, + proxy_opts: ProxyOpts { + proxy: None, + proxy_auth: None, + retries: 5, + }, + }, + subcommand: WalletSubCommand::OnlineWalletSubCommand(VerifyProof { + psbt: Some(psbt.to_string()), + msg: Some(message.to_string()), + }), + }, + }; + + assert_eq!(expected_cli_opts, cli_opts); + } + + #[cfg(all(feature = "reserves", feature = "electrum"))] + #[test] + fn test_parse_verify_proof_external() { + let psbt = r#"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"#.to_string(); + let address = "tb1qanjjv4cs20dgv32vncrxw702l8g4qtn2m9wn7d".to_string(); + let message = "Those coins belong to Satoshi Nakamoto".to_string(); + let cli_args = vec![ + "bdk-cli", + "--network", + "bitcoin", + "external_reserves", + &message, + &address, + &psbt, + "--server", + "ssl://electrum.blockstream.info:60002", + ]; + + let cli_opts = CliOpts::from_iter(&cli_args); + + let expected_cli_opts = CliOpts { + network: Network::Bitcoin, + subcommand: CliSubCommand::ExternalReserves { + message, + addresses: [address].to_vec(), + psbt, + electrum_opts: ElectrumOpts { + timeout: None, + server: "ssl://electrum.blockstream.info:60002".to_string(), + stop_gap: 10, + }, + }, + }; + + assert_eq!(expected_cli_opts, cli_opts); + } + + /// Encodes a partially signed transaction as base64 and returns the bytes of the resulting string. + #[cfg(all(feature = "reserves", feature = "electrum"))] + fn encode_psbt(psbt: PartiallySignedTransaction) -> Vec { + let mut encoded = Vec::::new(); + psbt.consensus_encode(&mut encoded).unwrap(); + let base64_psbt = base64::encode(&encoded); + + base64_psbt.as_bytes().to_vec() + } + + #[cfg(all(feature = "reserves", feature = "electrum"))] + #[test] + fn test_proof_of_reserves_wallet() { + let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string(); + let message = "Those coins belong to Satoshi Nakamoto"; + + let client = Client::new("ssl://electrum.blockstream.info:60002").unwrap(); + let wallet = Wallet::new( + &descriptor, + None, + Network::Testnet, + MemoryDatabase::default(), + ElectrumBlockchain::from(client), + ) + .unwrap(); + + wallet.sync(noop_progress(), None).unwrap(); + let balance = wallet.get_balance().unwrap(); + + let addr = wallet.get_address(bdk::wallet::AddressIndex::New).unwrap(); + assert_eq!( + "tb1qanjjv4cs20dgv32vncrxw702l8g4qtn2m9wn7d", + addr.to_string() + ); + + let cli_args = vec![ + "bdk-cli", + "--network", + "bitcoin", + "wallet", + "--descriptor", + &descriptor, + "produce_proof", + "--message", + message.clone(), + ]; + let cli_opts = CliOpts::from_iter(&cli_args); + + let wallet_subcmd = match cli_opts.subcommand { + CliSubCommand::Wallet { + wallet_opts: _, + subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand), + } => online_subcommand, + _ => panic!("unexpected subcommand"), + }; + let result = handle_online_wallet_subcommand(&wallet, wallet_subcmd).unwrap(); + let psbt: PartiallySignedTransaction = + serde_json::from_str(&result.as_object().unwrap().get("psbt").unwrap().to_string()) + .unwrap(); + let psbt = encode_psbt(psbt); + let psbt = str::from_utf8(&psbt).unwrap(); + assert_eq!(format!("{}", psbt), "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"); + + let psbt_b64 = &result + .as_object() + .unwrap() + .get("psbt_base64") + .unwrap() + .to_string(); + assert_eq!(&format!("{}", psbt), psbt_b64.trim_matches('\"')); + + let cli_args = vec![ + "bdk-cli", + "--network", + "bitcoin", + "wallet", + "--descriptor", + &descriptor, + "verify_proof", + "--psbt", + psbt, + "--message", + message.clone(), + ]; + let cli_opts = CliOpts::from_iter(&cli_args); + + let wallet_subcmd = match cli_opts.subcommand { + CliSubCommand::Wallet { + wallet_opts: _, + subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand), + } => online_subcommand, + _ => panic!("unexpected subcommand"), + }; + let result = handle_online_wallet_subcommand(&wallet, wallet_subcmd).unwrap(); + let spendable = result + .as_object() + .unwrap() + .get("spendable") + .unwrap() + .as_u64() + .unwrap(); + assert_eq!(spendable, balance); + } + + #[cfg(all(feature = "reserves", feature = "electrum"))] + #[test] + fn test_proof_of_reserves_veryfy() { + let message = "Those coins belong to Satoshi Nakamoto"; + let address = "tb1qanjjv4cs20dgv32vncrxw702l8g4qtn2m9wn7d"; + let psbt = "cHNidP8BAKcBAAAAA9A7AuVMuwIrkAuXtKDqw3BruovK2PH7E90MzCbyaAMlAAAAAAD/////sq+79fxJ+5F8951Xcbp8N5VYVJoesG1zd3AxStcGwPYAAAAAAP/////AUS4n/SkO9SSvIv8MI2Xdm3xdn77Sud6uX5PyqCganAEAAAAA/////wGwrQEAAAAAABl2qRSff9CW037SwOP38M/JJL7vT/zraIisAAAAAAABAQoAAAAAAAAAAAFRAQcAAAEBHxAnAAAAAAAAFgAU7OUmVxBT2oZFTJ4GZ3nq+dFQLmoiAgMrBVgHi+w4aUqEkz1lkwPiV12ufpFoWRFFQRW/1kSH40gwRQIhAPgByvkajQrNeQDSGik2gnxpo/P/owiEHR+0nWefkXurAiBgrAlDvwuTiaGEEWQW/Kd7L7u7YOQnqvrd46DR0A8yPgEBBwABCGwCSDBFAiEA+AHK+RqNCs15ANIaKTaCfGmj8/+jCIQdH7SdZ5+Re6sCIGCsCUO/C5OJoYQRZBb8p3svu7tg5Ceq+t3joNHQDzI+ASEDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+MAAQEfoIYBAAAAAAAWABTs5SZXEFPahkVMngZneer50VAuaiICAysFWAeL7DhpSoSTPWWTA+JXXa5+kWhZEUVBFb/WRIfjRzBEAiBSfiX0qP7vR+2Qx/mRJS8pwma8nTfOWKerzo6c0iSAfwIgEfX4Wt7YXd8MkKUEY627GWYCmKfMsJGcIC0U1wgc1vUBAQcAAQhrAkcwRAIgUn4l9Kj+70ftkMf5kSUvKcJmvJ03zlinq86OnNIkgH8CIBH1+Fre2F3fDJClBGOtuxlmApinzLCRnCAtFNcIHNb1ASEDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+MAAA=="; + + let cli_args = vec![ + "bdk-cli", + "--network", + "bitcoin", + "external_reserves", + message, + address, + address, // passing the address twice on purpose, to test passing of multiple addresses + psbt, + "--server", + "ssl://electrum.blockstream.info:60002", + ]; + let cli_opts = CliOpts::from_iter(&cli_args); + + let (message, addresses, psbt, electrum_opts) = match cli_opts.subcommand { + CliSubCommand::ExternalReserves { + message, + addresses, + psbt, + electrum_opts, + } => (message, addresses, psbt, electrum_opts), + _ => panic!("unexpected subcommand"), + }; + let result = handle_ext_reserves_subcommand( + Network::Testnet, + message, + addresses, + psbt, + electrum_opts, + ) + .unwrap(); + let spendable = result + .as_object() + .unwrap() + .get("spendable") + .unwrap() + .as_u64() + .unwrap(); + assert!(spendable > 0); + } }