diff --git a/Cargo.lock b/Cargo.lock index 2e4ed63..1627638 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,10 @@ name = "ark-lib" version = "0.0.0" dependencies = [ "bitcoin 0.30.0", + "ciborium", + "lazy_static", + "secp256k1-zkp", + "serde", ] [[package]] @@ -44,6 +48,7 @@ name = "ark-noah" version = "0.0.0" dependencies = [ "anyhow", + "ark-lib", "arkd-rpc-client", "bdk", "bdk_bitcoind_rpc", @@ -51,8 +56,12 @@ dependencies = [ "bdk_file_store", "bip39", "bitcoin 0.30.0", + "ciborium", + "lazy_static", "miniscript", "prost", + "serde", + "sled", "tokio", "tonic", ] @@ -62,15 +71,18 @@ name = "arkd" version = "0.1.0" dependencies = [ "anyhow", + "ark-lib", "bdk", "bdk_bitcoind_rpc", "bdk_electrum", "bdk_file_store", "bip39", "bitcoin 0.30.0", + "ciborium", "electrum-client 0.19.0", "lazy_static", "prost", + "serde", "sled", "tokio", "tonic", @@ -428,6 +440,33 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -622,6 +661,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "hashbrown" version = "0.12.3" @@ -1261,6 +1306,26 @@ dependencies = [ "cc", ] +[[package]] +name = "secp256k1-zkp" +version = "0.10.0" +source = "git+https://github.com/sanket1729/rust-secp256k1-zkp.git?rev=60e631c24588a0c9e271badd61959294848c665d#60e631c24588a0c9e271badd61959294848c665d" +dependencies = [ + "bitcoin-private", + "secp256k1 0.28.1", + "secp256k1-zkp-sys", + "serde", +] + +[[package]] +name = "secp256k1-zkp-sys" +version = "0.9.1" +source = "git+https://github.com/sanket1729/rust-secp256k1-zkp.git?rev=60e631c24588a0c9e271badd61959294848c665d#60e631c24588a0c9e271badd61959294848c665d" +dependencies = [ + "cc", + "secp256k1-sys 0.9.2", +] + [[package]] name = "serde" version = "1.0.195" diff --git a/Cargo.toml b/Cargo.toml index 573f531..ffc76bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,17 +10,18 @@ exclude = [ "napkin", ] +# TODO(stevenroose) at some point probably move these inline [workspace.dependencies] # Rust stack anyhow = "1" lazy_static = "=1.4.0" +serde = { version = "1", feature = [ "derive" ] } +ciborium = "0.2.1" # bitcoin stack bitcoin = "0.30" bip39 = { version = "2.0.0", features = [ "rand" ] } miniscript = "10.0" -# using sanket's branch for musig2 code -secp256k1-zkp = { git = "https://github.com/sanket1729/rust-secp256k1-zkp.git", rev = "60e631c24588a0c9e271badd61959294848c665d" } # bdk = "1.0.0-alpha.3" # bdk_electrum = "0.5.0" # bdk_file_store = "0.3.0" diff --git a/ark-lib/Cargo.toml b/ark-lib/Cargo.toml index 6e5fd5f..268bb49 100644 --- a/ark-lib/Cargo.toml +++ b/ark-lib/Cargo.toml @@ -5,5 +5,14 @@ license = "CC0-1.0" authors = [ "Steven Roose " ] edition = "2021" +[lib] +name = "ark" + [dependencies] -bitcoin = "0.30" +lazy_static.workspace = true +serde.workspace = true +ciborium.workspace = true +bitcoin.workspace = true + +# using sanket's branch for musig2 code +secp256k1-zkp = { git = "https://github.com/sanket1729/rust-secp256k1-zkp.git", rev = "60e631c24588a0c9e271badd61959294848c665d", features = [ "serde" ] } diff --git a/ark-lib/src/lib.rs b/ark-lib/src/lib.rs index b28b04f..e7b5299 100644 --- a/ark-lib/src/lib.rs +++ b/ark-lib/src/lib.rs @@ -1,3 +1,53 @@ +pub mod musig; +pub mod onboard; +mod util; + +use std::io::{self, Write}; + +use bitcoin::{Amount, OutPoint, Script, ScriptBuf, TxOut}; +use bitcoin::hashes::Hash; +use bitcoin::opcodes; +use bitcoin::secp256k1::{self, schnorr, PublicKey, XOnlyPublicKey}; +use serde::{Deserialize, Serialize}; + + +lazy_static::lazy_static! { + /// Global secp context. + static ref SECP: secp256k1::Secp256k1 = secp256k1::Secp256k1::new(); +} + + +#[derive(Debug, Deserialize, Serialize)] +pub enum Vtxo { + Onboard { + utxo: OutPoint, + spec: onboard::Spec, + exit_tx_signature: schnorr::Signature, + } +} + +impl Vtxo { + pub fn utxo(&self) -> OutPoint { + match self { + Vtxo::Onboard { utxo, .. } => *utxo, + } + } + + /// This is the same as [utxo] but encoded as a byte array. + pub fn id(&self) -> [u8; 36] { + let utxo = self.utxo(); + let mut ret = [0u8; 36]; + ret[0..32].copy_from_slice(&utxo.txid[..]); + ret[32..].copy_from_slice(&utxo.vout.to_le_bytes()); + ret + } + + pub fn encode(&self) -> Vec { + let mut buf = Vec::new(); + ciborium::into_writer(self, &mut buf).unwrap(); + buf + } +} diff --git a/ark-lib/src/musig.rs b/ark-lib/src/musig.rs new file mode 100644 index 0000000..4b09349 --- /dev/null +++ b/ark-lib/src/musig.rs @@ -0,0 +1,136 @@ + +pub use secp256k1_zkp as secp; +pub use secp256k1_zkp::{ + MusigAggNonce, MusigKeyAggCache, MusigPubNonce, MusigPartialSignature, MusigSecNonce, + MusigSession, MusigSessionId, Message, +}; +use bitcoin::secp256k1::{rand, schnorr, PublicKey, SecretKey, XOnlyPublicKey}; + +lazy_static::lazy_static! { + /// Global secp context. + pub static ref SECP: secp256k1_zkp::Secp256k1 = secp256k1_zkp::Secp256k1::new(); +} + +pub fn xonly_from(pk: secp256k1_zkp::XOnlyPublicKey) -> XOnlyPublicKey { + XOnlyPublicKey::from_slice(&pk.serialize()).unwrap() +} + +pub fn pubkey_to(pk: PublicKey) -> secp256k1_zkp::PublicKey { + secp256k1_zkp::PublicKey::from_slice(&pk.serialize()).unwrap() +} + +pub fn seckey_to(sk: SecretKey) -> secp256k1_zkp::SecretKey { + secp256k1_zkp::SecretKey::from_slice(&sk.secret_bytes()).unwrap() +} + +pub fn sig_from(s: secp256k1_zkp::schnorr::Signature) -> schnorr::Signature { + schnorr::Signature::from_slice(&s.serialize()).unwrap() +} + +pub fn key_agg(keys: &[PublicKey]) -> MusigKeyAggCache { + let mut keys = keys.iter().map(|k| pubkey_to(*k)).collect::>(); + keys.sort_by_key(|k| k.serialize()); + MusigKeyAggCache::new(&SECP, &keys) +} + +pub fn combine_keys(keys: &[PublicKey]) -> XOnlyPublicKey { + xonly_from(key_agg(keys).agg_pk()) +} + +/// Perform a deterministic partial sign for the given message and the +/// given counterparty key and nonce. +pub fn deterministic_partial_sign( + my_key: SecretKey, + their_key: PublicKey, + their_nonce: MusigPubNonce, + msg: [u8; 32], +) -> (MusigPubNonce, MusigPartialSignature) { + let my_pubkey = my_key.public_key(&crate::SECP); + //TODO(stevenroose) consider taking keypair for efficiency + let keypair = secp256k1_zkp::Keypair::from_seckey_slice(&SECP, &my_key.secret_bytes()).unwrap(); + let agg = key_agg(&[their_key, my_pubkey]); + + let session_id = MusigSessionId::assume_unique_per_nonce_gen(rand::random()); + + let msg = Message::from_digest(msg); + let (sec_nonce, pub_nonce) = secp::new_musig_nonce_pair( + &SECP, session_id, Some(&agg), Some(seckey_to(my_key)), pubkey_to(my_pubkey), Some(msg), None, + ).expect("asp nonce gen error"); + + let agg_nonce = MusigAggNonce::new(&SECP, &[their_nonce, pub_nonce]); + let session = MusigSession::new(&SECP, &agg, agg_nonce, msg); + let sig = session.partial_sign(&SECP, sec_nonce, &keypair, &agg) + .expect("asp partial sign error"); + (pub_nonce, sig) +} + +pub fn partial_sign( + privkey: SecretKey, + pubkeys: &[PublicKey], + sec_nonce: MusigSecNonce, + pub_nonces: &[MusigPubNonce], + sighash: [u8; 32], + other_sigs: Option<&[MusigPartialSignature]>, +) -> (MusigPartialSignature, Option) { + let agg = key_agg(pubkeys); + let agg_nonce = MusigAggNonce::new(&SECP, pub_nonces); + let msg = secp256k1_zkp::Message::from_digest(sighash); + let session = MusigSession::new(&SECP, &agg, agg_nonce, msg); + //TODO(stevenroose) consider taking keypair for efficiency + let keypair = secp256k1_zkp::Keypair::from_seckey_slice(&SECP, &privkey.secret_bytes()).unwrap(); + let my_sig = session.partial_sign(&SECP, sec_nonce, &keypair, &agg).expect("musig partial sign error"); + let final_sig = if let Some(others) = other_sigs { + let mut sigs = Vec::with_capacity(others.len() + 1); + sigs.extend_from_slice(others); + sigs.push(my_sig); + Some(session.partial_sig_agg(&sigs)) + } else { + None + }; + (my_sig, final_sig.map(sig_from)) +} + +//TODO(stevenroose) probably get rid of all this by having native byte serializers in secp +pub mod serde { + use super::*; + use ::serde::{Deserializer, Serializer}; + use ::serde::de::{self, Error}; + + struct BytesVisitor; + impl<'de> de::Visitor<'de> for BytesVisitor { + type Value = Vec; + fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "a byte object") + } + fn visit_bytes(self, v: &[u8]) -> Result { + Ok(v.to_vec()) + } + fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result { + Ok(v.to_vec()) + } + fn visit_byte_buf(self, v: Vec) -> Result { + Ok(v) + } + } + + pub mod pubnonce { + use super::*; + pub fn serialize(pub_nonce: &MusigPubNonce, s: S) -> Result { + s.serialize_bytes(&pub_nonce.serialize()) + } + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + let v = d.deserialize_byte_buf(BytesVisitor)?; + MusigPubNonce::from_slice(&v).map_err(D::Error::custom) + } + } + pub mod partialsig { + use super::*; + pub fn serialize(sig: &MusigPartialSignature, s: S) -> Result { + s.serialize_bytes(&sig.serialize()) + } + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + let v = d.deserialize_byte_buf(BytesVisitor)?; + MusigPartialSignature::from_slice(&v).map_err(D::Error::custom) + } + } +} diff --git a/ark-lib/src/onboard.rs b/ark-lib/src/onboard.rs new file mode 100644 index 0000000..f3da7b4 --- /dev/null +++ b/ark-lib/src/onboard.rs @@ -0,0 +1,153 @@ + +//! Onboard flow: +//! +//! * User starts by using the [new_user] function that crates the user's parts. +//! * ASP does a deterministic sign and sends ASP part using [new_asp]. +//! * User also signs and combines sigs using [finish] and stores vtxo. + +use bitcoin::{Amount, OutPoint, Sequence, ScriptBuf, Transaction, TxIn, TxOut, Witness}; +use bitcoin::blockdata::locktime::absolute::LockTime; +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::{PublicKey, SecretKey}; +use bitcoin::secp256k1::rand; +use bitcoin::sighash::{self, SighashCache, TapSighash}; +use serde::{Deserialize, Serialize}; + +use crate::{musig, util, SECP, Vtxo}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Spec { + pub user_key: PublicKey, + pub asp_key: PublicKey, + pub expiry_delta: u16, + pub exit_delta: u16, + #[serde(with = "bitcoin::amount::serde::as_sat")] + pub amount: Amount, +} + +pub fn onboard_spk(spec: &Spec) -> ScriptBuf { + let expiry = util::delayed_sign(spec.expiry_delta, spec.asp_key.x_only_public_key().0); + let taproot = bitcoin::taproot::TaprootBuilder::new() + .add_leaf(0, expiry).unwrap() + .finalize(&SECP, musig::combine_keys(&[spec.user_key, spec.asp_key])).unwrap(); + ScriptBuf::new_v1_p2tr_tweaked(taproot.output_key()) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UserPart { + pub spec: Spec, + pub utxo: OutPoint, + #[serde(with = "musig::serde::pubnonce")] + pub nonce: musig::MusigPubNonce, +} + +#[derive(Debug)] +pub struct PrivateUserPart { + pub session_id_bytes: [u8; 32], + pub sec_nonce: musig::MusigSecNonce, +} + +pub fn new_user(spec: Spec, utxo: OutPoint) -> (UserPart, PrivateUserPart) { + let session_id_bytes = rand::random::<[u8; 32]>(); + let session_id = musig::MusigSessionId::assume_unique_per_nonce_gen(session_id_bytes); + let agg = musig::key_agg(&[spec.user_key, spec.asp_key]); + + let (unlock_sighash, unlock_tx) = unlock_tx_sighash(&spec, utxo); + let (sec_nonce, pub_nonce) = agg.nonce_gen( + &musig::SECP, + session_id, + musig::pubkey_to(spec.user_key), + musig::secp::Message::from_digest(unlock_sighash.to_byte_array()), + Some(rand::random()), + ).expect("nonce gen"); + + let user_part = UserPart { + spec, utxo, nonce: pub_nonce, + }; + let private_user_part = PrivateUserPart { + session_id_bytes, sec_nonce, + }; + (user_part, private_user_part) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AspPart { + #[serde(with = "musig::serde::pubnonce")] + pub nonce: musig::MusigPubNonce, + #[serde(with = "musig::serde::partialsig")] + pub signature: musig::MusigPartialSignature, +} + +pub fn new_asp(user: &UserPart, seckey: SecretKey) -> AspPart { + let (unlock_sighash, _unlock_tx) = unlock_tx_sighash(&user.spec, user.utxo); + let msg = unlock_sighash.to_byte_array(); + let (pub_nonce, sig) = musig::deterministic_partial_sign(seckey, user.spec.user_key, user.nonce, msg); + AspPart { + nonce: pub_nonce, + signature: sig, + } +} + +pub fn create_unlock_tx(spec: &Spec, utxo: OutPoint) -> Transaction { + let exit_timeout = util::delayed_sign(spec.exit_delta, spec.user_key.x_only_public_key().0); + let unlock_tr = bitcoin::taproot::TaprootBuilder::new() + .add_leaf(0, exit_timeout).unwrap() + .finalize(&SECP, musig::combine_keys(&[spec.user_key, spec.asp_key])).unwrap(); + let unlock_spk = ScriptBuf::new_v1_p2tr_tweaked(unlock_tr.output_key()); + Transaction { + version: 2, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: utxo, + script_sig: ScriptBuf::new(), + sequence: Sequence::ZERO, + witness: Witness::new(), + }], + output: vec![ + TxOut { + script_pubkey: unlock_spk, + value: (spec.amount - util::DUST).to_sat(), + }, + util::dust_fee_anchor(), + ], + } +} + +pub fn unlock_tx_sighash(spec: &Spec, utxo: OutPoint) -> (TapSighash, Transaction) { + let unlock_tx = create_unlock_tx(spec, utxo); + let mut cache = SighashCache::new(&unlock_tx); + let prev = TxOut { + script_pubkey: onboard_spk(&spec), + value: spec.amount.to_sat(), + }; + let sighash = cache.taproot_key_spend_signature_hash( + 0, + &sighash::Prevouts::One(0, &prev), + sighash::TapSighashType::All, + ).expect("sighash calc error"); + (sighash, unlock_tx) +} + +pub fn finish( + user: UserPart, + private: PrivateUserPart, + asp: AspPart, + privkey: SecretKey, +) -> Vtxo { + let (unlock_sighash, _unlock_tx) = unlock_tx_sighash(&user.spec, user.utxo); + let (_user_sig, final_sig) = musig::partial_sign( + privkey, + &[user.spec.user_key, user.spec.asp_key], + private.sec_nonce, + &[user.nonce, asp.nonce], + unlock_sighash.to_byte_array(), + Some(&[asp.signature]), + ); + assert!(final_sig.is_some()); + + Vtxo::Onboard { + utxo: user.utxo, + spec: user.spec, + exit_tx_signature: final_sig.unwrap(), + } +} diff --git a/ark-lib/src/util.rs b/ark-lib/src/util.rs new file mode 100644 index 0000000..94336b2 --- /dev/null +++ b/ark-lib/src/util.rs @@ -0,0 +1,27 @@ + +use bitcoin::{Amount, Script, ScriptBuf, TxOut}; +use bitcoin::opcodes; +use bitcoin::secp256k1::XOnlyPublicKey; + +/// Taproot-compatible dust value of 330 satoshis. +pub const DUST: Amount = Amount::from_sat(330); + +/// Create a tapscript that is a checksig and a relative timelock. +pub fn delayed_sign(delay_blocks: u16, pubkey: XOnlyPublicKey) -> ScriptBuf { + let csv = bitcoin::blockdata::transaction::Sequence::from_height(delay_blocks); + bitcoin::Script::builder() + .push_int(csv.to_consensus_u32().try_into().unwrap()) + .push_opcode(opcodes::all::OP_CSV) + .push_opcode(opcodes::all::OP_DROP) + .push_slice(pubkey.serialize()) + .push_opcode(opcodes::all::OP_CHECKSIG) + .into_script() +} + +/// Create an OP_TRUE fee anchor with the dust amount. +pub fn dust_fee_anchor() -> TxOut { + TxOut { + script_pubkey: Script::builder().push_opcode(opcodes::OP_TRUE).into_script(), + value: DUST.to_sat(), + } +} diff --git a/arkd-rpc-client/src/arkd.rs b/arkd-rpc-client/src/arkd.rs index a83d486..ddd20fb 100644 --- a/arkd-rpc-client/src/arkd.rs +++ b/arkd-rpc-client/src/arkd.rs @@ -10,19 +10,13 @@ pub struct ArkInfo { #[derive(Clone, PartialEq, ::prost::Message)] pub struct OnboardCosignRequest { #[prost(bytes = "vec", tag = "1")] - pub onboard_tx: ::prost::alloc::vec::Vec, - #[prost(uint32, tag = "2")] - pub onboard_tx_vout: u32, - #[prost(bytes = "vec", tag = "3")] - pub exit_tx: ::prost::alloc::vec::Vec, - #[prost(bytes = "vec", tag = "4")] - pub exit_tx_signature: ::prost::alloc::vec::Vec, + pub user_part: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct OnboardCosignResponse { #[prost(bytes = "vec", tag = "1")] - pub exit_tx_signature: ::prost::alloc::vec::Vec, + pub asp_part: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/arkd/Cargo.toml b/arkd/Cargo.toml index ea86e8a..778f08e 100644 --- a/arkd/Cargo.toml +++ b/arkd/Cargo.toml @@ -9,8 +9,12 @@ edition = "2021" tonic-build = "0.10" [dependencies] +ark-lib = { path = "../ark-lib" } + anyhow.workspace = true lazy_static.workspace = true +serde.workspace = true +ciborium.workspace = true bitcoin.workspace = true bip39.workspace = true bdk.workspace = true diff --git a/arkd/rpc-protos/arkd.proto b/arkd/rpc-protos/arkd.proto index 183f9d8..c396a8e 100644 --- a/arkd/rpc-protos/arkd.proto +++ b/arkd/rpc-protos/arkd.proto @@ -16,14 +16,11 @@ message ArkInfo { } message OnboardCosignRequest { - bytes onboard_tx = 1; - uint32 onboard_tx_vout = 2; - bytes exit_tx = 3; - bytes exit_tx_signature = 4; + bytes user_part = 1; } message OnboardCosignResponse { - bytes exit_tx_signature = 1; + bytes asp_part = 1; } diff --git a/arkd/src/lib.rs b/arkd/src/lib.rs index 63f783f..d223982 100644 --- a/arkd/src/lib.rs +++ b/arkd/src/lib.rs @@ -2,6 +2,7 @@ mod database; mod rpc; +mod rpcserver; use std::fs; use std::net::SocketAddr; @@ -90,7 +91,11 @@ impl App { wallet: Mutex::new(wallet), bitcoind: bitcoind, }); - ret.start_public_rpc_server(); + + let app = ret.clone(); + let _ = tokio::spawn(async move { + app.start_public_rpc_server().await; + }); Ok(ret) } @@ -141,36 +146,7 @@ impl App { Ok(Amount::from_sat(balance.total())) } - pub async fn start_public_rpc_server(self: &Arc) { - let addr = self.config.public_rpc_address; - let server = rpc::ArkServiceServer::new(self.clone()); - //TODO(stevenroose) capture thread so we can cancel later - let _ = tokio::spawn(async move { - tonic::transport::Server::builder() - .add_service(server) - .serve(addr) - .await - }); + pub fn cosign_onboard(self: &Arc, user_part: ark::onboard::UserPart) -> ark::onboard::AspPart { + ark::onboard::new_asp(&user_part, self.master_key) } } - -#[tonic::async_trait] -impl rpc::ArkService for Arc { - async fn get_ark_info( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - let ret = rpc::ArkInfo { - pubkey: self.master_pubkey.serialize().to_vec(), - xonly_pubkey: self.master_pubkey.x_only_public_key().0.serialize().to_vec(), - }; - Ok(tonic::Response::new(ret)) - } - async fn request_onboard_cosign( - &self, - req: tonic::Request, - ) -> Result, tonic::Status> { - unimplemented!(); - } -} - diff --git a/arkd/src/rpc/arkd.rs b/arkd/src/rpc/arkd.rs index 0ab5988..f76a9a5 100644 --- a/arkd/src/rpc/arkd.rs +++ b/arkd/src/rpc/arkd.rs @@ -10,19 +10,13 @@ pub struct ArkInfo { #[derive(Clone, PartialEq, ::prost::Message)] pub struct OnboardCosignRequest { #[prost(bytes = "vec", tag = "1")] - pub onboard_tx: ::prost::alloc::vec::Vec, - #[prost(uint32, tag = "2")] - pub onboard_tx_vout: u32, - #[prost(bytes = "vec", tag = "3")] - pub exit_tx: ::prost::alloc::vec::Vec, - #[prost(bytes = "vec", tag = "4")] - pub exit_tx_signature: ::prost::alloc::vec::Vec, + pub user_part: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct OnboardCosignResponse { #[prost(bytes = "vec", tag = "1")] - pub exit_tx_signature: ::prost::alloc::vec::Vec, + pub asp_part: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/arkd/src/rpcserver.rs b/arkd/src/rpcserver.rs new file mode 100644 index 0000000..3a4794c --- /dev/null +++ b/arkd/src/rpcserver.rs @@ -0,0 +1,74 @@ + +use std::sync::Arc; + +use crate::App; +use crate::rpc; + +impl App { + pub async fn start_public_rpc_server(self: &Arc) { + let addr = self.config.public_rpc_address; + let server = rpc::ArkServiceServer::new(self.clone()); + //TODO(stevenroose) capture thread so we can cancel later + let _ = tokio::spawn(async move { + tonic::transport::Server::builder() + .add_service(server) + .serve(addr) + .await + }); + } +} + +macro_rules! badarg { + ($($arg:tt)*) => {{ + tonic::Status::invalid_argument(format!($($arg)*)) + }}; +} + +macro_rules! internal { + ($($arg:tt)*) => {{ + tonic::Status::internal(format!($($arg)*)) + }}; +} + +/// Just a trait to easily convert some kind of errors to tonic things. +trait ToStatus { + fn to_status(self) -> Result; +} + +impl ToStatus for anyhow::Result { + fn to_status(self) -> Result { + self.map_err(|e| tonic::Status::internal(format!("internal error: {}", e))) + } +} + +#[tonic::async_trait] +impl rpc::ArkService for Arc { + async fn get_ark_info( + &self, + _req: tonic::Request, + ) -> Result, tonic::Status> { + let ret = rpc::ArkInfo { + pubkey: self.master_pubkey.serialize().to_vec(), + xonly_pubkey: self.master_pubkey.x_only_public_key().0.serialize().to_vec(), + }; + Ok(tonic::Response::new(ret)) + } + + async fn request_onboard_cosign( + &self, + req: tonic::Request, + ) -> Result, tonic::Status> { + let req = req.into_inner(); + let user_part = ciborium::from_reader(&req.user_part[..]) + .map_err(|e| badarg!("invalid user part: {}", e))?; + let asp_part = self.cosign_onboard(user_part); + Ok(tonic::Response::new(rpc::OnboardCosignResponse { + asp_part: { + let mut buf = Vec::new(); + ciborium::into_writer(&asp_part, &mut buf).unwrap(); + buf + }, + })) + } +} + diff --git a/noah/Cargo.toml b/noah/Cargo.toml index 789c365..1ee9374 100644 --- a/noah/Cargo.toml +++ b/noah/Cargo.toml @@ -9,9 +9,13 @@ edition = "2021" name = "noah" [dependencies] +ark-lib = { path = "../ark-lib" } arkd-rpc-client = { path = "../arkd-rpc-client" } anyhow.workspace = true +lazy_static.workspace = true +serde.workspace = true +ciborium.workspace = true bitcoin.workspace = true bip39.workspace = true miniscript.workspace = true @@ -23,3 +27,4 @@ prost.workspace = true tonic.workspace = true tokio.workspace = true +sled = "0.34.7" diff --git a/noah/src/database.rs b/noah/src/database.rs new file mode 100644 index 0000000..b621fcb --- /dev/null +++ b/noah/src/database.rs @@ -0,0 +1,39 @@ + +use std::path::Path; + +use anyhow::{bail, Context}; +use bitcoin::consensus::encode; +use sled::transaction as tx; + +use ark::Vtxo; + +const VTXO_TREE: &str = "vtxos"; + + +pub struct Db { + db: sled::Db, +} + +impl Db { + pub fn open(path: &Path) -> anyhow::Result { + Ok(Db { + db: sled::open(path).context("failed to open db")?, + }) + } + + /// Utility function for transactions that fixes the annoying generics. + fn transaction(&self, + f: impl Fn(&tx::TransactionalTree) -> tx::ConflictableTransactionResult<(), ()>, + ) -> anyhow::Result<()> { + if let Err(e) = self.db.transaction(f) { + bail!("db error in transaction: {:?}", e) + } else { + Ok(()) + } + } + + pub fn store_vtxo(&self, vtxo: Vtxo) -> anyhow::Result<()> { + self.db.open_tree(VTXO_TREE)?.insert(vtxo.id(), vtxo.encode())?; + Ok(()) + } +} diff --git a/noah/src/lib.rs b/noah/src/lib.rs index 6caf46e..c142dbf 100644 --- a/noah/src/lib.rs +++ b/noah/src/lib.rs @@ -1,17 +1,26 @@ #![allow(unused)] +mod database; mod onchain; + use std::fs; use std::path::PathBuf; use std::str::FromStr; use anyhow::{bail, Context}; -use bitcoin::{Address, Amount, Network}; +use bitcoin::{bip32, secp256k1}; +use bitcoin::{Address, Amount, Network, OutPoint}; +use bitcoin::secp256k1::PublicKey; use arkd_rpc_client::ArkServiceClient; +lazy_static::lazy_static! { + /// Global secp context. + static ref SECP: secp256k1::Secp256k1 = secp256k1::Secp256k1::new(); +} + pub struct Config { pub network: Network, pub datadir: PathBuf, @@ -20,8 +29,12 @@ pub struct Config { pub struct Wallet { config: Config, + db: database::Db, onchain: onchain::Wallet, asp: ArkServiceClient, + vtxo_seed: bip32::ExtendedPrivKey, + // ASP info + asp_key: PublicKey, } impl Wallet { @@ -57,12 +70,24 @@ impl Wallet { let onchain = onchain::Wallet::create(config.network, seed, &config.datadir) .context("failed to create onchain wallet")?; + // open db + let db = database::Db::open(&config.datadir.join("db")).context("failed to open db")?; + + let vtxo_seed = { + let master = bip32::ExtendedPrivKey::new_master(config.network, &seed).unwrap(); + master.derive_priv(&SECP, &[350.into()]).unwrap() + }; + let asp_endpoint = tonic::transport::Uri::from_str(&config.asp_address) .context("invalid asp addr")?; - let asp = ArkServiceClient::connect(asp_endpoint) + let mut asp = ArkServiceClient::connect(asp_endpoint) .await.context("failed to connect to asp")?; - Ok(Wallet { config, onchain, asp }) + let ark_info = asp.get_ark_info(arkd_rpc_client::Empty{}) + .await.context("ark info request failed")?.into_inner(); + let asp_key = PublicKey::from_slice(&ark_info.pubkey).context("asp pubkey")?; + + Ok(Wallet { config, db, onchain, asp, vtxo_seed, asp_key }) } pub fn get_new_onchain_address(&mut self) -> anyhow::Result
{ @@ -72,4 +97,43 @@ impl Wallet { pub fn onchain_balance(&mut self) -> anyhow::Result { self.onchain.sync() } + + pub async fn onboard(&mut self, amount: Amount) -> anyhow::Result<()> { + let key = self.vtxo_seed.derive_priv(&SECP, &[0.into()]).unwrap(); //TODO(stevenroose) fix + let spec = ark::onboard::Spec { + user_key: key.private_key.public_key(&SECP), + asp_key: self.asp_key, + expiry_delta: 14 * 144, + exit_delta: 144, + amount: amount, + }; + let addr = Address::from_script(&ark::onboard::onboard_spk(&spec), self.config.network).unwrap(); + + // We create the onboard tx template, but don't sign it yet. + let onboard_tx = self.onchain.prepare_tx(addr, amount)?; + let utxo = OutPoint::new(onboard_tx.unsigned_tx.txid(), 0); + + // We ask the ASP to cosign our onboard unlock tx. + let (user_part, priv_user_part) = ark::onboard::new_user(spec, utxo); + let asp_part = { + let res = self.asp.request_onboard_cosign(arkd_rpc_client::OnboardCosignRequest { + user_part: { + let mut buf = Vec::new(); + ciborium::into_writer(&user_part, &mut buf).unwrap(); + buf + }, + }).await.context("error requesting onboard cosign")?; + ciborium::from_reader::(&res.into_inner().asp_part[..]) + .context("invalid ASP part in response")? + }; + + // Store vtxo first before we actually make the on-chain tx. + let vtxo = ark::onboard::finish(user_part, priv_user_part, asp_part, key.private_key); + self.db.store_vtxo(vtxo).context("db error storing vtxo")?; + + let tx = self.onchain.finish_tx(onboard_tx)?; + self.onchain.broadcast_tx(&tx)?; + + Ok(()) + } } diff --git a/noah/src/onchain/mod.rs b/noah/src/onchain/mod.rs index 98aac41..55220a3 100644 --- a/noah/src/onchain/mod.rs +++ b/noah/src/onchain/mod.rs @@ -15,7 +15,8 @@ use bdk_electrum::{ ElectrumExt, ElectrumUpdate, }; use bdk_file_store::Store; -use bitcoin::{Address, Amount, Network, Txid}; +use bitcoin::{Address, Amount, Network, Transaction, Txid}; +use bitcoin::psbt::PartiallySignedTransaction as Psbt; //TODO(stevenroose) when v0.31 use bitcoin::bip32; use miniscript::Descriptor; @@ -89,22 +90,32 @@ impl Wallet { Ok(Amount::from_sat(balance.total())) } - pub fn send_money(&mut self, address: Address, amount: Amount) -> anyhow::Result { + pub fn prepare_tx(&mut self, dest: Address, amount: Amount) -> anyhow::Result { let mut tx_builder = self.wallet.build_tx(); tx_builder - .add_recipient(address.script_pubkey(), amount.to_sat()) + .add_recipient(dest.script_pubkey(), amount.to_sat()) .enable_rbf(); + Ok(tx_builder.finish()?) + } - let mut psbt = tx_builder.finish()?; + pub fn finish_tx(&mut self, mut psbt: Psbt) -> anyhow::Result { let finalized = self.wallet.sign(&mut psbt, SignOptions::default())?; assert!(finalized); + Ok(psbt.extract_tx()) + } - let tx = psbt.extract_tx(); - bdk_bitcoind_rpc::bitcoincore_rpc::RpcApi::send_raw_transaction(&self.bitcoind, &tx)?; + pub fn broadcast_tx(&self, tx: &Transaction) -> anyhow::Result { + bdk_bitcoind_rpc::bitcoincore_rpc::RpcApi::send_raw_transaction(&self.bitcoind, tx)?; // self.electrum.transaction_broadcast(&tx)?; Ok(tx.txid()) } + pub fn send_money(&mut self, dest: Address, amount: Amount) -> anyhow::Result { + let mut psbt = self.prepare_tx(dest, amount)?; + let tx = self.finish_tx(psbt)?; + self.broadcast_tx(&tx) + } + pub fn new_address(&mut self) -> anyhow::Result
{ Ok(self.wallet.try_get_address(bdk::wallet::AddressIndex::New)?.address) }