WIP: onchain refund

on-chain-protocol
rishflab 3 years ago
parent 4a99b89df4
commit 0c9caef60f

40
Cargo.lock generated

@ -3725,16 +3725,16 @@ dependencies = [
"atty",
"backoff",
"base64 0.13.0",
"bdk",
"bdk-testutils",
"big-bytes",
"bitcoin",
"bitcoin-harness",
"bmrng",
"config",
"conquer-once",
"curve25519-dalek",
"dialoguer",
"bdk",
"bdk-testutils",
"big-bytes",
"bitcoin",
"bitcoin-harness",
"bmrng",
"config",
"conquer-once",
"curve25519-dalek",
"dialoguer",
"directories-next",
"ecdsa_fun",
"futures",
@ -3756,16 +3756,16 @@ dependencies = [
"reqwest",
"rust_decimal",
"serde",
"serde_cbor",
"serde_json",
"sha2 0.9.3",
"sigma_fun",
"sled",
"spectral",
"structopt",
"strum",
"tempfile",
"testcontainers 0.12.0",
"serde_cbor",
"serde_json",
"sha2 0.9.3",
"sigma_fun",
"sled",
"spectral",
"structopt",
"strum",
"tempfile",
"testcontainers 0.12.0",
"thiserror",
"time 0.2.26",
"tokio 1.4.0",

@ -96,6 +96,7 @@ fn final_challenge(
Ok((h_last, h_0))
}
#[derive(Clone)]
pub struct AdaptorSignature {
s_0_a: Scalar,
s_0_b: Scalar,

@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TxLock {
inner: PartiallySignedTransaction,
pub(in crate::bitcoin) output_descriptor: Descriptor<::bitcoin::PublicKey>,
pub(crate) output_descriptor: Descriptor<::bitcoin::PublicKey>,
}
impl TxLock {

@ -27,6 +27,6 @@ pub mod monero;
pub mod network;
pub mod protocol;
pub mod seed;
mod xmr_first_protocol;
pub mod xmr_first_protocol;
mod monero_ext;

@ -40,8 +40,8 @@ impl PrivateViewKey {
Self(private_key)
}
pub fn public(&self) -> PrivateViewKey {
PrivateViewKey(PublicKey::from_private_key(&self.0))
pub fn public(&self) -> PublicViewKey {
PublicViewKey(PublicKey::from_private_key(&self.0))
}
}
@ -59,14 +59,14 @@ impl From<PrivateViewKey> for PrivateKey {
}
}
impl From<PrivateViewKey> for PublicKey {
fn from(from: PrivateViewKey) -> Self {
impl From<PublicViewKey> for PublicKey {
fn from(from: PublicViewKey) -> Self {
from.0
}
}
#[derive(Clone, Copy, Debug)]
pub struct PrivateViewKey(PublicKey);
pub struct PublicViewKey(PublicKey);
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)]
pub struct Amount(u64);

@ -1,5 +1,7 @@
use crate::env::Config;
use crate::monero::{Amount, InsufficientFunds, PrivateViewKey, TransferProof, TxHash};
use crate::monero::{
Amount, InsufficientFunds, PrivateViewKey, PublicViewKey, TransferProof, TxHash,
};
use ::monero::{Address, Network, PrivateKey, PublicKey};
use anyhow::{Context, Result};
use monero_rpc::wallet;
@ -273,14 +275,14 @@ impl Wallet {
#[derive(Debug)]
pub struct TransferRequest {
pub public_spend_key: PublicKey,
pub public_view_key: PrivateViewKey,
pub public_view_key: PublicViewKey,
pub amount: Amount,
}
#[derive(Debug)]
pub struct WatchRequest {
pub public_spend_key: PublicKey,
pub public_view_key: PrivateViewKey,
pub public_view_key: PublicViewKey,
pub transfer_proof: TransferProof,
pub conf_target: u64,
pub expected: Amount,

@ -1,151 +1,14 @@
use crate::bitcoin::Txid;
use crate::monero::wallet::WatchRequest;
use crate::monero::{PrivateViewKey, Scalar, TransferRequest};
use crate::xmr_first_protocol::transactions::xmr_lock::XmrLock;
use anyhow::Result;
use monero::PublicKey;
use monero_adaptor::alice::Alice2;
use monero_adaptor::AdaptorSignature;
use rand::rngs::OsRng;
// start
pub struct Alice3 {
pub adaptor_sig: AdaptorSignature,
s_a: Scalar,
S_b_monero: monero::PublicKey,
}
// published xmr_lock, watching for btc_lock
pub struct Alice4 {
pub adaptor_sig: AdaptorSignature,
}
// published seen btc_lock, published btc_redeem
pub struct Alice5 {}
// watching for xmr_lock
pub struct Bob3 {
xmr_swap_amount: crate::monero::Amount,
btc_swap_amount: crate::bitcoin::Amount,
xmr_lock: XmrLock,
}
impl Bob3 {
pub fn watch_for_lock_xmr(&self, wallet: &crate::monero::Wallet) {
let req = WatchRequest {
public_spend_key: self.xmr_lock.public_spend_key,
public_view_key: self.xmr_lock.public_view_key,
transfer_proof: self.xmr_lock.transfer_proof.clone(),
conf_target: 1,
expected: self.xmr_swap_amount,
};
wallet.watch_for_transfer(req);
}
}
// published btc_lock, watching for xmr_redeem
pub struct Bob4;
mod alice;
mod bob;
pub mod alice;
pub mod bob;
mod state_machine;
mod transactions;
impl Alice3 {
pub fn new(alice2: Alice2, S_b_monero: PublicKey) -> Self {
Self {
adaptor_sig: alice2.adaptor_sig,
s_a: Scalar::random(&mut OsRng),
S_b_monero,
}
}
pub fn publish_xmr_lock(&self, wallet: &crate::monero::Wallet) -> Result<Alice4> {
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: self.s_a });
let public_spend_key = S_a + self.S_b_monero;
let public_view_key = self.v.public();
let req = TransferRequest {
public_spend_key,
public_view_key,
amount: self.xmr,
};
// we may have to send this to Bob
let _ = wallet.transfer(req)?;
}
}
impl Alice4 {
pub fn watch_for_btc_lock(&self, wallet: &crate::bitcoin::Wallet) -> Result<Alice4> {
wallet.subscribe_to(self.btc_lock());
}
}
pub struct SeenBtcLock {
s_0_b: monero::Scalar,
s_0_b: crate::monero::Scalar,
pub adaptor_sig: AdaptorSignature,
tx_lock_id: Txid,
tx_lock: bitcoin::Transaction,
}
#[cfg(test)]
mod test {
use crate::monero::Scalar;
use crate::xmr_first_protocol::Alice3;
use curve25519_dalek::constants::ED25519_BASEPOINT_POINT;
use curve25519_dalek::edwards::EdwardsPoint;
use monero_adaptor::alice::Alice0;
use monero_adaptor::bob::Bob0;
use rand::rngs::OsRng;
#[test]
fn happy_path() {
let msg_to_sign = b"hello world, monero is amazing!!";
let s_prime_a = Scalar::random(&mut OsRng);
let s_b = Scalar::random(&mut OsRng);
let pk = (s_prime_a + s_b) * ED25519_BASEPOINT_POINT;
let (r_a, R_a, R_prime_a) = {
let r_a = Scalar::random(&mut OsRng);
let R_a = r_a * ED25519_BASEPOINT_POINT;
let pk_hashed_to_point = hash_point_to_point(pk);
let R_prime_a = r_a * pk_hashed_to_point;
(r_a, R_a, R_prime_a)
};
let mut ring = [EdwardsPoint::default(); RING_SIZE];
ring[0] = pk;
ring[1..].fill_with(|| {
let x = Scalar::random(&mut OsRng);
x * ED25519_BASEPOINT_POINT
});
let alice = Alice0::new(ring, *msg_to_sign, R_a, R_prime_a, s_prime_a).unwrap();
let bob = Bob0::new(ring, *msg_to_sign, R_a, R_prime_a, s_b).unwrap();
let msg = alice.next_message();
let bob = bob.receive(msg);
let msg = bob.next_message();
let alice = alice.receive(msg).unwrap();
let msg = alice.next_message();
let bob = bob.receive(msg).unwrap();
let msg = bob.next_message();
let alice = alice.receive(msg);
let sig = alice.adaptor_sig.adapt(r_a);
assert!(sig.verify(ring, msg_to_sign).unwrap());
let alice = Alice::new(alice);
}
}

@ -1,115 +1,83 @@
use std::collections::VecDeque;
use std::task::Poll;
use anyhow::Result;
use monero::PublicKey;
use rand::rngs::OsRng;
pub struct StateMachine {
state: State,
actions: VecDeque<Action>,
events: VecDeque<Event>,
}
use monero_adaptor::alice::Alice2;
use monero_adaptor::AdaptorSignature;
impl StateMachine {
fn inject_event(&mut self, event: Event) {
match self.state {
State::WatchingForBtcLock => match event {
Event::BtcLockSeenInMempool => {
self.actions.push_back(Action::SignAndBroadcastBtcRedeem);
self.actions.push_back(Action::WatchForXmrRedeem);
self.state = State::WatchingForXmrRedeem;
}
Event::BtcLockTimeoutElapsed => {
self.actions.push_back(Action::BroadcastXmrRefund);
self.state = State::Aborted;
}
_ => {}
},
State::WatchingForXmrRedeem => match event {
Event::T2Elapsed => {
self.actions.push_back(Action::BroadcastXmrRefund);
self.actions.push_back(Action::SignAndBroadcastBtcPunish);
self.state = State::Punished;
}
Event::XmrRedeemSeenInMempool => {
self.actions.push_back(Action::SignAndBroadcastBtcPunish);
self.state = State::Success;
}
_ => {}
},
_ => {}
}
}
use crate::bitcoin::TxLock;
use crate::monero::{Scalar, TransferRequest};
use curve25519_dalek::edwards::EdwardsPoint;
fn poll(&mut self) -> Poll<Action> {
if let Some(action) = self.actions.pop_front() {
Poll::Ready(action)
} else {
Poll::Pending
}
}
// start
pub struct Alice3 {
pub xmr_swap_amount: crate::monero::Amount,
pub btc_swap_amount: crate::bitcoin::Amount,
// pub adaptor_sig: AdaptorSignature,
pub a: crate::bitcoin::SecretKey,
pub B: crate::bitcoin::PublicKey,
pub s_a: Scalar,
pub S_b_monero: EdwardsPoint,
pub v_a: crate::monero::PrivateViewKey,
}
#[derive(PartialEq, Debug)]
pub enum State {
WatchingForBtcLock,
WatchingForXmrRedeem,
Punished,
Success,
Aborted,
// published xmr_lock, watching for btc_lock
pub struct Alice4 {
a: crate::bitcoin::SecretKey,
B: crate::bitcoin::PublicKey,
btc_swap_amount: crate::bitcoin::Amount,
// pub adaptor_sig: AdaptorSignature,
}
pub enum Event {
BtcLockSeenInMempool,
T2Elapsed,
BtcLockTimeoutElapsed,
XmrRedeemSeenInMempool,
}
// published seen btc_lock, published btc_redeem
pub struct Alice5;
// These actions should not fail (are retried until successful) and should be
// idempotent This allows us to greatly simplify the state machine
pub enum Action {
WatchForXmrRedeem,
SignAndBroadcastBtcPunish,
SignAndBroadcastBtcRedeem,
BroadcastXmrRefund,
}
impl Alice3 {
pub fn new(
S_b_monero: EdwardsPoint,
B: crate::bitcoin::PublicKey,
xmr_swap_amount: crate::monero::Amount,
btc_swap_amount: crate::bitcoin::Amount,
) -> Self {
Self {
xmr_swap_amount,
btc_swap_amount,
// adaptor_sig: alice2.adaptor_sig,
a: crate::bitcoin::SecretKey::new_random(&mut OsRng),
B,
s_a: Scalar::random(&mut OsRng),
S_b_monero,
v_a: crate::monero::PrivateViewKey::new_random(&mut OsRng),
}
}
pub async fn publish_xmr_lock(&self, wallet: &crate::monero::Wallet) -> Result<Alice4> {
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: self.s_a });
#[cfg(test)]
mod tests {
use super::*;
let public_spend_key = S_a + self.S_b_monero;
let public_view_key = self.v_a.public();
#[test]
fn happy_path() {
let mut state_machine = StateMachine {
state: State::WatchingForBtcLock,
actions: Default::default(),
events: Default::default(),
let req = TransferRequest {
public_spend_key,
public_view_key,
amount: self.xmr_swap_amount,
};
state_machine.inject_event(Event::BtcLockSeenInMempool);
state_machine.inject_event(Event::XmrRedeemSeenInMempool);
assert_eq!(state_machine.state, State::Success);
}
#[test]
fn bob_fails_to_lock_btc() {
let mut state_machine = StateMachine {
state: State::WatchingForBtcLock,
actions: Default::default(),
events: Default::default(),
};
state_machine.events.push_back(Event::BtcLockTimeoutElapsed);
state_machine.run();
assert_eq!(state_machine.state, State::Aborted);
// we may have to send this to Bob
let _ = wallet.transfer(req).await?;
Ok(Alice4 {
a: self.a.clone(),
B: self.B,
btc_swap_amount: Default::default(),
// adaptor_sig: self.adaptor_sig.clone(),
})
}
}
#[test]
fn bob_fails_to_redeem_xmr_before_t2() {
let mut state_machine = StateMachine {
state: State::WatchingForBtcLock,
actions: Default::default(),
events: Default::default(),
};
state_machine.events.push_back(Event::BtcLockSeenInMempool);
state_machine.events.push_back(Event::T2Elapsed);
state_machine.run();
assert_eq!(state_machine.state, State::Punished);
impl Alice4 {
pub async fn watch_for_btc_lock(&self, wallet: &crate::bitcoin::Wallet) -> Result<Alice5> {
let btc_lock = TxLock::new(wallet, self.btc_swap_amount, self.a.public(), self.B).await?;
wallet.subscribe_to(btc_lock);
Ok(Alice5)
}
}

@ -1,119 +1,35 @@
use crate::monero::TransferProof;
use std::collections::VecDeque;
pub struct StateMachine {
state: State,
actions: VecDeque<Action>,
events: VecDeque<Event>,
}
impl StateMachine {
fn next(&mut self, event: Event) {
match self.state {
State::WatchingForXmrLock => match event {
Event::XmrConfirmed => {
self.actions.push_back(Action::SignAndBroadcastBtcLock);
self.state = State::WaitingForBtcRedeem;
}
Event::T1Elapsed => {
self.state = State::Aborted;
}
Event::XmrRefundSeenInMempool => {
self.state = State::Aborted;
}
_ => panic!("unhandled scenario"),
},
State::WaitingForBtcRedeem => match event {
Event::BtcRedeemSeenInMempool => {
self.actions.push_back(Action::BroadcastXmrRedeem);
self.state = State::Success;
}
Event::T1Elapsed => {
self.actions.push_back(Action::SignAndBroadcastBtcRefund);
self.state = State::Refunded;
}
Event::XmrRefundSeenInMempool => {
self.actions.push_back(Action::SignAndBroadcastBtcRefund);
self.state = State::Refunded;
}
_ => panic!("unhandled scenario"),
},
_ => {}
}
}
fn run(&mut self) {
while let Some(event) = self.events.pop_front() {
self.next(event);
}
}
}
#[derive(PartialEq, Debug)]
pub enum State {
WatchingForXmrLock,
WaitingForBtcRedeem,
Success,
Refunded,
Aborted,
}
pub enum Event {
XmrConfirmed,
// This will contain the s_a allowing bob to build xmr_redeem
BtcRedeemSeenInMempool,
XmrRefundSeenInMempool,
T1Elapsed,
use anyhow::Result;
use monero::PublicKey;
use rand::rngs::OsRng;
use monero_adaptor::alice::Alice2;
use monero_adaptor::AdaptorSignature;
use crate::bitcoin::Txid;
use crate::monero::wallet::WatchRequest;
use crate::monero::{Scalar, TransferRequest};
use crate::xmr_first_protocol::transactions::xmr_lock::XmrLock;
// watching for xmr_lock
pub struct Bob3 {
pub xmr_swap_amount: crate::monero::Amount,
pub btc_swap_amount: crate::bitcoin::Amount,
pub xmr_lock: XmrLock,
v_b: crate::monero::PrivateViewKey,
}
pub enum Action {
SignAndBroadcastBtcLock,
BroadcastXmrRedeem,
SignAndBroadcastBtcRefund,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn happy_path() {
let mut state_machine = StateMachine {
state: State::WatchingForXmrLock,
actions: Default::default(),
events: Default::default(),
};
state_machine.events.push_back(Event::XmrConfirmed);
state_machine
.events
.push_back(Event::BtcRedeemSeenInMempool);
state_machine.run();
assert_eq!(state_machine.state, State::Success);
}
#[test]
fn alice_fails_to_redeem_btc_before_t1() {
let mut state_machine = StateMachine {
state: State::WatchingForXmrLock,
actions: Default::default(),
events: Default::default(),
impl Bob3 {
pub fn watch_for_lock_xmr(&self, wallet: &crate::monero::Wallet) {
let req = WatchRequest {
public_spend_key: self.xmr_lock.public_spend_key,
public_view_key: self.v_b.public(),
transfer_proof: self.xmr_lock.transfer_proof.clone(),
conf_target: 1,
expected: self.xmr_swap_amount,
};
state_machine.events.push_back(Event::XmrConfirmed);
state_machine.events.push_back(Event::T1Elapsed);
state_machine.run();
assert_eq!(state_machine.state, State::Refunded);
}
#[test]
fn alice_tries_to_refund_xmr_after_redeeming_btc() {
let mut state_machine = StateMachine {
state: State::WatchingForXmrLock,
actions: Default::default(),
events: Default::default(),
};
state_machine.events.push_back(Event::XmrConfirmed);
state_machine.events.push_back(Event::T1Elapsed);
state_machine.run();
assert_eq!(state_machine.state, State::Refunded);
wallet.watch_for_transfer(req);
}
}
// published btc_lock, watching for xmr_redeem
pub struct Bob4;

@ -0,0 +1,115 @@
use std::collections::VecDeque;
use std::task::Poll;
pub struct StateMachine {
state: State,
actions: VecDeque<Action>,
events: VecDeque<Event>,
}
impl StateMachine {
fn inject_event(&mut self, event: Event) {
match self.state {
State::WatchingForBtcLock => match event {
Event::BtcLockSeenInMempool => {
self.actions.push_back(Action::SignAndBroadcastBtcRedeem);
self.actions.push_back(Action::WatchForXmrRedeem);
self.state = State::WatchingForXmrRedeem;
}
Event::BtcLockTimeoutElapsed => {
self.actions.push_back(Action::BroadcastXmrRefund);
self.state = State::Aborted;
}
_ => {}
},
State::WatchingForXmrRedeem => match event {
Event::T2Elapsed => {
self.actions.push_back(Action::BroadcastXmrRefund);
self.actions.push_back(Action::SignAndBroadcastBtcPunish);
self.state = State::Punished;
}
Event::XmrRedeemSeenInMempool => {
self.actions.push_back(Action::SignAndBroadcastBtcPunish);
self.state = State::Success;
}
_ => {}
},
_ => {}
}
}
fn poll(&mut self) -> Poll<Action> {
if let Some(action) = self.actions.pop_front() {
Poll::Ready(action)
} else {
Poll::Pending
}
}
}
#[derive(PartialEq, Debug)]
pub enum State {
WatchingForBtcLock,
WatchingForXmrRedeem,
Punished,
Success,
Aborted,
}
pub enum Event {
BtcLockSeenInMempool,
T2Elapsed,
BtcLockTimeoutElapsed,
XmrRedeemSeenInMempool,
}
// These actions should not fail (are retried until successful) and should be
// idempotent This allows us to greatly simplify the state machine
pub enum Action {
WatchForXmrRedeem,
SignAndBroadcastBtcPunish,
SignAndBroadcastBtcRedeem,
BroadcastXmrRefund,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn happy_path() {
let mut state_machine = StateMachine {
state: State::WatchingForBtcLock,
actions: Default::default(),
events: Default::default(),
};
state_machine.inject_event(Event::BtcLockSeenInMempool);
state_machine.inject_event(Event::XmrRedeemSeenInMempool);
assert_eq!(state_machine.state, State::Success);
}
#[test]
fn bob_fails_to_lock_btc() {
let mut state_machine = StateMachine {
state: State::WatchingForBtcLock,
actions: Default::default(),
events: Default::default(),
};
state_machine.events.push_back(Event::BtcLockTimeoutElapsed);
state_machine.poll();
assert_eq!(state_machine.state, State::Aborted);
}
#[test]
fn bob_fails_to_redeem_xmr_before_t2() {
let mut state_machine = StateMachine {
state: State::WatchingForBtcLock,
actions: Default::default(),
events: Default::default(),
};
state_machine.events.push_back(Event::BtcLockSeenInMempool);
state_machine.events.push_back(Event::T2Elapsed);
state_machine.run();
assert_eq!(state_machine.state, State::Punished);
}
}

@ -0,0 +1,118 @@
use std::collections::VecDeque;
pub struct StateMachine {
state: State,
actions: VecDeque<Action>,
events: VecDeque<Event>,
}
impl StateMachine {
fn next(&mut self, event: Event) {
match self.state {
State::WatchingForXmrLock => match event {
Event::XmrConfirmed => {
self.actions.push_back(Action::SignAndBroadcastBtcLock);
self.state = State::WaitingForBtcRedeem;
}
Event::T1Elapsed => {
self.state = State::Aborted;
}
Event::XmrRefundSeenInMempool => {
self.state = State::Aborted;
}
_ => panic!("unhandled scenario"),
},
State::WaitingForBtcRedeem => match event {
Event::BtcRedeemSeenInMempool => {
self.actions.push_back(Action::BroadcastXmrRedeem);
self.state = State::Success;
}
Event::T1Elapsed => {
self.actions.push_back(Action::SignAndBroadcastBtcRefund);
self.state = State::Refunded;
}
Event::XmrRefundSeenInMempool => {
self.actions.push_back(Action::SignAndBroadcastBtcRefund);
self.state = State::Refunded;
}
_ => panic!("unhandled scenario"),
},
_ => {}
}
}
fn run(&mut self) {
while let Some(event) = self.events.pop_front() {
self.next(event);
}
}
}
#[derive(PartialEq, Debug)]
pub enum State {
WatchingForXmrLock,
WaitingForBtcRedeem,
Success,
Refunded,
Aborted,
}
pub enum Event {
XmrConfirmed,
// This will contain the s_a allowing bob to build xmr_redeem
BtcRedeemSeenInMempool,
XmrRefundSeenInMempool,
T1Elapsed,
}
pub enum Action {
SignAndBroadcastBtcLock,
BroadcastXmrRedeem,
SignAndBroadcastBtcRefund,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn happy_path() {
let mut state_machine = StateMachine {
state: State::WatchingForXmrLock,
actions: Default::default(),
events: Default::default(),
};
state_machine.events.push_back(Event::XmrConfirmed);
state_machine
.events
.push_back(Event::BtcRedeemSeenInMempool);
state_machine.run();
assert_eq!(state_machine.state, State::Success);
}
#[test]
fn alice_fails_to_redeem_btc_before_t1() {
let mut state_machine = StateMachine {
state: State::WatchingForXmrLock,
actions: Default::default(),
events: Default::default(),
};
state_machine.events.push_back(Event::XmrConfirmed);
state_machine.events.push_back(Event::T1Elapsed);
state_machine.run();
assert_eq!(state_machine.state, State::Refunded);
}
#[test]
fn alice_tries_to_refund_xmr_after_redeeming_btc() {
let mut state_machine = StateMachine {
state: State::WatchingForXmrLock,
actions: Default::default(),
events: Default::default(),
};
state_machine.events.push_back(Event::XmrConfirmed);
state_machine.events.push_back(Event::T1Elapsed);
state_machine.run();
assert_eq!(state_machine.state, State::Refunded);
}
}

@ -0,0 +1,2 @@
// pub mod alice;
// pub mod bob;

@ -1,5 +1,5 @@
pub mod btc_lock;
pub mod btc_redeem;
// pub mod btc_redeem;
pub mod xmr_lock;
pub mod xmr_refund;
@ -8,174 +8,10 @@ use crate::bitcoin::{
build_shared_output_descriptor, Address, Amount, PartiallySignedTransaction, PublicKey,
Transaction, Txid, Wallet, TX_FEE,
};
use anyhow::Result;
use anyhow::{bail, Result};
use bdk::bitcoin::{OutPoint, Script, TxIn, TxOut};
use bdk::database::BatchDatabase;
use bdk::descriptor::Descriptor;
use ecdsa_fun::fun::Point;
use miniscript::DescriptorTrait;
use rand::thread_rng;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BtcLock {
inner: PartiallySignedTransaction,
pub(crate) output_descriptor: Descriptor<::bitcoin::PublicKey>,
}
impl BtcLock {
pub async fn new<B, D, C>(
wallet: &Wallet<B, D, C>,
amount: Amount,
A: PublicKey,
B: PublicKey,
) -> Result<Self>
where
D: BatchDatabase,
{
let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0);
let address = lock_output_descriptor
.address(wallet.get_network())
.expect("can derive address from descriptor");
let psbt = wallet.send_to_address(address, amount).await?;
Ok(Self {
inner: psbt,
output_descriptor: lock_output_descriptor,
})
}
/// Creates an instance of `TxLock` from a PSBT, the public keys of the
/// parties and the specified amount.
///
/// This function validates that the given PSBT does indeed pay that
/// specified amount to a shared output.
pub fn from_psbt(
psbt: PartiallySignedTransaction,
A: PublicKey,
B: PublicKey,
btc: Amount,
) -> Result<Self> {
let shared_output_candidate = match psbt.global.unsigned_tx.output.as_slice() {
[shared_output_candidate, _] if shared_output_candidate.value == btc.as_sat() => {
shared_output_candidate
}
[_, shared_output_candidate] if shared_output_candidate.value == btc.as_sat() => {
shared_output_candidate
}
// A single output is possible if Bob funds without any change necessary
[shared_output_candidate] if shared_output_candidate.value == btc.as_sat() => {
shared_output_candidate
}
[_, _] => {
bail!("Neither of the two provided outputs pays the right amount!");
}
[_] => {
bail!("The provided output does not pay the right amount!");
}
other => {
let num_outputs = other.len();
bail!(
"PSBT has {} outputs, expected one or two. Something is fishy!",
num_outputs
);
}
};
let descriptor = build_shared_output_descriptor(A.0, B.0);
let legit_shared_output_script = descriptor.script_pubkey();
if shared_output_candidate.script_pubkey != legit_shared_output_script {
bail!("Output script is not a shared output")
}
Ok(BtcLock {
inner: psbt,
output_descriptor: descriptor,
})
}
pub fn lock_amount(&self) -> Amount {
Amount::from_sat(self.inner.clone().extract_tx().output[self.lock_output_vout()].value)
}
pub fn txid(&self) -> Txid {
self.inner.clone().extract_tx().txid()
}
pub fn as_outpoint(&self) -> OutPoint {
// This is fine because a transaction that has that many outputs is not
// realistic
#[allow(clippy::cast_possible_truncation)]
OutPoint::new(self.txid(), self.lock_output_vout() as u32)
}
/// Calculate the size of the script used by this transaction.
pub fn script_size() -> usize {
build_shared_output_descriptor(
Point::random(&mut thread_rng()),
Point::random(&mut thread_rng()),
)
.script_pubkey()
.len()
}
pub fn script_pubkey(&self) -> Script {
self.output_descriptor.script_pubkey()
}
/// Retreive the index of the locked output in the transaction outputs
/// vector
fn lock_output_vout(&self) -> usize {
self.inner
.clone()
.extract_tx()
.output
.iter()
.position(|output| output.script_pubkey == self.output_descriptor.script_pubkey())
.expect("transaction contains lock output")
}
pub fn build_spend_transaction(
&self,
spend_address: &Address,
sequence: Option<u32>,
) -> Transaction {
let previous_output = self.as_outpoint();
let tx_in = TxIn {
previous_output,
script_sig: Default::default(),
sequence: sequence.unwrap_or(0xFFFF_FFFF),
witness: Vec::new(),
};
let tx_out = TxOut {
value: self.inner.clone().extract_tx().output[self.lock_output_vout()].value - TX_FEE,
script_pubkey: spend_address.script_pubkey(),
};
Transaction {
version: 2,
lock_time: 0,
input: vec![tx_in],
output: vec![tx_out],
}
}
}
impl From<BtcLock> for PartiallySignedTransaction {
fn from(from: BtcLock) -> Self {
from.inner
}
}
impl Watchable for BtcLock {
fn id(&self) -> Txid {
self.txid()
}
fn script(&self) -> Script {
self.output_descriptor.script_pubkey()
}
}

@ -3,7 +3,7 @@ use crate::bitcoin::{
build_shared_output_descriptor, Address, Amount, PartiallySignedTransaction, PublicKey,
Transaction, Txid, Wallet, TX_FEE,
};
use anyhow::Result;
use anyhow::{bail, Result};
use bdk::bitcoin::{OutPoint, Script, TxIn, TxOut};
use bdk::database::BatchDatabase;
use bdk::descriptor::Descriptor;
@ -11,7 +11,7 @@ use ecdsa_fun::fun::Point;
use miniscript::DescriptorTrait;
use rand::thread_rng;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub struct BtcLock {
inner: PartiallySignedTransaction,
pub(crate) output_descriptor: Descriptor<::bitcoin::PublicKey>,
@ -27,7 +27,7 @@ impl BtcLock {
where
D: BatchDatabase,
{
let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0);
let lock_output_descriptor = build_shared_output_descriptor(A.into(), B.into());
let address = lock_output_descriptor
.address(wallet.get_network())
.expect("can derive address from descriptor");
@ -77,7 +77,7 @@ impl BtcLock {
}
};
let descriptor = build_shared_output_descriptor(A.0, B.0);
let descriptor = build_shared_output_descriptor(A.into(), B.into());
let legit_shared_output_script = descriptor.script_pubkey();
if shared_output_candidate.script_pubkey != legit_shared_output_script {

@ -0,0 +1,146 @@
use crate::bitcoin::wallet::Watchable;
use crate::bitcoin::{
verify_encsig, verify_sig, Address, EmptyWitnessStack, EncryptedSignature, NoInputs,
NotThreeWitnesses, PublicKey, SecretKey, TooManyInputs, Transaction, TxLock,
};
use ::bitcoin::util::bip143::SigHashCache;
use ::bitcoin::{SigHash, SigHashType, Txid};
use anyhow::{bail, Context, Result};
use bitcoin::Script;
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
use ecdsa_fun::fun::Scalar;
use ecdsa_fun::nonce::Deterministic;
use ecdsa_fun::Signature;
use miniscript::{Descriptor, DescriptorTrait};
use sha2::Sha256;
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct TxRedeem {
inner: Transaction,
digest: SigHash,
lock_output_descriptor: Descriptor<::bitcoin::PublicKey>,
watch_script: Script,
}
impl TxRedeem {
pub fn new(tx_lock: &TxLock, redeem_address: &Address) -> Self {
// lock_input is the shared output that is now being used as an input for the
// redeem transaction
let tx_redeem = tx_lock.build_spend_transaction(redeem_address, None);
let digest = SigHashCache::new(&tx_redeem).signature_hash(
0, // Only one input: lock_input (lock transaction)
&tx_lock.output_descriptor.script_code(),
tx_lock.lock_amount().as_sat(),
SigHashType::All,
);
Self {
inner: tx_redeem,
digest,
lock_output_descriptor: tx_lock.output_descriptor.clone(),
watch_script: redeem_address.script_pubkey(),
}
}
pub fn txid(&self) -> Txid {
self.inner.txid()
}
pub fn digest(&self) -> SigHash {
self.digest
}
pub fn complete(
mut self,
encrypted_signature: EncryptedSignature,
a: SecretKey,
s_a: Scalar,
B: PublicKey,
) -> Result<Transaction> {
verify_encsig(
B,
PublicKey::from(s_a.clone()),
&self.digest(),
&encrypted_signature,
)
.context("Invalid encrypted signature received")?;
let sig_a = a.sign(self.digest());
let adaptor = Adaptor::<HashTranscript<Sha256>, Deterministic<Sha256>>::default();
let sig_b = adaptor.decrypt_signature(&s_a, encrypted_signature);
let satisfier = {
let mut satisfier = HashMap::with_capacity(2);
let A = ::bitcoin::PublicKey {
compressed: true,
key: a.public().into(),
};
let B = ::bitcoin::PublicKey {
compressed: true,
key: B.into(),
};
// The order in which these are inserted doesn't matter
satisfier.insert(A, (sig_a.into(), ::bitcoin::SigHashType::All));
satisfier.insert(B, (sig_b.into(), ::bitcoin::SigHashType::All));
satisfier
};
self.lock_output_descriptor
.satisfy(&mut self.inner.input[0], satisfier)
.context("Failed to sign Bitcoin redeem transaction")?;
Ok(self.inner)
}
pub fn extract_signature_by_key(
&self,
candidate_transaction: Transaction,
B: PublicKey,
) -> Result<Signature> {
let input = match candidate_transaction.input.as_slice() {
[input] => input,
[] => bail!(NoInputs),
[inputs @ ..] => bail!("too many inputs"),
};
let sigs = match input
.witness
.iter()
.map(|vec| vec.as_slice())
.collect::<Vec<_>>()
.as_slice()
{
[sig_1, sig_2, _script] => [sig_1, sig_2]
.iter()
.map(|sig| {
bitcoin::secp256k1::Signature::from_der(&sig[..sig.len() - 1])
.map(Signature::from)
})
.collect::<std::result::Result<Vec<_>, _>>(),
[] => bail!(EmptyWitnessStack),
[witnesses @ ..] => bail!("not three witnesses"),
}?;
let sig = sigs
.into_iter()
.find(|sig| verify_sig(&B, &self.digest(), &sig).is_ok())
.context("Neither signature on witness stack verifies against B")?;
Ok(sig)
}
}
impl Watchable for TxRedeem {
fn id(&self) -> Txid {
self.txid()
}
fn script(&self) -> Script {
self.watch_script.clone()
}
}

@ -1,6 +1,26 @@
use crate::monero::PrivateViewKey;
use crate::monero::TransferRequest;
use crate::xmr_first_protocol::alice::Alice4;
use anyhow::Result;
use monero_adaptor::AdaptorSignature;
pub struct XmrRedeem {
pub struct XmrRefund {
adaptor: AdaptorSignature,
}
impl XmrRefund {
pub async fn publish_xmr_refund(&self, wallet: &crate::monero::Wallet) -> Result<()> {
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: self.s_a });
let public_spend_key = S_a + self.S_b_monero;
let public_view_key = self.v_a.public();
let req = TransferRequest {
public_spend_key,
public_view_key,
amount: self.xmr_swap_amount,
};
let _ = wallet.transfer(req).await?;
Ok(())
}
}

@ -1,5 +1,5 @@
mod bitcoind;
mod electrs;
pub mod electrs;
use crate::harness;
use anyhow::{bail, Context, Result};
@ -625,7 +625,7 @@ fn random_prefix() -> String {
chars
}
async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) {
pub async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) {
let prefix = random_prefix();
let bitcoind_name = format!("{}_{}", prefix, "bitcoind");
let (bitcoind, bitcoind_url) =
@ -761,7 +761,7 @@ async fn init_monero_container(
}
#[allow(clippy::too_many_arguments)]
async fn init_test_wallets(
pub async fn init_test_wallets(
name: &str,
bitcoind_url: Url,
monero: &Monero,
@ -835,10 +835,10 @@ async fn init_test_wallets(
// This is just to keep the containers alive
#[allow(dead_code)]
struct Containers<'a> {
bitcoind_url: Url,
pub bitcoind_url: Url,
bitcoind: Container<'a, Cli, bitcoind::Bitcoind>,
monerods: Vec<Container<'a, Cli, image::Monero>>,
electrs: Container<'a, Cli, electrs::Electrs>,
pub electrs: Container<'a, Cli, electrs::Electrs>,
}
pub mod alice_run_until {

@ -0,0 +1,110 @@
pub mod harness;
use rand::rngs::OsRng;
use swap::bitcoin::TxLock;
use swap::env::GetConfig;
use swap::monero;
use swap::protocol::alice::event_loop::FixedRate;
use swap::protocol::CROSS_CURVE_PROOF_SYSTEM;
use swap::seed::Seed;
use swap::xmr_first_protocol::alice::{publish_xmr_refund, Alice3};
use swap::xmr_first_protocol::bob::Bob3;
use tempfile::tempdir;
use testcontainers::clients::Cli;
#[tokio::test]
async fn refund() {
let cli = Cli::default();
let env_config = harness::SlowCancelConfig::get_config();
let (monero, containers) = harness::init_containers(&cli).await;
let btc_swap_amount = bitcoin::Amount::from_sat(1_000_000);
let xmr_swap_amount =
monero::Amount::from_monero(btc_swap_amount.as_btc() / FixedRate::RATE).unwrap();
let alice_starting_balances = harness::StartingBalances {
xmr: xmr_swap_amount * 10,
btc: bitcoin::Amount::ZERO,
};
let electrs_rpc_port = containers
.electrs
.get_host_port(harness::electrs::RPC_PORT)
.expect("Could not map electrs rpc port");
let alice_seed = Seed::random().unwrap();
let (alice_bitcoin_wallet, alice_monero_wallet) = harness::init_test_wallets(
"Alice",
containers.bitcoind_url.clone(),
&monero,
alice_starting_balances.clone(),
tempdir().unwrap().path(),
electrs_rpc_port,
&alice_seed,
env_config.clone(),
)
.await;
let bob_seed = Seed::random().unwrap();
let bob_starting_balances = harness::StartingBalances {
xmr: monero::Amount::ZERO,
btc: btc_swap_amount * 10,
};
let (bob_bitcoin_wallet, bob_monero_wallet) = harness::init_test_wallets(
"Bob",
containers.bitcoind_url,
&monero,
bob_starting_balances.clone(),
tempdir().unwrap().path(),
electrs_rpc_port,
&bob_seed,
env_config,
)
.await;
let a = swap::bitcoin::SecretKey::new_random(&mut OsRng);
let b = swap::bitcoin::SecretKey::new_random(&mut OsRng);
let s_a = monero::Scalar::random(&mut OsRng);
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: s_a });
let s_b = monero::Scalar::random(&mut OsRng);
let S_b = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: s_b });
let (dleq_proof_s_b, (S_b_bitcoin, S_b_monero)) =
CROSS_CURVE_PROOF_SYSTEM.prove(&s_b, &mut OsRng);
let v_a = monero::PrivateViewKey::new_random(&mut OsRng);
let v_b = monero::PrivateViewKey::new_random(&mut OsRng);
let tx_lock = TxLock::new(&bob_bitcoin_wallet, btc_swap_amount, a.public(), b.public()).await?;
let alice = Alice3 {
xmr_swap_amount,
btc_swap_amount,
a,
B: b.public(),
s_a,
S_b_monero: monero::PublicKey {
point: S_b_monero.compress(),
},
v_a,
redeem_address: alice_bitcoin_wallet.new_address().await?,
};
let bob = Bob3 {
xmr_swap_amount,
btc_swap_amount,
tx_lock,
S: S_b,
v_b,
alice_redeem_address: bob_bitcoin_wallet.new_address().await?,
};
let alice = alice.publish_xmr_lock(&alice_monero_wallet).await.unwrap();
publish_xmr_refund(&alice_bitcoin_wallet).await.unwrap();
}

@ -0,0 +1,109 @@
pub mod harness;
use curve25519_dalek::constants::ED25519_BASEPOINT_POINT;
use curve25519_dalek::edwards::EdwardsPoint;
use monero_adaptor::alice::Alice0;
use monero_adaptor::bob::Bob0;
use rand::rngs::OsRng;
use swap::env::GetConfig;
use swap::monero;
use swap::monero::{PublicKey, Scalar};
use swap::protocol::alice::event_loop::FixedRate;
use swap::protocol::CROSS_CURVE_PROOF_SYSTEM;
use swap::seed::Seed;
use swap::xmr_first_protocol::alice::Alice3;
use swap::xmr_first_protocol::bob::Bob3;
use swap::xmr_first_protocol::{alice, bob};
use tempfile::tempdir;
use testcontainers::clients::Cli;
#[tokio::test]
async fn happy_path() {
let cli = Cli::default();
let env_config = harness::SlowCancelConfig::get_config();
let (monero, containers) = harness::init_containers(&cli).await;
let btc_amount = bitcoin::Amount::from_sat(1_000_000);
let xmr_amount = monero::Amount::from_monero(btc_amount.as_btc() / FixedRate::RATE).unwrap();
let alice_starting_balances = harness::StartingBalances {
xmr: xmr_amount * 10,
btc: bitcoin::Amount::ZERO,
};
let electrs_rpc_port = containers
.electrs
.get_host_port(harness::electrs::RPC_PORT)
.expect("Could not map electrs rpc port");
let alice_seed = Seed::random().unwrap();
let (alice_bitcoin_wallet, alice_monero_wallet) = harness::init_test_wallets(
"Alice",
containers.bitcoind_url.clone(),
&monero,
alice_starting_balances.clone(),
tempdir().unwrap().path(),
electrs_rpc_port,
&alice_seed,
env_config.clone(),
)
.await;
let bob_seed = Seed::random().unwrap();
let bob_starting_balances = harness::StartingBalances {
xmr: monero::Amount::ZERO,
btc: btc_amount * 10,
};
let (bob_bitcoin_wallet, bob_monero_wallet) = harness::init_test_wallets(
"Bob",
containers.bitcoind_url,
&monero,
bob_starting_balances.clone(),
tempdir().unwrap().path(),
electrs_rpc_port,
&bob_seed,
env_config,
)
.await;
let a = crate::bitcoin::SecretKey::new_random(rng);
let b = crate::bitcoin::SecretKey::new_random(rng);
let s_a = monero::Scalar::random(rng);
let s_b = monero::Scalar::random(rng);
let (dleq_proof_s_b, (S_b_bitcoin, S_b_monero)) = CROSS_CURVE_PROOF_SYSTEM.prove(&s_b, rng);
let v_a = monero::PrivateViewKey::new_random(rng);
let v_b = monero::PrivateViewKey::new_random(rng);
let alice = Alice3 {
xmr_swap_amount: xmr_amount,
btc_swap_amount: btc_amount,
a,
B: b.public(),
s_a,
S_b_monero,
v_a,
};
let bob = Bob3 {
xmr_swap_amount,
btc_swap_amount,
xmr_lock,
v_b,
};
alice.publish_xmr_lock(&alice_monero_wallet).await.unwrap();
bob.watch_for_lock_xmr(&bob_monero_wallet_wallet)
.await
.unwrap();
alice.publish_btc_redeem(&alice_btc_wallet).await.unwrap();
bob.publish_xmr_redeem(&alice_monero_wallet).await.unwrap();
}
Loading…
Cancel
Save