mirror of
https://github.com/aljazceru/clArk.git
synced 2025-12-18 05:34:19 +01:00
Interactive onboard implemented
This commit is contained in:
65
Cargo.lock
generated
65
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" ] }
|
||||
|
||||
@@ -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
136
ark-lib/src/musig.rs
Normal 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
153
ark-lib/src/onboard.rs
Normal 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
27
ark-lib/src/util.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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!();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
74
arkd/src/rpcserver.rs
Normal 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
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
39
noah/src/database.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user