mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-10-01 01:45:40 -04:00
Swap Monero for Bitcoin
Co-authored-by: rishflab <rishflab@hotmail.com> Co-authored-by: Philipp Hoenisch <philipp@hoenisch.at> Co-authored-by: Tobin C. Harding <tobin@coblox.tech>
This commit is contained in:
parent
818e522bd4
commit
1f99cf001c
77
.github/workflows/ci.yml
vendored
Normal file
77
.github/workflows/ci.yml
vendored
Normal 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
2
Cargo.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[workspace]
|
||||
members = ["monero-harness", "xmr-btc"]
|
18
monero-harness/Cargo.toml
Normal file
18
monero-harness/Cargo.toml
Normal 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"
|
21
monero-harness/LICENSE
Normal file
21
monero-harness/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 CoBloX
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
12
monero-harness/README.md
Normal file
12
monero-harness/README.md
Normal 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.
|
1
monero-harness/rust-toolchain
Normal file
1
monero-harness/rust-toolchain
Normal file
@ -0,0 +1 @@
|
||||
1.46.0
|
9
monero-harness/rustfmt.toml
Normal file
9
monero-harness/rustfmt.toml
Normal 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
293
monero-harness/src/image.rs
Normal 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
296
monero-harness/src/lib.rs
Normal 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
63
monero-harness/src/rpc.rs
Normal 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, ¶ms);
|
||||
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);
|
||||
}
|
||||
}
|
132
monero-harness/src/rpc/monerod.rs
Normal file
132
monero-harness/src/rpc/monerod.rs
Normal 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,
|
||||
}
|
397
monero-harness/src/rpc/wallet.rs
Normal file
397
monero-harness/src/rpc/wallet.rs
Normal 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,
|
||||
}
|
36
monero-harness/tests/client.rs
Normal file
36
monero-harness/tests/client.rs
Normal 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);
|
||||
}
|
47
monero-harness/tests/monerod.rs
Normal file
47
monero-harness/tests/monerod.rs
Normal 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);
|
||||
}
|
89
monero-harness/tests/wallet.rs
Normal file
89
monero-harness/tests/wallet.rs
Normal 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
1
rust-toolchain
Normal file
@ -0,0 +1 @@
|
||||
nightly-2020-08-13
|
9
rustfmt.toml
Normal file
9
rustfmt.toml
Normal 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
27
xmr-btc/Cargo.toml
Normal 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
516
xmr-btc/src/alice.rs
Normal 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
209
xmr-btc/src/bitcoin.rs
Normal 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)
|
||||
}
|
498
xmr-btc/src/bitcoin/transactions.rs
Normal file
498
xmr-btc/src/bitcoin/transactions.rs
Normal 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)
|
||||
}
|
||||
}
|
116
xmr-btc/src/bitcoin/wallet.rs
Normal file
116
xmr-btc/src/bitcoin/wallet.rs
Normal 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
472
xmr-btc/src/bob.rs
Normal 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
384
xmr-btc/src/lib.rs
Normal 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
123
xmr-btc/src/monero.rs
Normal 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<()>;
|
||||
}
|
125
xmr-btc/src/monero/wallet.rs
Normal file
125
xmr-btc/src/monero/wallet.rs
Normal 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(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user