Merge pull request #1 from comit-network/actual-work

Swap Monero for Bitcoin
This commit is contained in:
Lucas Soriano 2020-09-28 17:56:48 +10:00 committed by GitHub
commit 93f1d960f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 3956 additions and 0 deletions

77
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,77 @@
name: CI
on:
pull_request:
push:
branches:
- 'staging'
- 'trying'
- 'master'
jobs:
static_analysis:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
override: true
components: rustfmt, clippy
- name: Cache ~/.cargo/bin directory
uses: actions/cache@v1
with:
path: ~/.cargo/bin
key: ubuntu-rust-${{ env.RUST_TOOLCHAIN }}-cargo-bin-directory-v1
- name: Install tomlfmt
run: which cargo-tomlfmt || cargo install cargo-tomlfmt
- name: Check Cargo.toml formatting
run: |
cargo tomlfmt -d -p Cargo.toml
cargo tomlfmt -d -p xmr-btc/Cargo.toml
cargo tomlfmt -d -p monero-harness/Cargo.toml
- name: Check code formatting
run: cargo fmt --all -- --check
- name: Run clippy
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
build_test:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
override: true
- name: Cache target directory
uses: actions/cache@v1
with:
path: target
key: rust-${{ matrix.rust_toolchain }}-target-directory-${{ hashFiles('Cargo.lock') }}-v1
- name: Cache ~/.cargo/registry directory
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: rust-${{ matrix.rust_toolchain }}-cargo-registry-directory-${{ hashFiles('Cargo.lock') }}-v1
- name: Cargo check release code with default features
run: cargo check --workspace
- name: Cargo check all features
run: cargo check --workspace --all-targets --all-features
- name: Cargo test
run: cargo test --workspace --all-features

2
Cargo.toml Normal file
View File

@ -0,0 +1,2 @@
[workspace]
members = ["monero-harness", "xmr-btc"]

4
bors.toml Normal file
View File

@ -0,0 +1,4 @@
status = [
"static_analysis",
"build_test",
]

18
monero-harness/Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "monero-harness"
version = "0.1.0"
authors = ["CoBloX Team <team@coblox.tech>"]
edition = "2018"
[dependencies]
anyhow = "1"
futures = "0.3"
rand = "0.7"
reqwest = { version = "0.10", default-features = false, features = ["json", "native-tls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
spectral = "0.6"
testcontainers = "0.10"
tokio = { version = "0.2", default-features = false, features = ["blocking", "macros", "rt-core", "time"] }
tracing = "0.1"
url = "2"

12
monero-harness/README.md Normal file
View File

@ -0,0 +1,12 @@
Monero Harness
==============
Provides an implementation of `testcontainers::Image` for a monero image to run
`monerod` and `monero-wallet-rpc` in a docker container.
Also provides two standalone JSON RPC clients, one each for `monerod` and `monero-wallet-rpc`.
Example Usage
-------------
Please see `tests/*` for example usage.

View File

@ -0,0 +1 @@
1.46.0

View File

@ -0,0 +1,9 @@
edition = "2018"
condense_wildcard_suffixes = true
format_macro_matchers = true
merge_imports = true
use_field_init_shorthand = true
format_code_in_doc_comments = true
normalize_comments = true
wrap_comments = true
overflow_delimited_expr = true

293
monero-harness/src/image.rs Normal file
View File

@ -0,0 +1,293 @@
use std::{collections::HashMap, env::var, thread::sleep, time::Duration};
use testcontainers::{
core::{Container, Docker, Port, WaitForMessage},
Image,
};
pub const MONEROD_RPC_PORT: u16 = 48081;
pub const MINER_WALLET_RPC_PORT: u16 = 48083;
pub const ALICE_WALLET_RPC_PORT: u16 = 48084;
pub const BOB_WALLET_RPC_PORT: u16 = 48085;
#[derive(Debug)]
pub struct Monero {
tag: String,
args: Args,
ports: Option<Vec<Port>>,
entrypoint: Option<String>,
}
impl Image for Monero {
type Args = Args;
type EnvVars = HashMap<String, String>;
type Volumes = HashMap<String, String>;
type EntryPoint = str;
fn descriptor(&self) -> String {
format!("xmrto/monero:{}", self.tag)
}
fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
container
.logs()
.stdout
.wait_for_message(
"The daemon is running offline and will not attempt to sync to the Monero network",
)
.unwrap();
let additional_sleep_period =
var("MONERO_ADDITIONAL_SLEEP_PERIOD").map(|value| value.parse());
if let Ok(Ok(sleep_period)) = additional_sleep_period {
let sleep_period = Duration::from_millis(sleep_period);
sleep(sleep_period)
}
}
fn args(&self) -> <Self as Image>::Args {
self.args.clone()
}
fn volumes(&self) -> Self::Volumes {
HashMap::new()
}
fn env_vars(&self) -> Self::EnvVars {
HashMap::new()
}
fn ports(&self) -> Option<Vec<Port>> {
self.ports.clone()
}
fn with_args(self, args: <Self as Image>::Args) -> Self {
Monero { args, ..self }
}
fn with_entrypoint(self, entrypoint: &Self::EntryPoint) -> Self {
Self {
entrypoint: Some(entrypoint.to_string()),
..self
}
}
fn entrypoint(&self) -> Option<String> {
self.entrypoint.to_owned()
}
}
impl Default for Monero {
fn default() -> Self {
Monero {
tag: "v0.16.0.3".into(),
args: Args::default(),
ports: None,
entrypoint: Some("".into()),
}
}
}
impl Monero {
pub fn with_tag(self, tag_str: &str) -> Self {
Monero {
tag: tag_str.to_string(),
..self
}
}
pub fn with_mapped_port<P: Into<Port>>(mut self, port: P) -> Self {
let mut ports = self.ports.unwrap_or_default();
ports.push(port.into());
self.ports = Some(ports);
self
}
pub fn with_wallet(self, name: &str, rpc_port: u16) -> Self {
let wallet = WalletArgs::new(name, rpc_port);
let mut wallet_args = self.args.wallets;
wallet_args.push(wallet);
Self {
args: Args {
monerod: self.args.monerod,
wallets: wallet_args,
},
..self
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Args {
monerod: MonerodArgs,
wallets: Vec<WalletArgs>,
}
#[derive(Debug, Clone)]
pub struct MonerodArgs {
pub regtest: bool,
pub offline: bool,
pub rpc_payment_allow_free_loopback: bool,
pub confirm_external_bind: bool,
pub non_interactive: bool,
pub no_igd: bool,
pub hide_my_port: bool,
pub rpc_bind_ip: String,
pub rpc_bind_port: u16,
pub fixed_difficulty: u32,
pub data_dir: String,
}
#[derive(Debug, Clone)]
pub struct WalletArgs {
pub disable_rpc_login: bool,
pub confirm_external_bind: bool,
pub wallet_dir: String,
pub rpc_bind_ip: String,
pub rpc_bind_port: u16,
pub daemon_address: String,
pub log_level: u32,
}
/// Sane defaults for a mainnet regtest instance.
impl Default for MonerodArgs {
fn default() -> Self {
MonerodArgs {
regtest: true,
offline: true,
rpc_payment_allow_free_loopback: true,
confirm_external_bind: true,
non_interactive: true,
no_igd: true,
hide_my_port: true,
rpc_bind_ip: "0.0.0.0".to_string(),
rpc_bind_port: MONEROD_RPC_PORT,
fixed_difficulty: 1,
data_dir: "/monero".to_string(),
}
}
}
impl MonerodArgs {
// Return monerod args as is single string so we can pass it to bash.
fn args(&self) -> String {
let mut args = vec!["monerod".to_string()];
if self.regtest {
args.push("--regtest".to_string())
}
if self.offline {
args.push("--offline".to_string())
}
if self.rpc_payment_allow_free_loopback {
args.push("--rpc-payment-allow-free-loopback".to_string())
}
if self.confirm_external_bind {
args.push("--confirm-external-bind".to_string())
}
if self.non_interactive {
args.push("--non-interactive".to_string())
}
if self.no_igd {
args.push("--no-igd".to_string())
}
if self.hide_my_port {
args.push("--hide-my-port".to_string())
}
if !self.rpc_bind_ip.is_empty() {
args.push(format!("--rpc-bind-ip {}", self.rpc_bind_ip));
}
if self.rpc_bind_port != 0 {
args.push(format!("--rpc-bind-port {}", self.rpc_bind_port));
}
if !self.data_dir.is_empty() {
args.push(format!("--data-dir {}", self.data_dir));
}
if self.fixed_difficulty != 0 {
args.push(format!("--fixed-difficulty {}", self.fixed_difficulty));
}
args.join(" ")
}
}
impl WalletArgs {
pub fn new(wallet_dir: &str, rpc_port: u16) -> Self {
let daemon_address = format!("localhost:{}", MONEROD_RPC_PORT);
WalletArgs {
disable_rpc_login: true,
confirm_external_bind: true,
wallet_dir: wallet_dir.into(),
rpc_bind_ip: "0.0.0.0".into(),
rpc_bind_port: rpc_port,
daemon_address,
log_level: 4,
}
}
// Return monero-wallet-rpc args as is single string so we can pass it to bash.
fn args(&self) -> String {
let mut args = vec!["monero-wallet-rpc".to_string()];
if self.disable_rpc_login {
args.push("--disable-rpc-login".to_string())
}
if self.confirm_external_bind {
args.push("--confirm-external-bind".to_string())
}
if !self.wallet_dir.is_empty() {
args.push(format!("--wallet-dir {}", self.wallet_dir));
}
if !self.rpc_bind_ip.is_empty() {
args.push(format!("--rpc-bind-ip {}", self.rpc_bind_ip));
}
if self.rpc_bind_port != 0 {
args.push(format!("--rpc-bind-port {}", self.rpc_bind_port));
}
if !self.daemon_address.is_empty() {
args.push(format!("--daemon-address {}", self.daemon_address));
}
if self.log_level != 0 {
args.push(format!("--log-level {}", self.log_level));
}
args.join(" ")
}
}
impl IntoIterator for Args {
type Item = String;
type IntoIter = ::std::vec::IntoIter<String>;
fn into_iter(self) -> <Self as IntoIterator>::IntoIter {
let mut args = Vec::new();
args.push("/bin/bash".into());
args.push("-c".into());
let wallet_args: Vec<String> = self.wallets.iter().map(|wallet| wallet.args()).collect();
let wallet_args = wallet_args.join(" & ");
let cmd = format!("{} & {} ", self.monerod.args(), wallet_args);
args.push(cmd);
args.into_iter()
}
}

296
monero-harness/src/lib.rs Normal file
View File

@ -0,0 +1,296 @@
#![warn(
unused_extern_crates,
missing_debug_implementations,
missing_copy_implementations,
rust_2018_idioms,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::fallible_impl_from,
clippy::cast_precision_loss,
clippy::cast_possible_wrap,
clippy::dbg_macro
)]
#![forbid(unsafe_code)]
//! # monero-harness
//!
//! A simple lib to start a monero container (incl. monerod and
//! monero-wallet-rpc). Provides initialisation methods to generate blocks,
//! create and fund accounts, and start a continuous mining task mining blocks
//! every BLOCK_TIME_SECS seconds.
//!
//! Also provides standalone JSON RPC clients for monerod and monero-wallet-rpc.
pub mod image;
pub mod rpc;
use anyhow::Result;
use rand::Rng;
use serde::Deserialize;
use std::time::Duration;
use testcontainers::{clients::Cli, core::Port, Container, Docker};
use tokio::time;
use crate::{
image::{ALICE_WALLET_RPC_PORT, BOB_WALLET_RPC_PORT, MINER_WALLET_RPC_PORT, MONEROD_RPC_PORT},
rpc::{
monerod,
wallet::{self, GetAddress, Transfer},
},
};
/// How often we mine a block.
const BLOCK_TIME_SECS: u64 = 1;
/// Poll interval when checking if the wallet has synced with monerod.
const WAIT_WALLET_SYNC_MILLIS: u64 = 1000;
/// Wallet sub-account indices.
const ACCOUNT_INDEX_PRIMARY: u32 = 0;
#[derive(Debug)]
pub struct Monero<'c> {
pub docker: Container<'c, Cli, image::Monero>,
pub monerod_rpc_port: u16,
pub miner_wallet_rpc_port: u16,
pub alice_wallet_rpc_port: u16,
pub bob_wallet_rpc_port: u16,
}
impl<'c> Monero<'c> {
/// Starts a new regtest monero container.
pub fn new(cli: &'c Cli) -> Self {
let mut rng = rand::thread_rng();
let monerod_rpc_port: u16 = rng.gen_range(1024, u16::MAX);
let miner_wallet_rpc_port: u16 = rng.gen_range(1024, u16::MAX);
let alice_wallet_rpc_port: u16 = rng.gen_range(1024, u16::MAX);
let bob_wallet_rpc_port: u16 = rng.gen_range(1024, u16::MAX);
let image = image::Monero::default()
.with_mapped_port(Port {
local: monerod_rpc_port,
internal: MONEROD_RPC_PORT,
})
.with_mapped_port(Port {
local: miner_wallet_rpc_port,
internal: MINER_WALLET_RPC_PORT,
})
.with_wallet("miner", MINER_WALLET_RPC_PORT)
.with_mapped_port(Port {
local: alice_wallet_rpc_port,
internal: ALICE_WALLET_RPC_PORT,
})
.with_wallet("alice", ALICE_WALLET_RPC_PORT)
.with_mapped_port(Port {
local: bob_wallet_rpc_port,
internal: BOB_WALLET_RPC_PORT,
})
.with_wallet("bob", BOB_WALLET_RPC_PORT);
println!("running image ...");
let docker = cli.run(image);
println!("image ran");
Self {
docker,
monerod_rpc_port,
miner_wallet_rpc_port,
alice_wallet_rpc_port,
bob_wallet_rpc_port,
}
}
pub fn miner_wallet_rpc_client(&self) -> wallet::Client {
wallet::Client::localhost(self.miner_wallet_rpc_port)
}
pub fn alice_wallet_rpc_client(&self) -> wallet::Client {
wallet::Client::localhost(self.alice_wallet_rpc_port)
}
pub fn bob_wallet_rpc_client(&self) -> wallet::Client {
wallet::Client::localhost(self.bob_wallet_rpc_port)
}
pub fn monerod_rpc_client(&self) -> monerod::Client {
monerod::Client::localhost(self.monerod_rpc_port)
}
/// Initialise by creating a wallet, generating some `blocks`, and starting
/// a miner thread that mines to the primary account. Also create two
/// sub-accounts, one for Alice and one for Bob. If alice/bob_funding is
/// some, the value needs to be > 0.
pub async fn init(&self, alice_funding: u64, bob_funding: u64) -> Result<()> {
let miner_wallet = self.miner_wallet_rpc_client();
let alice_wallet = self.alice_wallet_rpc_client();
let bob_wallet = self.bob_wallet_rpc_client();
let monerod = self.monerod_rpc_client();
miner_wallet.create_wallet("miner_wallet").await?;
alice_wallet.create_wallet("alice_wallet").await?;
bob_wallet.create_wallet("bob_wallet").await?;
let miner = self.get_address_miner().await?.address;
let alice = self.get_address_alice().await?.address;
let bob = self.get_address_bob().await?.address;
let _ = monerod.generate_blocks(70, &miner).await?;
self.wait_for_miner_wallet_block_height().await?;
if alice_funding > 0 {
self.fund_account(&alice, &miner, alice_funding).await?;
self.wait_for_alice_wallet_block_height().await?;
let balance = self.get_balance_alice().await?;
debug_assert!(balance == alice_funding);
}
if bob_funding > 0 {
self.fund_account(&bob, &miner, bob_funding).await?;
self.wait_for_bob_wallet_block_height().await?;
let balance = self.get_balance_bob().await?;
debug_assert!(balance == bob_funding);
}
let _ = tokio::spawn(mine(monerod.clone(), miner));
Ok(())
}
/// Just create a wallet and start mining (you probably want `init()`).
pub async fn init_just_miner(&self, blocks: u32) -> Result<()> {
let wallet = self.miner_wallet_rpc_client();
let monerod = self.monerod_rpc_client();
wallet.create_wallet("miner_wallet").await?;
let miner = self.get_address_miner().await?.address;
let _ = monerod.generate_blocks(blocks, &miner).await?;
let _ = tokio::spawn(mine(monerod.clone(), miner));
Ok(())
}
async fn fund_account(&self, address: &str, miner: &str, funding: u64) -> Result<()> {
let monerod = self.monerod_rpc_client();
self.transfer_from_primary(funding, address).await?;
let _ = monerod.generate_blocks(10, miner).await?;
self.wait_for_miner_wallet_block_height().await?;
Ok(())
}
async fn wait_for_miner_wallet_block_height(&self) -> Result<()> {
self.wait_for_wallet_height(self.miner_wallet_rpc_client())
.await
}
pub async fn wait_for_alice_wallet_block_height(&self) -> Result<()> {
self.wait_for_wallet_height(self.alice_wallet_rpc_client())
.await
}
pub async fn wait_for_bob_wallet_block_height(&self) -> Result<()> {
self.wait_for_wallet_height(self.bob_wallet_rpc_client())
.await
}
// It takes a little while for the wallet to sync with monerod.
async fn wait_for_wallet_height(&self, wallet: wallet::Client) -> Result<()> {
let monerod = self.monerod_rpc_client();
let height = monerod.get_block_count().await?;
while wallet.block_height().await?.height < height {
time::delay_for(Duration::from_millis(WAIT_WALLET_SYNC_MILLIS)).await;
}
Ok(())
}
/// Get addresses for the primary account.
pub async fn get_address_miner(&self) -> Result<GetAddress> {
let wallet = self.miner_wallet_rpc_client();
wallet.get_address(ACCOUNT_INDEX_PRIMARY).await
}
/// Get addresses for the Alice's account.
pub async fn get_address_alice(&self) -> Result<GetAddress> {
let wallet = self.alice_wallet_rpc_client();
wallet.get_address(ACCOUNT_INDEX_PRIMARY).await
}
/// Get addresses for the Bob's account.
pub async fn get_address_bob(&self) -> Result<GetAddress> {
let wallet = self.bob_wallet_rpc_client();
wallet.get_address(ACCOUNT_INDEX_PRIMARY).await
}
/// Gets the balance of the wallet primary account.
pub async fn get_balance_primary(&self) -> Result<u64> {
let wallet = self.miner_wallet_rpc_client();
wallet.get_balance(ACCOUNT_INDEX_PRIMARY).await
}
/// Gets the balance of Alice's account.
pub async fn get_balance_alice(&self) -> Result<u64> {
let wallet = self.alice_wallet_rpc_client();
wallet.get_balance(ACCOUNT_INDEX_PRIMARY).await
}
/// Gets the balance of Bob's account.
pub async fn get_balance_bob(&self) -> Result<u64> {
let wallet = self.bob_wallet_rpc_client();
wallet.get_balance(ACCOUNT_INDEX_PRIMARY).await
}
/// Transfers moneroj from the primary account.
pub async fn transfer_from_primary(&self, amount: u64, address: &str) -> Result<Transfer> {
let wallet = self.miner_wallet_rpc_client();
wallet
.transfer(ACCOUNT_INDEX_PRIMARY, amount, address)
.await
}
/// Transfers moneroj from Alice's account.
pub async fn transfer_from_alice(&self, amount: u64, address: &str) -> Result<Transfer> {
let wallet = self.alice_wallet_rpc_client();
wallet
.transfer(ACCOUNT_INDEX_PRIMARY, amount, address)
.await
}
/// Transfers moneroj from Bob's account.
pub async fn transfer_from_bob(&self, amount: u64, address: &str) -> Result<Transfer> {
let wallet = self.bob_wallet_rpc_client();
wallet
.transfer(ACCOUNT_INDEX_PRIMARY, amount, address)
.await
}
}
/// Mine a block ever BLOCK_TIME_SECS seconds.
async fn mine(monerod: monerod::Client, reward_address: String) -> Result<()> {
loop {
time::delay_for(Duration::from_secs(BLOCK_TIME_SECS)).await;
monerod.generate_blocks(1, &reward_address).await?;
}
}
// We should be able to use monero-rs for this but it does not include all
// the fields.
#[derive(Clone, Debug, Deserialize)]
pub struct BlockHeader {
pub block_size: u32,
pub depth: u32,
pub difficulty: u32,
pub hash: String,
pub height: u32,
pub major_version: u32,
pub minor_version: u32,
pub nonce: u32,
pub num_txes: u32,
pub orphan_status: bool,
pub prev_hash: String,
pub reward: u64,
pub timestamp: u32,
}

63
monero-harness/src/rpc.rs Normal file
View File

@ -0,0 +1,63 @@
//! JSON RPC clients for `monerd` and `monero-wallet-rpc`.
pub mod monerod;
pub mod wallet;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug, Clone)]
pub struct Request<T> {
/// JSON RPC version, we hard cod this to 2.0.
jsonrpc: String,
/// Client controlled identifier, we hard code this to 1.
id: String,
/// The method to call.
method: String,
/// The method parameters.
params: T,
}
/// JSON RPC request.
impl<T> Request<T> {
pub fn new(method: &str, params: T) -> Self {
Self {
jsonrpc: "2.0".to_owned(),
id: "1".to_owned(),
method: method.to_owned(),
params,
}
}
}
/// JSON RPC response.
#[derive(Deserialize, Serialize, Debug, Clone)]
struct Response<T> {
pub id: String,
pub jsonrpc: String,
pub result: T,
}
#[cfg(test)]
mod tests {
use super::*;
use spectral::prelude::*;
#[derive(Serialize, Debug, Clone)]
struct Params {
val: u32,
}
#[test]
fn can_serialize_request_with_params() {
// Dummy method and parameters.
let params = Params { val: 0 };
let method = "get_block";
let r = Request::new(method, &params);
let got = serde_json::to_string(&r).expect("failed to serialize request");
let want =
"{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"method\":\"get_block\",\"params\":{\"val\":0}}"
.to_string();
assert_that!(got).is_equal_to(want);
}
}

View File

@ -0,0 +1,132 @@
use crate::{
rpc::{Request, Response},
BlockHeader,
};
use anyhow::Result;
use reqwest::Url;
use serde::{Deserialize, Serialize};
// #[cfg(not(test))]
// use tracing::debug;
//
// #[cfg(test)]
use std::eprintln as debug;
/// RPC client for monerod and monero-wallet-rpc.
#[derive(Debug, Clone)]
pub struct Client {
pub inner: reqwest::Client,
pub url: Url,
}
impl Client {
/// New local host monerod RPC client.
pub fn localhost(port: u16) -> Self {
let url = format!("http://127.0.0.1:{}/json_rpc", port);
let url = Url::parse(&url).expect("url is well formed");
Self {
inner: reqwest::Client::new(),
url,
}
}
pub async fn generate_blocks(
&self,
amount_of_blocks: u32,
wallet_address: &str,
) -> Result<GenerateBlocks> {
let params = GenerateBlocksParams {
amount_of_blocks,
wallet_address: wallet_address.to_owned(),
};
let request = Request::new("generateblocks", params);
let response = self
.inner
.post(self.url.clone())
.json(&request)
.send()
.await?
.text()
.await?;
debug!("generate blocks response: {}", response);
let res: Response<GenerateBlocks> = serde_json::from_str(&response)?;
Ok(res.result)
}
// $ curl http://127.0.0.1:18081/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"get_block_header_by_height","params":{"height":1}}' -H 'Content-Type: application/json'
pub async fn get_block_header_by_height(&self, height: u32) -> Result<BlockHeader> {
let params = GetBlockHeaderByHeightParams { height };
let request = Request::new("get_block_header_by_height", params);
let response = self
.inner
.post(self.url.clone())
.json(&request)
.send()
.await?
.text()
.await?;
debug!("get block header by height response: {}", response);
let res: Response<GetBlockHeaderByHeight> = serde_json::from_str(&response)?;
Ok(res.result.block_header)
}
pub async fn get_block_count(&self) -> Result<u32> {
let request = Request::new("get_block_count", "");
let response = self
.inner
.post(self.url.clone())
.json(&request)
.send()
.await?
.text()
.await?;
debug!("get block count response: {}", response);
let res: Response<BlockCount> = serde_json::from_str(&response)?;
Ok(res.result.count)
}
}
#[derive(Clone, Debug, Serialize)]
struct GenerateBlocksParams {
amount_of_blocks: u32,
wallet_address: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct GenerateBlocks {
pub blocks: Vec<String>,
pub height: u32,
pub status: String,
}
#[derive(Clone, Debug, Serialize)]
struct GetBlockHeaderByHeightParams {
height: u32,
}
#[derive(Clone, Debug, Deserialize)]
struct GetBlockHeaderByHeight {
block_header: BlockHeader,
status: String,
untrusted: bool,
}
#[derive(Clone, Debug, Deserialize)]
struct BlockCount {
count: u32,
status: String,
}

View File

@ -0,0 +1,397 @@
use crate::rpc::{Request, Response};
use anyhow::Result;
use reqwest::Url;
use serde::{Deserialize, Serialize};
// TODO: Either use println! directly or import tracing also?
use std::println as debug;
/// JSON RPC client for monero-wallet-rpc.
#[derive(Debug)]
pub struct Client {
pub inner: reqwest::Client,
pub url: Url,
}
impl Client {
/// Constructs a monero-wallet-rpc client with localhost endpoint.
pub fn localhost(port: u16) -> Self {
let url = format!("http://127.0.0.1:{}/json_rpc", port);
let url = Url::parse(&url).expect("url is well formed");
Client::new(url)
}
/// Constructs a monero-wallet-rpc client with `url` endpoint.
pub fn new(url: Url) -> Self {
Self {
inner: reqwest::Client::new(),
url,
}
}
/// Get addresses for account by index.
pub async fn get_address(&self, account_index: u32) -> Result<GetAddress> {
let params = GetAddressParams { account_index };
let request = Request::new("get_address", params);
let response = self
.inner
.post(self.url.clone())
.json(&request)
.send()
.await?
.text()
.await?;
debug!("get address RPC response: {}", response);
let r: Response<GetAddress> = serde_json::from_str(&response)?;
Ok(r.result)
}
/// Gets the balance of account by index.
pub async fn get_balance(&self, index: u32) -> Result<u64> {
let params = GetBalanceParams {
account_index: index,
};
let request = Request::new("get_balance", params);
let response = self
.inner
.post(self.url.clone())
.json(&request)
.send()
.await?
.text()
.await?;
debug!(
"get balance of account index {} RPC response: {}",
index, response
);
let res: Response<GetBalance> = serde_json::from_str(&response)?;
let balance = res.result.balance;
Ok(balance)
}
pub async fn create_account(&self, label: &str) -> Result<CreateAccount> {
let params = LabelParams {
label: label.to_owned(),
};
let request = Request::new("create_account", params);
let response = self
.inner
.post(self.url.clone())
.json(&request)
.send()
.await?
.text()
.await?;
debug!("create account RPC response: {}", response);
let r: Response<CreateAccount> = serde_json::from_str(&response)?;
Ok(r.result)
}
/// Get accounts, filtered by tag ("" for no filtering).
pub async fn get_accounts(&self, tag: &str) -> Result<GetAccounts> {
let params = TagParams {
tag: tag.to_owned(),
};
let request = Request::new("get_accounts", params);
let response = self
.inner
.post(self.url.clone())
.json(&request)
.send()
.await?
.text()
.await?;
debug!("get accounts RPC response: {}", response);
let r: Response<GetAccounts> = serde_json::from_str(&response)?;
Ok(r.result)
}
/// Creates a wallet using `filename`.
pub async fn create_wallet(&self, filename: &str) -> Result<()> {
let params = CreateWalletParams {
filename: filename.to_owned(),
language: "English".to_owned(),
};
let request = Request::new("create_wallet", params);
let response = self
.inner
.post(self.url.clone())
.json(&request)
.send()
.await?
.text()
.await?;
debug!("create wallet RPC response: {}", response);
Ok(())
}
/// Transfers `amount` moneroj from `account_index` to `address`.
pub async fn transfer(
&self,
account_index: u32,
amount: u64,
address: &str,
) -> Result<Transfer> {
let dest = vec![Destination {
amount,
address: address.to_owned(),
}];
self.multi_transfer(account_index, dest).await
}
/// Transfers moneroj from `account_index` to `destinations`.
pub async fn multi_transfer(
&self,
account_index: u32,
destinations: Vec<Destination>,
) -> Result<Transfer> {
let params = TransferParams {
account_index,
destinations,
get_tx_key: true,
};
let request = Request::new("transfer", params);
let response = self
.inner
.post(self.url.clone())
.json(&request)
.send()
.await?
.text()
.await?;
debug!("transfer RPC response: {}", response);
let r: Response<Transfer> = serde_json::from_str(&response)?;
Ok(r.result)
}
/// Get wallet block height, this might be behind monerod height.
pub(crate) async fn block_height(&self) -> Result<BlockHeight> {
let request = Request::new("get_height", "");
let response = self
.inner
.post(self.url.clone())
.json(&request)
.send()
.await?
.text()
.await?;
debug!("wallet height RPC response: {}", response);
let r: Response<BlockHeight> = serde_json::from_str(&response)?;
Ok(r.result)
}
/// Check a transaction in the blockchain with its secret key.
pub async fn check_tx_key(
&self,
tx_id: &str,
tx_key: &str,
address: &str,
) -> Result<CheckTxKey> {
let params = CheckTxKeyParams {
tx_id: tx_id.to_owned(),
tx_key: tx_key.to_owned(),
address: address.to_owned(),
};
let request = Request::new("check_tx_key", params);
let response = self
.inner
.post(self.url.clone())
.json(&request)
.send()
.await?
.text()
.await?;
debug!("transfer RPC response: {}", response);
let r: Response<CheckTxKey> = serde_json::from_str(&response)?;
Ok(r.result)
}
pub async fn generate_from_keys(
&self,
address: &str,
spend_key: &str,
view_key: &str,
) -> Result<GenerateFromKeys> {
let params = GenerateFromKeysParams {
restore_height: 0,
filename: view_key.into(),
address: address.into(),
spendkey: spend_key.into(),
viewkey: view_key.into(),
password: "".into(),
autosave_current: true,
};
let request = Request::new("generate_from_keys", params);
let response = self
.inner
.post(self.url.clone())
.json(&request)
.send()
.await?
.text()
.await?;
debug!("generate_from_keys RPC response: {}", response);
let r: Response<GenerateFromKeys> = serde_json::from_str(&response)?;
Ok(r.result)
}
}
#[derive(Serialize, Debug, Clone)]
struct GetAddressParams {
account_index: u32,
}
#[derive(Deserialize, Debug, Clone)]
pub struct GetAddress {
pub address: String,
}
#[derive(Serialize, Debug, Clone)]
struct GetBalanceParams {
account_index: u32,
}
#[derive(Deserialize, Debug, Clone)]
struct GetBalance {
balance: u64,
blocks_to_unlock: u32,
multisig_import_needed: bool,
time_to_unlock: u32,
unlocked_balance: u64,
}
#[derive(Serialize, Debug, Clone)]
struct LabelParams {
label: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct CreateAccount {
pub account_index: u32,
pub address: String,
}
#[derive(Serialize, Debug, Clone)]
struct TagParams {
tag: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct GetAccounts {
pub subaddress_accounts: Vec<SubAddressAccount>,
pub total_balance: u64,
pub total_unlocked_balance: u64,
}
#[derive(Deserialize, Debug, Clone)]
pub struct SubAddressAccount {
pub account_index: u32,
pub balance: u32,
pub base_address: String,
pub label: String,
pub tag: String,
pub unlocked_balance: u64,
}
#[derive(Serialize, Debug, Clone)]
struct CreateWalletParams {
filename: String,
language: String,
}
#[derive(Serialize, Debug, Clone)]
struct TransferParams {
// Transfer from this account.
account_index: u32,
// Destinations to receive XMR:
destinations: Vec<Destination>,
// Return the transaction key after sending.
get_tx_key: bool,
}
#[derive(Serialize, Debug, Clone)]
pub struct Destination {
amount: u64,
address: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Transfer {
pub amount: u64,
pub fee: u64,
pub multisig_txset: String,
pub tx_blob: String,
pub tx_hash: String,
pub tx_key: String,
pub tx_metadata: String,
pub unsigned_txset: String,
}
#[derive(Clone, Copy, Debug, Deserialize)]
pub struct BlockHeight {
pub height: u32,
}
#[derive(Serialize, Debug, Clone)]
struct CheckTxKeyParams {
#[serde(rename = "txid")]
tx_id: String,
tx_key: String,
address: String,
}
#[derive(Clone, Copy, Debug, Deserialize)]
pub struct CheckTxKey {
pub confirmations: u32,
pub in_pool: bool,
pub received: u64,
}
#[derive(Clone, Debug, Serialize)]
pub struct GenerateFromKeysParams {
pub restore_height: u32,
pub filename: String,
pub address: String,
pub spendkey: String,
pub viewkey: String,
pub password: String,
pub autosave_current: bool,
}
#[derive(Clone, Debug, Deserialize)]
pub struct GenerateFromKeys {
pub address: String,
pub info: String,
}

View File

@ -0,0 +1,36 @@
use monero_harness::Monero;
use spectral::prelude::*;
use testcontainers::clients::Cli;
const ALICE_FUND_AMOUNT: u64 = 1_000_000_000_000;
const BOB_FUND_AMOUNT: u64 = 0;
fn init_cli() -> Cli {
Cli::default()
}
async fn init_monero(tc: &'_ Cli) -> Monero<'_> {
let monero = Monero::new(tc);
let _ = monero.init(ALICE_FUND_AMOUNT, BOB_FUND_AMOUNT).await;
monero
}
#[tokio::test]
async fn init_accounts_for_alice_and_bob() {
let cli = init_cli();
let monero = init_monero(&cli).await;
let got_balance_alice = monero
.get_balance_alice()
.await
.expect("failed to get alice's balance");
let got_balance_bob = monero
.get_balance_bob()
.await
.expect("failed to get bob's balance");
assert_that!(got_balance_alice).is_equal_to(ALICE_FUND_AMOUNT);
assert_that!(got_balance_bob).is_equal_to(BOB_FUND_AMOUNT);
}

View File

@ -0,0 +1,47 @@
use monero_harness::{rpc::monerod::Client, Monero};
use spectral::prelude::*;
use std::time::Duration;
use testcontainers::clients::Cli;
use tokio::time;
fn init_cli() -> Cli {
Cli::default()
}
#[tokio::test]
async fn connect_to_monerod() {
let tc = init_cli();
let monero = Monero::new(&tc);
let cli = Client::localhost(monero.monerod_rpc_port);
let header = cli
.get_block_header_by_height(0)
.await
.expect("failed to get block 0");
assert_that!(header.height).is_equal_to(0);
}
#[tokio::test]
async fn miner_is_running_and_producing_blocks() {
let tc = init_cli();
let monero = Monero::new(&tc);
let cli = Client::localhost(monero.monerod_rpc_port);
monero
.init_just_miner(2)
.await
.expect("Failed to initialize");
// Only need 3 seconds since we mine a block every second but
// give it 5 just for good measure.
time::delay_for(Duration::from_secs(5)).await;
// We should have at least 5 blocks by now.
let header = cli
.get_block_header_by_height(5)
.await
.expect("failed to get block");
assert_that!(header.height).is_equal_to(5);
}

View File

@ -0,0 +1,89 @@
use monero_harness::{rpc::wallet::Client, Monero};
use spectral::prelude::*;
use testcontainers::clients::Cli;
#[tokio::test]
async fn wallet_and_accounts() {
let tc = Cli::default();
let monero = Monero::new(&tc);
let miner_wallet = Client::localhost(monero.miner_wallet_rpc_port);
println!("creating wallet ...");
let _ = miner_wallet
.create_wallet("wallet")
.await
.expect("failed to create wallet");
let got = miner_wallet
.get_balance(0)
.await
.expect("failed to get balance");
let want = 0;
assert_that!(got).is_equal_to(want);
}
#[tokio::test]
async fn create_account_and_retrieve_it() {
let tc = Cli::default();
let monero = Monero::new(&tc);
let cli = Client::localhost(monero.miner_wallet_rpc_port);
let label = "Iron Man"; // This is intentionally _not_ Alice or Bob.
let _ = cli
.create_wallet("wallet")
.await
.expect("failed to create wallet");
let _ = cli
.create_account(label)
.await
.expect("failed to create account");
let mut found: bool = false;
let accounts = cli
.get_accounts("") // Empty filter.
.await
.expect("failed to get accounts");
for account in accounts.subaddress_accounts {
if account.label == label {
found = true;
}
}
assert!(found);
}
#[tokio::test]
async fn transfer_and_check_tx_key() {
let fund_alice = 1_000_000_000_000;
let fund_bob = 0;
let tc = Cli::default();
let monero = Monero::new(&tc);
let _ = monero.init(fund_alice, fund_bob).await;
let address_bob = monero
.get_address_bob()
.await
.expect("failed to get Bob's address")
.address;
let transfer_amount = 100;
let transfer = monero
.transfer_from_alice(transfer_amount, &address_bob)
.await
.expect("transfer failed");
let tx_id = transfer.tx_hash;
let tx_key = transfer.tx_key;
let cli = monero.miner_wallet_rpc_client();
let res = cli
.check_tx_key(&tx_id, &tx_key, &address_bob)
.await
.expect("failed to check tx by key");
assert_that!(res.received).is_equal_to(transfer_amount);
}

1
rust-toolchain Normal file
View File

@ -0,0 +1 @@
nightly-2020-08-13

9
rustfmt.toml Normal file
View File

@ -0,0 +1,9 @@
edition = "2018"
condense_wildcard_suffixes = true
format_macro_matchers = true
merge_imports = true
use_field_init_shorthand = true
format_code_in_doc_comments = true
normalize_comments = true
wrap_comments = true
overflow_delimited_expr = true

27
xmr-btc/Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "xmr-btc"
version = "0.1.0"
authors = ["CoBloX Team <team@coblox.tech>"]
edition = "2018"
[dependencies]
anyhow = "1"
async-trait = "0.1"
bitcoin = { version = "0.23", features = ["rand"] }
cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "a3e57a70d332b4ce9600663453b9bd02936d76bf" }
curve25519-dalek = "2"
ecdsa_fun = { version = "0.3.1", features = ["libsecp_compat"] }
ed25519-dalek = "1.0.0-pre.4" # Cannot be 1 because they depend on curve25519-dalek version 3
miniscript = "1"
monero = "0.9"
rand = "0.7"
sha2 = "0.9"
thiserror = "1"
[dev-dependencies]
base64 = "0.12"
bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "d402b36d3d6406150e3bfb71492ff4a0a7cb290e" }
monero-harness = { path = "../monero-harness" }
reqwest = { version = "0.10", default-features = false }
testcontainers = "0.10"
tokio = { version = "0.2", default-features = false, features = ["blocking", "macros", "rt-core", "time", "rt-threaded"] }

516
xmr-btc/src/alice.rs Normal file
View File

@ -0,0 +1,516 @@
use anyhow::{anyhow, Result};
use ecdsa_fun::adaptor::{Adaptor, EncryptedSignature};
use rand::{CryptoRng, RngCore};
use crate::{bitcoin, bitcoin::GetRawTransaction, bob, monero, monero::ImportOutput};
use ecdsa_fun::{nonce::Deterministic, Signature};
use sha2::Sha256;
#[derive(Debug)]
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,
}
#[derive(Debug)]
pub struct Message1 {
pub(crate) tx_cancel_sig: Signature,
pub(crate) tx_refund_encsig: EncryptedSignature,
}
#[derive(Debug)]
pub struct Message2 {
pub(crate) tx_lock_proof: monero::TransferProof,
}
#[derive(Debug)]
pub struct State0 {
a: bitcoin::SecretKey,
s_a: cross_curve_dleq::Scalar,
v_a: monero::PrivateViewKey,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
}
impl State0 {
pub fn new<R: RngCore + CryptoRng>(
rng: &mut R,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
) -> Self {
let a = bitcoin::SecretKey::new_random(rng);
let s_a = cross_curve_dleq::Scalar::random(rng);
let v_a = monero::PrivateViewKey::new_random(rng);
Self {
a,
s_a,
v_a,
redeem_address,
punish_address,
btc,
xmr,
refund_timelock,
punish_timelock,
}
}
pub fn next_message<R: RngCore + CryptoRng>(&self, rng: &mut R) -> Message0 {
let dleq_proof_s_a = cross_curve_dleq::Proof::new(rng, &self.s_a);
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,
refund_timelock: self.refund_timelock,
punish_timelock: self.punish_timelock,
refund_address: msg.refund_address,
redeem_address: self.redeem_address,
punish_address: self.punish_address,
})
}
}
#[derive(Debug)]
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,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
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,
refund_timelock: self.refund_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(Debug)]
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,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
}
impl State2 {
pub fn next_message(&self) -> Message1 {
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.a.public(),
self.B.clone(),
);
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.clone(), tx_refund.digest());
let tx_cancel_sig = self.a.sign(tx_cancel.digest());
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.refund_timelock,
self.a.public(),
self.B.clone(),
);
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,
btc: self.btc,
xmr: self.xmr,
refund_timelock: self.refund_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(Debug)]
pub struct State3 {
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,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
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 State3 {
pub async fn watch_for_lock_btc<W>(self, bitcoin_wallet: &W) -> Result<State4>
where
W: bitcoin::GetRawTransaction,
{
let _ = bitcoin_wallet
.get_raw_transaction(self.tx_lock.txid())
.await?;
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,
refund_timelock: self.refund_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,
})
}
}
#[derive(Debug)]
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,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
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<(State4b, monero::Amount)>
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((
State4b {
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,
refund_timelock: self.refund_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,
},
fee,
))
}
pub async fn punish<W: bitcoin::BroadcastSignedTransaction>(
&self,
bitcoin_wallet: &W,
) -> Result<()> {
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.a.public(),
self.B.clone(),
);
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.clone(), 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.clone(), sig_b),
)?;
let _ = bitcoin_wallet
.broadcast_signed_transaction(signed_tx_punish)
.await?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct State4b {
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,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
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,
}
impl State4b {
pub fn next_message(&self) -> Message2 {
Message2 {
tx_lock_proof: self.tx_lock_proof.clone(),
}
}
pub fn receive(self, msg: bob::Message3) -> State5 {
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,
refund_timelock: self.refund_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,
}
}
// 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: GetRawTransaction,
M: ImportOutput,
{
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.a.public(),
self.B.clone(),
);
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
let tx_refund_encsig = self.a.encsign(self.S_b_bitcoin.clone(), tx_refund.digest());
let tx_refund_candidate = bitcoin_wallet.get_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::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order(s_b.to_bytes()));
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
.import_output(monero::PrivateKey::from_scalar(s), self.v)
.await?;
Ok(())
}
}
#[derive(Debug)]
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,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
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,
}
impl State5 {
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.clone(), sig_b),
)?;
bitcoin_wallet
.broadcast_signed_transaction(sig_tx_redeem)
.await?;
Ok(())
}
}

209
xmr-btc/src/bitcoin.rs Normal file
View File

@ -0,0 +1,209 @@
pub mod transactions;
#[cfg(test)]
pub mod wallet;
use anyhow::{anyhow, bail, Result};
use async_trait::async_trait;
use bitcoin::{
hashes::{hex::ToHex, Hash},
secp256k1,
util::psbt::PartiallySignedTransaction,
SigHash, Transaction,
};
use ecdsa_fun::{
adaptor::Adaptor,
fun::{
marker::{Jacobian, Mark},
Point, Scalar,
},
nonce::Deterministic,
ECDSA,
};
use miniscript::{Descriptor, Segwitv0};
use rand::{CryptoRng, RngCore};
use sha2::Sha256;
use std::str::FromStr;
pub use crate::bitcoin::transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund};
pub use bitcoin::{Address, Amount, OutPoint, Txid};
pub use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
#[cfg(test)]
pub use wallet::{make_wallet, Wallet};
pub const TX_FEE: u64 = 10_000;
#[derive(Debug, Clone)]
pub struct SecretKey {
inner: Scalar,
public: Point,
}
impl SecretKey {
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
let scalar = Scalar::random(rng);
let ecdsa = ECDSA::<()>::default();
let public = ecdsa.verification_key_for(&scalar);
Self {
inner: scalar,
public,
}
}
pub fn public(&self) -> PublicKey {
PublicKey(self.public.clone())
}
pub fn to_bytes(&self) -> [u8; 32] {
self.inner.to_bytes()
}
pub fn sign(&self, digest: SigHash) -> Signature {
let ecdsa = ECDSA::<Deterministic<Sha256>>::default();
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, Clone)]
pub struct PublicKey(Point);
impl From<PublicKey> for Point<Jacobian> {
fn from(from: PublicKey) -> Self {
from.0.mark::<Jacobian>()
}
}
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<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]
pub trait BuildTxLockPsbt {
async fn build_tx_lock_psbt(
&self,
output_address: Address,
output_amount: Amount,
) -> Result<PartiallySignedTransaction>;
}
#[async_trait]
pub trait SignTxLock {
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction>;
}
#[async_trait]
pub trait BroadcastSignedTransaction {
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid>;
}
#[async_trait]
pub trait GetRawTransaction {
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction>;
}
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)
}

View File

@ -0,0 +1,498 @@
use crate::bitcoin::{
build_shared_output_descriptor, verify_sig, BuildTxLockPsbt, OutPoint, PublicKey, Txid, TX_FEE,
};
use anyhow::{bail, Context, Result};
use bitcoin::{
util::{bip143::SighashComponents, psbt::PartiallySignedTransaction},
Address, Amount, Network, SigHash, Transaction, TxIn, TxOut,
};
use ecdsa_fun::Signature;
use miniscript::Descriptor;
use std::collections::HashMap;
#[derive(Debug, Clone)]
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,
{
let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0);
let address = lock_output_descriptor
.address(Network::Regtest)
.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 {
#[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())
.expect("transaction contains lock output")
}
fn build_spend_transaction(
&self,
spend_address: &Address,
sequence: Option<u32>,
) -> (Transaction, TxIn) {
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(),
};
let transaction = Transaction {
version: 2,
lock_time: 0,
input: vec![tx_in.clone()],
output: vec![tx_out],
};
(transaction, tx_in)
}
}
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, lock_input) = tx_lock.build_spend_transaction(redeem_address, None);
let digest = SighashComponents::new(&tx_redeem).sighash_all(
&lock_input,
&tx_lock.output_descriptor.witness_script(),
tx_lock.lock_amount().as_sat(),
);
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)?;
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: u32, 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,
witness: Vec::new(),
};
let tx_out = TxOut {
value: tx_lock.lock_amount().as_sat() - TX_FEE,
script_pubkey: cancel_output_descriptor.script_pubkey(),
};
let transaction = Transaction {
version: 2,
lock_time: 0,
input: vec![tx_in.clone()],
output: vec![tx_out],
};
let digest = SighashComponents::new(&transaction).sighash_all(
&tx_in,
&tx_lock.output_descriptor.witness_script(),
tx_lock.lock_amount().as_sat(),
);
Self {
inner: transaction,
digest,
output_descriptor: cancel_output_descriptor,
}
}
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)?;
Ok(tx_cancel)
}
fn build_spend_transaction(
&self,
spend_address: &Address,
sequence: Option<u32>,
) -> (Transaction, TxIn) {
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.amount().as_sat() - TX_FEE,
script_pubkey: spend_address.script_pubkey(),
};
let transaction = Transaction {
version: 2,
lock_time: 0,
input: vec![tx_in.clone()],
output: vec![tx_out],
};
(transaction, tx_in)
}
}
#[derive(Debug)]
pub struct TxRefund {
inner: Transaction,
digest: SigHash,
}
impl TxRefund {
pub fn new(tx_cancel: &TxCancel, refund_address: &Address) -> Self {
let (tx_punish, cancel_input) = tx_cancel.build_spend_transaction(refund_address, None);
let digest = SighashComponents::new(&tx_punish).sighash_all(
&cancel_input,
&tx_cancel.output_descriptor.witness_script(),
tx_cancel.amount().as_sat(),
);
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)?;
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: u32) -> Self {
let (tx_punish, lock_input) =
tx_cancel.build_spend_transaction(punish_address, Some(punish_timelock));
let digest = SighashComponents::new(&tx_punish).sighash_all(
&lock_input,
&tx_cancel.output_descriptor.witness_script(),
tx_cancel.amount().as_sat(),
);
Self {
inner: tx_punish,
digest,
}
}
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)?;
Ok(tx_punish)
}
}

View File

@ -0,0 +1,116 @@
use crate::bitcoin::{
BroadcastSignedTransaction, BuildTxLockPsbt, GetRawTransaction, SignTxLock, TxLock,
};
use anyhow::Result;
use async_trait::async_trait;
use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid};
use bitcoin_harness::{bitcoind_rpc::PsbtBase64, Bitcoind};
use reqwest::Url;
use std::time::Duration;
use tokio::time;
#[derive(Debug)]
pub struct Wallet(pub bitcoin_harness::Wallet);
impl Wallet {
pub async fn new(name: &str, url: &Url) -> Result<Self> {
let wallet = bitcoin_harness::Wallet::new(name, url.clone()).await?;
Ok(Self(wallet))
}
pub async fn balance(&self) -> Result<Amount> {
let balance = self.0.balance().await?;
Ok(balance)
}
pub async fn new_address(&self) -> Result<Address> {
self.0.new_address().await.map_err(Into::into)
}
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
let fee = self
.0
.get_wallet_transaction(txid)
.await
.map(|res| bitcoin::Amount::from_btc(-res.fee))??;
Ok(fee)
}
}
pub async fn make_wallet(
name: &str,
bitcoind: &Bitcoind<'_>,
fund_amount: Amount,
) -> Result<Wallet> {
let wallet = Wallet::new(name, &bitcoind.node_url).await?;
let buffer = Amount::from_btc(1.0).unwrap();
let amount = fund_amount + buffer;
let address = wallet.0.new_address().await.unwrap();
bitcoind.mint(address, amount).await.unwrap();
Ok(wallet)
}
#[async_trait]
impl BuildTxLockPsbt for Wallet {
async fn build_tx_lock_psbt(
&self,
output_address: Address,
output_amount: Amount,
) -> Result<PartiallySignedTransaction> {
let psbt = self.0.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.0.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.0.send_raw_transaction(transaction).await?;
// TODO: Instead of guessing how long it will take for the transaction to be
// mined we should ask bitcoind for the number of confirmations on `txid`
// give time for transaction to be mined
time::delay_for(Duration::from_millis(1100)).await;
Ok(txid)
}
}
#[async_trait]
impl GetRawTransaction for Wallet {
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
let tx = self.0.get_raw_transaction(txid).await?;
Ok(tx)
}
}

472
xmr-btc/src/bob.rs Normal file
View File

@ -0,0 +1,472 @@
use crate::{
alice,
bitcoin::{self, BuildTxLockPsbt, GetRawTransaction, TxCancel},
monero,
};
use anyhow::{anyhow, Result};
use ecdsa_fun::{
adaptor::{Adaptor, EncryptedSignature},
nonce::Deterministic,
Signature,
};
use rand::{CryptoRng, RngCore};
use sha2::Sha256;
#[derive(Debug)]
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 struct Message1 {
pub(crate) tx_lock: bitcoin::TxLock,
}
#[derive(Debug)]
pub struct Message2 {
pub(crate) tx_punish_sig: Signature,
pub(crate) tx_cancel_sig: Signature,
}
#[derive(Debug)]
pub struct Message3 {
pub(crate) tx_redeem_encsig: EncryptedSignature,
}
#[derive(Debug)]
pub struct State0 {
b: bitcoin::SecretKey,
s_b: cross_curve_dleq::Scalar,
v_b: monero::PrivateViewKey,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
}
impl State0 {
pub fn new<R: RngCore + CryptoRng>(
rng: &mut R,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
) -> 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,
refund_timelock,
punish_timelock,
refund_address,
}
}
pub fn next_message<R: RngCore + CryptoRng>(&self, rng: &mut R) -> Message0 {
let dleq_proof_s_b = cross_curve_dleq::Proof::new(rng, &self.s_b);
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,
{
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.clone(), 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,
refund_timelock: self.refund_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: msg.redeem_address,
punish_address: msg.punish_address,
tx_lock,
})
}
}
#[derive(Debug)]
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,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
}
impl State1 {
pub fn next_message(&self) -> Message1 {
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.refund_timelock,
self.A.clone(),
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.clone(),
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,
refund_timelock: self.refund_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,
})
}
}
#[derive(Debug)]
pub struct State2 {
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,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_cancel_sig_a: Signature,
tx_refund_encsig: EncryptedSignature,
}
impl State2 {
pub fn next_message(&self) -> Message2 {
let tx_cancel = TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.A.clone(),
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());
Message2 {
tx_punish_sig,
tx_cancel_sig,
}
}
pub async fn lock_btc<W>(self, bitcoin_wallet: &W) -> Result<State2b>
where
W: bitcoin::SignTxLock + bitcoin::BroadcastSignedTransaction,
{
let signed_tx_lock = bitcoin_wallet.sign_tx_lock(self.tx_lock.clone()).await?;
let _ = bitcoin_wallet
.broadcast_signed_transaction(signed_tx_lock)
.await?;
Ok(State2b {
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,
refund_timelock: self.refund_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,
})
}
}
#[derive(Debug, Clone)]
pub struct State2b {
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,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_cancel_sig_a: Signature,
tx_refund_encsig: EncryptedSignature,
}
impl State2b {
pub async fn watch_for_lock_xmr<W>(self, xmr_wallet: &W, msg: alice::Message2) -> Result<State3>
where
W: monero::CheckTransfer,
{
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
.check_transfer(S, self.v.public(), msg.tx_lock_proof, self.xmr)
.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,
refund_timelock: self.refund_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 refund_btc<W: bitcoin::BroadcastSignedTransaction>(
&self,
bitcoin_wallet: &W,
) -> Result<()> {
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.A.clone(),
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.clone(), 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.clone(), sig_a),
(self.b.public(), sig_b),
)?;
let _ = bitcoin_wallet
.broadcast_signed_transaction(signed_tx_refund)
.await?;
}
Ok(())
}
#[cfg(test)]
pub fn tx_lock_id(&self) -> bitcoin::Txid {
self.tx_lock.txid()
}
}
#[derive(Debug, Clone)]
pub struct State3 {
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,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_cancel_sig_a: Signature,
tx_refund_encsig: EncryptedSignature,
}
impl State3 {
pub fn next_message(&self) -> Message3 {
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin.clone(), tx_redeem.digest());
Message3 { tx_redeem_encsig }
}
pub async fn watch_for_redeem_btc<W>(self, bitcoin_wallet: &W) -> Result<State4>
where
W: GetRawTransaction,
{
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin.clone(), tx_redeem.digest());
let tx_redeem_candidate = bitcoin_wallet.get_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.clone(), tx_redeem_sig, tx_redeem_encsig)?;
let s_a =
monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order(s_a.to_bytes()));
Ok(State4 {
A: self.A,
b: self.b,
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,
refund_timelock: self.refund_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_refund_encsig: self.tx_refund_encsig,
tx_cancel_sig: self.tx_cancel_sig_a,
})
}
}
#[derive(Debug)]
pub struct State4 {
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
s_a: monero::PrivateKey,
s_b: cross_curve_dleq::Scalar,
S_a_monero: monero::PublicKey,
S_a_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_refund_encsig: EncryptedSignature,
tx_cancel_sig: Signature,
}
impl State4 {
pub async fn claim_xmr<W>(&self, monero_wallet: &W) -> Result<()>
where
W: monero::ImportOutput,
{
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.import_output(s, self.v).await?;
Ok(())
}
}

384
xmr-btc/src/lib.rs Normal file
View File

@ -0,0 +1,384 @@
#![warn(
unused_extern_crates,
missing_debug_implementations,
missing_copy_implementations,
rust_2018_idioms,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::fallible_impl_from,
clippy::cast_precision_loss,
clippy::cast_possible_wrap,
clippy::dbg_macro
)]
#![cfg_attr(not(test), warn(clippy::unwrap_used))]
#![forbid(unsafe_code)]
#![allow(non_snake_case)]
pub mod alice;
pub mod bitcoin;
pub mod bob;
pub mod monero;
#[cfg(test)]
mod tests {
use crate::{
alice, bitcoin,
bitcoin::{Amount, TX_FEE},
bob, monero,
};
use bitcoin_harness::Bitcoind;
use monero_harness::Monero;
use rand::rngs::OsRng;
use testcontainers::clients::Cli;
const TEN_XMR: u64 = 10_000_000_000_000;
pub async fn init_bitcoind(tc_client: &Cli) -> Bitcoind<'_> {
let bitcoind = Bitcoind::new(tc_client, "0.19.1").expect("failed to create bitcoind");
let _ = bitcoind.init(5).await;
bitcoind
}
#[tokio::test]
async fn happy_path() {
let cli = Cli::default();
let monero = Monero::new(&cli);
let bitcoind = init_bitcoind(&cli).await;
// must be bigger than our hardcoded fee of 10_000
let btc_amount = bitcoin::Amount::from_sat(10_000_000);
let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000);
let fund_alice = TEN_XMR;
let fund_bob = 0;
monero.init(fund_alice, fund_bob).await.unwrap();
let alice_monero_wallet = monero::AliceWallet(&monero);
let bob_monero_wallet = monero::BobWallet(&monero);
let alice_btc_wallet = bitcoin::Wallet::new("alice", &bitcoind.node_url)
.await
.unwrap();
let bob_btc_wallet = bitcoin::make_wallet("bob", &bitcoind, btc_amount)
.await
.unwrap();
let alice_initial_btc_balance = alice_btc_wallet.balance().await.unwrap();
let bob_initial_btc_balance = bob_btc_wallet.balance().await.unwrap();
let alice_initial_xmr_balance = alice_monero_wallet.0.get_balance_alice().await.unwrap();
let bob_initial_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap();
let redeem_address = alice_btc_wallet.new_address().await.unwrap();
let punish_address = redeem_address.clone();
let refund_address = bob_btc_wallet.new_address().await.unwrap();
let refund_timelock = 1;
let punish_timelock = 1;
let alice_state0 = alice::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
refund_timelock,
punish_timelock,
redeem_address,
punish_address,
);
let bob_state0 = bob::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
refund_timelock,
punish_timelock,
refund_address.clone(),
);
let alice_message0 = alice_state0.next_message(&mut OsRng);
let bob_message0 = bob_state0.next_message(&mut OsRng);
let alice_state1 = alice_state0.receive(bob_message0).unwrap();
let bob_state1 = bob_state0
.receive(&bob_btc_wallet, alice_message0)
.await
.unwrap();
let bob_message1 = bob_state1.next_message();
let alice_state2 = alice_state1.receive(bob_message1);
let alice_message1 = alice_state2.next_message();
let bob_state2 = bob_state1.receive(alice_message1).unwrap();
let bob_message2 = bob_state2.next_message();
let alice_state3 = alice_state2.receive(bob_message2).unwrap();
let bob_state2b = bob_state2.lock_btc(&bob_btc_wallet).await.unwrap();
let lock_txid = bob_state2b.tx_lock_id();
let alice_state4 = alice_state3
.watch_for_lock_btc(&alice_btc_wallet)
.await
.unwrap();
let (alice_state4b, lock_tx_monero_fee) =
alice_state4.lock_xmr(&alice_monero_wallet).await.unwrap();
let alice_message2 = alice_state4b.next_message();
let bob_state3 = bob_state2b
.watch_for_lock_xmr(&bob_monero_wallet, alice_message2)
.await
.unwrap();
let bob_message3 = bob_state3.next_message();
let alice_state5 = alice_state4b.receive(bob_message3);
alice_state5.redeem_btc(&alice_btc_wallet).await.unwrap();
let bob_state4 = bob_state3
.watch_for_redeem_btc(&bob_btc_wallet)
.await
.unwrap();
bob_state4.claim_xmr(&bob_monero_wallet).await.unwrap();
let alice_final_btc_balance = alice_btc_wallet.balance().await.unwrap();
let bob_final_btc_balance = bob_btc_wallet.balance().await.unwrap();
let lock_tx_bitcoin_fee = bob_btc_wallet.transaction_fee(lock_txid).await.unwrap();
assert_eq!(
alice_final_btc_balance,
alice_initial_btc_balance + btc_amount - bitcoin::Amount::from_sat(bitcoin::TX_FEE)
);
assert_eq!(
bob_final_btc_balance,
bob_initial_btc_balance - btc_amount - lock_tx_bitcoin_fee
);
let alice_final_xmr_balance = alice_monero_wallet.0.get_balance_alice().await.unwrap();
bob_monero_wallet
.0
.wait_for_bob_wallet_block_height()
.await
.unwrap();
let bob_final_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap();
assert_eq!(
alice_final_xmr_balance,
alice_initial_xmr_balance - u64::from(xmr_amount) - u64::from(lock_tx_monero_fee)
);
assert_eq!(
bob_final_xmr_balance,
bob_initial_xmr_balance + u64::from(xmr_amount)
);
}
#[tokio::test]
async fn both_refund() {
let cli = Cli::default();
let monero = Monero::new(&cli);
let bitcoind = init_bitcoind(&cli).await;
// must be bigger than our hardcoded fee of 10_000
let btc_amount = bitcoin::Amount::from_sat(10_000_000);
let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000);
let alice_btc_wallet = bitcoin::Wallet::new("alice", &bitcoind.node_url)
.await
.unwrap();
let bob_btc_wallet = bitcoin::make_wallet("bob", &bitcoind, btc_amount)
.await
.unwrap();
let fund_alice = TEN_XMR;
let fund_bob = 0;
monero.init(fund_alice, fund_bob).await.unwrap();
let alice_monero_wallet = monero::AliceWallet(&monero);
let bob_monero_wallet = monero::BobWallet(&monero);
let alice_initial_btc_balance = alice_btc_wallet.balance().await.unwrap();
let bob_initial_btc_balance = bob_btc_wallet.balance().await.unwrap();
let bob_initial_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap();
let redeem_address = alice_btc_wallet.new_address().await.unwrap();
let punish_address = redeem_address.clone();
let refund_address = bob_btc_wallet.new_address().await.unwrap();
let refund_timelock = 1;
let punish_timelock = 1;
let alice_state0 = alice::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
refund_timelock,
punish_timelock,
redeem_address,
punish_address,
);
let bob_state0 = bob::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
refund_timelock,
punish_timelock,
refund_address.clone(),
);
let alice_message0 = alice_state0.next_message(&mut OsRng);
let bob_message0 = bob_state0.next_message(&mut OsRng);
let alice_state1 = alice_state0.receive(bob_message0).unwrap();
let bob_state1 = bob_state0
.receive(&bob_btc_wallet, alice_message0)
.await
.unwrap();
let bob_message1 = bob_state1.next_message();
let alice_state2 = alice_state1.receive(bob_message1);
let alice_message1 = alice_state2.next_message();
let bob_state2 = bob_state1.receive(alice_message1).unwrap();
let bob_message2 = bob_state2.next_message();
let alice_state3 = alice_state2.receive(bob_message2).unwrap();
let bob_state2b = bob_state2.lock_btc(&bob_btc_wallet).await.unwrap();
let alice_state4 = alice_state3
.watch_for_lock_btc(&alice_btc_wallet)
.await
.unwrap();
let (alice_state4b, _lock_tx_monero_fee) =
alice_state4.lock_xmr(&alice_monero_wallet).await.unwrap();
bob_state2b.refund_btc(&bob_btc_wallet).await.unwrap();
alice_state4b
.refund_xmr(&alice_btc_wallet, &alice_monero_wallet)
.await
.unwrap();
let alice_final_btc_balance = alice_btc_wallet.balance().await.unwrap();
let bob_final_btc_balance = bob_btc_wallet.balance().await.unwrap();
// lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal
// to TX_FEE
let lock_tx_bitcoin_fee = bob_btc_wallet
.transaction_fee(bob_state2b.tx_lock_id())
.await
.unwrap();
assert_eq!(alice_final_btc_balance, alice_initial_btc_balance);
assert_eq!(
bob_final_btc_balance,
// The 2 * TX_FEE corresponds to tx_refund and tx_cancel.
bob_initial_btc_balance - Amount::from_sat(2 * TX_FEE) - lock_tx_bitcoin_fee
);
alice_monero_wallet
.0
.wait_for_alice_wallet_block_height()
.await
.unwrap();
let alice_final_xmr_balance = alice_monero_wallet.0.get_balance_alice().await.unwrap();
let bob_final_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap();
// Because we create a new wallet when claiming Monero, we can only assert on
// this new wallet owning all of `xmr_amount` after refund
assert_eq!(alice_final_xmr_balance, u64::from(xmr_amount));
assert_eq!(bob_final_xmr_balance, bob_initial_xmr_balance);
}
#[tokio::test]
async fn alice_punishes() {
let cli = Cli::default();
let bitcoind = init_bitcoind(&cli).await;
// must be bigger than our hardcoded fee of 10_000
let btc_amount = bitcoin::Amount::from_sat(10_000_000);
let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000);
let alice_btc_wallet = bitcoin::Wallet::new("alice", &bitcoind.node_url)
.await
.unwrap();
let bob_btc_wallet = bitcoin::make_wallet("bob", &bitcoind, btc_amount)
.await
.unwrap();
let alice_initial_btc_balance = alice_btc_wallet.balance().await.unwrap();
let bob_initial_btc_balance = bob_btc_wallet.balance().await.unwrap();
let redeem_address = alice_btc_wallet.new_address().await.unwrap();
let punish_address = redeem_address.clone();
let refund_address = bob_btc_wallet.new_address().await.unwrap();
let refund_timelock = 1;
let punish_timelock = 1;
let alice_state0 = alice::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
refund_timelock,
punish_timelock,
redeem_address,
punish_address,
);
let bob_state0 = bob::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
refund_timelock,
punish_timelock,
refund_address.clone(),
);
let alice_message0 = alice_state0.next_message(&mut OsRng);
let bob_message0 = bob_state0.next_message(&mut OsRng);
let alice_state1 = alice_state0.receive(bob_message0).unwrap();
let bob_state1 = bob_state0
.receive(&bob_btc_wallet, alice_message0)
.await
.unwrap();
let bob_message1 = bob_state1.next_message();
let alice_state2 = alice_state1.receive(bob_message1);
let alice_message1 = alice_state2.next_message();
let bob_state2 = bob_state1.receive(alice_message1).unwrap();
let bob_message2 = bob_state2.next_message();
let alice_state3 = alice_state2.receive(bob_message2).unwrap();
let bob_state2b = bob_state2.lock_btc(&bob_btc_wallet).await.unwrap();
let alice_state4 = alice_state3
.watch_for_lock_btc(&alice_btc_wallet)
.await
.unwrap();
alice_state4.punish(&alice_btc_wallet).await.unwrap();
let alice_final_btc_balance = alice_btc_wallet.balance().await.unwrap();
let bob_final_btc_balance = bob_btc_wallet.balance().await.unwrap();
// lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal
// to TX_FEE
let lock_tx_bitcoin_fee = bob_btc_wallet
.transaction_fee(bob_state2b.tx_lock_id())
.await
.unwrap();
assert_eq!(
alice_final_btc_balance,
alice_initial_btc_balance + btc_amount - Amount::from_sat(2 * TX_FEE)
);
assert_eq!(
bob_final_btc_balance,
bob_initial_btc_balance - btc_amount - lock_tx_bitcoin_fee
);
}
}

123
xmr-btc/src/monero.rs Normal file
View File

@ -0,0 +1,123 @@
#[cfg(test)]
pub mod wallet;
use std::ops::Add;
use anyhow::Result;
use async_trait::async_trait;
use rand::{CryptoRng, RngCore};
pub use curve25519_dalek::scalar::Scalar;
pub use monero::{Address, PrivateKey, PublicKey};
pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
let scalar = Scalar::random(rng);
PrivateKey::from_scalar(scalar)
}
#[cfg(test)]
pub use wallet::{AliceWallet, BobWallet};
#[derive(Clone, Copy, Debug)]
pub struct PrivateViewKey(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)
}
pub fn public(&self) -> PublicViewKey {
PublicViewKey(PublicKey::from_private_key(&self.0))
}
}
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)]
pub struct Amount(u64);
impl Amount {
/// 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)
}
}
impl From<Amount> for u64 {
fn from(from: Amount) -> u64 {
from.0
}
}
#[derive(Clone, Debug)]
pub struct TransferProof {
tx_hash: TxHash,
tx_key: PrivateKey,
}
#[derive(Clone, Debug)]
pub struct TxHash(String);
impl From<TxHash> for String {
fn from(from: TxHash) -> Self {
from.0
}
}
#[async_trait]
pub trait Transfer {
async fn transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
amount: Amount,
) -> Result<(TransferProof, Amount)>;
}
#[async_trait]
pub trait CheckTransfer {
async fn check_transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
transfer_proof: TransferProof,
amount: Amount,
) -> Result<()>;
}
#[async_trait]
pub trait ImportOutput {
async fn import_output(
&self,
private_spend_key: PrivateKey,
private_view_key: PrivateViewKey,
) -> Result<()>;
}

View File

@ -0,0 +1,125 @@
use crate::monero::{
Amount, CheckTransfer, ImportOutput, PrivateViewKey, PublicKey, PublicViewKey, Transfer,
TransferProof, TxHash,
};
use anyhow::{bail, Result};
use async_trait::async_trait;
use monero::{Address, Network, PrivateKey};
use monero_harness::Monero;
use std::str::FromStr;
#[derive(Debug)]
pub struct AliceWallet<'c>(pub &'c Monero<'c>);
#[async_trait]
impl Transfer for AliceWallet<'_> {
async fn transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
amount: Amount,
) -> Result<(TransferProof, Amount)> {
let destination_address =
Address::standard(Network::Mainnet, public_spend_key, public_view_key.into());
let res = self
.0
.transfer_from_alice(amount.0, &destination_address.to_string())
.await?;
let tx_hash = TxHash(res.tx_hash);
let tx_key = PrivateKey::from_str(&res.tx_key)?;
let fee = Amount::from_piconero(res.fee);
Ok((TransferProof { tx_hash, tx_key }, fee))
}
}
#[derive(Debug)]
pub struct BobWallet<'c>(pub &'c Monero<'c>);
#[async_trait]
impl CheckTransfer for BobWallet<'_> {
async fn check_transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
transfer_proof: TransferProof,
amount: Amount,
) -> Result<()> {
let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key.into());
let cli = self.0.bob_wallet_rpc_client();
let res = cli
.check_tx_key(
&String::from(transfer_proof.tx_hash),
&transfer_proof.tx_key.to_string(),
&address.to_string(),
)
.await?;
if res.received != u64::from(amount) {
bail!(
"tx_lock doesn't pay enough: expected {:?}, got {:?}",
res.received,
amount
)
}
Ok(())
}
}
#[async_trait]
impl ImportOutput for BobWallet<'_> {
async fn import_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(Network::Mainnet, public_spend_key, public_view_key);
let _ = self
.0
.bob_wallet_rpc_client()
.generate_from_keys(
&address.to_string(),
&private_spend_key.to_string(),
&PrivateKey::from(private_view_key).to_string(),
)
.await?;
Ok(())
}
}
#[async_trait]
impl ImportOutput for AliceWallet<'_> {
async fn import_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(Network::Mainnet, public_spend_key, public_view_key);
let _ = self
.0
.alice_wallet_rpc_client()
.generate_from_keys(
&address.to_string(),
&private_spend_key.to_string(),
&PrivateKey::from(private_view_key).to_string(),
)
.await?;
Ok(())
}
}