mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-08-07 05:52:31 -04:00
Merge xmr_btc crate
Created network, storage and protocol modules. Organised files into the modules where the belong. xmr_btc crate moved into isolated modulein swap crate. Remove the xmr_btc module and integrate into swap crate. Consolidate message related code Reorganise imports Remove unused parent Message enum Remove unused parent State enum Remove unused dependencies from Cargo.toml
This commit is contained in:
parent
a0d859147a
commit
c900d12593
52 changed files with 1372 additions and 1933 deletions
|
@ -1,202 +1,288 @@
|
|||
use anyhow::{Context, Result};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use async_trait::async_trait;
|
||||
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
||||
use bitcoin::util::psbt::PartiallySignedTransaction;
|
||||
use bitcoin_harness::{bitcoind_rpc::PsbtBase64, BitcoindRpcApi};
|
||||
use reqwest::Url;
|
||||
use std::time::Duration;
|
||||
use tokio::time::interval;
|
||||
use xmr_btc::{
|
||||
bitcoin::{
|
||||
BroadcastSignedTransaction, BuildTxLockPsbt, GetBlockHeight, SignTxLock,
|
||||
TransactionBlockHeight, WatchForRawTransaction,
|
||||
},
|
||||
config::Config,
|
||||
};
|
||||
use bitcoin::hashes::{hex::ToHex, Hash};
|
||||
use ecdsa_fun::{adaptor::Adaptor, fun::Point, nonce::Deterministic, ECDSA};
|
||||
use miniscript::{Descriptor, Segwitv0};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub use ::bitcoin::{Address, Transaction};
|
||||
pub use xmr_btc::bitcoin::*;
|
||||
use crate::{config::Config, ExpiredTimelocks};
|
||||
|
||||
pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600;
|
||||
use crate::bitcoin::timelocks::{BlockHeight, Timelock};
|
||||
pub use crate::bitcoin::transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund};
|
||||
pub use ::bitcoin::{util::psbt::PartiallySignedTransaction, *};
|
||||
pub use ecdsa_fun::{adaptor::EncryptedSignature, fun::Scalar, Signature};
|
||||
pub use wallet::Wallet;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Wallet {
|
||||
pub inner: bitcoin_harness::Wallet,
|
||||
pub network: bitcoin::Network,
|
||||
pub mod timelocks;
|
||||
pub mod transactions;
|
||||
pub mod wallet;
|
||||
|
||||
// TODO: Configurable tx-fee (note: parties have to agree prior to swapping)
|
||||
// Current reasoning:
|
||||
// tx with largest weight (as determined by get_weight() upon broadcast in e2e
|
||||
// test) = 609 assuming segwit and 60 sat/vB:
|
||||
// (609 / 4) * 60 (sat/vB) = 9135 sats
|
||||
// Recommended: Overpay a bit to ensure we don't have to wait too long for test
|
||||
// runs.
|
||||
pub const TX_FEE: u64 = 15_000;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct SecretKey {
|
||||
inner: Scalar,
|
||||
public: Point,
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
pub async fn new(name: &str, url: Url, network: bitcoin::Network) -> Result<Self> {
|
||||
let wallet = bitcoin_harness::Wallet::new(name, url).await?;
|
||||
impl SecretKey {
|
||||
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
||||
let scalar = Scalar::random(rng);
|
||||
|
||||
Ok(Self {
|
||||
inner: wallet,
|
||||
network,
|
||||
})
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
let public = ecdsa.verification_key_for(&scalar);
|
||||
|
||||
Self {
|
||||
inner: scalar,
|
||||
public,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn balance(&self) -> Result<Amount> {
|
||||
let balance = self.inner.balance().await?;
|
||||
Ok(balance)
|
||||
pub fn public(&self) -> PublicKey {
|
||||
PublicKey(self.public)
|
||||
}
|
||||
|
||||
pub async fn new_address(&self) -> Result<Address> {
|
||||
self.inner.new_address().await.map_err(Into::into)
|
||||
pub fn to_bytes(&self) -> [u8; 32] {
|
||||
self.inner.to_bytes()
|
||||
}
|
||||
|
||||
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
|
||||
let fee = self
|
||||
.inner
|
||||
.get_wallet_transaction(txid)
|
||||
.await
|
||||
.map(|res| {
|
||||
res.fee.map(|signed_amount| {
|
||||
signed_amount
|
||||
.abs()
|
||||
.to_unsigned()
|
||||
.expect("Absolute value is always positive")
|
||||
})
|
||||
})?
|
||||
.context("Rpc response did not contain a fee")?;
|
||||
pub fn sign(&self, digest: SigHash) -> Signature {
|
||||
let ecdsa = ECDSA::<Deterministic<Sha256>>::default();
|
||||
|
||||
Ok(fee)
|
||||
ecdsa.sign(&self.inner, &digest.into_inner())
|
||||
}
|
||||
|
||||
// TxRefund encsigning explanation:
|
||||
//
|
||||
// A and B, are the Bitcoin Public Keys which go on the joint output for
|
||||
// TxLock_Bitcoin. S_a and S_b, are the Monero Public Keys which go on the
|
||||
// joint output for TxLock_Monero
|
||||
|
||||
// tx_refund: multisig(A, B), published by bob
|
||||
// bob can produce sig on B for tx_refund using b
|
||||
// alice sends over an encrypted signature on A for tx_refund using a encrypted
|
||||
// with S_b we want to leak s_b
|
||||
|
||||
// produced (by Alice) encsig - published (by Bob) sig = s_b (it's not really
|
||||
// subtraction, it's recover)
|
||||
|
||||
// self = a, Y = S_b, digest = tx_refund
|
||||
pub fn encsign(&self, Y: PublicKey, digest: SigHash) -> EncryptedSignature {
|
||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
||||
|
||||
adaptor.encrypted_sign(&self.inner, &Y.0, &digest.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PublicKey(Point);
|
||||
|
||||
impl From<PublicKey> for Point {
|
||||
fn from(from: PublicKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Scalar> for SecretKey {
|
||||
fn from(scalar: Scalar) -> Self {
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
let public = ecdsa.verification_key_for(&scalar);
|
||||
|
||||
Self {
|
||||
inner: scalar,
|
||||
public,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SecretKey> for Scalar {
|
||||
fn from(sk: SecretKey) -> Self {
|
||||
sk.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Scalar> for PublicKey {
|
||||
fn from(scalar: Scalar) -> Self {
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
PublicKey(ecdsa.verification_key_for(&scalar))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_sig(
|
||||
verification_key: &PublicKey,
|
||||
transaction_sighash: &SigHash,
|
||||
sig: &Signature,
|
||||
) -> Result<()> {
|
||||
let ecdsa = ECDSA::verify_only();
|
||||
|
||||
if ecdsa.verify(&verification_key.0, &transaction_sighash.into_inner(), &sig) {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!(InvalidSignature)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
||||
#[error("signature is invalid")]
|
||||
pub struct InvalidSignature;
|
||||
|
||||
pub fn verify_encsig(
|
||||
verification_key: PublicKey,
|
||||
encryption_key: PublicKey,
|
||||
digest: &SigHash,
|
||||
encsig: &EncryptedSignature,
|
||||
) -> Result<()> {
|
||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
||||
|
||||
if adaptor.verify_encrypted_signature(
|
||||
&verification_key.0,
|
||||
&encryption_key.0,
|
||||
&digest.into_inner(),
|
||||
&encsig,
|
||||
) {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!(InvalidEncryptedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, thiserror::Error)]
|
||||
#[error("encrypted signature is invalid")]
|
||||
pub struct InvalidEncryptedSignature;
|
||||
|
||||
pub fn build_shared_output_descriptor(A: Point, B: Point) -> Descriptor<bitcoin::PublicKey> {
|
||||
const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))";
|
||||
|
||||
// NOTE: This shouldn't be a source of error, but maybe it is
|
||||
let A = ToHex::to_hex(&secp256k1::PublicKey::from(A));
|
||||
let B = ToHex::to_hex(&secp256k1::PublicKey::from(B));
|
||||
|
||||
let miniscript = MINISCRIPT_TEMPLATE.replace("A", &A).replace("B", &B);
|
||||
|
||||
let miniscript = miniscript::Miniscript::<bitcoin::PublicKey, Segwitv0>::from_str(&miniscript)
|
||||
.expect("a valid miniscript");
|
||||
|
||||
Descriptor::Wsh(miniscript)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BuildTxLockPsbt for Wallet {
|
||||
pub trait BuildTxLockPsbt {
|
||||
async fn build_tx_lock_psbt(
|
||||
&self,
|
||||
output_address: Address,
|
||||
output_amount: Amount,
|
||||
) -> Result<PartiallySignedTransaction> {
|
||||
let psbt = self.inner.fund_psbt(output_address, output_amount).await?;
|
||||
let as_hex = base64::decode(psbt)?;
|
||||
|
||||
let psbt = bitcoin::consensus::deserialize(&as_hex)?;
|
||||
|
||||
Ok(psbt)
|
||||
}
|
||||
) -> Result<PartiallySignedTransaction>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SignTxLock for Wallet {
|
||||
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction> {
|
||||
let psbt = PartiallySignedTransaction::from(tx_lock);
|
||||
|
||||
let psbt = bitcoin::consensus::serialize(&psbt);
|
||||
let as_base64 = base64::encode(psbt);
|
||||
|
||||
let psbt = self
|
||||
.inner
|
||||
.wallet_process_psbt(PsbtBase64(as_base64))
|
||||
.await?;
|
||||
let PsbtBase64(signed_psbt) = PsbtBase64::from(psbt);
|
||||
|
||||
let as_hex = base64::decode(signed_psbt)?;
|
||||
let psbt: PartiallySignedTransaction = bitcoin::consensus::deserialize(&as_hex)?;
|
||||
|
||||
let tx = psbt.extract_tx();
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
pub trait SignTxLock {
|
||||
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BroadcastSignedTransaction for Wallet {
|
||||
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid> {
|
||||
let txid = self.inner.send_raw_transaction(transaction).await?;
|
||||
tracing::info!("Bitcoin tx broadcasted! TXID = {}", txid);
|
||||
Ok(txid)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
|
||||
// to `ConstantBackoff`.
|
||||
#[async_trait]
|
||||
impl WatchForRawTransaction for Wallet {
|
||||
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction {
|
||||
(|| async { Ok(self.inner.get_raw_transaction(txid).await?) })
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await
|
||||
.expect("transient errors to be retried")
|
||||
}
|
||||
pub trait BroadcastSignedTransaction {
|
||||
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GetRawTransaction for Wallet {
|
||||
// todo: potentially replace with option
|
||||
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
||||
Ok(self.inner.get_raw_transaction(txid).await?)
|
||||
}
|
||||
pub trait WatchForRawTransaction {
|
||||
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GetBlockHeight for Wallet {
|
||||
async fn get_block_height(&self) -> BlockHeight {
|
||||
let height = (|| async { Ok(self.inner.client.getblockcount().await?) })
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await
|
||||
.expect("transient errors to be retried");
|
||||
|
||||
BlockHeight::new(height)
|
||||
}
|
||||
pub trait WaitForTransactionFinality {
|
||||
async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TransactionBlockHeight for Wallet {
|
||||
async fn transaction_block_height(&self, txid: Txid) -> BlockHeight {
|
||||
#[derive(Debug)]
|
||||
enum Error {
|
||||
Io,
|
||||
NotYetMined,
|
||||
}
|
||||
|
||||
let height = (|| async {
|
||||
let block_height = self
|
||||
.inner
|
||||
.transaction_block_height(txid)
|
||||
.await
|
||||
.map_err(|_| backoff::Error::Transient(Error::Io))?;
|
||||
|
||||
let block_height =
|
||||
block_height.ok_or_else(|| backoff::Error::Transient(Error::NotYetMined))?;
|
||||
|
||||
Result::<_, backoff::Error<Error>>::Ok(block_height)
|
||||
})
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await
|
||||
.expect("transient errors to be retried");
|
||||
|
||||
BlockHeight::new(height)
|
||||
}
|
||||
pub trait GetBlockHeight {
|
||||
async fn get_block_height(&self) -> BlockHeight;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WaitForTransactionFinality for Wallet {
|
||||
async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()> {
|
||||
// TODO(Franck): This assumes that bitcoind runs with txindex=1
|
||||
pub trait TransactionBlockHeight {
|
||||
async fn transaction_block_height(&self, txid: Txid) -> BlockHeight;
|
||||
}
|
||||
|
||||
// Divide by 4 to not check too often yet still be aware of the new block early
|
||||
// on.
|
||||
let mut interval = interval(config.bitcoin_avg_block_time / 4);
|
||||
#[async_trait]
|
||||
pub trait WaitForBlockHeight {
|
||||
async fn wait_for_block_height(&self, height: BlockHeight);
|
||||
}
|
||||
|
||||
loop {
|
||||
let tx = self.inner.client.get_raw_transaction_verbose(txid).await?;
|
||||
if let Some(confirmations) = tx.confirmations {
|
||||
if confirmations >= config.bitcoin_finality_confirmations {
|
||||
break;
|
||||
}
|
||||
}
|
||||
interval.tick().await;
|
||||
}
|
||||
#[async_trait]
|
||||
pub trait GetRawTransaction {
|
||||
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction>;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
#[async_trait]
|
||||
pub trait Network {
|
||||
fn get_network(&self) -> bitcoin::Network;
|
||||
}
|
||||
|
||||
pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result<SecretKey> {
|
||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
||||
|
||||
let s = adaptor
|
||||
.recover_decryption_key(&S.0, &sig, &encsig)
|
||||
.map(SecretKey::from)
|
||||
.ok_or_else(|| anyhow!("secret recovery failure"))?;
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub async fn poll_until_block_height_is_gte<B>(client: &B, target: BlockHeight)
|
||||
where
|
||||
B: GetBlockHeight,
|
||||
{
|
||||
while client.get_block_height().await < target {
|
||||
tokio::time::delay_for(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Network for Wallet {
|
||||
fn get_network(&self) -> bitcoin::Network {
|
||||
self.network
|
||||
pub async fn current_epoch<W>(
|
||||
bitcoin_wallet: &W,
|
||||
cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
lock_tx_id: ::bitcoin::Txid,
|
||||
) -> anyhow::Result<ExpiredTimelocks>
|
||||
where
|
||||
W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight,
|
||||
{
|
||||
let current_block_height = bitcoin_wallet.get_block_height().await;
|
||||
let lock_tx_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await;
|
||||
let cancel_timelock_height = lock_tx_height + cancel_timelock;
|
||||
let punish_timelock_height = cancel_timelock_height + punish_timelock;
|
||||
|
||||
match (
|
||||
current_block_height < cancel_timelock_height,
|
||||
current_block_height < punish_timelock_height,
|
||||
) {
|
||||
(true, _) => Ok(ExpiredTimelocks::None),
|
||||
(false, true) => Ok(ExpiredTimelocks::Cancel),
|
||||
(false, false) => Ok(ExpiredTimelocks::Punish),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait_for_cancel_timelock_to_expire<W>(
|
||||
bitcoin_wallet: &W,
|
||||
cancel_timelock: Timelock,
|
||||
lock_tx_id: ::bitcoin::Txid,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight,
|
||||
{
|
||||
let tx_lock_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await;
|
||||
|
||||
poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + cancel_timelock).await;
|
||||
Ok(())
|
||||
}
|
||||
|
|
49
swap/src/bitcoin/timelocks.rs
Normal file
49
swap/src/bitcoin/timelocks.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Add;
|
||||
|
||||
/// Represent a timelock, expressed in relative block height as defined in
|
||||
/// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki).
|
||||
/// E.g. The timelock expires 10 blocks after the reference transaction is
|
||||
/// mined.
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(transparent)]
|
||||
pub struct Timelock(u32);
|
||||
|
||||
impl Timelock {
|
||||
pub const fn new(number_of_blocks: u32) -> Self {
|
||||
Self(number_of_blocks)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Timelock> for u32 {
|
||||
fn from(timelock: Timelock) -> Self {
|
||||
timelock.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent a block height, or block number, expressed in absolute block
|
||||
/// count. E.g. The transaction was included in block #655123, 655123 block
|
||||
/// after the genesis block.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct BlockHeight(u32);
|
||||
|
||||
impl From<BlockHeight> for u32 {
|
||||
fn from(height: BlockHeight) -> Self {
|
||||
height.0
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockHeight {
|
||||
pub const fn new(block_height: u32) -> Self {
|
||||
Self(block_height)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Timelock> for BlockHeight {
|
||||
type Output = BlockHeight;
|
||||
|
||||
fn add(self, rhs: Timelock) -> Self::Output {
|
||||
BlockHeight(self.0 + rhs.0)
|
||||
}
|
||||
}
|
512
swap/src/bitcoin/transactions.rs
Normal file
512
swap/src/bitcoin/transactions.rs
Normal file
|
@ -0,0 +1,512 @@
|
|||
use anyhow::{bail, Context, Result};
|
||||
use bitcoin::{
|
||||
util::{bip143::SigHashCache, psbt::PartiallySignedTransaction},
|
||||
Address, Amount, SigHash, SigHashType, Transaction, TxIn, TxOut,
|
||||
};
|
||||
use ecdsa_fun::Signature;
|
||||
use miniscript::{Descriptor, NullCtx};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::bitcoin::{
|
||||
build_shared_output_descriptor, timelocks::Timelock, verify_sig, BuildTxLockPsbt, Network,
|
||||
OutPoint, PublicKey, Txid, TX_FEE,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TxLock {
|
||||
inner: Transaction,
|
||||
output_descriptor: Descriptor<::bitcoin::PublicKey>,
|
||||
}
|
||||
|
||||
impl TxLock {
|
||||
pub async fn new<W>(wallet: &W, amount: Amount, A: PublicKey, B: PublicKey) -> Result<Self>
|
||||
where
|
||||
W: BuildTxLockPsbt + Network,
|
||||
{
|
||||
let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0);
|
||||
let address = lock_output_descriptor
|
||||
.address(wallet.get_network(), NullCtx)
|
||||
.expect("can derive address from descriptor");
|
||||
|
||||
// We construct a psbt for convenience
|
||||
let psbt = wallet.build_tx_lock_psbt(address, amount).await?;
|
||||
|
||||
// We don't take advantage of psbt functionality yet, instead we convert to a
|
||||
// raw transaction
|
||||
let inner = psbt.extract_tx();
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
output_descriptor: lock_output_descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn lock_amount(&self) -> Amount {
|
||||
Amount::from_sat(self.inner.output[self.lock_output_vout()].value)
|
||||
}
|
||||
|
||||
pub fn txid(&self) -> Txid {
|
||||
self.inner.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.inner.txid(), self.lock_output_vout() as u32)
|
||||
}
|
||||
|
||||
/// Retreive the index of the locked output in the transaction outputs
|
||||
/// vector
|
||||
fn lock_output_vout(&self) -> usize {
|
||||
self.inner
|
||||
.output
|
||||
.iter()
|
||||
.position(|output| {
|
||||
output.script_pubkey == self.output_descriptor.script_pubkey(NullCtx)
|
||||
})
|
||||
.expect("transaction contains lock output")
|
||||
}
|
||||
|
||||
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.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<TxLock> for PartiallySignedTransaction {
|
||||
fn from(from: TxLock) -> Self {
|
||||
PartiallySignedTransaction::from_unsigned_tx(from.inner).expect("to be unsigned")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TxRedeem {
|
||||
inner: Transaction,
|
||||
digest: SigHash,
|
||||
}
|
||||
|
||||
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.witness_script(NullCtx),
|
||||
tx_lock.lock_amount().as_sat(),
|
||||
SigHashType::All,
|
||||
);
|
||||
|
||||
Self {
|
||||
inner: tx_redeem,
|
||||
digest,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn txid(&self) -> Txid {
|
||||
self.inner.txid()
|
||||
}
|
||||
|
||||
pub fn digest(&self) -> SigHash {
|
||||
self.digest
|
||||
}
|
||||
|
||||
pub fn add_signatures(
|
||||
self,
|
||||
tx_lock: &TxLock,
|
||||
(A, sig_a): (PublicKey, Signature),
|
||||
(B, sig_b): (PublicKey, Signature),
|
||||
) -> Result<Transaction> {
|
||||
let satisfier = {
|
||||
let mut satisfier = HashMap::with_capacity(2);
|
||||
|
||||
let A = ::bitcoin::PublicKey {
|
||||
compressed: true,
|
||||
key: A.0.into(),
|
||||
};
|
||||
let B = ::bitcoin::PublicKey {
|
||||
compressed: true,
|
||||
key: B.0.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
|
||||
};
|
||||
|
||||
let mut tx_redeem = self.inner;
|
||||
tx_lock
|
||||
.output_descriptor
|
||||
.satisfy(&mut tx_redeem.input[0], satisfier, NullCtx)?;
|
||||
|
||||
Ok(tx_redeem)
|
||||
}
|
||||
|
||||
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!(TooManyInputs(inputs.len())),
|
||||
};
|
||||
|
||||
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!(NotThreeWitnesses(witnesses.len())),
|
||||
}?;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||
#[error("transaction does not spend anything")]
|
||||
pub struct NoInputs;
|
||||
|
||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||
#[error("transaction has {0} inputs, expected 1")]
|
||||
pub struct TooManyInputs(usize);
|
||||
|
||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||
#[error("empty witness stack")]
|
||||
pub struct EmptyWitnessStack;
|
||||
|
||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||
#[error("input has {0} witnesses, expected 3")]
|
||||
pub struct NotThreeWitnesses(usize);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TxCancel {
|
||||
inner: Transaction,
|
||||
digest: SigHash,
|
||||
output_descriptor: Descriptor<::bitcoin::PublicKey>,
|
||||
}
|
||||
|
||||
impl TxCancel {
|
||||
pub fn new(tx_lock: &TxLock, cancel_timelock: Timelock, A: PublicKey, B: PublicKey) -> Self {
|
||||
let cancel_output_descriptor = build_shared_output_descriptor(A.0, B.0);
|
||||
|
||||
let tx_in = TxIn {
|
||||
previous_output: tx_lock.as_outpoint(),
|
||||
script_sig: Default::default(),
|
||||
sequence: cancel_timelock.into(),
|
||||
witness: Vec::new(),
|
||||
};
|
||||
|
||||
let tx_out = TxOut {
|
||||
value: tx_lock.lock_amount().as_sat() - TX_FEE,
|
||||
script_pubkey: cancel_output_descriptor.script_pubkey(NullCtx),
|
||||
};
|
||||
|
||||
let transaction = Transaction {
|
||||
version: 2,
|
||||
lock_time: 0,
|
||||
input: vec![tx_in],
|
||||
output: vec![tx_out],
|
||||
};
|
||||
|
||||
let digest = SigHashCache::new(&transaction).signature_hash(
|
||||
0, // Only one input: lock_input (lock transaction)
|
||||
&tx_lock.output_descriptor.witness_script(NullCtx),
|
||||
tx_lock.lock_amount().as_sat(),
|
||||
SigHashType::All,
|
||||
);
|
||||
|
||||
Self {
|
||||
inner: transaction,
|
||||
digest,
|
||||
output_descriptor: cancel_output_descriptor,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn txid(&self) -> Txid {
|
||||
self.inner.txid()
|
||||
}
|
||||
|
||||
pub fn digest(&self) -> SigHash {
|
||||
self.digest
|
||||
}
|
||||
|
||||
fn amount(&self) -> Amount {
|
||||
Amount::from_sat(self.inner.output[0].value)
|
||||
}
|
||||
|
||||
pub fn as_outpoint(&self) -> OutPoint {
|
||||
OutPoint::new(self.inner.txid(), 0)
|
||||
}
|
||||
|
||||
pub fn add_signatures(
|
||||
self,
|
||||
tx_lock: &TxLock,
|
||||
(A, sig_a): (PublicKey, Signature),
|
||||
(B, sig_b): (PublicKey, Signature),
|
||||
) -> Result<Transaction> {
|
||||
let satisfier = {
|
||||
let mut satisfier = HashMap::with_capacity(2);
|
||||
|
||||
let A = ::bitcoin::PublicKey {
|
||||
compressed: true,
|
||||
key: A.0.into(),
|
||||
};
|
||||
let B = ::bitcoin::PublicKey {
|
||||
compressed: true,
|
||||
key: B.0.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
|
||||
};
|
||||
|
||||
let mut tx_cancel = self.inner;
|
||||
tx_lock
|
||||
.output_descriptor
|
||||
.satisfy(&mut tx_cancel.input[0], satisfier, NullCtx)?;
|
||||
|
||||
Ok(tx_cancel)
|
||||
}
|
||||
|
||||
fn build_spend_transaction(
|
||||
&self,
|
||||
spend_address: &Address,
|
||||
sequence: Option<Timelock>,
|
||||
) -> Transaction {
|
||||
let previous_output = self.as_outpoint();
|
||||
|
||||
let tx_in = TxIn {
|
||||
previous_output,
|
||||
script_sig: Default::default(),
|
||||
sequence: sequence.map(Into::into).unwrap_or(0xFFFF_FFFF),
|
||||
witness: Vec::new(),
|
||||
};
|
||||
|
||||
let tx_out = TxOut {
|
||||
value: self.amount().as_sat() - TX_FEE,
|
||||
script_pubkey: spend_address.script_pubkey(),
|
||||
};
|
||||
|
||||
Transaction {
|
||||
version: 2,
|
||||
lock_time: 0,
|
||||
input: vec![tx_in],
|
||||
output: vec![tx_out],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TxRefund {
|
||||
inner: Transaction,
|
||||
digest: SigHash,
|
||||
}
|
||||
|
||||
impl TxRefund {
|
||||
pub fn new(tx_cancel: &TxCancel, refund_address: &Address) -> Self {
|
||||
let tx_punish = tx_cancel.build_spend_transaction(refund_address, None);
|
||||
|
||||
let digest = SigHashCache::new(&tx_punish).signature_hash(
|
||||
0, // Only one input: cancel transaction
|
||||
&tx_cancel.output_descriptor.witness_script(NullCtx),
|
||||
tx_cancel.amount().as_sat(),
|
||||
SigHashType::All,
|
||||
);
|
||||
|
||||
Self {
|
||||
inner: tx_punish,
|
||||
digest,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn txid(&self) -> Txid {
|
||||
self.inner.txid()
|
||||
}
|
||||
|
||||
pub fn digest(&self) -> SigHash {
|
||||
self.digest
|
||||
}
|
||||
|
||||
pub fn add_signatures(
|
||||
self,
|
||||
tx_cancel: &TxCancel,
|
||||
(A, sig_a): (PublicKey, Signature),
|
||||
(B, sig_b): (PublicKey, Signature),
|
||||
) -> Result<Transaction> {
|
||||
let satisfier = {
|
||||
let mut satisfier = HashMap::with_capacity(2);
|
||||
|
||||
let A = ::bitcoin::PublicKey {
|
||||
compressed: true,
|
||||
key: A.0.into(),
|
||||
};
|
||||
let B = ::bitcoin::PublicKey {
|
||||
compressed: true,
|
||||
key: B.0.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
|
||||
};
|
||||
|
||||
let mut tx_refund = self.inner;
|
||||
tx_cancel
|
||||
.output_descriptor
|
||||
.satisfy(&mut tx_refund.input[0], satisfier, NullCtx)?;
|
||||
|
||||
Ok(tx_refund)
|
||||
}
|
||||
|
||||
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!(TooManyInputs(inputs.len())),
|
||||
};
|
||||
|
||||
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!(NotThreeWitnesses(witnesses.len())),
|
||||
}?;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TxPunish {
|
||||
inner: Transaction,
|
||||
digest: SigHash,
|
||||
}
|
||||
|
||||
impl TxPunish {
|
||||
pub fn new(tx_cancel: &TxCancel, punish_address: &Address, punish_timelock: Timelock) -> Self {
|
||||
let tx_punish = tx_cancel.build_spend_transaction(punish_address, Some(punish_timelock));
|
||||
|
||||
let digest = SigHashCache::new(&tx_punish).signature_hash(
|
||||
0, // Only one input: cancel transaction
|
||||
&tx_cancel.output_descriptor.witness_script(NullCtx),
|
||||
tx_cancel.amount().as_sat(),
|
||||
SigHashType::All,
|
||||
);
|
||||
|
||||
Self {
|
||||
inner: tx_punish,
|
||||
digest,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn txid(&self) -> Txid {
|
||||
self.inner.txid()
|
||||
}
|
||||
|
||||
pub fn digest(&self) -> SigHash {
|
||||
self.digest
|
||||
}
|
||||
|
||||
pub fn add_signatures(
|
||||
self,
|
||||
tx_cancel: &TxCancel,
|
||||
(A, sig_a): (PublicKey, Signature),
|
||||
(B, sig_b): (PublicKey, Signature),
|
||||
) -> Result<Transaction> {
|
||||
let satisfier = {
|
||||
let mut satisfier = HashMap::with_capacity(2);
|
||||
|
||||
let A = ::bitcoin::PublicKey {
|
||||
compressed: true,
|
||||
key: A.0.into(),
|
||||
};
|
||||
let B = ::bitcoin::PublicKey {
|
||||
compressed: true,
|
||||
key: B.0.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
|
||||
};
|
||||
|
||||
let mut tx_punish = self.inner;
|
||||
tx_cancel
|
||||
.output_descriptor
|
||||
.satisfy(&mut tx_punish.input[0], satisfier, NullCtx)?;
|
||||
|
||||
Ok(tx_punish)
|
||||
}
|
||||
}
|
201
swap/src/bitcoin/wallet.rs
Normal file
201
swap/src/bitcoin/wallet.rs
Normal file
|
@ -0,0 +1,201 @@
|
|||
use ::bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid};
|
||||
use anyhow::{Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
||||
use bitcoin_harness::{bitcoind_rpc::PsbtBase64, BitcoindRpcApi};
|
||||
use reqwest::Url;
|
||||
use std::time::Duration;
|
||||
use tokio::time::interval;
|
||||
|
||||
use crate::{
|
||||
bitcoin::{
|
||||
timelocks::BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, GetBlockHeight,
|
||||
GetRawTransaction, Network, SignTxLock, TransactionBlockHeight, TxLock,
|
||||
WaitForTransactionFinality, WatchForRawTransaction,
|
||||
},
|
||||
config::Config,
|
||||
};
|
||||
|
||||
pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Wallet {
|
||||
pub inner: bitcoin_harness::Wallet,
|
||||
pub network: bitcoin::Network,
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
pub async fn new(name: &str, url: Url, network: bitcoin::Network) -> Result<Self> {
|
||||
let wallet = bitcoin_harness::Wallet::new(name, url).await?;
|
||||
|
||||
Ok(Self {
|
||||
inner: wallet,
|
||||
network,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn balance(&self) -> Result<Amount> {
|
||||
let balance = self.inner.balance().await?;
|
||||
Ok(balance)
|
||||
}
|
||||
|
||||
pub async fn new_address(&self) -> Result<Address> {
|
||||
self.inner.new_address().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
|
||||
let fee = self
|
||||
.inner
|
||||
.get_wallet_transaction(txid)
|
||||
.await
|
||||
.map(|res| {
|
||||
res.fee.map(|signed_amount| {
|
||||
signed_amount
|
||||
.abs()
|
||||
.to_unsigned()
|
||||
.expect("Absolute value is always positive")
|
||||
})
|
||||
})?
|
||||
.context("Rpc response did not contain a fee")?;
|
||||
|
||||
Ok(fee)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BuildTxLockPsbt for Wallet {
|
||||
async fn build_tx_lock_psbt(
|
||||
&self,
|
||||
output_address: Address,
|
||||
output_amount: Amount,
|
||||
) -> Result<PartiallySignedTransaction> {
|
||||
let psbt = self.inner.fund_psbt(output_address, output_amount).await?;
|
||||
let as_hex = base64::decode(psbt)?;
|
||||
|
||||
let psbt = bitcoin::consensus::deserialize(&as_hex)?;
|
||||
|
||||
Ok(psbt)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SignTxLock for Wallet {
|
||||
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction> {
|
||||
let psbt = PartiallySignedTransaction::from(tx_lock);
|
||||
|
||||
let psbt = bitcoin::consensus::serialize(&psbt);
|
||||
let as_base64 = base64::encode(psbt);
|
||||
|
||||
let psbt = self
|
||||
.inner
|
||||
.wallet_process_psbt(PsbtBase64(as_base64))
|
||||
.await?;
|
||||
let PsbtBase64(signed_psbt) = PsbtBase64::from(psbt);
|
||||
|
||||
let as_hex = base64::decode(signed_psbt)?;
|
||||
let psbt: PartiallySignedTransaction = bitcoin::consensus::deserialize(&as_hex)?;
|
||||
|
||||
let tx = psbt.extract_tx();
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BroadcastSignedTransaction for Wallet {
|
||||
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid> {
|
||||
let txid = self.inner.send_raw_transaction(transaction).await?;
|
||||
tracing::info!("Bitcoin tx broadcasted! TXID = {}", txid);
|
||||
Ok(txid)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
|
||||
// to `ConstantBackoff`.
|
||||
#[async_trait]
|
||||
impl WatchForRawTransaction for Wallet {
|
||||
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction {
|
||||
(|| async { Ok(self.inner.get_raw_transaction(txid).await?) })
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await
|
||||
.expect("transient errors to be retried")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GetRawTransaction for Wallet {
|
||||
// todo: potentially replace with option
|
||||
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
||||
Ok(self.inner.get_raw_transaction(txid).await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GetBlockHeight for Wallet {
|
||||
async fn get_block_height(&self) -> BlockHeight {
|
||||
let height = (|| async { Ok(self.inner.client.getblockcount().await?) })
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await
|
||||
.expect("transient errors to be retried");
|
||||
|
||||
BlockHeight::new(height)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TransactionBlockHeight for Wallet {
|
||||
async fn transaction_block_height(&self, txid: Txid) -> BlockHeight {
|
||||
#[derive(Debug)]
|
||||
enum Error {
|
||||
Io,
|
||||
NotYetMined,
|
||||
}
|
||||
|
||||
let height = (|| async {
|
||||
let block_height = self
|
||||
.inner
|
||||
.transaction_block_height(txid)
|
||||
.await
|
||||
.map_err(|_| backoff::Error::Transient(Error::Io))?;
|
||||
|
||||
let block_height =
|
||||
block_height.ok_or_else(|| backoff::Error::Transient(Error::NotYetMined))?;
|
||||
|
||||
Result::<_, backoff::Error<Error>>::Ok(block_height)
|
||||
})
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await
|
||||
.expect("transient errors to be retried");
|
||||
|
||||
BlockHeight::new(height)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WaitForTransactionFinality for Wallet {
|
||||
async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()> {
|
||||
// TODO(Franck): This assumes that bitcoind runs with txindex=1
|
||||
|
||||
// Divide by 4 to not check too often yet still be aware of the new block early
|
||||
// on.
|
||||
let mut interval = interval(config.bitcoin_avg_block_time / 4);
|
||||
|
||||
loop {
|
||||
let tx = self.inner.client.get_raw_transaction_verbose(txid).await?;
|
||||
if let Some(confirmations) = tx.confirmations {
|
||||
if confirmations >= config.bitcoin_finality_confirmations {
|
||||
break;
|
||||
}
|
||||
}
|
||||
interval.tick().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Network for Wallet {
|
||||
fn get_network(&self) -> bitcoin::Network {
|
||||
self.network
|
||||
}
|
||||
}
|
|
@ -2,6 +2,8 @@ use libp2p::{core::Multiaddr, PeerId};
|
|||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::monero;
|
||||
|
||||
#[derive(structopt::StructOpt, Debug)]
|
||||
pub struct Options {
|
||||
// TODO: Default value should points to proper configuration folder in home folder
|
||||
|
@ -13,7 +15,7 @@ pub struct Options {
|
|||
}
|
||||
|
||||
#[derive(structopt::StructOpt, Debug)]
|
||||
#[structopt(name = "xmr-btc-swap", about = "XMR BTC atomic swap")]
|
||||
#[structopt(name = "xmr_btc-swap", about = "XMR BTC atomic swap")]
|
||||
pub enum Command {
|
||||
SellXmr {
|
||||
#[structopt(long = "bitcoind-rpc", default_value = "http://127.0.0.1:8332")]
|
||||
|
@ -32,10 +34,10 @@ pub enum Command {
|
|||
listen_addr: Multiaddr,
|
||||
|
||||
#[structopt(long = "send-xmr", help = "Monero amount as floating point nr without denomination (e.g. 125.1)", parse(try_from_str = parse_xmr))]
|
||||
send_monero: xmr_btc::monero::Amount,
|
||||
send_monero: monero::Amount,
|
||||
|
||||
#[structopt(long = "receive-btc", help = "Bitcoin amount as floating point nr without denomination (e.g. 1.25)", parse(try_from_str = parse_btc))]
|
||||
receive_bitcoin: bitcoin::Amount,
|
||||
receive_bitcoin: ::bitcoin::Amount,
|
||||
},
|
||||
BuyXmr {
|
||||
#[structopt(long = "connect-peer-id")]
|
||||
|
@ -57,10 +59,10 @@ pub enum Command {
|
|||
monero_wallet_rpc_url: Url,
|
||||
|
||||
#[structopt(long = "send-btc", help = "Bitcoin amount as floating point nr without denomination (e.g. 1.25)", parse(try_from_str = parse_btc))]
|
||||
send_bitcoin: bitcoin::Amount,
|
||||
send_bitcoin: ::bitcoin::Amount,
|
||||
|
||||
#[structopt(long = "receive-xmr", help = "Monero amount as floating point nr without denomination (e.g. 125.1)", parse(try_from_str = parse_xmr))]
|
||||
receive_monero: xmr_btc::monero::Amount,
|
||||
receive_monero: monero::Amount,
|
||||
},
|
||||
History,
|
||||
Resume(Resume),
|
||||
|
@ -116,7 +118,7 @@ fn parse_btc(str: &str) -> anyhow::Result<bitcoin::Amount> {
|
|||
Ok(amount)
|
||||
}
|
||||
|
||||
fn parse_xmr(str: &str) -> anyhow::Result<xmr_btc::monero::Amount> {
|
||||
let amount = xmr_btc::monero::Amount::parse_monero(str)?;
|
||||
fn parse_xmr(str: &str) -> anyhow::Result<monero::Amount> {
|
||||
let amount = monero::Amount::parse_monero(str)?;
|
||||
Ok(amount)
|
||||
}
|
||||
|
|
129
swap/src/config.rs
Normal file
129
swap/src/config.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
use crate::bitcoin::timelocks::Timelock;
|
||||
use conquer_once::Lazy;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Config {
|
||||
pub bob_time_to_act: Duration,
|
||||
pub bitcoin_finality_confirmations: u32,
|
||||
pub bitcoin_avg_block_time: Duration,
|
||||
pub monero_max_finality_time: Duration,
|
||||
pub monero_finality_confirmations: u32,
|
||||
pub bitcoin_cancel_timelock: Timelock,
|
||||
pub bitcoin_punish_timelock: Timelock,
|
||||
pub bitcoin_network: ::bitcoin::Network,
|
||||
pub monero_network: ::monero::Network,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn mainnet() -> Self {
|
||||
Self {
|
||||
bob_time_to_act: *mainnet::BOB_TIME_TO_ACT,
|
||||
bitcoin_finality_confirmations: mainnet::BITCOIN_FINALITY_CONFIRMATIONS,
|
||||
bitcoin_avg_block_time: *mainnet::BITCOIN_AVG_BLOCK_TIME,
|
||||
// We apply a scaling factor (1.5) so that the swap is not aborted when the
|
||||
// blockchain is slow
|
||||
monero_max_finality_time: (*mainnet::MONERO_AVG_BLOCK_TIME).mul_f64(1.5)
|
||||
* mainnet::MONERO_FINALITY_CONFIRMATIONS,
|
||||
monero_finality_confirmations: mainnet::MONERO_FINALITY_CONFIRMATIONS,
|
||||
bitcoin_cancel_timelock: mainnet::BITCOIN_CANCEL_TIMELOCK,
|
||||
bitcoin_punish_timelock: mainnet::BITCOIN_PUNISH_TIMELOCK,
|
||||
bitcoin_network: ::bitcoin::Network::Bitcoin,
|
||||
monero_network: ::monero::Network::Mainnet,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn testnet() -> Self {
|
||||
Self {
|
||||
bob_time_to_act: *testnet::BOB_TIME_TO_ACT,
|
||||
bitcoin_finality_confirmations: testnet::BITCOIN_FINALITY_CONFIRMATIONS,
|
||||
bitcoin_avg_block_time: *testnet::BITCOIN_AVG_BLOCK_TIME,
|
||||
// We apply a scaling factor (1.5) so that the swap is not aborted when the
|
||||
// blockchain is slow
|
||||
monero_max_finality_time: (*testnet::MONERO_AVG_BLOCK_TIME).mul_f64(1.5)
|
||||
* testnet::MONERO_FINALITY_CONFIRMATIONS,
|
||||
monero_finality_confirmations: testnet::MONERO_FINALITY_CONFIRMATIONS,
|
||||
bitcoin_cancel_timelock: testnet::BITCOIN_CANCEL_TIMELOCK,
|
||||
bitcoin_punish_timelock: testnet::BITCOIN_PUNISH_TIMELOCK,
|
||||
bitcoin_network: ::bitcoin::Network::Testnet,
|
||||
monero_network: ::monero::Network::Stagenet,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn regtest() -> Self {
|
||||
Self {
|
||||
bob_time_to_act: *regtest::BOB_TIME_TO_ACT,
|
||||
bitcoin_finality_confirmations: regtest::BITCOIN_FINALITY_CONFIRMATIONS,
|
||||
bitcoin_avg_block_time: *regtest::BITCOIN_AVG_BLOCK_TIME,
|
||||
// We apply a scaling factor (1.5) so that the swap is not aborted when the
|
||||
// blockchain is slow
|
||||
monero_max_finality_time: (*regtest::MONERO_AVG_BLOCK_TIME).mul_f64(1.5)
|
||||
* regtest::MONERO_FINALITY_CONFIRMATIONS,
|
||||
monero_finality_confirmations: regtest::MONERO_FINALITY_CONFIRMATIONS,
|
||||
bitcoin_cancel_timelock: regtest::BITCOIN_CANCEL_TIMELOCK,
|
||||
bitcoin_punish_timelock: regtest::BITCOIN_PUNISH_TIMELOCK,
|
||||
bitcoin_network: ::bitcoin::Network::Regtest,
|
||||
monero_network: ::monero::Network::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod mainnet {
|
||||
use super::*;
|
||||
|
||||
// For each step, we are giving Bob 10 minutes to act.
|
||||
pub static BOB_TIME_TO_ACT: Lazy<Duration> = Lazy::new(|| Duration::from_secs(10 * 60));
|
||||
|
||||
pub static BITCOIN_FINALITY_CONFIRMATIONS: u32 = 3;
|
||||
|
||||
pub static BITCOIN_AVG_BLOCK_TIME: Lazy<Duration> = Lazy::new(|| Duration::from_secs(10 * 60));
|
||||
|
||||
pub static MONERO_FINALITY_CONFIRMATIONS: u32 = 15;
|
||||
|
||||
pub static MONERO_AVG_BLOCK_TIME: Lazy<Duration> = Lazy::new(|| Duration::from_secs(2 * 60));
|
||||
|
||||
// Set to 12 hours, arbitrary value to be reviewed properly
|
||||
pub static BITCOIN_CANCEL_TIMELOCK: Timelock = Timelock::new(72);
|
||||
pub static BITCOIN_PUNISH_TIMELOCK: Timelock = Timelock::new(72);
|
||||
}
|
||||
|
||||
mod testnet {
|
||||
use super::*;
|
||||
|
||||
pub static BOB_TIME_TO_ACT: Lazy<Duration> = Lazy::new(|| Duration::from_secs(5 * 60));
|
||||
|
||||
// This does not reflect recommended values for mainnet!
|
||||
pub static BITCOIN_FINALITY_CONFIRMATIONS: u32 = 1;
|
||||
|
||||
pub static BITCOIN_AVG_BLOCK_TIME: Lazy<Duration> = Lazy::new(|| Duration::from_secs(5 * 60));
|
||||
|
||||
// This does not reflect recommended values for mainnet!
|
||||
pub static MONERO_FINALITY_CONFIRMATIONS: u32 = 5;
|
||||
|
||||
// The average blocktime on Monero stagenet is not as constant as on mainnet,
|
||||
// hence 4 minutes it set
|
||||
pub static MONERO_AVG_BLOCK_TIME: Lazy<Duration> = Lazy::new(|| Duration::from_secs(4 * 60));
|
||||
|
||||
// This does not reflect recommended values for mainnet!
|
||||
pub static BITCOIN_CANCEL_TIMELOCK: Timelock = Timelock::new(6);
|
||||
pub static BITCOIN_PUNISH_TIMELOCK: Timelock = Timelock::new(6);
|
||||
}
|
||||
|
||||
mod regtest {
|
||||
use super::*;
|
||||
|
||||
// In test, we set a shorter time to fail fast
|
||||
pub static BOB_TIME_TO_ACT: Lazy<Duration> = Lazy::new(|| Duration::from_secs(30));
|
||||
|
||||
pub static BITCOIN_FINALITY_CONFIRMATIONS: u32 = 1;
|
||||
|
||||
pub static BITCOIN_AVG_BLOCK_TIME: Lazy<Duration> = Lazy::new(|| Duration::from_secs(5));
|
||||
|
||||
pub static MONERO_FINALITY_CONFIRMATIONS: u32 = 1;
|
||||
|
||||
pub static MONERO_AVG_BLOCK_TIME: Lazy<Duration> = Lazy::new(|| Duration::from_secs(60));
|
||||
|
||||
pub static BITCOIN_CANCEL_TIMELOCK: Timelock = Timelock::new(50);
|
||||
|
||||
pub static BITCOIN_PUNISH_TIMELOCK: Timelock = Timelock::new(50);
|
||||
}
|
|
@ -3,13 +3,39 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
|||
use std::{fmt::Display, path::Path};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod alice;
|
||||
mod bob;
|
||||
pub mod alice;
|
||||
pub mod bob;
|
||||
|
||||
pub use alice::*;
|
||||
pub use bob::*;
|
||||
pub use alice::Alice;
|
||||
pub use bob::Bob;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub enum Swap {
|
||||
Alice(Alice),
|
||||
Bob(Bob),
|
||||
}
|
||||
|
||||
impl From<Alice> for Swap {
|
||||
fn from(from: Alice) -> Self {
|
||||
Swap::Alice(from)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bob> for Swap {
|
||||
fn from(from: Bob) -> Self {
|
||||
Swap::Bob(from)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Swap {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Swap::Alice(alice) => Display::fmt(alice, f),
|
||||
Swap::Bob(bob) => Display::fmt(bob, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Database(sled::Db);
|
||||
|
||||
impl Database {
|
||||
|
@ -85,37 +111,13 @@ where
|
|||
Ok(serde_cbor::from_slice(&v)?)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub enum Swap {
|
||||
Alice(Alice),
|
||||
Bob(Bob),
|
||||
}
|
||||
|
||||
impl From<Alice> for Swap {
|
||||
fn from(from: Alice) -> Self {
|
||||
Swap::Alice(from)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bob> for Swap {
|
||||
fn from(from: Bob) -> Self {
|
||||
Swap::Bob(from)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Swap {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Swap::Alice(alice) => Display::fmt(alice, f),
|
||||
Swap::Bob(bob) => Display::fmt(bob, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::database::{Alice, AliceEndState, Bob, BobEndState};
|
||||
use crate::database::{
|
||||
alice::{Alice, AliceEndState},
|
||||
bob::{Bob, BobEndState},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_write_and_read_to_multiple_keys() {
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
use crate::{alice::swap::AliceState, SwapAmounts};
|
||||
use bitcoin::hashes::core::fmt::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xmr_btc::{
|
||||
alice,
|
||||
|
||||
use crate::{
|
||||
bitcoin::{EncryptedSignature, TxCancel, TxRefund},
|
||||
monero,
|
||||
protocol::{alice, alice::swap::AliceState},
|
||||
serde::monero_private_key,
|
||||
SwapAmounts,
|
||||
};
|
||||
|
||||
// Large enum variant is fine because this is only used for storage
|
||||
// Large enum variant is fine because this is only used for database
|
||||
// and is dropped once written in DB.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use crate::{bob::swap::BobState, SwapAmounts};
|
||||
use bitcoin::hashes::core::fmt::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xmr_btc::bob;
|
||||
|
||||
use crate::{
|
||||
protocol::{bob, bob::swap::BobState},
|
||||
SwapAmounts,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub enum Bob {
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
#![warn(
|
||||
unused_extern_crates,
|
||||
missing_debug_implementations,
|
||||
missing_copy_implementations,
|
||||
rust_2018_idioms,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
|
@ -10,19 +8,25 @@
|
|||
clippy::cast_possible_wrap,
|
||||
clippy::dbg_macro
|
||||
)]
|
||||
#![cfg_attr(not(test), warn(clippy::unwrap_used))]
|
||||
#![forbid(unsafe_code)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(
|
||||
non_snake_case,
|
||||
missing_debug_implementations,
|
||||
missing_copy_implementations
|
||||
)]
|
||||
|
||||
use ::serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
pub mod alice;
|
||||
pub mod bitcoin;
|
||||
pub mod bob;
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod database;
|
||||
pub mod monero;
|
||||
pub mod network;
|
||||
pub mod protocol;
|
||||
pub mod serde;
|
||||
pub mod trace;
|
||||
|
||||
pub type Never = std::convert::Infallible;
|
||||
|
@ -48,7 +52,7 @@ pub struct SwapAmounts {
|
|||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||
pub btc: bitcoin::Amount,
|
||||
/// Amount of XMR to swap.
|
||||
#[serde(with = "xmr_btc::serde::monero_amount")]
|
||||
#[serde(with = "serde::monero_amount")]
|
||||
pub xmr: monero::Amount,
|
||||
}
|
||||
|
||||
|
@ -63,3 +67,10 @@ impl Display for SwapAmounts {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ExpiredTimelocks {
|
||||
None,
|
||||
Cancel,
|
||||
Punish,
|
||||
}
|
||||
|
|
|
@ -20,20 +20,18 @@ use rand::rngs::OsRng;
|
|||
use std::sync::Arc;
|
||||
use structopt::StructOpt;
|
||||
use swap::{
|
||||
alice,
|
||||
alice::swap::AliceState,
|
||||
bitcoin, bob,
|
||||
bob::swap::BobState,
|
||||
bitcoin,
|
||||
cli::{Command, Options, Resume},
|
||||
config::Config,
|
||||
database::{Database, Swap},
|
||||
monero,
|
||||
network::transport::build,
|
||||
protocol::{alice, alice::swap::AliceState, bob, bob::swap::BobState},
|
||||
trace::init_tracing,
|
||||
SwapAmounts,
|
||||
};
|
||||
use tracing::{info, log::LevelFilter};
|
||||
use uuid::Uuid;
|
||||
use xmr_btc::{alice::State0, config::Config, cross_curve_dleq};
|
||||
|
||||
#[macro_use]
|
||||
extern crate prettytable;
|
||||
|
@ -76,10 +74,10 @@ async fn main() -> Result<()> {
|
|||
let rng = &mut OsRng;
|
||||
let a = bitcoin::SecretKey::new_random(rng);
|
||||
let s_a = cross_curve_dleq::Scalar::random(rng);
|
||||
let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng);
|
||||
let v_a = monero::PrivateViewKey::new_random(rng);
|
||||
let redeem_address = bitcoin_wallet.as_ref().new_address().await?;
|
||||
let punish_address = redeem_address.clone();
|
||||
let state0 = State0::new(
|
||||
let state0 = alice::state::State0::new(
|
||||
a,
|
||||
s_a,
|
||||
v_a,
|
||||
|
@ -129,7 +127,7 @@ async fn main() -> Result<()> {
|
|||
.await?;
|
||||
|
||||
let refund_address = bitcoin_wallet.new_address().await?;
|
||||
let state0 = xmr_btc::bob::State0::new(
|
||||
let state0 = bob::state::State0::new(
|
||||
&mut OsRng,
|
||||
send_bitcoin,
|
||||
receive_monero,
|
||||
|
@ -248,9 +246,10 @@ async fn setup_wallets(
|
|||
bitcoin_wallet_name: &str,
|
||||
monero_wallet_rpc_url: url::Url,
|
||||
config: Config,
|
||||
) -> Result<(Arc<bitcoin::Wallet>, Arc<monero::Wallet>)> {
|
||||
) -> Result<(Arc<swap::bitcoin::Wallet>, Arc<swap::monero::Wallet>)> {
|
||||
let bitcoin_wallet =
|
||||
bitcoin::Wallet::new(bitcoin_wallet_name, bitcoind_url, config.bitcoin_network).await?;
|
||||
swap::bitcoin::Wallet::new(bitcoin_wallet_name, bitcoind_url, config.bitcoin_network)
|
||||
.await?;
|
||||
let bitcoin_balance = bitcoin_wallet.balance().await?;
|
||||
info!(
|
||||
"Connection to Bitcoin wallet succeeded, balance: {}",
|
||||
|
@ -273,8 +272,8 @@ async fn alice_swap(
|
|||
swap_id: Uuid,
|
||||
state: AliceState,
|
||||
listen_addr: Multiaddr,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
monero_wallet: Arc<monero::Wallet>,
|
||||
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
|
||||
monero_wallet: Arc<swap::monero::Wallet>,
|
||||
config: Config,
|
||||
db: Database,
|
||||
) -> Result<AliceState> {
|
||||
|
@ -306,8 +305,8 @@ async fn alice_swap(
|
|||
async fn bob_swap(
|
||||
swap_id: Uuid,
|
||||
state: BobState,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
monero_wallet: Arc<monero::Wallet>,
|
||||
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
|
||||
monero_wallet: Arc<swap::monero::Wallet>,
|
||||
db: Database,
|
||||
alice_peer_id: PeerId,
|
||||
alice_addr: Multiaddr,
|
||||
|
|
|
@ -1,143 +1,281 @@
|
|||
pub mod wallet;
|
||||
|
||||
use ::bitcoin::hashes::core::fmt::Formatter;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
||||
use monero_harness::rpc::wallet;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use url::Url;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rust_decimal::{
|
||||
prelude::{FromPrimitive, ToPrimitive},
|
||||
Decimal,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
ops::{Add, Mul, Sub},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
pub use xmr_btc::monero::*;
|
||||
use crate::{bitcoin, serde::monero_private_key};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Wallet {
|
||||
pub inner: wallet::Client,
|
||||
pub network: Network,
|
||||
pub use curve25519_dalek::scalar::Scalar;
|
||||
pub use monero::*;
|
||||
pub use wallet::Wallet;
|
||||
|
||||
pub const PICONERO_OFFSET: u64 = 1_000_000_000_000;
|
||||
|
||||
pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
|
||||
let scalar = Scalar::random(rng);
|
||||
|
||||
PrivateKey::from_scalar(scalar)
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
pub fn new(url: Url, network: Network) -> Self {
|
||||
Self {
|
||||
inner: wallet::Client::new(url),
|
||||
network,
|
||||
}
|
||||
pub fn private_key_from_secp256k1_scalar(scalar: bitcoin::Scalar) -> PrivateKey {
|
||||
let mut bytes = scalar.to_bytes();
|
||||
|
||||
// we must reverse the bytes because a secp256k1 scalar is big endian, whereas a
|
||||
// ed25519 scalar is little endian
|
||||
bytes.reverse();
|
||||
|
||||
PrivateKey::from_scalar(Scalar::from_bytes_mod_order(bytes))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PrivateViewKey(#[serde(with = "monero_private_key")] PrivateKey);
|
||||
|
||||
impl PrivateViewKey {
|
||||
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
||||
let scalar = Scalar::random(rng);
|
||||
let private_key = PrivateKey::from_scalar(scalar);
|
||||
|
||||
Self(private_key)
|
||||
}
|
||||
|
||||
/// Get the balance of the primary account.
|
||||
pub async fn get_balance(&self) -> Result<Amount> {
|
||||
let amount = self.inner.get_balance(0).await?;
|
||||
pub fn public(&self) -> PublicViewKey {
|
||||
PublicViewKey(PublicKey::from_private_key(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Amount::from_piconero(amount))
|
||||
impl Add for PrivateViewKey {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PrivateViewKey> for PrivateKey {
|
||||
fn from(from: PrivateViewKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicViewKey> for PublicKey {
|
||||
fn from(from: PublicViewKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PublicViewKey(PublicKey);
|
||||
|
||||
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)]
|
||||
pub struct Amount(u64);
|
||||
|
||||
impl Amount {
|
||||
pub const ZERO: Self = Self(0);
|
||||
/// Create an [Amount] with piconero precision and the given number of
|
||||
/// piconeros.
|
||||
///
|
||||
/// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR.
|
||||
pub fn from_piconero(amount: u64) -> Self {
|
||||
Amount(amount)
|
||||
}
|
||||
|
||||
pub fn as_piconero(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn parse_monero(amount: &str) -> Result<Self> {
|
||||
let decimal = Decimal::from_str(amount)?;
|
||||
let piconeros_dec =
|
||||
decimal.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64"));
|
||||
let piconeros = piconeros_dec
|
||||
.to_u64()
|
||||
.ok_or_else(|| OverflowError(amount.to_owned()))?;
|
||||
Ok(Amount(piconeros))
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Amount {
|
||||
type Output = Amount;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Amount {
|
||||
type Output = Amount;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<u64> for Amount {
|
||||
type Output = Amount;
|
||||
|
||||
fn mul(self, rhs: u64) -> Self::Output {
|
||||
Self(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Amount> for u64 {
|
||||
fn from(from: Amount) -> u64 {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Amount {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let mut decimal = Decimal::from(self.0);
|
||||
decimal
|
||||
.set_scale(12)
|
||||
.expect("12 is smaller than max precision of 28");
|
||||
write!(f, "{} XMR", decimal)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TransferProof {
|
||||
tx_hash: TxHash,
|
||||
#[serde(with = "monero_private_key")]
|
||||
tx_key: PrivateKey,
|
||||
}
|
||||
|
||||
impl TransferProof {
|
||||
pub fn new(tx_hash: TxHash, tx_key: PrivateKey) -> Self {
|
||||
Self { tx_hash, tx_key }
|
||||
}
|
||||
pub fn tx_hash(&self) -> TxHash {
|
||||
self.tx_hash.clone()
|
||||
}
|
||||
pub fn tx_key(&self) -> PrivateKey {
|
||||
self.tx_key
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add constructor/ change String to fixed length byte array
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TxHash(pub String);
|
||||
|
||||
impl From<TxHash> for String {
|
||||
fn from(from: TxHash) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Transfer for Wallet {
|
||||
pub trait Transfer {
|
||||
async fn transfer(
|
||||
&self,
|
||||
public_spend_key: PublicKey,
|
||||
public_view_key: PublicViewKey,
|
||||
amount: Amount,
|
||||
) -> Result<(TransferProof, Amount)> {
|
||||
let destination_address =
|
||||
Address::standard(self.network, public_spend_key, public_view_key.into());
|
||||
|
||||
let res = self
|
||||
.inner
|
||||
.transfer(0, amount.as_piconero(), &destination_address.to_string())
|
||||
.await?;
|
||||
|
||||
let tx_hash = TxHash(res.tx_hash);
|
||||
tracing::info!("Monero tx broadcasted!, tx hash: {:?}", tx_hash);
|
||||
let tx_key = PrivateKey::from_str(&res.tx_key)?;
|
||||
|
||||
let fee = Amount::from_piconero(res.fee);
|
||||
|
||||
let transfer_proof = TransferProof::new(tx_hash, tx_key);
|
||||
tracing::debug!(" Transfer proof: {:?}", transfer_proof);
|
||||
|
||||
Ok((transfer_proof, fee))
|
||||
}
|
||||
) -> anyhow::Result<(TransferProof, Amount)>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CreateWalletForOutput for Wallet {
|
||||
async fn create_and_load_wallet_for_output(
|
||||
&self,
|
||||
private_spend_key: PrivateKey,
|
||||
private_view_key: PrivateViewKey,
|
||||
) -> Result<()> {
|
||||
let public_spend_key = PublicKey::from_private_key(&private_spend_key);
|
||||
let public_view_key = PublicKey::from_private_key(&private_view_key.into());
|
||||
|
||||
let address = Address::standard(self.network, public_spend_key, public_view_key);
|
||||
|
||||
let _ = self
|
||||
.inner
|
||||
.generate_from_keys(
|
||||
&address.to_string(),
|
||||
&private_spend_key.to_string(),
|
||||
&PrivateKey::from(private_view_key).to_string(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
|
||||
// to `ConstantBackoff`.
|
||||
|
||||
#[async_trait]
|
||||
impl WatchForTransfer for Wallet {
|
||||
pub trait WatchForTransfer {
|
||||
async fn watch_for_transfer(
|
||||
&self,
|
||||
public_spend_key: PublicKey,
|
||||
public_view_key: PublicViewKey,
|
||||
transfer_proof: TransferProof,
|
||||
expected_amount: Amount,
|
||||
amount: Amount,
|
||||
expected_confirmations: u32,
|
||||
) -> Result<(), InsufficientFunds> {
|
||||
enum Error {
|
||||
TxNotFound,
|
||||
InsufficientConfirmations,
|
||||
InsufficientFunds { expected: Amount, actual: Amount },
|
||||
}
|
||||
) -> Result<(), InsufficientFunds>;
|
||||
}
|
||||
|
||||
let address = Address::standard(self.network, public_spend_key, public_view_key.into());
|
||||
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
||||
#[error("transaction does not pay enough: expected {expected:?}, got {actual:?}")]
|
||||
pub struct InsufficientFunds {
|
||||
pub expected: Amount,
|
||||
pub actual: Amount,
|
||||
}
|
||||
|
||||
let res = (|| async {
|
||||
// NOTE: Currently, this is conflating IO errors with the transaction not being
|
||||
// in the blockchain yet, or not having enough confirmations on it. All these
|
||||
// errors warrant a retry, but the strategy should probably differ per case
|
||||
let proof = self
|
||||
.inner
|
||||
.check_tx_key(
|
||||
&String::from(transfer_proof.tx_hash()),
|
||||
&transfer_proof.tx_key().to_string(),
|
||||
&address.to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| backoff::Error::Transient(Error::TxNotFound))?;
|
||||
#[async_trait]
|
||||
pub trait CreateWalletForOutput {
|
||||
async fn create_and_load_wallet_for_output(
|
||||
&self,
|
||||
private_spend_key: PrivateKey,
|
||||
private_view_key: PrivateViewKey,
|
||||
) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
if proof.received != expected_amount.as_piconero() {
|
||||
return Err(backoff::Error::Permanent(Error::InsufficientFunds {
|
||||
expected: expected_amount,
|
||||
actual: Amount::from_piconero(proof.received),
|
||||
}));
|
||||
}
|
||||
#[derive(thiserror::Error, Debug, Clone, PartialEq)]
|
||||
#[error("Overflow, cannot convert {0} to u64")]
|
||||
pub struct OverflowError(pub String);
|
||||
|
||||
if proof.confirmations < expected_confirmations {
|
||||
return Err(backoff::Error::Transient(Error::InsufficientConfirmations));
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Ok(proof)
|
||||
})
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await;
|
||||
#[test]
|
||||
fn display_monero_min() {
|
||||
let min_pics = 1;
|
||||
let amount = Amount::from_piconero(min_pics);
|
||||
let monero = amount.to_string();
|
||||
assert_eq!("0.000000000001 XMR", monero);
|
||||
}
|
||||
|
||||
if let Err(Error::InsufficientFunds { expected, actual }) = res {
|
||||
return Err(InsufficientFunds { expected, actual });
|
||||
};
|
||||
#[test]
|
||||
fn display_monero_one() {
|
||||
let min_pics = 1000000000000;
|
||||
let amount = Amount::from_piconero(min_pics);
|
||||
let monero = amount.to_string();
|
||||
assert_eq!("1.000000000000 XMR", monero);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
#[test]
|
||||
fn display_monero_max() {
|
||||
let max_pics = 18_446_744_073_709_551_615;
|
||||
let amount = Amount::from_piconero(max_pics);
|
||||
let monero = amount.to_string();
|
||||
assert_eq!("18446744.073709551615 XMR", monero);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_monero_min() {
|
||||
let monero_min = "0.000000000001";
|
||||
let amount = Amount::parse_monero(monero_min).unwrap();
|
||||
let pics = amount.0;
|
||||
assert_eq!(1, pics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_monero() {
|
||||
let monero = "123";
|
||||
let amount = Amount::parse_monero(monero).unwrap();
|
||||
let pics = amount.0;
|
||||
assert_eq!(123000000000000, pics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_monero_max() {
|
||||
let monero = "18446744.073709551615";
|
||||
let amount = Amount::parse_monero(monero).unwrap();
|
||||
let pics = amount.0;
|
||||
assert_eq!(18446744073709551615, pics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_monero_overflows() {
|
||||
let overflow_pics = "18446744.073709551616";
|
||||
let error = Amount::parse_monero(overflow_pics).unwrap_err();
|
||||
assert_eq!(
|
||||
error.downcast_ref::<OverflowError>().unwrap(),
|
||||
&OverflowError(overflow_pics.to_owned())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
147
swap/src/monero/wallet.rs
Normal file
147
swap/src/monero/wallet.rs
Normal file
|
@ -0,0 +1,147 @@
|
|||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
||||
use monero::{Address, Network, PrivateKey, PublicKey};
|
||||
use monero_harness::rpc::wallet;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use url::Url;
|
||||
|
||||
use crate::monero::{
|
||||
Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicViewKey, Transfer,
|
||||
TransferProof, TxHash, WatchForTransfer,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Wallet {
|
||||
pub inner: wallet::Client,
|
||||
pub network: Network,
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
pub fn new(url: Url, network: Network) -> Self {
|
||||
Self {
|
||||
inner: wallet::Client::new(url),
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the balance of the primary account.
|
||||
pub async fn get_balance(&self) -> Result<Amount> {
|
||||
let amount = self.inner.get_balance(0).await?;
|
||||
|
||||
Ok(Amount::from_piconero(amount))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Transfer for Wallet {
|
||||
async fn transfer(
|
||||
&self,
|
||||
public_spend_key: PublicKey,
|
||||
public_view_key: PublicViewKey,
|
||||
amount: Amount,
|
||||
) -> Result<(TransferProof, Amount)> {
|
||||
let destination_address =
|
||||
Address::standard(self.network, public_spend_key, public_view_key.into());
|
||||
|
||||
let res = self
|
||||
.inner
|
||||
.transfer(0, amount.as_piconero(), &destination_address.to_string())
|
||||
.await?;
|
||||
|
||||
let tx_hash = TxHash(res.tx_hash);
|
||||
tracing::info!("Monero tx broadcasted!, tx hash: {:?}", tx_hash);
|
||||
let tx_key = PrivateKey::from_str(&res.tx_key)?;
|
||||
|
||||
let fee = Amount::from_piconero(res.fee);
|
||||
|
||||
let transfer_proof = TransferProof::new(tx_hash, tx_key);
|
||||
tracing::debug!(" Transfer proof: {:?}", transfer_proof);
|
||||
|
||||
Ok((transfer_proof, fee))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CreateWalletForOutput for Wallet {
|
||||
async fn create_and_load_wallet_for_output(
|
||||
&self,
|
||||
private_spend_key: PrivateKey,
|
||||
private_view_key: PrivateViewKey,
|
||||
) -> Result<()> {
|
||||
let public_spend_key = PublicKey::from_private_key(&private_spend_key);
|
||||
let public_view_key = PublicKey::from_private_key(&private_view_key.into());
|
||||
|
||||
let address = Address::standard(self.network, public_spend_key, public_view_key);
|
||||
|
||||
let _ = self
|
||||
.inner
|
||||
.generate_from_keys(
|
||||
&address.to_string(),
|
||||
&private_spend_key.to_string(),
|
||||
&PrivateKey::from(private_view_key).to_string(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
|
||||
// to `ConstantBackoff`.
|
||||
|
||||
#[async_trait]
|
||||
impl WatchForTransfer for Wallet {
|
||||
async fn watch_for_transfer(
|
||||
&self,
|
||||
public_spend_key: PublicKey,
|
||||
public_view_key: PublicViewKey,
|
||||
transfer_proof: TransferProof,
|
||||
expected_amount: Amount,
|
||||
expected_confirmations: u32,
|
||||
) -> Result<(), InsufficientFunds> {
|
||||
enum Error {
|
||||
TxNotFound,
|
||||
InsufficientConfirmations,
|
||||
InsufficientFunds { expected: Amount, actual: Amount },
|
||||
}
|
||||
|
||||
let address = Address::standard(self.network, public_spend_key, public_view_key.into());
|
||||
|
||||
let res = (|| async {
|
||||
// NOTE: Currently, this is conflating IO errors with the transaction not being
|
||||
// in the blockchain yet, or not having enough confirmations on it. All these
|
||||
// errors warrant a retry, but the strategy should probably differ per case
|
||||
let proof = self
|
||||
.inner
|
||||
.check_tx_key(
|
||||
&String::from(transfer_proof.tx_hash()),
|
||||
&transfer_proof.tx_key().to_string(),
|
||||
&address.to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| backoff::Error::Transient(Error::TxNotFound))?;
|
||||
|
||||
if proof.received != expected_amount.as_piconero() {
|
||||
return Err(backoff::Error::Permanent(Error::InsufficientFunds {
|
||||
expected: expected_amount,
|
||||
actual: Amount::from_piconero(proof.received),
|
||||
}));
|
||||
}
|
||||
|
||||
if proof.confirmations < expected_confirmations {
|
||||
return Err(backoff::Error::Transient(Error::InsufficientConfirmations));
|
||||
}
|
||||
|
||||
Ok(proof)
|
||||
})
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await;
|
||||
|
||||
if let Err(Error::InsufficientFunds { expected, actual }) = res {
|
||||
return Err(InsufficientFunds { expected, actual });
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
use crate::monero;
|
||||
use async_trait::async_trait;
|
||||
use futures::prelude::*;
|
||||
use libp2p::{
|
||||
|
@ -8,8 +9,10 @@ use serde::{Deserialize, Serialize};
|
|||
use std::{fmt::Debug, io, marker::PhantomData};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::SwapAmounts;
|
||||
use xmr_btc::{alice, bob, monero};
|
||||
use crate::{
|
||||
protocol::{alice, bob},
|
||||
SwapAmounts,
|
||||
};
|
||||
|
||||
/// Time to wait for a response back once we send a request.
|
||||
pub const TIMEOUT: u64 = 3600; // One hour.
|
||||
|
|
2
swap/src/protocol.rs
Normal file
2
swap/src/protocol.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod alice;
|
||||
pub mod bob;
|
|
@ -1,15 +1,6 @@
|
|||
//! Run an XMR/BTC swap in the role of Alice.
|
||||
//! Alice holds XMR and wishes receive BTC.
|
||||
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
|
||||
use crate::{
|
||||
network::{
|
||||
peer_tracker::{self, PeerTracker},
|
||||
request_response::AliceToBob,
|
||||
transport::SwapTransport,
|
||||
TokioExecutor,
|
||||
},
|
||||
SwapAmounts,
|
||||
};
|
||||
pub use self::{amounts::*, message0::*, message1::*, message2::*, message3::*, state::*};
|
||||
use anyhow::Result;
|
||||
use libp2p::{
|
||||
core::{identity::Keypair, Multiaddr},
|
||||
|
@ -17,7 +8,17 @@ use libp2p::{
|
|||
NetworkBehaviour, PeerId,
|
||||
};
|
||||
use tracing::{debug, info};
|
||||
use xmr_btc::bob;
|
||||
|
||||
use crate::{
|
||||
network::{
|
||||
peer_tracker::{self, PeerTracker},
|
||||
request_response::AliceToBob,
|
||||
transport::SwapTransport,
|
||||
TokioExecutor,
|
||||
},
|
||||
protocol::bob,
|
||||
SwapAmounts,
|
||||
};
|
||||
|
||||
mod amounts;
|
||||
pub mod event_loop;
|
||||
|
@ -25,6 +26,7 @@ mod message0;
|
|||
mod message1;
|
||||
mod message2;
|
||||
mod message3;
|
||||
pub mod state;
|
||||
mod steps;
|
||||
pub mod swap;
|
||||
|
||||
|
@ -133,10 +135,10 @@ impl From<message3::OutEvent> for OutEvent {
|
|||
pub struct Behaviour {
|
||||
pt: PeerTracker,
|
||||
amounts: Amounts,
|
||||
message0: Message0,
|
||||
message1: Message1,
|
||||
message2: Message2,
|
||||
message3: Message3,
|
||||
message0: Message0Behaviour,
|
||||
message1: Message1Behaviour,
|
||||
message2: Message2Behaviour,
|
||||
message3: Message3Behaviour,
|
||||
#[behaviour(ignore)]
|
||||
identity: Keypair,
|
||||
}
|
||||
|
@ -158,31 +160,19 @@ impl Behaviour {
|
|||
}
|
||||
|
||||
/// Send Message0 to Bob in response to receiving his Message0.
|
||||
pub fn send_message0(
|
||||
&mut self,
|
||||
channel: ResponseChannel<AliceToBob>,
|
||||
msg: xmr_btc::alice::Message0,
|
||||
) {
|
||||
pub fn send_message0(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message0) {
|
||||
self.message0.send(channel, msg);
|
||||
debug!("Sent Message0");
|
||||
}
|
||||
|
||||
/// Send Message1 to Bob in response to receiving his Message1.
|
||||
pub fn send_message1(
|
||||
&mut self,
|
||||
channel: ResponseChannel<AliceToBob>,
|
||||
msg: xmr_btc::alice::Message1,
|
||||
) {
|
||||
pub fn send_message1(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message1) {
|
||||
self.message1.send(channel, msg);
|
||||
debug!("Sent Message1");
|
||||
}
|
||||
|
||||
/// Send Message2 to Bob in response to receiving his Message2.
|
||||
pub fn send_message2(
|
||||
&mut self,
|
||||
channel: ResponseChannel<AliceToBob>,
|
||||
msg: xmr_btc::alice::Message2,
|
||||
) {
|
||||
pub fn send_message2(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message2) {
|
||||
self.message2.send(channel, msg);
|
||||
debug!("Sent Message2");
|
||||
}
|
||||
|
@ -195,10 +185,10 @@ impl Default for Behaviour {
|
|||
Self {
|
||||
pt: PeerTracker::default(),
|
||||
amounts: Amounts::default(),
|
||||
message0: Message0::default(),
|
||||
message1: Message1::default(),
|
||||
message2: Message2::default(),
|
||||
message3: Message3::default(),
|
||||
message0: Message0Behaviour::default(),
|
||||
message1: Message1Behaviour::default(),
|
||||
message2: Message2Behaviour::default(),
|
||||
message3: Message3Behaviour::default(),
|
||||
identity,
|
||||
}
|
||||
}
|
|
@ -14,8 +14,8 @@ use std::{
|
|||
use tracing::{debug, error};
|
||||
|
||||
use crate::{
|
||||
alice::amounts,
|
||||
network::request_response::{AliceToBob, AmountsProtocol, BobToAlice, Codec, TIMEOUT},
|
||||
protocol::alice::amounts,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
|
@ -1,15 +1,19 @@
|
|||
use crate::{
|
||||
alice::{Behaviour, OutEvent},
|
||||
network::{request_response::AliceToBob, transport::SwapTransport, TokioExecutor},
|
||||
SwapAmounts,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use futures::FutureExt;
|
||||
use libp2p::{
|
||||
core::Multiaddr, futures::StreamExt, request_response::ResponseChannel, PeerId, Swarm,
|
||||
};
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use xmr_btc::{alice, bob};
|
||||
|
||||
use crate::{
|
||||
network::{request_response::AliceToBob, transport::SwapTransport, TokioExecutor},
|
||||
protocol::{
|
||||
alice,
|
||||
alice::{Behaviour, OutEvent},
|
||||
bob,
|
||||
},
|
||||
SwapAmounts,
|
||||
};
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Channels<T> {
|
||||
|
@ -36,7 +40,7 @@ pub struct EventLoopHandle {
|
|||
msg1: Receiver<(bob::Message1, ResponseChannel<AliceToBob>)>,
|
||||
msg2: Receiver<(bob::Message2, ResponseChannel<AliceToBob>)>,
|
||||
msg3: Receiver<bob::Message3>,
|
||||
request: Receiver<crate::alice::amounts::OutEvent>,
|
||||
request: Receiver<crate::protocol::alice::amounts::OutEvent>,
|
||||
conn_established: Receiver<PeerId>,
|
||||
send_amounts: Sender<(ResponseChannel<AliceToBob>, SwapAmounts)>,
|
||||
send_msg0: Sender<(ResponseChannel<AliceToBob>, alice::Message0)>,
|
||||
|
@ -80,7 +84,7 @@ impl EventLoopHandle {
|
|||
.ok_or_else(|| anyhow!("Failed to receive Bitcoin encrypted signature from Bob"))
|
||||
}
|
||||
|
||||
pub async fn recv_request(&mut self) -> Result<crate::alice::amounts::OutEvent> {
|
||||
pub async fn recv_request(&mut self) -> Result<crate::protocol::alice::amounts::OutEvent> {
|
||||
self.request
|
||||
.recv()
|
||||
.await
|
||||
|
@ -131,7 +135,7 @@ pub struct EventLoop {
|
|||
msg1: Sender<(bob::Message1, ResponseChannel<AliceToBob>)>,
|
||||
msg2: Sender<(bob::Message2, ResponseChannel<AliceToBob>)>,
|
||||
msg3: Sender<bob::Message3>,
|
||||
request: Sender<crate::alice::amounts::OutEvent>,
|
||||
request: Sender<crate::protocol::alice::amounts::OutEvent>,
|
||||
conn_established: Sender<PeerId>,
|
||||
send_amounts: Receiver<(ResponseChannel<AliceToBob>, SwapAmounts)>,
|
||||
send_msg0: Receiver<(ResponseChannel<AliceToBob>, alice::Message0)>,
|
|
@ -1,11 +1,12 @@
|
|||
use libp2p::{
|
||||
request_response::{
|
||||
handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig,
|
||||
RequestResponseEvent, RequestResponseMessage,
|
||||
RequestResponseEvent, RequestResponseMessage, ResponseChannel,
|
||||
},
|
||||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||
NetworkBehaviour,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
task::{Context, Poll},
|
||||
|
@ -13,9 +14,11 @@ use std::{
|
|||
};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message0Protocol, TIMEOUT};
|
||||
use libp2p::request_response::ResponseChannel;
|
||||
use xmr_btc::bob;
|
||||
use crate::{
|
||||
bitcoin, monero,
|
||||
network::request_response::{AliceToBob, BobToAlice, Codec, Message0Protocol, TIMEOUT},
|
||||
protocol::bob,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OutEvent {
|
||||
|
@ -25,18 +28,29 @@ pub enum OutEvent {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message0 {
|
||||
pub(crate) A: bitcoin::PublicKey,
|
||||
pub(crate) S_a_monero: monero::PublicKey,
|
||||
pub(crate) S_a_bitcoin: bitcoin::PublicKey,
|
||||
pub(crate) dleq_proof_s_a: cross_curve_dleq::Proof,
|
||||
pub(crate) v_a: monero::PrivateViewKey,
|
||||
pub(crate) redeem_address: bitcoin::Address,
|
||||
pub(crate) punish_address: bitcoin::Address,
|
||||
}
|
||||
|
||||
/// A `NetworkBehaviour` that represents send/recv of message 0.
|
||||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Message0 {
|
||||
pub struct Message0Behaviour {
|
||||
rr: RequestResponse<Codec<Message0Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message0 {
|
||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: xmr_btc::alice::Message0) {
|
||||
impl Message0Behaviour {
|
||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message0) {
|
||||
let msg = AliceToBob::Message0(Box::new(msg));
|
||||
self.rr.send_response(channel, msg);
|
||||
}
|
||||
|
@ -53,7 +67,7 @@ impl Message0 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Message0 {
|
||||
impl Default for Message0Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
|
@ -70,7 +84,9 @@ impl Default for Message0 {
|
|||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message0 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
|
||||
for Message0Behaviour
|
||||
{
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
|
@ -1,3 +1,4 @@
|
|||
use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
|
||||
use libp2p::{
|
||||
request_response::{
|
||||
handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig,
|
||||
|
@ -6,6 +7,7 @@ use libp2p::{
|
|||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||
NetworkBehaviour,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
task::{Context, Poll},
|
||||
|
@ -13,8 +15,10 @@ use std::{
|
|||
};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message1Protocol, TIMEOUT};
|
||||
use xmr_btc::bob;
|
||||
use crate::{
|
||||
network::request_response::{AliceToBob, BobToAlice, Codec, Message1Protocol, TIMEOUT},
|
||||
protocol::bob,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OutEvent {
|
||||
|
@ -26,18 +30,24 @@ pub enum OutEvent {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message1 {
|
||||
pub(crate) tx_cancel_sig: Signature,
|
||||
pub(crate) tx_refund_encsig: EncryptedSignature,
|
||||
}
|
||||
|
||||
/// A `NetworkBehaviour` that represents send/recv of message 1.
|
||||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Message1 {
|
||||
pub struct Message1Behaviour {
|
||||
rr: RequestResponse<Codec<Message1Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message1 {
|
||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: xmr_btc::alice::Message1) {
|
||||
impl Message1Behaviour {
|
||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message1) {
|
||||
let msg = AliceToBob::Message1(Box::new(msg));
|
||||
self.rr.send_response(channel, msg);
|
||||
}
|
||||
|
@ -55,7 +65,7 @@ impl Message1 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Message1 {
|
||||
impl Default for Message1Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
|
@ -72,7 +82,9 @@ impl Default for Message1 {
|
|||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message1 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
|
||||
for Message1Behaviour
|
||||
{
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
|
@ -6,6 +6,7 @@ use libp2p::{
|
|||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||
NetworkBehaviour,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
task::{Context, Poll},
|
||||
|
@ -13,8 +14,11 @@ use std::{
|
|||
};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message2Protocol, TIMEOUT};
|
||||
use xmr_btc::bob;
|
||||
use crate::{
|
||||
monero,
|
||||
network::request_response::{AliceToBob, BobToAlice, Codec, Message2Protocol, TIMEOUT},
|
||||
protocol::bob,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OutEvent {
|
||||
|
@ -26,18 +30,23 @@ pub enum OutEvent {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message2 {
|
||||
pub tx_lock_proof: monero::TransferProof,
|
||||
}
|
||||
|
||||
/// A `NetworkBehaviour` that represents receiving of message 2 from Bob.
|
||||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Message2 {
|
||||
pub struct Message2Behaviour {
|
||||
rr: RequestResponse<Codec<Message2Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message2 {
|
||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: xmr_btc::alice::Message2) {
|
||||
impl Message2Behaviour {
|
||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message2) {
|
||||
let msg = AliceToBob::Message2(msg);
|
||||
self.rr.send_response(channel, msg);
|
||||
}
|
||||
|
@ -55,7 +64,7 @@ impl Message2 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Message2 {
|
||||
impl Default for Message2Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
|
@ -72,7 +81,9 @@ impl Default for Message2 {
|
|||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message2 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
|
||||
for Message2Behaviour
|
||||
{
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
|
@ -13,8 +13,10 @@ use std::{
|
|||
};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message3Protocol, TIMEOUT};
|
||||
use xmr_btc::bob;
|
||||
use crate::{
|
||||
network::request_response::{AliceToBob, BobToAlice, Codec, Message3Protocol, TIMEOUT},
|
||||
protocol::bob,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OutEvent {
|
||||
|
@ -25,13 +27,13 @@ pub enum OutEvent {
|
|||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Message3 {
|
||||
pub struct Message3Behaviour {
|
||||
rr: RequestResponse<Codec<Message3Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message3 {
|
||||
impl Message3Behaviour {
|
||||
fn poll(
|
||||
&mut self,
|
||||
_: &mut Context<'_>,
|
||||
|
@ -45,7 +47,7 @@ impl Message3 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Message3 {
|
||||
impl Default for Message3Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
|
@ -62,7 +64,9 @@ impl Default for Message3 {
|
|||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message3 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
|
||||
for Message3Behaviour
|
||||
{
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
531
swap/src/protocol/alice/state.rs
Normal file
531
swap/src/protocol/alice/state.rs
Normal file
|
@ -0,0 +1,531 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use ecdsa_fun::{
|
||||
adaptor::{Adaptor, EncryptedSignature},
|
||||
nonce::Deterministic,
|
||||
};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
bitcoin,
|
||||
bitcoin::{
|
||||
current_epoch, timelocks::Timelock, wait_for_cancel_timelock_to_expire, GetBlockHeight,
|
||||
TransactionBlockHeight, WatchForRawTransaction,
|
||||
},
|
||||
monero,
|
||||
monero::CreateWalletForOutput,
|
||||
protocol::{alice, bob},
|
||||
ExpiredTimelocks,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct State0 {
|
||||
pub a: bitcoin::SecretKey,
|
||||
pub s_a: cross_curve_dleq::Scalar,
|
||||
pub v_a: monero::PrivateViewKey,
|
||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||
pub btc: bitcoin::Amount,
|
||||
pub xmr: monero::Amount,
|
||||
pub cancel_timelock: Timelock,
|
||||
pub punish_timelock: Timelock,
|
||||
pub redeem_address: bitcoin::Address,
|
||||
pub punish_address: bitcoin::Address,
|
||||
}
|
||||
|
||||
impl State0 {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
a: bitcoin::SecretKey,
|
||||
s_a: cross_curve_dleq::Scalar,
|
||||
v_a: monero::PrivateViewKey,
|
||||
btc: bitcoin::Amount,
|
||||
xmr: monero::Amount,
|
||||
cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
redeem_address: bitcoin::Address,
|
||||
punish_address: bitcoin::Address,
|
||||
) -> Self {
|
||||
Self {
|
||||
a,
|
||||
s_a,
|
||||
v_a,
|
||||
redeem_address,
|
||||
punish_address,
|
||||
btc,
|
||||
xmr,
|
||||
cancel_timelock,
|
||||
punish_timelock,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_message<R: RngCore + CryptoRng>(&self, rng: &mut R) -> alice::Message0 {
|
||||
info!("Producing first message");
|
||||
let dleq_proof_s_a = cross_curve_dleq::Proof::new(rng, &self.s_a);
|
||||
|
||||
alice::Message0 {
|
||||
A: self.a.public(),
|
||||
S_a_monero: monero::PublicKey::from_private_key(&monero::PrivateKey {
|
||||
scalar: self.s_a.into_ed25519(),
|
||||
}),
|
||||
S_a_bitcoin: self.s_a.into_secp256k1().into(),
|
||||
dleq_proof_s_a,
|
||||
v_a: self.v_a,
|
||||
redeem_address: self.redeem_address.clone(),
|
||||
punish_address: self.punish_address.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive(self, msg: bob::Message0) -> Result<State1> {
|
||||
msg.dleq_proof_s_b.verify(
|
||||
msg.S_b_bitcoin.clone().into(),
|
||||
msg.S_b_monero
|
||||
.point
|
||||
.decompress()
|
||||
.ok_or_else(|| anyhow!("S_b is not a monero curve point"))?,
|
||||
)?;
|
||||
|
||||
let v = self.v_a + msg.v_b;
|
||||
|
||||
Ok(State1 {
|
||||
a: self.a,
|
||||
B: msg.B,
|
||||
s_a: self.s_a,
|
||||
S_b_monero: msg.S_b_monero,
|
||||
S_b_bitcoin: msg.S_b_bitcoin,
|
||||
v,
|
||||
btc: self.btc,
|
||||
xmr: self.xmr,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: msg.refund_address,
|
||||
redeem_address: self.redeem_address,
|
||||
punish_address: self.punish_address,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct State1 {
|
||||
a: bitcoin::SecretKey,
|
||||
B: bitcoin::PublicKey,
|
||||
s_a: cross_curve_dleq::Scalar,
|
||||
S_b_monero: monero::PublicKey,
|
||||
S_b_bitcoin: bitcoin::PublicKey,
|
||||
v: monero::PrivateViewKey,
|
||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||
btc: bitcoin::Amount,
|
||||
xmr: monero::Amount,
|
||||
cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
refund_address: bitcoin::Address,
|
||||
redeem_address: bitcoin::Address,
|
||||
punish_address: bitcoin::Address,
|
||||
}
|
||||
|
||||
impl State1 {
|
||||
pub fn receive(self, msg: bob::Message1) -> State2 {
|
||||
State2 {
|
||||
a: self.a,
|
||||
B: self.B,
|
||||
s_a: self.s_a,
|
||||
S_b_monero: self.S_b_monero,
|
||||
S_b_bitcoin: self.S_b_bitcoin,
|
||||
v: self.v,
|
||||
btc: self.btc,
|
||||
xmr: self.xmr,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address,
|
||||
redeem_address: self.redeem_address,
|
||||
punish_address: self.punish_address,
|
||||
tx_lock: msg.tx_lock,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct State2 {
|
||||
a: bitcoin::SecretKey,
|
||||
B: bitcoin::PublicKey,
|
||||
s_a: cross_curve_dleq::Scalar,
|
||||
S_b_monero: monero::PublicKey,
|
||||
S_b_bitcoin: bitcoin::PublicKey,
|
||||
v: monero::PrivateViewKey,
|
||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||
btc: bitcoin::Amount,
|
||||
xmr: monero::Amount,
|
||||
cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
refund_address: bitcoin::Address,
|
||||
redeem_address: bitcoin::Address,
|
||||
punish_address: bitcoin::Address,
|
||||
tx_lock: bitcoin::TxLock,
|
||||
}
|
||||
|
||||
impl State2 {
|
||||
pub fn next_message(&self) -> alice::Message1 {
|
||||
let tx_cancel =
|
||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B);
|
||||
|
||||
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
|
||||
// Alice encsigns the refund transaction(bitcoin) digest with Bob's monero
|
||||
// pubkey(S_b). The refund transaction spends the output of
|
||||
// tx_lock_bitcoin to Bob's refund address.
|
||||
// recover(encsign(a, S_b, d), sign(a, d), S_b) = s_b where d is a digest, (a,
|
||||
// A) is alice's keypair and (s_b, S_b) is bob's keypair.
|
||||
let tx_refund_encsig = self.a.encsign(self.S_b_bitcoin, tx_refund.digest());
|
||||
|
||||
let tx_cancel_sig = self.a.sign(tx_cancel.digest());
|
||||
alice::Message1 {
|
||||
tx_refund_encsig,
|
||||
tx_cancel_sig,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive(self, msg: bob::Message2) -> Result<State3> {
|
||||
let tx_cancel =
|
||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B);
|
||||
bitcoin::verify_sig(&self.B, &tx_cancel.digest(), &msg.tx_cancel_sig)?;
|
||||
let tx_punish =
|
||||
bitcoin::TxPunish::new(&tx_cancel, &self.punish_address, self.punish_timelock);
|
||||
bitcoin::verify_sig(&self.B, &tx_punish.digest(), &msg.tx_punish_sig)?;
|
||||
|
||||
Ok(State3 {
|
||||
a: self.a,
|
||||
B: self.B,
|
||||
s_a: self.s_a,
|
||||
S_b_monero: self.S_b_monero,
|
||||
S_b_bitcoin: self.S_b_bitcoin,
|
||||
v: self.v,
|
||||
// TODO(Franck): Review if these amounts are actually needed
|
||||
btc: self.btc,
|
||||
xmr: self.xmr,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address,
|
||||
redeem_address: self.redeem_address,
|
||||
punish_address: self.punish_address,
|
||||
tx_lock: self.tx_lock,
|
||||
tx_punish_sig_bob: msg.tx_punish_sig,
|
||||
tx_cancel_sig_bob: msg.tx_cancel_sig,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct State3 {
|
||||
pub a: bitcoin::SecretKey,
|
||||
pub B: bitcoin::PublicKey,
|
||||
pub s_a: cross_curve_dleq::Scalar,
|
||||
pub S_b_monero: monero::PublicKey,
|
||||
pub S_b_bitcoin: bitcoin::PublicKey,
|
||||
pub v: monero::PrivateViewKey,
|
||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||
pub btc: bitcoin::Amount,
|
||||
pub xmr: monero::Amount,
|
||||
pub cancel_timelock: Timelock,
|
||||
pub punish_timelock: Timelock,
|
||||
pub refund_address: bitcoin::Address,
|
||||
pub redeem_address: bitcoin::Address,
|
||||
pub punish_address: bitcoin::Address,
|
||||
pub tx_lock: bitcoin::TxLock,
|
||||
pub tx_punish_sig_bob: bitcoin::Signature,
|
||||
pub tx_cancel_sig_bob: bitcoin::Signature,
|
||||
}
|
||||
|
||||
impl State3 {
|
||||
pub async fn watch_for_lock_btc<W>(self, bitcoin_wallet: &W) -> Result<State4>
|
||||
where
|
||||
W: bitcoin::WatchForRawTransaction,
|
||||
{
|
||||
tracing::info!("watching for lock btc with txid: {}", self.tx_lock.txid());
|
||||
let tx = bitcoin_wallet
|
||||
.watch_for_raw_transaction(self.tx_lock.txid())
|
||||
.await;
|
||||
|
||||
tracing::info!("tx lock seen with txid: {}", tx.txid());
|
||||
|
||||
Ok(State4 {
|
||||
a: self.a,
|
||||
B: self.B,
|
||||
s_a: self.s_a,
|
||||
S_b_monero: self.S_b_monero,
|
||||
S_b_bitcoin: self.S_b_bitcoin,
|
||||
v: self.v,
|
||||
btc: self.btc,
|
||||
xmr: self.xmr,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address,
|
||||
redeem_address: self.redeem_address,
|
||||
punish_address: self.punish_address,
|
||||
tx_lock: self.tx_lock,
|
||||
tx_punish_sig_bob: self.tx_punish_sig_bob,
|
||||
tx_cancel_sig_bob: self.tx_cancel_sig_bob,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn wait_for_cancel_timelock_to_expire<W>(&self, bitcoin_wallet: &W) -> Result<()>
|
||||
where
|
||||
W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight,
|
||||
{
|
||||
wait_for_cancel_timelock_to_expire(
|
||||
bitcoin_wallet,
|
||||
self.cancel_timelock,
|
||||
self.tx_lock.txid(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn expired_timelocks<W>(&self, bitcoin_wallet: &W) -> Result<ExpiredTimelocks>
|
||||
where
|
||||
W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight,
|
||||
{
|
||||
current_epoch(
|
||||
bitcoin_wallet,
|
||||
self.cancel_timelock,
|
||||
self.punish_timelock,
|
||||
self.tx_lock.txid(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct State4 {
|
||||
a: bitcoin::SecretKey,
|
||||
B: bitcoin::PublicKey,
|
||||
s_a: cross_curve_dleq::Scalar,
|
||||
S_b_monero: monero::PublicKey,
|
||||
S_b_bitcoin: bitcoin::PublicKey,
|
||||
v: monero::PrivateViewKey,
|
||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||
btc: bitcoin::Amount,
|
||||
xmr: monero::Amount,
|
||||
cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
refund_address: bitcoin::Address,
|
||||
redeem_address: bitcoin::Address,
|
||||
punish_address: bitcoin::Address,
|
||||
tx_lock: bitcoin::TxLock,
|
||||
tx_punish_sig_bob: bitcoin::Signature,
|
||||
tx_cancel_sig_bob: bitcoin::Signature,
|
||||
}
|
||||
|
||||
impl State4 {
|
||||
pub async fn lock_xmr<W>(self, monero_wallet: &W) -> Result<State5>
|
||||
where
|
||||
W: monero::Transfer,
|
||||
{
|
||||
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey {
|
||||
scalar: self.s_a.into_ed25519(),
|
||||
});
|
||||
let S_b = self.S_b_monero;
|
||||
|
||||
let (tx_lock_proof, fee) = monero_wallet
|
||||
.transfer(S_a + S_b, self.v.public(), self.xmr)
|
||||
.await?;
|
||||
|
||||
Ok(State5 {
|
||||
a: self.a,
|
||||
B: self.B,
|
||||
s_a: self.s_a,
|
||||
S_b_monero: self.S_b_monero,
|
||||
S_b_bitcoin: self.S_b_bitcoin,
|
||||
v: self.v,
|
||||
btc: self.btc,
|
||||
xmr: self.xmr,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address,
|
||||
redeem_address: self.redeem_address,
|
||||
punish_address: self.punish_address,
|
||||
tx_lock: self.tx_lock,
|
||||
tx_lock_proof,
|
||||
tx_punish_sig_bob: self.tx_punish_sig_bob,
|
||||
tx_cancel_sig_bob: self.tx_cancel_sig_bob,
|
||||
lock_xmr_fee: fee,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn punish<W: bitcoin::BroadcastSignedTransaction>(
|
||||
&self,
|
||||
bitcoin_wallet: &W,
|
||||
) -> Result<()> {
|
||||
let tx_cancel =
|
||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B);
|
||||
let tx_punish =
|
||||
bitcoin::TxPunish::new(&tx_cancel, &self.punish_address, self.punish_timelock);
|
||||
|
||||
{
|
||||
let sig_a = self.a.sign(tx_cancel.digest());
|
||||
let sig_b = self.tx_cancel_sig_bob.clone();
|
||||
|
||||
let signed_tx_cancel = tx_cancel.clone().add_signatures(
|
||||
&self.tx_lock,
|
||||
(self.a.public(), sig_a),
|
||||
(self.B, sig_b),
|
||||
)?;
|
||||
|
||||
let _ = bitcoin_wallet
|
||||
.broadcast_signed_transaction(signed_tx_cancel)
|
||||
.await?;
|
||||
}
|
||||
|
||||
{
|
||||
let sig_a = self.a.sign(tx_punish.digest());
|
||||
let sig_b = self.tx_punish_sig_bob.clone();
|
||||
|
||||
let signed_tx_punish =
|
||||
tx_punish.add_signatures(&tx_cancel, (self.a.public(), sig_a), (self.B, sig_b))?;
|
||||
|
||||
let _ = bitcoin_wallet
|
||||
.broadcast_signed_transaction(signed_tx_punish)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct State5 {
|
||||
a: bitcoin::SecretKey,
|
||||
B: bitcoin::PublicKey,
|
||||
s_a: cross_curve_dleq::Scalar,
|
||||
S_b_monero: monero::PublicKey,
|
||||
S_b_bitcoin: bitcoin::PublicKey,
|
||||
v: monero::PrivateViewKey,
|
||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||
btc: bitcoin::Amount,
|
||||
xmr: monero::Amount,
|
||||
cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
refund_address: bitcoin::Address,
|
||||
redeem_address: bitcoin::Address,
|
||||
punish_address: bitcoin::Address,
|
||||
tx_lock: bitcoin::TxLock,
|
||||
tx_lock_proof: monero::TransferProof,
|
||||
|
||||
tx_punish_sig_bob: bitcoin::Signature,
|
||||
|
||||
tx_cancel_sig_bob: bitcoin::Signature,
|
||||
lock_xmr_fee: monero::Amount,
|
||||
}
|
||||
|
||||
impl State5 {
|
||||
pub fn next_message(&self) -> alice::Message2 {
|
||||
alice::Message2 {
|
||||
tx_lock_proof: self.tx_lock_proof.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive(self, msg: bob::Message3) -> State6 {
|
||||
State6 {
|
||||
a: self.a,
|
||||
B: self.B,
|
||||
s_a: self.s_a,
|
||||
S_b_monero: self.S_b_monero,
|
||||
S_b_bitcoin: self.S_b_bitcoin,
|
||||
v: self.v,
|
||||
btc: self.btc,
|
||||
xmr: self.xmr,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address,
|
||||
redeem_address: self.redeem_address,
|
||||
punish_address: self.punish_address,
|
||||
tx_lock: self.tx_lock,
|
||||
tx_punish_sig_bob: self.tx_punish_sig_bob,
|
||||
tx_redeem_encsig: msg.tx_redeem_encsig,
|
||||
lock_xmr_fee: self.lock_xmr_fee,
|
||||
}
|
||||
}
|
||||
|
||||
// watch for refund on btc, recover s_b and refund xmr
|
||||
pub async fn refund_xmr<B, M>(self, bitcoin_wallet: &B, monero_wallet: &M) -> Result<()>
|
||||
where
|
||||
B: WatchForRawTransaction,
|
||||
M: CreateWalletForOutput,
|
||||
{
|
||||
let tx_cancel =
|
||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B);
|
||||
|
||||
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
|
||||
|
||||
let tx_refund_encsig = self.a.encsign(self.S_b_bitcoin, tx_refund.digest());
|
||||
|
||||
let tx_refund_candidate = bitcoin_wallet
|
||||
.watch_for_raw_transaction(tx_refund.txid())
|
||||
.await;
|
||||
|
||||
let tx_refund_sig =
|
||||
tx_refund.extract_signature_by_key(tx_refund_candidate, self.a.public())?;
|
||||
|
||||
let s_b = bitcoin::recover(self.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)?;
|
||||
let s_b = monero::private_key_from_secp256k1_scalar(s_b.into());
|
||||
|
||||
let s = s_b.scalar + self.s_a.into_ed25519();
|
||||
|
||||
// NOTE: This actually generates and opens a new wallet, closing the currently
|
||||
// open one.
|
||||
monero_wallet
|
||||
.create_and_load_wallet_for_output(monero::PrivateKey::from_scalar(s), self.v)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct State6 {
|
||||
a: bitcoin::SecretKey,
|
||||
B: bitcoin::PublicKey,
|
||||
s_a: cross_curve_dleq::Scalar,
|
||||
S_b_monero: monero::PublicKey,
|
||||
S_b_bitcoin: bitcoin::PublicKey,
|
||||
v: monero::PrivateViewKey,
|
||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||
btc: bitcoin::Amount,
|
||||
xmr: monero::Amount,
|
||||
cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
refund_address: bitcoin::Address,
|
||||
redeem_address: bitcoin::Address,
|
||||
punish_address: bitcoin::Address,
|
||||
tx_lock: bitcoin::TxLock,
|
||||
|
||||
tx_punish_sig_bob: bitcoin::Signature,
|
||||
tx_redeem_encsig: EncryptedSignature,
|
||||
lock_xmr_fee: monero::Amount,
|
||||
}
|
||||
|
||||
impl State6 {
|
||||
pub async fn redeem_btc<W: bitcoin::BroadcastSignedTransaction>(
|
||||
&self,
|
||||
bitcoin_wallet: &W,
|
||||
) -> Result<()> {
|
||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
||||
|
||||
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
|
||||
|
||||
let sig_a = self.a.sign(tx_redeem.digest());
|
||||
let sig_b =
|
||||
adaptor.decrypt_signature(&self.s_a.into_secp256k1(), self.tx_redeem_encsig.clone());
|
||||
|
||||
let sig_tx_redeem =
|
||||
tx_redeem.add_signatures(&self.tx_lock, (self.a.public(), sig_a), (self.B, sig_b))?;
|
||||
bitcoin_wallet
|
||||
.broadcast_signed_transaction(sig_tx_redeem)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn lock_xmr_fee(&self) -> monero::Amount {
|
||||
self.lock_xmr_fee
|
||||
}
|
||||
}
|
|
@ -1,7 +1,3 @@
|
|||
use crate::{
|
||||
alice::event_loop::EventLoopHandle, bitcoin, monero, network::request_response::AliceToBob,
|
||||
SwapAmounts,
|
||||
};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic};
|
||||
use futures::{
|
||||
|
@ -14,25 +10,30 @@ use sha2::Sha256;
|
|||
use std::{sync::Arc, time::Duration};
|
||||
use tokio::time::timeout;
|
||||
use tracing::{info, trace};
|
||||
use xmr_btc::{
|
||||
alice,
|
||||
alice::State3,
|
||||
|
||||
use crate::{
|
||||
bitcoin,
|
||||
bitcoin::{
|
||||
poll_until_block_height_is_gte, BlockHeight, BroadcastSignedTransaction,
|
||||
EncryptedSignature, GetBlockHeight, GetRawTransaction, Timelock, TransactionBlockHeight,
|
||||
TxCancel, TxLock, TxRefund, WaitForTransactionFinality, WatchForRawTransaction,
|
||||
poll_until_block_height_is_gte,
|
||||
timelocks::{BlockHeight, Timelock},
|
||||
BroadcastSignedTransaction, EncryptedSignature, GetBlockHeight, GetRawTransaction,
|
||||
TransactionBlockHeight, TxCancel, TxLock, TxRefund, WaitForTransactionFinality,
|
||||
WatchForRawTransaction,
|
||||
},
|
||||
config::Config,
|
||||
cross_curve_dleq,
|
||||
monero,
|
||||
monero::Transfer,
|
||||
network::request_response::AliceToBob,
|
||||
protocol::{alice, alice::event_loop::EventLoopHandle},
|
||||
SwapAmounts,
|
||||
};
|
||||
|
||||
pub async fn negotiate(
|
||||
state0: xmr_btc::alice::State0,
|
||||
state0: alice::State0,
|
||||
amounts: SwapAmounts,
|
||||
event_loop_handle: &mut EventLoopHandle,
|
||||
config: Config,
|
||||
) -> Result<(ResponseChannel<AliceToBob>, State3)> {
|
||||
) -> Result<(ResponseChannel<AliceToBob>, alice::State3)> {
|
||||
trace!("Starting negotiate");
|
||||
|
||||
// todo: we can move this out, we dont need to timeout here
|
||||
|
@ -115,7 +116,7 @@ where
|
|||
pub async fn lock_xmr<W>(
|
||||
channel: ResponseChannel<AliceToBob>,
|
||||
amounts: SwapAmounts,
|
||||
state3: State3,
|
||||
state3: alice::State3,
|
||||
event_loop_handle: &mut EventLoopHandle,
|
||||
monero_wallet: Arc<W>,
|
||||
) -> Result<()>
|
|
@ -1,20 +1,5 @@
|
|||
//! Run an XMR/BTC swap in the role of Alice.
|
||||
//! Alice holds XMR and wishes receive BTC.
|
||||
use crate::{
|
||||
alice::{
|
||||
event_loop::EventLoopHandle,
|
||||
steps::{
|
||||
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction,
|
||||
extract_monero_private_key, lock_xmr, negotiate, publish_bitcoin_punish_transaction,
|
||||
publish_bitcoin_redeem_transaction, publish_cancel_transaction,
|
||||
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin,
|
||||
},
|
||||
},
|
||||
bitcoin::EncryptedSignature,
|
||||
database::{Database, Swap},
|
||||
network::request_response::AliceToBob,
|
||||
SwapAmounts,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_recursion::async_recursion;
|
||||
use futures::{
|
||||
|
@ -26,12 +11,28 @@ use rand::{CryptoRng, RngCore};
|
|||
use std::{fmt, sync::Arc};
|
||||
use tracing::info;
|
||||
use uuid::Uuid;
|
||||
use xmr_btc::{
|
||||
alice::{State0, State3},
|
||||
bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction},
|
||||
|
||||
use crate::{
|
||||
bitcoin,
|
||||
bitcoin::{
|
||||
EncryptedSignature, TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction,
|
||||
},
|
||||
config::Config,
|
||||
database::{Database, Swap},
|
||||
monero,
|
||||
monero::CreateWalletForOutput,
|
||||
ExpiredTimelocks,
|
||||
network::request_response::AliceToBob,
|
||||
protocol::alice::{
|
||||
event_loop::EventLoopHandle,
|
||||
state::{State0, State3},
|
||||
steps::{
|
||||
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction,
|
||||
extract_monero_private_key, lock_xmr, negotiate, publish_bitcoin_punish_transaction,
|
||||
publish_bitcoin_redeem_transaction, publish_cancel_transaction,
|
||||
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin,
|
||||
},
|
||||
},
|
||||
ExpiredTimelocks, SwapAmounts,
|
||||
};
|
||||
|
||||
trait Rng: RngCore + CryptoRng + Send {}
|
||||
|
@ -105,8 +106,8 @@ impl fmt::Display for AliceState {
|
|||
pub async fn swap(
|
||||
state: AliceState,
|
||||
event_loop_handle: EventLoopHandle,
|
||||
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
|
||||
monero_wallet: Arc<crate::monero::Wallet>,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
monero_wallet: Arc<monero::Wallet>,
|
||||
config: Config,
|
||||
swap_id: Uuid,
|
||||
db: Database,
|
|
@ -1,24 +1,22 @@
|
|||
//! Run an XMR/BTC swap in the role of Bob.
|
||||
//! Bob holds BTC and wishes receive XMR.
|
||||
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
|
||||
use crate::{
|
||||
network::{
|
||||
peer_tracker::{self, PeerTracker},
|
||||
transport::SwapTransport,
|
||||
TokioExecutor,
|
||||
},
|
||||
SwapAmounts,
|
||||
};
|
||||
pub use self::{amounts::*, message0::*, message1::*, message2::*, message3::*, state::*};
|
||||
use anyhow::Result;
|
||||
use libp2p::{
|
||||
core::{identity::Keypair, Multiaddr},
|
||||
NetworkBehaviour, PeerId,
|
||||
};
|
||||
use tracing::{debug, info};
|
||||
use xmr_btc::{
|
||||
alice,
|
||||
|
||||
use crate::{
|
||||
bitcoin::EncryptedSignature,
|
||||
bob::{self},
|
||||
network::{
|
||||
peer_tracker::{self, PeerTracker},
|
||||
transport::SwapTransport,
|
||||
TokioExecutor,
|
||||
},
|
||||
protocol::{alice, bob},
|
||||
SwapAmounts,
|
||||
};
|
||||
|
||||
mod amounts;
|
||||
|
@ -27,6 +25,7 @@ mod message0;
|
|||
mod message1;
|
||||
mod message2;
|
||||
mod message3;
|
||||
pub mod state;
|
||||
pub mod swap;
|
||||
|
||||
pub type Swarm = libp2p::Swarm<Behaviour>;
|
||||
|
@ -112,10 +111,10 @@ impl From<message3::OutEvent> for OutEvent {
|
|||
pub struct Behaviour {
|
||||
pt: PeerTracker,
|
||||
amounts: Amounts,
|
||||
message0: Message0,
|
||||
message1: Message1,
|
||||
message2: Message2,
|
||||
message3: Message3,
|
||||
message0: Message0Behaviour,
|
||||
message1: Message1Behaviour,
|
||||
message2: Message2Behaviour,
|
||||
message3: Message3Behaviour,
|
||||
#[behaviour(ignore)]
|
||||
identity: Keypair,
|
||||
}
|
||||
|
@ -174,10 +173,10 @@ impl Default for Behaviour {
|
|||
Self {
|
||||
pt: PeerTracker::default(),
|
||||
amounts: Amounts::default(),
|
||||
message0: Message0::default(),
|
||||
message1: Message1::default(),
|
||||
message2: Message2::default(),
|
||||
message3: Message3::default(),
|
||||
message0: Message0Behaviour::default(),
|
||||
message1: Message1Behaviour::default(),
|
||||
message2: Message2Behaviour::default(),
|
||||
message3: Message3Behaviour::default(),
|
||||
identity,
|
||||
}
|
||||
}
|
|
@ -1,7 +1,3 @@
|
|||
use crate::{
|
||||
bob::{Behaviour, OutEvent},
|
||||
network::{transport::SwapTransport, TokioExecutor},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::FutureExt;
|
||||
use libp2p::{core::Multiaddr, PeerId};
|
||||
|
@ -10,7 +6,15 @@ use tokio::{
|
|||
sync::mpsc::{Receiver, Sender},
|
||||
};
|
||||
use tracing::{debug, error, info};
|
||||
use xmr_btc::{alice, bitcoin::EncryptedSignature, bob};
|
||||
|
||||
use crate::{
|
||||
bitcoin::EncryptedSignature,
|
||||
network::{transport::SwapTransport, TokioExecutor},
|
||||
protocol::{
|
||||
alice,
|
||||
bob::{self, Behaviour, OutEvent},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Channels<T> {
|
|
@ -6,6 +6,7 @@ use libp2p::{
|
|||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||
NetworkBehaviour, PeerId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
task::{Context, Poll},
|
||||
|
@ -13,8 +14,21 @@ use std::{
|
|||
};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message0Protocol, TIMEOUT};
|
||||
use xmr_btc::{alice, bob};
|
||||
use crate::{
|
||||
bitcoin, monero,
|
||||
network::request_response::{AliceToBob, BobToAlice, Codec, Message0Protocol, TIMEOUT},
|
||||
protocol::{alice, bob},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message0 {
|
||||
pub(crate) B: bitcoin::PublicKey,
|
||||
pub(crate) S_b_monero: monero::PublicKey,
|
||||
pub(crate) S_b_bitcoin: bitcoin::PublicKey,
|
||||
pub(crate) dleq_proof_s_b: cross_curve_dleq::Proof,
|
||||
pub(crate) v_b: monero::PrivateViewKey,
|
||||
pub(crate) refund_address: bitcoin::Address,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OutEvent {
|
||||
|
@ -25,13 +39,13 @@ pub enum OutEvent {
|
|||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Message0 {
|
||||
pub struct Message0Behaviour {
|
||||
rr: RequestResponse<Codec<Message0Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message0 {
|
||||
impl Message0Behaviour {
|
||||
pub fn send(&mut self, alice: PeerId, msg: bob::Message0) {
|
||||
let msg = BobToAlice::Message0(Box::new(msg));
|
||||
let _id = self.rr.send_request(&alice, msg);
|
||||
|
@ -50,7 +64,7 @@ impl Message0 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Message0 {
|
||||
impl Default for Message0Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
|
@ -67,7 +81,9 @@ impl Default for Message0 {
|
|||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message0 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
|
||||
for Message0Behaviour
|
||||
{
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
|
@ -6,15 +6,25 @@ use libp2p::{
|
|||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||
NetworkBehaviour, PeerId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message1Protocol, TIMEOUT};
|
||||
use xmr_btc::{alice, bob};
|
||||
use crate::{
|
||||
bitcoin,
|
||||
network::request_response::{AliceToBob, BobToAlice, Codec, Message1Protocol, TIMEOUT},
|
||||
protocol::alice,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message1 {
|
||||
pub(crate) tx_lock: bitcoin::TxLock,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OutEvent {
|
||||
|
@ -25,14 +35,14 @@ pub enum OutEvent {
|
|||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Message1 {
|
||||
pub struct Message1Behaviour {
|
||||
rr: RequestResponse<Codec<Message1Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message1 {
|
||||
pub fn send(&mut self, alice: PeerId, msg: bob::Message1) {
|
||||
impl Message1Behaviour {
|
||||
pub fn send(&mut self, alice: PeerId, msg: Message1) {
|
||||
let msg = BobToAlice::Message1(msg);
|
||||
let _id = self.rr.send_request(&alice, msg);
|
||||
}
|
||||
|
@ -50,7 +60,7 @@ impl Message1 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Message1 {
|
||||
impl Default for Message1Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
|
@ -67,7 +77,9 @@ impl Default for Message1 {
|
|||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message1 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
|
||||
for Message1Behaviour
|
||||
{
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
|
@ -1,3 +1,4 @@
|
|||
use ecdsa_fun::Signature;
|
||||
use libp2p::{
|
||||
request_response::{
|
||||
handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig,
|
||||
|
@ -6,15 +7,25 @@ use libp2p::{
|
|||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||
NetworkBehaviour, PeerId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message2Protocol, TIMEOUT};
|
||||
use xmr_btc::{alice, bob};
|
||||
use crate::{
|
||||
network::request_response::{AliceToBob, BobToAlice, Codec, Message2Protocol, TIMEOUT},
|
||||
protocol::alice,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message2 {
|
||||
pub(crate) tx_punish_sig: Signature,
|
||||
pub(crate) tx_cancel_sig: Signature,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OutEvent {
|
||||
|
@ -25,14 +36,14 @@ pub enum OutEvent {
|
|||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Message2 {
|
||||
pub struct Message2Behaviour {
|
||||
rr: RequestResponse<Codec<Message2Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message2 {
|
||||
pub fn send(&mut self, alice: PeerId, msg: bob::Message2) {
|
||||
impl Message2Behaviour {
|
||||
pub fn send(&mut self, alice: PeerId, msg: Message2) {
|
||||
let msg = BobToAlice::Message2(msg);
|
||||
let _id = self.rr.send_request(&alice, msg);
|
||||
}
|
||||
|
@ -50,7 +61,7 @@ impl Message2 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Message2 {
|
||||
impl Default for Message2Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
|
@ -67,7 +78,9 @@ impl Default for Message2 {
|
|||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message2 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
|
||||
for Message2Behaviour
|
||||
{
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
|
@ -6,6 +6,7 @@ use libp2p::{
|
|||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||
NetworkBehaviour, PeerId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
task::{Context, Poll},
|
||||
|
@ -13,8 +14,15 @@ use std::{
|
|||
};
|
||||
use tracing::error;
|
||||
|
||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message3Protocol, TIMEOUT};
|
||||
use xmr_btc::bob;
|
||||
use crate::{
|
||||
bitcoin::EncryptedSignature,
|
||||
network::request_response::{AliceToBob, BobToAlice, Codec, Message3Protocol, TIMEOUT},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message3 {
|
||||
pub tx_redeem_encsig: EncryptedSignature,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum OutEvent {
|
||||
|
@ -25,14 +33,14 @@ pub enum OutEvent {
|
|||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Message3 {
|
||||
pub struct Message3Behaviour {
|
||||
rr: RequestResponse<Codec<Message3Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message3 {
|
||||
pub fn send(&mut self, alice: PeerId, msg: bob::Message3) {
|
||||
impl Message3Behaviour {
|
||||
pub fn send(&mut self, alice: PeerId, msg: Message3) {
|
||||
let msg = BobToAlice::Message3(msg);
|
||||
let _id = self.rr.send_request(&alice, msg);
|
||||
}
|
||||
|
@ -50,7 +58,7 @@ impl Message3 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Message3 {
|
||||
impl Default for Message3Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
|
@ -67,7 +75,9 @@ impl Default for Message3 {
|
|||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message3 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
|
||||
for Message3Behaviour
|
||||
{
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
595
swap/src/protocol/bob/state.rs
Normal file
595
swap/src/protocol/bob/state.rs
Normal file
|
@ -0,0 +1,595 @@
|
|||
use ::bitcoin::{Transaction, Txid};
|
||||
use anyhow::{anyhow, Result};
|
||||
use ecdsa_fun::{
|
||||
adaptor::{Adaptor, EncryptedSignature},
|
||||
nonce::Deterministic,
|
||||
Signature,
|
||||
};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::{
|
||||
bitcoin::{
|
||||
self, current_epoch, timelocks::Timelock, wait_for_cancel_timelock_to_expire,
|
||||
BroadcastSignedTransaction, BuildTxLockPsbt, GetBlockHeight, GetRawTransaction, Network,
|
||||
TransactionBlockHeight, TxCancel, WatchForRawTransaction,
|
||||
},
|
||||
monero,
|
||||
protocol::{alice, bob},
|
||||
serde::monero_private_key,
|
||||
ExpiredTimelocks,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct State0 {
|
||||
b: bitcoin::SecretKey,
|
||||
s_b: cross_curve_dleq::Scalar,
|
||||
v_b: monero::PrivateViewKey,
|
||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||
btc: bitcoin::Amount,
|
||||
xmr: monero::Amount,
|
||||
cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
refund_address: bitcoin::Address,
|
||||
min_monero_confirmations: u32,
|
||||
}
|
||||
|
||||
impl State0 {
|
||||
pub fn new<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
btc: bitcoin::Amount,
|
||||
xmr: monero::Amount,
|
||||
cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
refund_address: bitcoin::Address,
|
||||
min_monero_confirmations: u32,
|
||||
) -> Self {
|
||||
let b = bitcoin::SecretKey::new_random(rng);
|
||||
|
||||
let s_b = cross_curve_dleq::Scalar::random(rng);
|
||||
let v_b = monero::PrivateViewKey::new_random(rng);
|
||||
|
||||
Self {
|
||||
b,
|
||||
s_b,
|
||||
v_b,
|
||||
btc,
|
||||
xmr,
|
||||
cancel_timelock,
|
||||
punish_timelock,
|
||||
refund_address,
|
||||
min_monero_confirmations,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_message<R: RngCore + CryptoRng>(&self, rng: &mut R) -> bob::Message0 {
|
||||
let dleq_proof_s_b = cross_curve_dleq::Proof::new(rng, &self.s_b);
|
||||
|
||||
bob::Message0 {
|
||||
B: self.b.public(),
|
||||
S_b_monero: monero::PublicKey::from_private_key(&monero::PrivateKey {
|
||||
scalar: self.s_b.into_ed25519(),
|
||||
}),
|
||||
S_b_bitcoin: self.s_b.into_secp256k1().into(),
|
||||
dleq_proof_s_b,
|
||||
v_b: self.v_b,
|
||||
refund_address: self.refund_address.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn receive<W>(self, wallet: &W, msg: alice::Message0) -> anyhow::Result<State1>
|
||||
where
|
||||
W: BuildTxLockPsbt + Network,
|
||||
{
|
||||
msg.dleq_proof_s_a.verify(
|
||||
msg.S_a_bitcoin.clone().into(),
|
||||
msg.S_a_monero
|
||||
.point
|
||||
.decompress()
|
||||
.ok_or_else(|| anyhow!("S_a is not a monero curve point"))?,
|
||||
)?;
|
||||
|
||||
let tx_lock = bitcoin::TxLock::new(wallet, self.btc, msg.A, self.b.public()).await?;
|
||||
let v = msg.v_a + self.v_b;
|
||||
|
||||
Ok(State1 {
|
||||
A: msg.A,
|
||||
b: self.b,
|
||||
s_b: self.s_b,
|
||||
S_a_monero: msg.S_a_monero,
|
||||
S_a_bitcoin: msg.S_a_bitcoin,
|
||||
v,
|
||||
btc: self.btc,
|
||||
xmr: self.xmr,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address,
|
||||
redeem_address: msg.redeem_address,
|
||||
punish_address: msg.punish_address,
|
||||
tx_lock,
|
||||
min_monero_confirmations: self.min_monero_confirmations,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct State1 {
|
||||
A: bitcoin::PublicKey,
|
||||
b: bitcoin::SecretKey,
|
||||
s_b: cross_curve_dleq::Scalar,
|
||||
S_a_monero: monero::PublicKey,
|
||||
S_a_bitcoin: bitcoin::PublicKey,
|
||||
v: monero::PrivateViewKey,
|
||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||
btc: bitcoin::Amount,
|
||||
xmr: monero::Amount,
|
||||
cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
refund_address: bitcoin::Address,
|
||||
redeem_address: bitcoin::Address,
|
||||
punish_address: bitcoin::Address,
|
||||
tx_lock: bitcoin::TxLock,
|
||||
min_monero_confirmations: u32,
|
||||
}
|
||||
|
||||
impl State1 {
|
||||
pub fn next_message(&self) -> bob::Message1 {
|
||||
bob::Message1 {
|
||||
tx_lock: self.tx_lock.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive(self, msg: alice::Message1) -> Result<State2> {
|
||||
let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
|
||||
|
||||
bitcoin::verify_sig(&self.A, &tx_cancel.digest(), &msg.tx_cancel_sig)?;
|
||||
bitcoin::verify_encsig(
|
||||
self.A,
|
||||
self.s_b.into_secp256k1().into(),
|
||||
&tx_refund.digest(),
|
||||
&msg.tx_refund_encsig,
|
||||
)?;
|
||||
|
||||
Ok(State2 {
|
||||
A: self.A,
|
||||
b: self.b,
|
||||
s_b: self.s_b,
|
||||
S_a_monero: self.S_a_monero,
|
||||
S_a_bitcoin: self.S_a_bitcoin,
|
||||
v: self.v,
|
||||
btc: self.btc,
|
||||
xmr: self.xmr,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address,
|
||||
redeem_address: self.redeem_address,
|
||||
punish_address: self.punish_address,
|
||||
tx_lock: self.tx_lock,
|
||||
tx_cancel_sig_a: msg.tx_cancel_sig,
|
||||
tx_refund_encsig: msg.tx_refund_encsig,
|
||||
min_monero_confirmations: self.min_monero_confirmations,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct State2 {
|
||||
pub A: bitcoin::PublicKey,
|
||||
pub b: bitcoin::SecretKey,
|
||||
pub s_b: cross_curve_dleq::Scalar,
|
||||
pub S_a_monero: monero::PublicKey,
|
||||
pub S_a_bitcoin: bitcoin::PublicKey,
|
||||
pub v: monero::PrivateViewKey,
|
||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||
btc: bitcoin::Amount,
|
||||
pub xmr: monero::Amount,
|
||||
pub cancel_timelock: Timelock,
|
||||
pub punish_timelock: Timelock,
|
||||
pub refund_address: bitcoin::Address,
|
||||
pub redeem_address: bitcoin::Address,
|
||||
pub punish_address: bitcoin::Address,
|
||||
pub tx_lock: bitcoin::TxLock,
|
||||
pub tx_cancel_sig_a: Signature,
|
||||
pub tx_refund_encsig: EncryptedSignature,
|
||||
pub min_monero_confirmations: u32,
|
||||
}
|
||||
|
||||
impl State2 {
|
||||
pub fn next_message(&self) -> bob::Message2 {
|
||||
let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||
let tx_cancel_sig = self.b.sign(tx_cancel.digest());
|
||||
let tx_punish =
|
||||
bitcoin::TxPunish::new(&tx_cancel, &self.punish_address, self.punish_timelock);
|
||||
let tx_punish_sig = self.b.sign(tx_punish.digest());
|
||||
|
||||
bob::Message2 {
|
||||
tx_punish_sig,
|
||||
tx_cancel_sig,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn lock_btc<W>(self, bitcoin_wallet: &W) -> Result<State3>
|
||||
where
|
||||
W: bitcoin::SignTxLock + bitcoin::BroadcastSignedTransaction,
|
||||
{
|
||||
let signed_tx_lock = bitcoin_wallet.sign_tx_lock(self.tx_lock.clone()).await?;
|
||||
|
||||
tracing::info!("{}", self.tx_lock.txid());
|
||||
let _ = bitcoin_wallet
|
||||
.broadcast_signed_transaction(signed_tx_lock)
|
||||
.await?;
|
||||
|
||||
Ok(State3 {
|
||||
A: self.A,
|
||||
b: self.b,
|
||||
s_b: self.s_b,
|
||||
S_a_monero: self.S_a_monero,
|
||||
S_a_bitcoin: self.S_a_bitcoin,
|
||||
v: self.v,
|
||||
btc: self.btc,
|
||||
xmr: self.xmr,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address,
|
||||
redeem_address: self.redeem_address,
|
||||
punish_address: self.punish_address,
|
||||
tx_lock: self.tx_lock,
|
||||
tx_cancel_sig_a: self.tx_cancel_sig_a,
|
||||
tx_refund_encsig: self.tx_refund_encsig,
|
||||
min_monero_confirmations: self.min_monero_confirmations,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct State3 {
|
||||
pub A: bitcoin::PublicKey,
|
||||
pub b: bitcoin::SecretKey,
|
||||
pub s_b: cross_curve_dleq::Scalar,
|
||||
S_a_monero: monero::PublicKey,
|
||||
S_a_bitcoin: bitcoin::PublicKey,
|
||||
v: monero::PrivateViewKey,
|
||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||
btc: bitcoin::Amount,
|
||||
xmr: monero::Amount,
|
||||
pub cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
pub refund_address: bitcoin::Address,
|
||||
redeem_address: bitcoin::Address,
|
||||
punish_address: bitcoin::Address,
|
||||
pub tx_lock: bitcoin::TxLock,
|
||||
pub tx_cancel_sig_a: Signature,
|
||||
pub tx_refund_encsig: EncryptedSignature,
|
||||
pub min_monero_confirmations: u32,
|
||||
}
|
||||
|
||||
impl State3 {
|
||||
pub async fn watch_for_lock_xmr<W>(self, xmr_wallet: &W, msg: alice::Message2) -> Result<State4>
|
||||
where
|
||||
W: monero::WatchForTransfer,
|
||||
{
|
||||
let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(
|
||||
self.s_b.into_ed25519(),
|
||||
));
|
||||
let S = self.S_a_monero + S_b_monero;
|
||||
|
||||
xmr_wallet
|
||||
.watch_for_transfer(
|
||||
S,
|
||||
self.v.public(),
|
||||
msg.tx_lock_proof,
|
||||
self.xmr,
|
||||
self.min_monero_confirmations,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(State4 {
|
||||
A: self.A,
|
||||
b: self.b,
|
||||
s_b: self.s_b,
|
||||
S_a_monero: self.S_a_monero,
|
||||
S_a_bitcoin: self.S_a_bitcoin,
|
||||
v: self.v,
|
||||
btc: self.btc,
|
||||
xmr: self.xmr,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address,
|
||||
redeem_address: self.redeem_address,
|
||||
punish_address: self.punish_address,
|
||||
tx_lock: self.tx_lock,
|
||||
tx_cancel_sig_a: self.tx_cancel_sig_a,
|
||||
tx_refund_encsig: self.tx_refund_encsig,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn wait_for_cancel_timelock_to_expire<W>(&self, bitcoin_wallet: &W) -> Result<()>
|
||||
where
|
||||
W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight,
|
||||
{
|
||||
wait_for_cancel_timelock_to_expire(
|
||||
bitcoin_wallet,
|
||||
self.cancel_timelock,
|
||||
self.tx_lock.txid(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn state4(&self) -> State4 {
|
||||
State4 {
|
||||
A: self.A,
|
||||
b: self.b.clone(),
|
||||
s_b: self.s_b,
|
||||
S_a_monero: self.S_a_monero,
|
||||
S_a_bitcoin: self.S_a_bitcoin,
|
||||
v: self.v,
|
||||
btc: self.btc,
|
||||
xmr: self.xmr,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address.clone(),
|
||||
redeem_address: self.redeem_address.clone(),
|
||||
punish_address: self.punish_address.clone(),
|
||||
tx_lock: self.tx_lock.clone(),
|
||||
tx_cancel_sig_a: self.tx_cancel_sig_a.clone(),
|
||||
tx_refund_encsig: self.tx_refund_encsig.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tx_lock_id(&self) -> bitcoin::Txid {
|
||||
self.tx_lock.txid()
|
||||
}
|
||||
|
||||
pub async fn current_epoch<W>(&self, bitcoin_wallet: &W) -> Result<ExpiredTimelocks>
|
||||
where
|
||||
W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight,
|
||||
{
|
||||
current_epoch(
|
||||
bitcoin_wallet,
|
||||
self.cancel_timelock,
|
||||
self.punish_timelock,
|
||||
self.tx_lock.txid(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct State4 {
|
||||
pub A: bitcoin::PublicKey,
|
||||
pub b: bitcoin::SecretKey,
|
||||
pub s_b: cross_curve_dleq::Scalar,
|
||||
S_a_monero: monero::PublicKey,
|
||||
pub S_a_bitcoin: bitcoin::PublicKey,
|
||||
v: monero::PrivateViewKey,
|
||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||
btc: bitcoin::Amount,
|
||||
xmr: monero::Amount,
|
||||
pub cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
pub refund_address: bitcoin::Address,
|
||||
pub redeem_address: bitcoin::Address,
|
||||
punish_address: bitcoin::Address,
|
||||
pub tx_lock: bitcoin::TxLock,
|
||||
pub tx_cancel_sig_a: Signature,
|
||||
pub tx_refund_encsig: EncryptedSignature,
|
||||
}
|
||||
|
||||
impl State4 {
|
||||
pub fn next_message(&self) -> bob::Message3 {
|
||||
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
|
||||
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest());
|
||||
|
||||
bob::Message3 { tx_redeem_encsig }
|
||||
}
|
||||
|
||||
pub fn tx_redeem_encsig(&self) -> EncryptedSignature {
|
||||
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
|
||||
self.b.encsign(self.S_a_bitcoin, tx_redeem.digest())
|
||||
}
|
||||
|
||||
pub async fn check_for_tx_cancel<W>(&self, bitcoin_wallet: &W) -> Result<Transaction>
|
||||
where
|
||||
W: GetRawTransaction,
|
||||
{
|
||||
let tx_cancel =
|
||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||
|
||||
let sig_a = self.tx_cancel_sig_a.clone();
|
||||
let sig_b = self.b.sign(tx_cancel.digest());
|
||||
|
||||
let tx_cancel = tx_cancel
|
||||
.clone()
|
||||
.add_signatures(&self.tx_lock, (self.A, sig_a), (self.b.public(), sig_b))
|
||||
.expect(
|
||||
"sig_{a,b} to be valid signatures for
|
||||
tx_cancel",
|
||||
);
|
||||
|
||||
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
pub async fn submit_tx_cancel<W>(&self, bitcoin_wallet: &W) -> Result<Txid>
|
||||
where
|
||||
W: BroadcastSignedTransaction,
|
||||
{
|
||||
let tx_cancel =
|
||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||
|
||||
let sig_a = self.tx_cancel_sig_a.clone();
|
||||
let sig_b = self.b.sign(tx_cancel.digest());
|
||||
|
||||
let tx_cancel = tx_cancel
|
||||
.clone()
|
||||
.add_signatures(&self.tx_lock, (self.A, sig_a), (self.b.public(), sig_b))
|
||||
.expect(
|
||||
"sig_{a,b} to be valid signatures for
|
||||
tx_cancel",
|
||||
);
|
||||
|
||||
let tx_id = bitcoin_wallet
|
||||
.broadcast_signed_transaction(tx_cancel)
|
||||
.await?;
|
||||
Ok(tx_id)
|
||||
}
|
||||
|
||||
pub async fn watch_for_redeem_btc<W>(&self, bitcoin_wallet: &W) -> Result<State5>
|
||||
where
|
||||
W: WatchForRawTransaction,
|
||||
{
|
||||
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
|
||||
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest());
|
||||
|
||||
let tx_redeem_candidate = bitcoin_wallet
|
||||
.watch_for_raw_transaction(tx_redeem.txid())
|
||||
.await;
|
||||
|
||||
let tx_redeem_sig =
|
||||
tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?;
|
||||
let s_a = bitcoin::recover(self.S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)?;
|
||||
let s_a = monero::private_key_from_secp256k1_scalar(s_a.into());
|
||||
|
||||
Ok(State5 {
|
||||
A: self.A,
|
||||
b: self.b.clone(),
|
||||
s_a,
|
||||
s_b: self.s_b,
|
||||
S_a_monero: self.S_a_monero,
|
||||
S_a_bitcoin: self.S_a_bitcoin,
|
||||
v: self.v,
|
||||
btc: self.btc,
|
||||
xmr: self.xmr,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address.clone(),
|
||||
redeem_address: self.redeem_address.clone(),
|
||||
punish_address: self.punish_address.clone(),
|
||||
tx_lock: self.tx_lock.clone(),
|
||||
tx_refund_encsig: self.tx_refund_encsig.clone(),
|
||||
tx_cancel_sig: self.tx_cancel_sig_a.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn wait_for_cancel_timelock_to_expire<W>(&self, bitcoin_wallet: &W) -> Result<()>
|
||||
where
|
||||
W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight,
|
||||
{
|
||||
wait_for_cancel_timelock_to_expire(
|
||||
bitcoin_wallet,
|
||||
self.cancel_timelock,
|
||||
self.tx_lock.txid(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn expired_timelock<W>(&self, bitcoin_wallet: &W) -> Result<ExpiredTimelocks>
|
||||
where
|
||||
W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight,
|
||||
{
|
||||
current_epoch(
|
||||
bitcoin_wallet,
|
||||
self.cancel_timelock,
|
||||
self.punish_timelock,
|
||||
self.tx_lock.txid(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn refund_btc<W: bitcoin::BroadcastSignedTransaction>(
|
||||
&self,
|
||||
bitcoin_wallet: &W,
|
||||
) -> Result<()> {
|
||||
let tx_cancel =
|
||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
|
||||
|
||||
{
|
||||
let sig_b = self.b.sign(tx_cancel.digest());
|
||||
let sig_a = self.tx_cancel_sig_a.clone();
|
||||
|
||||
let signed_tx_cancel = tx_cancel.clone().add_signatures(
|
||||
&self.tx_lock,
|
||||
(self.A, sig_a),
|
||||
(self.b.public(), sig_b),
|
||||
)?;
|
||||
|
||||
let _ = bitcoin_wallet
|
||||
.broadcast_signed_transaction(signed_tx_cancel)
|
||||
.await?;
|
||||
}
|
||||
|
||||
{
|
||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
||||
|
||||
let sig_b = self.b.sign(tx_refund.digest());
|
||||
let sig_a = adaptor
|
||||
.decrypt_signature(&self.s_b.into_secp256k1(), self.tx_refund_encsig.clone());
|
||||
|
||||
let signed_tx_refund = tx_refund.add_signatures(
|
||||
&tx_cancel.clone(),
|
||||
(self.A, sig_a),
|
||||
(self.b.public(), sig_b),
|
||||
)?;
|
||||
|
||||
let _ = bitcoin_wallet
|
||||
.broadcast_signed_transaction(signed_tx_refund)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn tx_lock_id(&self) -> bitcoin::Txid {
|
||||
self.tx_lock.txid()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct State5 {
|
||||
A: bitcoin::PublicKey,
|
||||
pub b: bitcoin::SecretKey,
|
||||
#[serde(with = "monero_private_key")]
|
||||
s_a: monero::PrivateKey,
|
||||
pub s_b: cross_curve_dleq::Scalar,
|
||||
S_a_monero: monero::PublicKey,
|
||||
pub S_a_bitcoin: bitcoin::PublicKey,
|
||||
pub v: monero::PrivateViewKey,
|
||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||
btc: bitcoin::Amount,
|
||||
xmr: monero::Amount,
|
||||
cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
refund_address: bitcoin::Address,
|
||||
pub redeem_address: bitcoin::Address,
|
||||
punish_address: bitcoin::Address,
|
||||
pub tx_lock: bitcoin::TxLock,
|
||||
tx_refund_encsig: EncryptedSignature,
|
||||
tx_cancel_sig: Signature,
|
||||
}
|
||||
|
||||
impl State5 {
|
||||
pub async fn claim_xmr<W>(&self, monero_wallet: &W) -> Result<()>
|
||||
where
|
||||
W: monero::CreateWalletForOutput,
|
||||
{
|
||||
let s_b = monero::PrivateKey {
|
||||
scalar: self.s_b.into_ed25519(),
|
||||
};
|
||||
|
||||
let s = self.s_a + s_b;
|
||||
|
||||
// NOTE: This actually generates and opens a new wallet, closing the currently
|
||||
// open one.
|
||||
monero_wallet
|
||||
.create_and_load_wallet_for_output(s, self.v)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn tx_lock_id(&self) -> bitcoin::Txid {
|
||||
self.tx_lock.txid()
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
use crate::{bob::event_loop::EventLoopHandle, database, database::Database, SwapAmounts};
|
||||
use anyhow::{bail, Result};
|
||||
use async_recursion::async_recursion;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
|
@ -6,25 +5,28 @@ use std::{fmt, sync::Arc};
|
|||
use tokio::select;
|
||||
use tracing::info;
|
||||
use uuid::Uuid;
|
||||
use xmr_btc::{
|
||||
bob::{self, State2},
|
||||
ExpiredTimelocks,
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
database::{Database, Swap},
|
||||
protocol::bob::{self, event_loop::EventLoopHandle, state::*},
|
||||
ExpiredTimelocks, SwapAmounts,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BobState {
|
||||
Started {
|
||||
state0: bob::State0,
|
||||
state0: State0,
|
||||
amounts: SwapAmounts,
|
||||
},
|
||||
Negotiated(bob::State2),
|
||||
BtcLocked(bob::State3),
|
||||
XmrLocked(bob::State4),
|
||||
EncSigSent(bob::State4),
|
||||
BtcRedeemed(bob::State5),
|
||||
CancelTimelockExpired(bob::State4),
|
||||
BtcCancelled(bob::State4),
|
||||
BtcRefunded(bob::State4),
|
||||
Negotiated(State2),
|
||||
BtcLocked(State3),
|
||||
XmrLocked(State4),
|
||||
EncSigSent(State4),
|
||||
BtcRedeemed(State5),
|
||||
CancelTimelockExpired(State4),
|
||||
BtcCancelled(State4),
|
||||
BtcRefunded(State4),
|
||||
XmrRedeemed,
|
||||
BtcPunished,
|
||||
SafelyAborted,
|
||||
|
@ -133,8 +135,7 @@ where
|
|||
|
||||
let state = BobState::Negotiated(state2);
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
|
||||
.await?;
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until(
|
||||
state,
|
||||
is_target_state,
|
||||
|
@ -155,8 +156,7 @@ where
|
|||
|
||||
let state = BobState::BtcLocked(state3);
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
|
||||
.await?;
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until(
|
||||
state,
|
||||
is_target_state,
|
||||
|
@ -209,8 +209,7 @@ where
|
|||
BobState::CancelTimelockExpired(state4)
|
||||
};
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
|
||||
.await?;
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until(
|
||||
state,
|
||||
is_target_state,
|
||||
|
@ -251,8 +250,7 @@ where
|
|||
BobState::CancelTimelockExpired(state)
|
||||
};
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
|
||||
.await?;
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until(
|
||||
state,
|
||||
is_target_state,
|
||||
|
@ -287,8 +285,7 @@ where
|
|||
};
|
||||
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
|
||||
.await?;
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until(
|
||||
state,
|
||||
is_target_state,
|
||||
|
@ -307,8 +304,7 @@ where
|
|||
|
||||
let state = BobState::XmrRedeemed;
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
|
||||
.await?;
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until(
|
||||
state,
|
||||
is_target_state,
|
||||
|
@ -331,7 +327,7 @@ where
|
|||
}
|
||||
|
||||
let state = BobState::BtcCancelled(state4);
|
||||
db.insert_latest_state(swap_id, database::Swap::Bob(state.clone().into()))
|
||||
db.insert_latest_state(swap_id, Swap::Bob(state.clone().into()))
|
||||
.await?;
|
||||
|
||||
run_until(
|
||||
|
@ -360,8 +356,7 @@ where
|
|||
};
|
||||
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
|
||||
.await?;
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until(
|
||||
state,
|
||||
is_target_state,
|
||||
|
@ -383,12 +378,12 @@ where
|
|||
}
|
||||
|
||||
pub async fn negotiate<R>(
|
||||
state0: xmr_btc::bob::State0,
|
||||
state0: crate::protocol::bob::state::State0,
|
||||
amounts: SwapAmounts,
|
||||
swarm: &mut EventLoopHandle,
|
||||
mut rng: R,
|
||||
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
|
||||
) -> Result<State2>
|
||||
) -> Result<bob::state::State2>
|
||||
where
|
||||
R: RngCore + CryptoRng + Send,
|
||||
{
|
99
swap/src/serde.rs
Normal file
99
swap/src/serde.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
pub mod monero_private_key {
|
||||
use monero::{
|
||||
consensus::{Decodable, Encodable},
|
||||
PrivateKey,
|
||||
};
|
||||
use serde::{de, de::Visitor, ser::Error, Deserializer, Serializer};
|
||||
use std::{fmt, io::Cursor};
|
||||
|
||||
struct BytesVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for BytesVisitor {
|
||||
type Value = PrivateKey;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(formatter, "a byte array representing a Monero private key")
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, s: &[u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
let mut s = s;
|
||||
PrivateKey::consensus_decode(&mut s).map_err(|err| E::custom(format!("{:?}", err)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize<S>(x: &PrivateKey, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut bytes = Cursor::new(vec![]);
|
||||
x.consensus_encode(&mut bytes)
|
||||
.map_err(|err| S::Error::custom(format!("{:?}", err)))?;
|
||||
s.serialize_bytes(bytes.into_inner().as_ref())
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<PrivateKey, <D as Deserializer<'de>>::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let key = deserializer.deserialize_bytes(BytesVisitor)?;
|
||||
Ok(key)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod monero_amount {
|
||||
use crate::monero::Amount;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<S>(x: &Amount, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
s.serialize_u64(x.as_piconero())
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Amount, <D as Deserializer<'de>>::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let picos = u64::deserialize(deserializer)?;
|
||||
let amount = Amount::from_piconero(picos);
|
||||
|
||||
Ok(amount)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::rngs::OsRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MoneroPrivateKey(#[serde(with = "monero_private_key")] crate::monero::PrivateKey);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MoneroAmount(#[serde(with = "monero_amount")] crate::monero::Amount);
|
||||
|
||||
#[test]
|
||||
fn serde_monero_private_key() {
|
||||
let key = MoneroPrivateKey(monero::PrivateKey::from_scalar(
|
||||
crate::monero::Scalar::random(&mut OsRng),
|
||||
));
|
||||
let encoded = serde_cbor::to_vec(&key).unwrap();
|
||||
let decoded: MoneroPrivateKey = serde_cbor::from_slice(&encoded).unwrap();
|
||||
assert_eq!(key, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_monero_amount() {
|
||||
let amount = MoneroAmount(crate::monero::Amount::from_piconero(1000));
|
||||
let encoded = serde_cbor::to_vec(&amount).unwrap();
|
||||
let decoded: MoneroAmount = serde_cbor::from_slice(&encoded).unwrap();
|
||||
assert_eq!(amount, decoded);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue