Interactive onboard implemented

This commit is contained in:
Steven Roose
2024-01-17 13:17:14 +00:00
parent fd241ac574
commit 9933368b48
17 changed files with 664 additions and 65 deletions

65
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -5,5 +5,14 @@ license = "CC0-1.0"
authors = [ "Steven Roose <steven@roose.io>" ]
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" ] }

View File

@@ -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::All> = 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<u8> {
let mut buf = Vec::new();
ciborium::into_writer(self, &mut buf).unwrap();
buf
}
}

136
ark-lib/src/musig.rs Normal file
View File

@@ -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::All> = 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::<Vec<_>>();
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<schnorr::Signature>) {
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<u8>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "a byte object")
}
fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
Ok(v.to_vec())
}
fn visit_borrowed_bytes<E: de::Error>(self, v: &'de [u8]) -> Result<Self::Value, E> {
Ok(v.to_vec())
}
fn visit_byte_buf<E: de::Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
Ok(v)
}
}
pub mod pubnonce {
use super::*;
pub fn serialize<S: Serializer>(pub_nonce: &MusigPubNonce, s: S) -> Result<S::Ok, S::Error> {
s.serialize_bytes(&pub_nonce.serialize())
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<MusigPubNonce, D::Error> {
let v = d.deserialize_byte_buf(BytesVisitor)?;
MusigPubNonce::from_slice(&v).map_err(D::Error::custom)
}
}
pub mod partialsig {
use super::*;
pub fn serialize<S: Serializer>(sig: &MusigPartialSignature, s: S) -> Result<S::Ok, S::Error> {
s.serialize_bytes(&sig.serialize())
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<MusigPartialSignature, D::Error> {
let v = d.deserialize_byte_buf(BytesVisitor)?;
MusigPartialSignature::from_slice(&v).map_err(D::Error::custom)
}
}
}

153
ark-lib/src/onboard.rs Normal file
View File

@@ -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(),
}
}

27
ark-lib/src/util.rs Normal file
View File

@@ -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(),
}
}

View File

@@ -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<u8>,
#[prost(uint32, tag = "2")]
pub onboard_tx_vout: u32,
#[prost(bytes = "vec", tag = "3")]
pub exit_tx: ::prost::alloc::vec::Vec<u8>,
#[prost(bytes = "vec", tag = "4")]
pub exit_tx_signature: ::prost::alloc::vec::Vec<u8>,
pub user_part: ::prost::alloc::vec::Vec<u8>,
}
#[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<u8>,
pub asp_part: ::prost::alloc::vec::Vec<u8>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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<Self>) {
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<Self>, 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<App> {
async fn get_ark_info(
&self,
_: tonic::Request<rpc::Empty>,
) -> Result<tonic::Response<rpc::ArkInfo>, 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<rpc::OnboardCosignRequest>,
) -> Result<tonic::Response<rpc::OnboardCosignResponse>, tonic::Status> {
unimplemented!();
}
}

View File

@@ -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<u8>,
#[prost(uint32, tag = "2")]
pub onboard_tx_vout: u32,
#[prost(bytes = "vec", tag = "3")]
pub exit_tx: ::prost::alloc::vec::Vec<u8>,
#[prost(bytes = "vec", tag = "4")]
pub exit_tx_signature: ::prost::alloc::vec::Vec<u8>,
pub user_part: ::prost::alloc::vec::Vec<u8>,
}
#[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<u8>,
pub asp_part: ::prost::alloc::vec::Vec<u8>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]

74
arkd/src/rpcserver.rs Normal file
View File

@@ -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<Self>) {
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<T> {
fn to_status(self) -> Result<T, tonic::Status>;
}
impl<T> ToStatus<T> for anyhow::Result<T> {
fn to_status(self) -> Result<T, tonic::Status> {
self.map_err(|e| tonic::Status::internal(format!("internal error: {}", e)))
}
}
#[tonic::async_trait]
impl rpc::ArkService for Arc<App> {
async fn get_ark_info(
&self,
_req: tonic::Request<rpc::Empty>,
) -> Result<tonic::Response<rpc::ArkInfo>, 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<rpc::OnboardCosignRequest>,
) -> Result<tonic::Response<rpc::OnboardCosignResponse>, 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
},
}))
}
}

View File

@@ -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"

39
noah/src/database.rs Normal file
View File

@@ -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<Db> {
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(())
}
}

View File

@@ -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::All> = 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<tonic::transport::Channel>,
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<Address> {
@@ -72,4 +97,43 @@ impl Wallet {
pub fn onchain_balance(&mut self) -> anyhow::Result<Amount> {
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::<ark::onboard::AspPart, _>(&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(())
}
}

View File

@@ -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<Txid> {
pub fn prepare_tx(&mut self, dest: Address, amount: Amount) -> anyhow::Result<Psbt> {
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<Transaction> {
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<Txid> {
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<Txid> {
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<Address> {
Ok(self.wallet.try_get_address(bdk::wallet::AddressIndex::New)?.address)
}