diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca29e9a..165e4a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,8 +66,8 @@ jobs: - run: cargo test --workspace - name: Smoke test ${{ matrix.target }} binary run: | - target/debug/maker & + target/debug/maker --data-dir=/tmp/maker --generate-seed & sleep 5s # Wait for maker to start - target/debug/taker & + target/debug/taker --data-dir=/tmp/taker --generate-seed & sleep 5s # Wait for taker to start curl --fail http://localhost:8000/alive diff --git a/Cargo.lock b/Cargo.lock index babdcf1..ddcd073 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -269,6 +269,37 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "3.0.0-beta.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcd70aa5597dbc42f7217a543f9ef2768b2ef823ba29036072d30e1d88e98406" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", + "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5bb0d655624a0b8770d1c178fb8ffcb1f91cc722cb08f451e3dc72465421ac" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -365,6 +396,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "daemon" version = "0.1.0" @@ -372,7 +413,9 @@ dependencies = [ "anyhow", "bdk", "cfd_protocol", + "clap", "futures", + "hkdf", "rand 0.6.5", "rocket", "rocket_db_pools", @@ -381,6 +424,7 @@ dependencies = [ "serde", "serde_json", "serde_with", + "sha2", "sqlx", "tempfile", "tokio", @@ -779,6 +823,26 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest", +] + [[package]] name = "http" version = "0.2.4" @@ -1099,6 +1163,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "os_str_bytes" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6acbef58a60fe69ab50510a55bc8cdd4d6cf2283d27ad338f54cb52747a9cf2d" + [[package]] name = "parking_lot" version = "0.11.2" @@ -1177,6 +1247,30 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -1984,6 +2078,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.76" @@ -2009,6 +2109,24 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.29" @@ -2230,6 +2348,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + [[package]] name = "unicode-xid" version = "0.2.2" @@ -2276,6 +2400,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version_check" version = "0.9.3" @@ -2434,6 +2564,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/daemon/Cargo.toml b/daemon/Cargo.toml index 9ce52f7..09e26e6 100644 --- a/daemon/Cargo.toml +++ b/daemon/Cargo.toml @@ -7,7 +7,9 @@ edition = "2018" anyhow = "1" bdk = { git = "https://github.com/bitcoindevkit/bdk/" } cfd_protocol = { path = "../cfd_protocol" } +clap = "3.0.0-beta.4" futures = { version = "0.3", default-features = false } +hkdf = "0.11" rand = "0.6" rocket = { git = "https://github.com/SergioBenitez/Rocket", features = ["json"] } rocket_db_pools = { git = "https://github.com/SergioBenitez/Rocket", features = ["sqlx_sqlite"] } @@ -16,6 +18,7 @@ rust_decimal_macros = "1.15" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_with = { version = "1", features = ["macros"] } +sha2 = "0.9" sqlx = { version = "0.5", features = ["offline"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "net"] } tokio-util = { version = "0.6", features = ["codec"] } diff --git a/daemon/src/maker.rs b/daemon/src/maker.rs index b277b0e..eb7b3c6 100644 --- a/daemon/src/maker.rs +++ b/daemon/src/maker.rs @@ -1,12 +1,14 @@ +use crate::seed::Seed; use anyhow::Result; use bdk::bitcoin::secp256k1::{schnorrsig, SECP256K1}; -use bdk::bitcoin::{self, Amount}; +use bdk::bitcoin::{Amount, Network}; use bdk::blockchain::{ElectrumBlockchain, NoopProgress}; +use bdk::KeychainKind; +use clap::Clap; use model::cfd::{Cfd, Order}; use rocket::fairing::AdHoc; -use rocket::figment::util::map; -use rocket::figment::value::{Map, Value}; use rocket_db_pools::Database; +use std::path::PathBuf; use tokio::sync::{mpsc, watch}; mod db; @@ -15,6 +17,7 @@ mod maker_cfd_actor; mod maker_inc_connections_actor; mod model; mod routes_maker; +mod seed; mod send_wire_message_actor; mod to_sse_event; mod wire; @@ -23,19 +26,55 @@ mod wire; #[database("maker")] pub struct Db(sqlx::SqlitePool); +#[derive(Clap)] +struct Opts { + /// The port to listen on for p2p connections. + #[clap(long, default_value = "9999")] + p2p_port: u16, + + /// The port to listen on for the HTTP API. + #[clap(long, default_value = "8001")] + http_port: u16, + + /// URL to the electrum backend to use for the wallet. + #[clap(long, default_value = "ssl://electrum.blockstream.info:60002")] + electrum: String, + + /// Where to permanently store data, defaults to the current working directory. + #[clap(long)] + data_dir: Option, + + /// Generate a seed file within the data directory. + #[clap(long)] + generate_seed: bool, +} + #[rocket::main] async fn main() -> Result<()> { - let client = - bdk::electrum_client::Client::new("ssl://electrum.blockstream.info:60002").unwrap(); + let opts = Opts::parse(); + + let data_dir = opts + .data_dir + .unwrap_or_else(|| std::env::current_dir().expect("unable to get cwd")); + + if !data_dir.exists() { + tokio::fs::create_dir_all(&data_dir).await?; + } + + let seed = Seed::initialize(&data_dir.join("maker_seed"), opts.generate_seed).await?; + + let client = bdk::electrum_client::Client::new(&opts.electrum).unwrap(); // TODO: Replace with sqlite once https://github.com/bitcoindevkit/bdk/pull/376 is merged. - let db = bdk::sled::open("/tmp/maker.db")?; + let db = bdk::sled::open(data_dir.join("maker_wallet_db"))?; let wallet_db = db.open_tree("wallet")?; + let ext_priv_key = seed.derive_extended_priv_key(Network::Testnet)?; + let wallet = bdk::Wallet::new( - "wpkh(tprv8ZgxMBicQKsPd95j7aKDzWZw9Z2SiLxpz5J5iFUdqFf1unqtoonSTteF1ZSrrB831BY1eufyHehediNH76DvcDSS2JDDyDXCQKJbyd7ozVf/*)#3vkm30lf", - None, - bitcoin::Network::Testnet, + bdk::template::Bip84(ext_priv_key, KeychainKind::External), + Some(bdk::template::Bip84(ext_priv_key, KeychainKind::Internal)), + ext_priv_key.network, wallet_db, ElectrumBlockchain::from(client), ) @@ -48,15 +87,11 @@ async fn main() -> Result<()> { let (order_feed_sender, order_feed_receiver) = watch::channel::>(None); let (_balance_feed_sender, balance_feed_receiver) = watch::channel::(Amount::ZERO); - let db: Map<_, Value> = map! { - "url" => "./maker.sqlite".into(), - }; - let figment = rocket::Config::figment() - .merge(("databases", map!["maker" => db])) - .merge(("port", 8001)); + .merge(("databases.maker.url", data_dir.join("maker.sqlite"))) + .merge(("port", opts.http_port)); - let listener = tokio::net::TcpListener::bind("0.0.0.0:9999").await?; + let listener = tokio::net::TcpListener::bind(&format!("0.0.0.0:{}", opts.p2p_port)).await?; let local_addr = listener.local_addr().unwrap(); println!("Listening on {}", local_addr); diff --git a/daemon/src/seed.rs b/daemon/src/seed.rs new file mode 100644 index 0000000..a79c17d --- /dev/null +++ b/daemon/src/seed.rs @@ -0,0 +1,73 @@ +use anyhow::{anyhow, bail, Result}; +use bdk::bitcoin::util::bip32::ExtendedPrivKey; +use bdk::bitcoin::Network; +use hkdf::Hkdf; +use rand::Rng; +use sha2::Sha256; +use std::convert::TryInto; +use std::path::Path; + +pub struct Seed([u8; 256]); + +impl Seed { + /// Initialize a [`Seed`] from a path. + /// + /// Fails if the file does not exist or it exists but would be overwritten. + pub async fn initialize(seed_file: &Path, generate: bool) -> Result { + let exists = seed_file.exists(); + + let seed = match (exists, generate) { + (true, false) => Seed::read_from(seed_file).await?, + (false, true) => { + let seed = Seed::default(); + seed.write_to(seed_file).await?; + + seed + } + (true, true) => bail!("Refusing to overwrite seed at {}", seed_file.display()), + (false, false) => bail!("Seed file at {} does not exist", seed_file.display()), + }; + + Ok(seed) + } + + pub async fn read_from(path: &Path) -> Result { + let bytes = tokio::fs::read(path).await?; + + let bytes = bytes + .try_into() + .map_err(|_| anyhow!("Bytes from seed file don't fit into array"))?; + + Ok(Seed(bytes)) + } + + pub async fn write_to(&self, path: &Path) -> Result<()> { + if path.exists() { + anyhow::bail!("Refusing to overwrite file at {}", path.display()) + } + + tokio::fs::write(path, &self.0).await?; + + Ok(()) + } + + pub fn derive_extended_priv_key(&self, network: Network) -> Result { + let h = Hkdf::::new(None, &self.0); + let mut okm = [0u8; 64]; + h.expand(b"BITCOIN_WALLET_SEED", &mut okm) + .expect("okm array is of correct length"); + + let ext_priv_key = ExtendedPrivKey::new_master(network, &okm)?; + + Ok(ext_priv_key) + } +} + +impl Default for Seed { + fn default() -> Self { + let mut seed = [0u8; 256]; + rand::thread_rng().fill(&mut seed); + + Self(seed) + } +} diff --git a/daemon/src/taker.rs b/daemon/src/taker.rs index 76869e2..d259338 100644 --- a/daemon/src/taker.rs +++ b/daemon/src/taker.rs @@ -1,18 +1,22 @@ use anyhow::Result; use bdk::bitcoin::secp256k1::{schnorrsig, SECP256K1}; -use bdk::bitcoin::{self, Amount}; +use bdk::bitcoin::{Amount, Network}; use bdk::blockchain::{ElectrumBlockchain, NoopProgress}; +use bdk::KeychainKind; +use clap::Clap; use model::cfd::{Cfd, Order}; use rocket::fairing::AdHoc; -use rocket::figment::util::map; -use rocket::figment::value::{Map, Value}; use rocket_db_pools::Database; +use seed::Seed; +use std::net::SocketAddr; +use std::path::PathBuf; use tokio::sync::watch; mod db; mod keypair; mod model; mod routes_taker; +mod seed; mod send_wire_message_actor; mod taker_cfd_actor; mod taker_inc_message_actor; @@ -23,19 +27,55 @@ mod wire; #[database("taker")] pub struct Db(sqlx::SqlitePool); +#[derive(Clap)] +struct Opts { + /// The IP address of the taker to connect to. + #[clap(long, default_value = "127.0.0.1:9999")] + taker: SocketAddr, + + /// The port to listen on for the HTTP API. + #[clap(long, default_value = "8000")] + http_port: u16, + + /// URL to the electrum backend to use for the wallet. + #[clap(long, default_value = "ssl://electrum.blockstream.info:60002")] + electrum: String, + + /// Where to permanently store data, defaults to the current working directory. + #[clap(long)] + data_dir: Option, + + /// Generate a seed file within the data directory. + #[clap(long)] + generate_seed: bool, +} + #[rocket::main] async fn main() -> Result<()> { - let client = - bdk::electrum_client::Client::new("ssl://electrum.blockstream.info:60002").unwrap(); + let opts = Opts::parse(); + + let data_dir = opts + .data_dir + .unwrap_or_else(|| std::env::current_dir().expect("unable to get cwd")); + + if !data_dir.exists() { + tokio::fs::create_dir_all(&data_dir).await?; + } + + let seed = Seed::initialize(&data_dir.join("taker_seed"), opts.generate_seed).await?; + + let client = bdk::electrum_client::Client::new(&opts.electrum).unwrap(); // TODO: Replace with sqlite once https://github.com/bitcoindevkit/bdk/pull/376 is merged. - let db = bdk::sled::open("/tmp/taker.db")?; + let db = bdk::sled::open(data_dir.join("taker_wallet_db"))?; let wallet_db = db.open_tree("wallet")?; + let ext_priv_key = seed.derive_extended_priv_key(Network::Testnet)?; + let wallet = bdk::Wallet::new( - "wpkh(tprv8ZgxMBicQKsPfL3BRRo2gK3rMQwsy49vhEHCsaRJSM3gNrwnDwpdzLVQzbsDo738VHyrMK3FJAaxsBkpu8gk77SUQ197RNyF46brV2EVKRZ/*)#29cd5ajg", - None, - bitcoin::Network::Testnet, + bdk::template::Bip84(ext_priv_key, KeychainKind::External), + Some(bdk::template::Bip84(ext_priv_key, KeychainKind::Internal)), + ext_priv_key.network, wallet_db, ElectrumBlockchain::from(client), ) @@ -48,19 +88,17 @@ async fn main() -> Result<()> { let (order_feed_sender, order_feed_receiver) = watch::channel::>(None); let (_balance_feed_sender, balance_feed_receiver) = watch::channel::(Amount::ZERO); - let socket = tokio::net::TcpSocket::new_v4().unwrap(); + let socket = tokio::net::TcpSocket::new_v4()?; let connection = socket - .connect("127.0.0.1:9999".parse().unwrap()) + .connect(opts.taker) .await .expect("Maker should be online first"); let (read, write) = connection.into_split(); - let db: Map<_, Value> = map! { - "url" => "./taker.sqlite".into(), - }; - - let figment = rocket::Config::figment().merge(("databases", map!["taker" => db])); + let figment = rocket::Config::figment() + .merge(("databases.taker.url", data_dir.join("taker.sqlite"))) + .merge(("port", opts.http_port)); rocket::custom(figment) .manage(cfd_feed_receiver)