-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Straylight
committed
Dec 29, 2022
0 parents
commit 58acbfd
Showing
21 changed files
with
4,710 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/target | ||
Cargo.lock |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
max_width=100 | ||
edition="2021" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.