Skip to content

Commit

Permalink
Initialize repository
Browse files Browse the repository at this point in the history
  • Loading branch information
Straylight committed Dec 29, 2022
0 parents commit 58acbfd
Show file tree
Hide file tree
Showing 21 changed files with 4,710 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
Cargo.lock
675 changes: 675 additions & 0 deletions COPYING

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "whirlpool-client"
version = "1.0.0"
authors = ["Straylight <straylight_orbit@protonmail.com>"]
description = "Whirlpool Coinjoin Client"
license = "GPL-3.0-only"
keywords = ["coinjoin", "whirlpool", "bitcoin"]
edition = "2021"

[features]
default = ["client"]
client = ["port_check", "rand", "socks", "tungstenite", "ureq"]

[dependencies]
bitcoin = { version = "0.29", features = ["default", "base64", "rand", "serde"] }
blind-rsa-signatures = "0.12"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

port_check = { version = "0.1.5", optional = true }
rand = { version = "0.8", optional = true }
socks = { version = "0.3", optional = true }
tungstenite = {version = "0.17", features = ["rustls-tls-native-roots"], optional = true }
ureq = { version = "2.4", features = ["socks-proxy"] , optional = true }
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## Samourai Whirlpool Client

A Samourai Whirlpool client written in pure Rust.

It includes a Tor-only client implementation that can be turned off by disabling the `client`
feature. In that case, alternative clients (such as those supporting the `async/await` paradigm
or I2P connectivity) may be written using the Whirlpool primitives that the library provides.

## Basic Usage

`client::API` provides an interface to the REST API. Exposes pool info, tx0 creation and tx0
broadcast functionality.

`client::start` starts a new mix using `mix::Params`. The supplied input must come either from
premix or postmix (i.e. must be a tx0 descendant). Optionally notifies the caller with mix progress
if the `notify` parameter is supplied and set up to do so.

## Donations

If you find this library useful, donations are greatly appreciated. Our donation address is
`bc1qdqyddz0fh8d24gkwhuu5apcf8uzk4nyxw2035a`

## Issues/Questions

Email address in `Cargo.toml`

PGP key:

```
-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEYcBg1RYJKwYBBAHaRw8BAQdAlidgYUg/BziI+qJrEeBYpcJHQkur3KLT
Ubmrq4NnVBnNQXN0cmF5bGlnaHRfb3JiaXRAcHJvdG9ubWFpbC5jb20gPHN0
cmF5bGlnaHRfb3JiaXRAcHJvdG9ubWFpbC5jb20+wo8EEBYKACAFAmHAYNUG
CwkHCAMCBBUICgIEFgIBAAIZAQIbAwIeAQAhCRAuw2tD1SBUPBYhBC3Fy4ua
0Z4tk+DZmS7Da0PVIFQ8+kMA/0sF1fSezjin1keftDfjuCEyYdHCQgWEuwSb
Qvlwm+OGAQDzgZ7xdub1eL5rVzEMuVdtC3qOxOwOa02vS48XHGDJBc44BGHA
YNUSCisGAQQBl1UBBQEBB0BRCat3z3/ayilbLPvN6g9dNli2n5lceU4EAURj
k3hZCgMBCAfCeAQYFggACQUCYcBg1QIbDAAhCRAuw2tD1SBUPBYhBC3Fy4ua
0Z4tk+DZmS7Da0PVIFQ87BoBAOV+4dVY5iyJ3TL2Yaqc/fwADW53avrDO3sd
yiJSUkVPAQC4lWifjKlVYUT3yPICSbv7mtdYAFzDCbZTptksBV+EBg==
=/C35
-----END PGP PUBLIC KEY BLOCK-----
```

## License

The library is licensed under GPLv3. See [COPYING](COPYING) for details.
2 changes: 2 additions & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
max_width=100
edition="2021"
50 changes: 50 additions & 0 deletions src/alt_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// whirlpool-client-rs
// Copyright (C) 2022 Straylight <straylight_orbit@protonmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

//! This module contains functionality for converting alternate identity requests into HTTP requests.
//! Care must be taken to never send these requests through the same identity as the one responsible
//! for mix coordination.
use crate::{endpoints::Endpoints, http, mix::AlternateIdentityRequest};
use serde::Deserialize;

/// Response payload returned by alternate identity (output related) endpoints.
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum AltIdResponse {
Error { message: String },
Ok {},
}

impl AlternateIdentityRequest {
pub fn into_request(self, endpoints: &Endpoints) -> http::Request<AltIdResponse> {
let url = match &self {
AlternateIdentityRequest::CheckOutput { .. } => endpoints.check_output.clone(),
AlternateIdentityRequest::RegisterOutput { .. } => endpoints.register_output.clone(),
};
let body: Vec<u8> = self.try_into().unwrap();

http::Request {
url,
method: http::Method::POST,
body: Some(http::Body {
body,
content_type: "application/json",
}),
alt_id: true,
de_type: std::marker::PhantomData,
}
}
}
32 changes: 32 additions & 0 deletions src/codec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// whirlpool-client-rs
// Copyright (C) 2022 Straylight <straylight_orbit@protonmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

mod decode;
mod encode;
mod stomp;

#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
Stomp(stomp::Error),
Json(serde_json::Error),
Z85,
RSA(blind_rsa_signatures::Error),
Bitcoin(bitcoin::consensus::encode::Error),
UnsupportedNetwork(String),
UnknownWhirlpoolMessage,
UnsolictedMessage,
ServerError(String),
}
234 changes: 234 additions & 0 deletions src/codec/decode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// whirlpool-client-rs
// Copyright (C) 2022 Straylight <straylight_orbit@protonmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

use std::collections::HashMap;

use serde::Deserialize;

use crate::mix::CoordinatorResponse;
use crate::util::z85;

use super::{stomp::decode::ServerFrame, Error};

impl TryFrom<&[u8]> for CoordinatorResponse {
type Error = Error;

fn try_from(data: &[u8]) -> Result<Self, Error> {
let frame = ServerFrame::parse(&data)?;

match frame {
ServerFrame::Connected => Ok(CoordinatorResponse::Connected),

ServerFrame::Message { body, headers } => {
match message_type(&headers).ok_or(Error::UnknownWhirlpoolMessage)? {
"SubscribePoolResponse" => {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Response {
network_id: String,
denomination: u64,
must_mix_balance_min: u64,
must_mix_balance_cap: u64,
must_mix_balance_max: u64,
}

let response: Response = serde_json::from_slice(body)?;

Ok(CoordinatorResponse::SubscribedPool {
network: parse_network(&response.network_id)?,
denomination: response.denomination,
min_amount: response.must_mix_balance_min,
cap_amount: response.must_mix_balance_cap,
max_amount: response.must_mix_balance_max,
})
}

"ConfirmInputMixStatusNotification" => {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Response {
mix_id: String,
public_key_64: String,
}

let response: Response = serde_json::from_slice(body)?;
let public_key = blind_rsa_signatures::PublicKey::from_der(
&z85::decode(response.public_key_64).ok_or(Error::Z85)?,
)?;

Ok(CoordinatorResponse::ConfirmInputNotification {
mix_id: response.mix_id,
public_key,
})
}

"ConfirmInputResponse" => {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Response {
mix_id: String,
signed_bordereau_64: String,
}

let response: Response = serde_json::from_slice(body)?;
let blind_signature = blind_rsa_signatures::BlindSignature::new(
z85::decode(&response.signed_bordereau_64).ok_or(Error::Z85)?,
);

if blind_signature.0.len() != 256 {
log::error!(
"BLIND_SIG error: length: {} Z85:\n{}\n",
blind_signature.0.len(),
response.signed_bordereau_64
);
}

Ok(CoordinatorResponse::ConfirmedInput {
mix_id: response.mix_id,
blind_signature,
})
}

"RegisterOutputMixStatusNotification" => {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Response {
mix_id: String,
inputs_hash: String,
}

let response: Response = serde_json::from_slice(body)?;

Ok(CoordinatorResponse::RegisterOutputNotification {
mix_id: response.mix_id,
inputs_hash: response.inputs_hash,
})
}

"SigningMixStatusNotification" => {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Response {
mix_id: String,
transaction_64: String,
}
use bitcoin::consensus::Decodable;

let response: Response = serde_json::from_slice(body)?;
let transaction_64 =
z85::decode(response.transaction_64).ok_or(Error::Z85)?;
let transaction =
bitcoin::Transaction::consensus_decode(&mut transaction_64.as_slice())?;

Ok(CoordinatorResponse::SigningNotification {
mix_id: response.mix_id,
transaction,
})
}

"SuccessMixStatusNotification" => {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Response {
mix_id: String,
}

let response: Response = serde_json::from_slice(body)?;

Ok(CoordinatorResponse::MixSuccessful {
mix_id: response.mix_id,
})
}

"RevealOutputMixStatusNotification" => {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Response {
mix_id: String,
}

let response: Response = serde_json::from_slice(body)?;

Ok(CoordinatorResponse::RevealNotification {
mix_id: response.mix_id,
})
}

"FailMixStatusNotification" => {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Response {
mix_id: String,
}

let response: Response = serde_json::from_slice(body)?;

Ok(CoordinatorResponse::MixFailed {
mix_id: response.mix_id,
})
}

"ErrorResponse" => {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Response {
error_code: u32,
message: String,
}

let response: Response = serde_json::from_slice(body)?;

Ok(CoordinatorResponse::Error {
code: response.error_code,
message: response.message,
})
}

_ => Err(Error::UnknownWhirlpoolMessage),
}
}

ServerFrame::Receipt(_) => Err(Error::UnsolictedMessage),

ServerFrame::Error(error) => Err(Error::ServerError(error.to_owned())),
}
}
}

fn message_type<'a>(headers: &'a HashMap<&'a str, &'a str>) -> Option<&'a str> {
// example: com.samourai.whirlpool.protocol.websocket.messages.SubscribePoolResponse
headers.get("messageType")?.split('.').nth_back(0)
}

fn parse_network(value: &str) -> Result<bitcoin::Network, Error> {
match value {
"main" => Ok(bitcoin::Network::Bitcoin),
"test" => Ok(bitcoin::Network::Testnet),
"regtest" => Ok(bitcoin::Network::Regtest),
other => Err(Error::UnsupportedNetwork(other.to_owned())),
}
}

impl From<blind_rsa_signatures::Error> for Error {
fn from(error: blind_rsa_signatures::Error) -> Self {
Error::RSA(error)
}
}

impl From<bitcoin::consensus::encode::Error> for Error {
fn from(error: bitcoin::consensus::encode::Error) -> Self {
Error::Bitcoin(error)
}
}
Loading

0 comments on commit 58acbfd

Please sign in to comment.