mirror of
https://github.com/jwinterm/mwixnet.git
synced 2026-10-17 06:11:26 +02:00
Merge pull request #22 from scilio/reorg
async/await and reorg protection
This commit is contained in:
Generated
+4254
-716
File diff suppressed because it is too large
Load Diff
+49
-28
@@ -9,46 +9,67 @@ edition = "2021"
|
||||
members = ["onion"]
|
||||
|
||||
[dependencies]
|
||||
blake2 = { package = "blake2-rfc", version = "0.2"}
|
||||
arti-client = { version = "0.18.0", default-features = false, features = ["async-std", "rustls", "onion-service-client", "onion-service-service"] }
|
||||
arti-hyper = "0.18.0"
|
||||
async-std = { version = "1", features = ["tokio1"] }
|
||||
async-trait = "0.1.74"
|
||||
blake2 = { package = "blake2-rfc", version = "0.2" }
|
||||
byteorder = "1"
|
||||
bytes = "0.5.6"
|
||||
chacha20 = "0.8.1"
|
||||
bytes = "1.5.0"
|
||||
chacha20 = "0.9.1"
|
||||
chrono = "0.4.31"
|
||||
clap = { version = "2.33", features = ["yaml"] }
|
||||
curve25519-dalek = "2.1"
|
||||
ctrlc = { version = "3.1", features = ["termination"] }
|
||||
curve25519-dalek = "4.1.2"
|
||||
dirs = "2.0"
|
||||
ed25519-dalek = "1.0.1"
|
||||
ed25519-dalek = "2.1.1"
|
||||
function_name = "0.3.0"
|
||||
futures = "0.3"
|
||||
hmac = { version = "0.12.0", features = ["std"]}
|
||||
hyper = { version = "0.14", features = ["full"] }
|
||||
hyper-proxy = "0.9.1"
|
||||
itertools = { version = "0.10.3"}
|
||||
jsonrpc-core = "18.0"
|
||||
jsonrpc-derive = "18.0"
|
||||
jsonrpc-http-server = "18.0"
|
||||
fs-mistrust = "0.7.9"
|
||||
hmac = { version = "0.12.0", features = ["std"] }
|
||||
hyper = "0.14.28"
|
||||
hyper-tls = "0.6.0"
|
||||
itertools = { version = "0.12.0" }
|
||||
jsonrpc-core = "18.0.0"
|
||||
jsonrpc-derive = "18.0.0"
|
||||
jsonrpc-http-server = "18.0.0"
|
||||
lazy_static = "1"
|
||||
pbkdf2 = "0.8.0"
|
||||
rand = "0.7.3"
|
||||
remove_dir_all = "0.8.2"
|
||||
ring = "0.16"
|
||||
rpassword = "4.0"
|
||||
serde = { version = "1", features= ["derive"]}
|
||||
rusqlite = { version = "0.31.0", features = ["bundled"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_derive = "1"
|
||||
serde_json = "1"
|
||||
sha2 = "0.10.0"
|
||||
thiserror = "1.0.31"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
toml = "0.5"
|
||||
thiserror = "1.0.30"
|
||||
tls-api = "0.9.0"
|
||||
tls-api-native-tls = "0.9.0"
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
toml = "0.8.8"
|
||||
tor-hscrypto = "0.18.0"
|
||||
tor-hsrproxy = "0.18.0"
|
||||
tor-hsservice = "0.18.0"
|
||||
tor-llcrypto = "0.18.0"
|
||||
tor-keymgr = "0.18.0"
|
||||
tor-rtcompat = "0.18.0"
|
||||
x25519-dalek = "0.6.0"
|
||||
grin_onion = { path = "./onion" }
|
||||
grin_secp256k1zkp = { version = "0.7.12", features = ["bullet-proof-sizing"]}
|
||||
grin_util = { git = "https://github.com/mimblewimble/grin", branch = "master" }
|
||||
grin_api = { git = "https://github.com/mimblewimble/grin", branch = "master" }
|
||||
grin_core = { git = "https://github.com/mimblewimble/grin", branch = "master" }
|
||||
grin_chain = { git = "https://github.com/mimblewimble/grin", branch = "master" }
|
||||
grin_keychain = { git = "https://github.com/mimblewimble/grin", branch = "master" }
|
||||
grin_servers = { git = "https://github.com/mimblewimble/grin", branch = "master" }
|
||||
grin_store = { git = "https://github.com/mimblewimble/grin", branch = "master" }
|
||||
grin_wallet_api = { git = "https://github.com/mimblewimble/grin-wallet", tag = "v5.2.0-beta.1" }
|
||||
grin_wallet_impls = { git = "https://github.com/mimblewimble/grin-wallet", tag = "v5.2.0-beta.1" }
|
||||
grin_wallet_libwallet = { git = "https://github.com/mimblewimble/grin-wallet", tag = "v5.2.0-beta.1" }
|
||||
grin_wallet_util = { git = "https://github.com/mimblewimble/grin-wallet", tag = "v5.2.0-beta.1" }
|
||||
grin_secp256k1zkp = { version = "0.7.14", features = ["bullet-proof-sizing"] }
|
||||
grin_api = "5.3.1"
|
||||
grin_core = "5.3.1"
|
||||
grin_chain = "5.3.1"
|
||||
grin_keychain = "5.3.1"
|
||||
grin_p2p = "5.3.1"
|
||||
grin_servers = "5.3.1"
|
||||
grin_store = "5.3.1"
|
||||
grin_util = "5.3.1"
|
||||
grin_wallet_api = "5.3.1"
|
||||
grin_wallet_config = "5.3.1"
|
||||
grin_wallet_controller = "5.3.1"
|
||||
grin_wallet_impls = "5.3.1"
|
||||
grin_wallet_libwallet = "5.3.1"
|
||||
grin_wallet_util = "5.3.1"
|
||||
log = "0.4.20"
|
||||
@@ -47,6 +47,7 @@ In case of errors, the API will return a `SwapError` type with one of the follow
|
||||
- `FeeTooLow`: The provided fee is too low.
|
||||
- `StoreError`: An error occurred when saving swap to the data store.
|
||||
- `ClientError`: An error occurred during client communication.
|
||||
- `SwapTxNotFound`: The previous swap transaction was not found in data store.
|
||||
- `UnknownError`: An unknown error occurred.
|
||||
|
||||
### Example
|
||||
|
||||
+17
-17
@@ -6,33 +6,33 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
blake2 = { package = "blake2-rfc", version = "0.2"}
|
||||
blake2 = { package = "blake2-rfc", version = "0.2" }
|
||||
byteorder = "1"
|
||||
bytes = "0.5.6"
|
||||
chacha20 = "0.8.1"
|
||||
curve25519-dalek = "2.1"
|
||||
ed25519-dalek = "1.0.1"
|
||||
hmac = { version = "0.12.0", features = ["std"]}
|
||||
itertools = { version = "0.10.3"}
|
||||
hmac = { version = "0.12.0", features = ["std"] }
|
||||
itertools = { version = "0.10.3" }
|
||||
lazy_static = "1"
|
||||
rand = "0.7.3"
|
||||
rpassword = "4.0"
|
||||
serde = { version = "1", features= ["derive"]}
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_derive = "1"
|
||||
serde_json = "1"
|
||||
sha2 = "0.10.0"
|
||||
thiserror = "1.0.31"
|
||||
thiserror = "1"
|
||||
toml = "0.5"
|
||||
x25519-dalek = "0.6.0"
|
||||
grin_secp256k1zkp = { version = "0.7.11", features = ["bullet-proof-sizing"]}
|
||||
grin_util = { git = "https://github.com/mimblewimble/grin", branch = "master" }
|
||||
grin_api = { git = "https://github.com/mimblewimble/grin", branch = "master" }
|
||||
grin_core = { git = "https://github.com/mimblewimble/grin", branch = "master" }
|
||||
grin_chain = { git = "https://github.com/mimblewimble/grin", branch = "master" }
|
||||
grin_keychain = { git = "https://github.com/mimblewimble/grin", branch = "master" }
|
||||
grin_servers = { git = "https://github.com/mimblewimble/grin", branch = "master" }
|
||||
grin_store = { git = "https://github.com/mimblewimble/grin", branch = "master" }
|
||||
grin_wallet_api = { git = "https://github.com/mimblewimble/grin-wallet", tag = "v5.2.0-beta.1" }
|
||||
grin_wallet_impls = { git = "https://github.com/mimblewimble/grin-wallet", tag = "v5.2.0-beta.1" }
|
||||
grin_wallet_libwallet = { git = "https://github.com/mimblewimble/grin-wallet", tag = "v5.2.0-beta.1" }
|
||||
grin_wallet_util = { git = "https://github.com/mimblewimble/grin-wallet", tag = "v5.2.0-beta.1" }
|
||||
grin_secp256k1zkp = { version = "0.7.14", features = ["bullet-proof-sizing"] }
|
||||
grin_api = "5.3.1"
|
||||
grin_core = "5.3.1"
|
||||
grin_chain = "5.3.1"
|
||||
grin_keychain = "5.3.1"
|
||||
grin_servers = "5.3.1"
|
||||
grin_store = "5.3.1"
|
||||
grin_util = "5.3.1"
|
||||
grin_wallet_api = "5.3.1"
|
||||
grin_wallet_impls = "5.3.1"
|
||||
grin_wallet_libwallet = "5.3.1"
|
||||
grin_wallet_util = "5.3.1"
|
||||
@@ -162,7 +162,7 @@ pub fn sign(sk: &SecretKey, message: &[u8]) -> Result<DalekSignature, DalekError
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::crypto::dalek::test_util::rand_keypair;
|
||||
use crate::test_util::rand_keypair;
|
||||
use grin_core::ser::{self, ProtocolVersion};
|
||||
use grin_util::ToHex;
|
||||
use rand::Rng;
|
||||
|
||||
+24
-23
@@ -1,15 +1,15 @@
|
||||
pub mod crypto;
|
||||
pub mod onion;
|
||||
pub mod util;
|
||||
|
||||
use crate::crypto::secp::{random_secret, Commitment, SecretKey};
|
||||
use crate::onion::{new_stream_cipher, Onion, OnionError, Payload, RawBytes};
|
||||
|
||||
use chacha20::cipher::StreamCipher;
|
||||
use grin_core::core::FeeFields;
|
||||
use secp256k1zkp::pedersen::RangeProof;
|
||||
use x25519_dalek::PublicKey as xPublicKey;
|
||||
use x25519_dalek::{SharedSecret, StaticSecret};
|
||||
use x25519_dalek::PublicKey as xPublicKey;
|
||||
|
||||
use crate::crypto::secp::{Commitment, random_secret, SecretKey};
|
||||
use crate::onion::{new_stream_cipher, Onion, OnionError, Payload, RawBytes};
|
||||
|
||||
pub mod crypto;
|
||||
pub mod onion;
|
||||
pub mod util;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Hop {
|
||||
@@ -28,7 +28,7 @@ pub fn new_hop(
|
||||
Hop {
|
||||
server_pubkey: xPublicKey::from(&StaticSecret::from(server_key.0.clone())),
|
||||
excess: hop_excess.clone(),
|
||||
fee: FeeFields::from(fee as u32),
|
||||
fee: FeeFields::from(fee),
|
||||
rangeproof: proof,
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ pub fn create_onion(commitment: &Commitment, hops: &Vec<Hop>) -> Result<Onion, O
|
||||
for i in (0..shared_secrets.len()).rev() {
|
||||
let mut cipher = new_stream_cipher(&shared_secrets[i])?;
|
||||
for j in i..shared_secrets.len() {
|
||||
cipher.apply_keystream(&mut enc_payloads[j]);
|
||||
cipher.apply_keystream(enc_payloads[j].as_mut_slice());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,14 +84,15 @@ pub fn create_onion(commitment: &Commitment, hops: &Vec<Hop>) -> Result<Onion, O
|
||||
}
|
||||
|
||||
pub mod test_util {
|
||||
use super::*;
|
||||
use grin_core::core::hash::Hash;
|
||||
use grin_util::ToHex;
|
||||
use rand::{RngCore, thread_rng};
|
||||
use secp256k1zkp::Secp256k1;
|
||||
|
||||
use crate::crypto::dalek::DalekPublicKey;
|
||||
use crate::crypto::secp;
|
||||
|
||||
use grin_core::core::hash::Hash;
|
||||
use grin_util::ToHex;
|
||||
use rand::{thread_rng, RngCore};
|
||||
use secp256k1zkp::Secp256k1;
|
||||
use super::*;
|
||||
|
||||
pub fn rand_onion() -> Onion {
|
||||
let commit = rand_commit();
|
||||
@@ -116,20 +117,20 @@ pub mod test_util {
|
||||
}
|
||||
|
||||
pub fn rand_commit() -> Commitment {
|
||||
secp::commit(rand::thread_rng().next_u64(), &secp::random_secret()).unwrap()
|
||||
secp::commit(thread_rng().next_u64(), &random_secret()).unwrap()
|
||||
}
|
||||
|
||||
pub fn rand_hash() -> Hash {
|
||||
Hash::from_hex(secp::random_secret().to_hex().as_str()).unwrap()
|
||||
Hash::from_hex(random_secret().to_hex().as_str()).unwrap()
|
||||
}
|
||||
|
||||
pub fn rand_proof() -> RangeProof {
|
||||
let secp = Secp256k1::new();
|
||||
secp.bullet_proof(
|
||||
rand::thread_rng().next_u64(),
|
||||
secp::random_secret(),
|
||||
secp::random_secret(),
|
||||
secp::random_secret(),
|
||||
thread_rng().next_u64(),
|
||||
random_secret(),
|
||||
random_secret(),
|
||||
random_secret(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
@@ -153,8 +154,8 @@ pub mod test_util {
|
||||
let rp = secp.bullet_proof(
|
||||
out_value,
|
||||
blind.clone(),
|
||||
secp::random_secret(),
|
||||
secp::random_secret(),
|
||||
random_secret(),
|
||||
random_secret(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
+22
-20
@@ -1,22 +1,23 @@
|
||||
use crate::crypto::secp::{self, Commitment, RangeProof, SecretKey};
|
||||
use crate::util::{read_optional, vec_to_array, write_optional};
|
||||
|
||||
use chacha20::cipher::{NewCipher, StreamCipher};
|
||||
use chacha20::{ChaCha20, Key, Nonce};
|
||||
use grin_core::core::FeeFields;
|
||||
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
|
||||
use grin_util::{self, ToHex};
|
||||
use hmac::digest::InvalidLength;
|
||||
use hmac::{Hmac, Mac};
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::Deserialize;
|
||||
use sha2::Sha256;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::result::Result;
|
||||
|
||||
use chacha20::{ChaCha20, Key, Nonce};
|
||||
use chacha20::cipher::{NewCipher, StreamCipher};
|
||||
use grin_core::core::FeeFields;
|
||||
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
|
||||
use grin_util::{self, ToHex};
|
||||
use hmac::{Hmac, Mac};
|
||||
use hmac::digest::InvalidLength;
|
||||
use serde::Deserialize;
|
||||
use serde::ser::SerializeStruct;
|
||||
use sha2::Sha256;
|
||||
use thiserror::Error;
|
||||
use x25519_dalek::{PublicKey as xPublicKey, SharedSecret, StaticSecret};
|
||||
|
||||
use crate::crypto::secp::{self, Commitment, RangeProof, SecretKey};
|
||||
use crate::util::{read_optional, vec_to_array, write_optional};
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
pub type RawBytes = Vec<u8>;
|
||||
|
||||
@@ -328,12 +329,13 @@ impl From<ser::Error> for OnionError {
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::crypto::secp::random_secret;
|
||||
use crate::{new_hop, Hop};
|
||||
|
||||
use grin_core::core::FeeFields;
|
||||
|
||||
use crate::{create_onion, Hop, new_hop};
|
||||
use crate::crypto::secp::random_secret;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Test end-to-end Onion creation and unwrapping logic.
|
||||
#[test]
|
||||
fn onion() {
|
||||
@@ -376,12 +378,12 @@ pub mod tests {
|
||||
hops.push(hop);
|
||||
}
|
||||
|
||||
let mut onion_packet = test_util::create_onion(&commitment, &hops).unwrap();
|
||||
let mut onion_packet = create_onion(&commitment, &hops).unwrap();
|
||||
|
||||
let mut payload = Payload {
|
||||
next_ephemeral_pk: onion_packet.ephemeral_pubkey.clone(),
|
||||
excess: random_secret(),
|
||||
fee: FeeFields::from(fee_per_hop as u32),
|
||||
fee: FeeFields::from(fee_per_hop),
|
||||
rangeproof: None,
|
||||
};
|
||||
for i in 0..5 {
|
||||
@@ -393,6 +395,6 @@ pub mod tests {
|
||||
assert!(payload.rangeproof.is_some());
|
||||
assert_eq!(payload.rangeproof.unwrap(), hops[4].rangeproof.unwrap());
|
||||
assert_eq!(secp::commit(out_value, &final_blind).unwrap(), final_commit);
|
||||
assert_eq!(payload.fee, FeeFields::from(fee_per_hop as u32));
|
||||
assert_eq!(payload.fee, FeeFields::from(fee_per_hop));
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -19,10 +19,10 @@ use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// let mut writer = vec![];
|
||||
/// let mut buf = vec![];
|
||||
/// let optional_value: Option<u32> = Some(10);
|
||||
/// write_optional(&mut writer, &optional_value);
|
||||
/// assert_eq!(buf, &[1, 0, 0, 0, 10]);
|
||||
/// grin_onion::util::write_optional(&mut grin_core::ser::BinWriter::default(&mut buf), &optional_value).unwrap();
|
||||
/// assert_eq!(&buf, &[1, 0, 0, 0, 10]);
|
||||
/// ```
|
||||
pub fn write_optional<O: Writeable, W: Writer>(
|
||||
writer: &mut W,
|
||||
@@ -58,8 +58,8 @@ pub fn write_optional<O: Writeable, W: Writer>(
|
||||
///
|
||||
/// ```
|
||||
/// let mut buf: &[u8] = &[1, 0, 0, 0, 10];
|
||||
/// let mut reader = BinReader::new(&mut buf, ProtocolVersion::local(), DeserializationMode::default());
|
||||
/// let optional_value: Option<u32> = read_optional(&mut reader).unwrap();
|
||||
/// let mut reader = grin_core::ser::BinReader::new(&mut buf, grin_core::ser::ProtocolVersion::local(), grin_core::ser::DeserializationMode::default());
|
||||
/// let optional_value: Option<u32> = grin_onion::util::read_optional(&mut reader).unwrap();
|
||||
/// assert_eq!(optional_value, Some(10));
|
||||
/// ```
|
||||
pub fn read_optional<O: Readable, R: Reader>(reader: &mut R) -> Result<Option<O>, ser::Error> {
|
||||
@@ -87,7 +87,7 @@ pub fn read_optional<O: Readable, R: Reader>(reader: &mut R) -> Result<Option<O>
|
||||
///
|
||||
/// ```
|
||||
/// let v = vec![0, 1, 2, 3, 4, 5];
|
||||
/// let a = vec_to_array::<4>(&v).unwrap();
|
||||
/// let a = grin_onion::util::vec_to_array::<4>(&v).unwrap();
|
||||
/// assert_eq!(a, [0, 1, 2, 3]);
|
||||
/// ```
|
||||
pub fn vec_to_array<const S: usize>(vec: &Vec<u8>) -> Result<[u8; S], ser::Error> {
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
use config::ServerConfig;
|
||||
use node::HttpGrinNode;
|
||||
use store::SwapStore;
|
||||
use wallet::HttpWallet;
|
||||
|
||||
use crate::client::{MixClient, MixClientImpl};
|
||||
use crate::node::GrinNode;
|
||||
use crate::store::StoreError;
|
||||
use clap::App;
|
||||
use grin_core::global;
|
||||
use grin_core::global::ChainTypes;
|
||||
use grin_onion::crypto;
|
||||
use grin_onion::crypto::dalek::DalekPublicKey;
|
||||
use grin_util::{StopState, ZeroingString};
|
||||
use rpassword;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
|
||||
mod client;
|
||||
mod config;
|
||||
mod node;
|
||||
mod servers;
|
||||
mod store;
|
||||
mod tor;
|
||||
mod tx;
|
||||
mod wallet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::{sleep, spawn};
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::App;
|
||||
use grin_core::global;
|
||||
use grin_core::global::ChainTypes;
|
||||
use grin_util::{StopState, ZeroingString};
|
||||
use rand::{Rng, thread_rng};
|
||||
use rpassword;
|
||||
use tor_rtcompat::PreferredRuntime;
|
||||
|
||||
use grin_onion::crypto;
|
||||
use grin_onion::crypto::dalek::DalekPublicKey;
|
||||
use mwixnet::config::{self, ServerConfig};
|
||||
use mwixnet::mix_client::{MixClient, MixClientImpl};
|
||||
use mwixnet::node::GrinNode;
|
||||
use mwixnet::node::HttpGrinNode;
|
||||
use mwixnet::servers;
|
||||
use mwixnet::store::StoreError;
|
||||
use mwixnet::store::SwapStore;
|
||||
use mwixnet::tor;
|
||||
use mwixnet::wallet::HttpWallet;
|
||||
|
||||
const DEFAULT_INTERVAL: u32 = 12 * 60 * 60;
|
||||
|
||||
@@ -37,7 +34,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let yml = load_yaml!("../mwixnet.yml");
|
||||
let yml = load_yaml!("mwixnet.yml");
|
||||
let args = App::from_yaml(yml).get_matches();
|
||||
let chain_type = if args.is_present("testnet") {
|
||||
ChainTypes::Testnet
|
||||
@@ -59,7 +56,6 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.value_of("round_time")
|
||||
.map(|t| t.parse::<u32>().unwrap());
|
||||
let bind_addr = args.value_of("bind_addr");
|
||||
let socks_addr = args.value_of("socks_addr");
|
||||
let grin_node_url = args.value_of("grin_node_url");
|
||||
let grin_node_secret_path = args.value_of("grin_node_secret_path");
|
||||
let wallet_owner_url = args.value_of("wallet_owner_url");
|
||||
@@ -84,7 +80,6 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
key: crypto::secp::random_secret(),
|
||||
interval_s: round_time.unwrap_or(DEFAULT_INTERVAL),
|
||||
addr: bind_addr.unwrap_or("127.0.0.1:3000").parse()?,
|
||||
socks_proxy_addr: socks_addr.unwrap_or("127.0.0.1:3001").parse()?,
|
||||
grin_node_url: match grin_node_url {
|
||||
Some(u) => u.parse()?,
|
||||
None => config::grin_node_url(&chain_type),
|
||||
@@ -146,11 +141,6 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
server_config.addr = bind_addr.parse()?;
|
||||
}
|
||||
|
||||
// Override socks_addr, if supplied
|
||||
if let Some(socks_addr) = socks_addr {
|
||||
server_config.socks_proxy_addr = socks_addr.parse()?;
|
||||
}
|
||||
|
||||
// Override prev_server, if supplied
|
||||
if let Some(prev_server) = prev_server {
|
||||
server_config.prev_server = Some(prev_server);
|
||||
@@ -168,18 +158,21 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
);
|
||||
|
||||
// Node API health check
|
||||
if let Err(e) = node.get_chain_height() {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
if let Err(e) = rt.block_on(node.async_get_chain_tip()) {
|
||||
eprintln!("Node communication failure. Is node listening?");
|
||||
return Err(e.into());
|
||||
};
|
||||
|
||||
// Open wallet
|
||||
let wallet_pass = prompt_wallet_password(&args.value_of("wallet_pass"));
|
||||
let wallet = HttpWallet::open_wallet(
|
||||
let wallet = rt.block_on(HttpWallet::async_open_wallet(
|
||||
&server_config.wallet_owner_url,
|
||||
&server_config.wallet_owner_api_secret(),
|
||||
&wallet_pass,
|
||||
);
|
||||
));
|
||||
let wallet = match wallet {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
@@ -188,21 +181,29 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
};
|
||||
|
||||
let mut tor_process = tor::init_tor_listener(&server_config)?;
|
||||
let tor_instance = rt.block_on(tor::async_init_tor(
|
||||
PreferredRuntime::current().unwrap(),
|
||||
&config::get_grin_path(&chain_type).to_str().unwrap(),
|
||||
&server_config,
|
||||
))?;
|
||||
let tor_instance = Arc::new(grin_util::Mutex::new(tor_instance));
|
||||
let tor_clone = tor_instance.clone();
|
||||
|
||||
let stop_state = Arc::new(StopState::new());
|
||||
let stop_state_clone = stop_state.clone();
|
||||
|
||||
let rt = Runtime::new()?;
|
||||
rt.spawn(async move {
|
||||
futures::executor::block_on(build_signals_fut());
|
||||
let _ = tor_process.kill();
|
||||
tor_clone.lock().stop();
|
||||
stop_state_clone.stop();
|
||||
});
|
||||
|
||||
let next_mixer: Option<Arc<dyn MixClient>> = server_config.next_server.clone().map(|pk| {
|
||||
let client: Arc<dyn MixClient> =
|
||||
Arc::new(MixClientImpl::new(server_config.clone(), pk.clone()));
|
||||
let client: Arc<dyn MixClient> = Arc::new(MixClientImpl::new(
|
||||
server_config.clone(),
|
||||
tor_instance.clone(),
|
||||
pk.clone(),
|
||||
));
|
||||
client
|
||||
});
|
||||
|
||||
@@ -213,13 +214,26 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
server_config.server_pubkey().to_hex()
|
||||
);
|
||||
|
||||
servers::mix_rpc::listen(
|
||||
let (_, http_server) = servers::mix_rpc::listen(
|
||||
rt.handle(),
|
||||
server_config,
|
||||
next_mixer,
|
||||
Arc::new(wallet),
|
||||
Arc::new(node),
|
||||
stop_state,
|
||||
)
|
||||
)?;
|
||||
|
||||
let close_handle = http_server.close_handle();
|
||||
let round_handle = spawn(move || loop {
|
||||
if stop_state.is_stopped() {
|
||||
close_handle.close();
|
||||
break;
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100));
|
||||
});
|
||||
|
||||
http_server.wait();
|
||||
round_handle.join().unwrap();
|
||||
} else {
|
||||
println!(
|
||||
"Starting SWAP server with public key {:?}",
|
||||
@@ -237,15 +251,75 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
)?;
|
||||
|
||||
// Start the mwixnet JSON-RPC HTTP 'swap' server
|
||||
servers::swap_rpc::listen(
|
||||
server_config,
|
||||
let (swap_server, http_server) = servers::swap_rpc::listen(
|
||||
rt.handle(),
|
||||
&server_config,
|
||||
next_mixer,
|
||||
Arc::new(wallet),
|
||||
Arc::new(node),
|
||||
store,
|
||||
stop_state,
|
||||
)
|
||||
)?;
|
||||
|
||||
let close_handle = http_server.close_handle();
|
||||
let round_handle = spawn(move || {
|
||||
let mut rng = thread_rng();
|
||||
let mut secs = 0u32;
|
||||
let mut reorg_secs = 0u32;
|
||||
let mut reorg_window = rng.gen_range(900u32, 3600u32);
|
||||
let prev_tx = Arc::new(Mutex::new(None));
|
||||
let server = swap_server.clone();
|
||||
|
||||
loop {
|
||||
if stop_state.is_stopped() {
|
||||
close_handle.close();
|
||||
break;
|
||||
}
|
||||
|
||||
sleep(Duration::from_secs(1));
|
||||
secs = (secs + 1) % server_config.interval_s;
|
||||
reorg_secs = (reorg_secs + 1) % reorg_window;
|
||||
|
||||
if secs == 0 {
|
||||
let prev_tx_clone = prev_tx.clone();
|
||||
let server_clone = server.clone();
|
||||
rt.spawn(async move {
|
||||
let result = server_clone.lock().await.execute_round().await;
|
||||
let mut prev_tx_lock = prev_tx_clone.lock().unwrap();
|
||||
*prev_tx_lock = match result {
|
||||
Ok(Some(tx)) => Some(tx),
|
||||
_ => None,
|
||||
};
|
||||
});
|
||||
reorg_secs = 0;
|
||||
reorg_window = rng.gen_range(900u32, 3600u32);
|
||||
} else if reorg_secs == 0 {
|
||||
let prev_tx_clone = prev_tx.clone();
|
||||
let server_clone = server.clone();
|
||||
rt.spawn(async move {
|
||||
let tx_option = {
|
||||
let prev_tx_lock = prev_tx_clone.lock().unwrap();
|
||||
prev_tx_lock.clone()
|
||||
}; // Lock is dropped here
|
||||
|
||||
if let Some(tx) = tx_option {
|
||||
let result = server_clone.lock().await.check_reorg(&tx).await;
|
||||
let mut prev_tx_lock = prev_tx_clone.lock().unwrap();
|
||||
*prev_tx_lock = match result {
|
||||
Ok(Some(tx)) => Some(tx),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
});
|
||||
reorg_window = rng.gen_range(900u32, 3600u32);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
http_server.wait();
|
||||
round_handle.join().unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -38,10 +38,6 @@ args:
|
||||
help: Address to bind the rpc server to (e.g. 127.0.0.1:3000)
|
||||
long: bind_addr
|
||||
takes_value: true
|
||||
- socks_addr:
|
||||
help: Address to bind the SOCKS5 tor proxy to (e.g. 127.0.0.1:3001)
|
||||
long: socks_addr
|
||||
takes_value: true
|
||||
- prev_server:
|
||||
help: Hex public key of the previous swap/mix server
|
||||
long: prev_server
|
||||
-172
@@ -1,172 +0,0 @@
|
||||
use crate::config::ServerConfig;
|
||||
use crate::crypto::dalek;
|
||||
use crate::servers::mix_rpc::MixReq;
|
||||
use crate::tx::TxComponents;
|
||||
use crate::{tor, DalekPublicKey};
|
||||
use grin_onion::onion::Onion;
|
||||
|
||||
use grin_api::client;
|
||||
use grin_api::client::TimeOut;
|
||||
use grin_api::json_rpc::build_request;
|
||||
use grin_core::ser;
|
||||
use grin_core::ser::ProtocolVersion;
|
||||
use grin_wallet_util::OnionV3Address;
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
|
||||
use serde_json;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Error types for interacting with nodes
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ClientError {
|
||||
#[error("Tor Error: {0:?}")]
|
||||
Tor(tor::TorError),
|
||||
#[error("API Error: {0:?}")]
|
||||
API(grin_api::Error),
|
||||
#[error("Dalek Error: {0:?}")]
|
||||
Dalek(dalek::DalekError),
|
||||
#[error("Error parsing response: {0:?}")]
|
||||
ResponseParse(serde_json::Error),
|
||||
#[error("Custom client error: {0:?}")]
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
/// A client for consuming a mix API
|
||||
pub trait MixClient: Send + Sync {
|
||||
/// Swaps the outputs provided and returns the final swapped outputs and kernels.
|
||||
fn mix_outputs(&self, onions: &Vec<Onion>) -> Result<(Vec<usize>, TxComponents), ClientError>;
|
||||
}
|
||||
|
||||
pub struct MixClientImpl {
|
||||
config: ServerConfig,
|
||||
addr: OnionV3Address,
|
||||
}
|
||||
|
||||
impl MixClientImpl {
|
||||
pub fn new(config: ServerConfig, next_pubkey: DalekPublicKey) -> Self {
|
||||
let addr = OnionV3Address::from_bytes(next_pubkey.as_ref().to_bytes());
|
||||
MixClientImpl { config, addr }
|
||||
}
|
||||
|
||||
fn send_json_request<D: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
addr: &OnionV3Address,
|
||||
method: &str,
|
||||
params: &serde_json::Value,
|
||||
) -> Result<D, ClientError> {
|
||||
let _tor = tor::init_tor_sender(&self.config).map_err(ClientError::Tor)?;
|
||||
|
||||
let proxy = {
|
||||
let proxy_uri = format!("http://{:?}", self.config.socks_proxy_addr)
|
||||
.parse()
|
||||
.unwrap();
|
||||
let proxy = Proxy::new(Intercept::All, proxy_uri);
|
||||
//proxy.set_authorization(Authorization::basic("John Doe", "Agent1234"));
|
||||
let connector = HttpConnector::new();
|
||||
let proxy_connector = ProxyConnector::from_proxy(connector, proxy).unwrap();
|
||||
proxy_connector
|
||||
};
|
||||
|
||||
let url = format!("{}/v1", addr.to_http_str());
|
||||
let mut req = client::create_post_request(&url, None, &build_request(method, params))
|
||||
.map_err(ClientError::API)?;
|
||||
|
||||
let uri = url.parse().unwrap();
|
||||
if let Some(headers) = proxy.http_headers(&uri) {
|
||||
req.headers_mut().extend(headers.clone().into_iter());
|
||||
}
|
||||
|
||||
let res = client::send_request(req, TimeOut::default()).map_err(ClientError::API)?;
|
||||
|
||||
serde_json::from_str(&res).map_err(ClientError::ResponseParse)
|
||||
}
|
||||
}
|
||||
|
||||
impl MixClient for MixClientImpl {
|
||||
fn mix_outputs(&self, onions: &Vec<Onion>) -> Result<(Vec<usize>, TxComponents), ClientError> {
|
||||
let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap();
|
||||
let sig =
|
||||
dalek::sign(&self.config.key, serialized.as_slice()).map_err(ClientError::Dalek)?;
|
||||
let mix = MixReq::new(onions.clone(), sig);
|
||||
|
||||
let params = serde_json::json!(mix);
|
||||
|
||||
self.send_json_request::<(Vec<usize>, TxComponents)>(&self.addr, "mix", ¶ms)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod mock {
|
||||
use super::{ClientError, MixClient};
|
||||
use crate::tx::TxComponents;
|
||||
use grin_onion::onion::Onion;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct MockMixClient {
|
||||
results: HashMap<Vec<Onion>, (Vec<usize>, TxComponents)>,
|
||||
}
|
||||
|
||||
impl MockMixClient {
|
||||
pub fn new() -> MockMixClient {
|
||||
MockMixClient {
|
||||
results: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_response(&mut self, onions: &Vec<Onion>, r: (Vec<usize>, TxComponents)) {
|
||||
self.results.insert(onions.clone(), r);
|
||||
}
|
||||
}
|
||||
|
||||
impl MixClient for MockMixClient {
|
||||
fn mix_outputs(
|
||||
&self,
|
||||
onions: &Vec<Onion>,
|
||||
) -> Result<(Vec<usize>, TxComponents), ClientError> {
|
||||
self.results
|
||||
.get(onions)
|
||||
.map(|r| Ok(r.clone()))
|
||||
.unwrap_or(Err(ClientError::Custom("No response set for input".into())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test_util {
|
||||
use super::{ClientError, MixClient};
|
||||
use crate::crypto::dalek;
|
||||
use crate::crypto::secp::SecretKey;
|
||||
use crate::servers::mix::MixServer;
|
||||
use crate::tx::TxComponents;
|
||||
use crate::DalekPublicKey;
|
||||
use grin_core::ser;
|
||||
use grin_core::ser::ProtocolVersion;
|
||||
use grin_onion::onion::Onion;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Implementation of the 'MixClient' trait that calls a mix server implementation directly.
|
||||
/// No JSON-RPC serialization or socket communication occurs.
|
||||
#[derive(Clone)]
|
||||
pub struct DirectMixClient {
|
||||
pub key: SecretKey,
|
||||
pub mix_server: Arc<dyn MixServer>,
|
||||
}
|
||||
|
||||
impl MixClient for DirectMixClient {
|
||||
fn mix_outputs(
|
||||
&self,
|
||||
onions: &Vec<Onion>,
|
||||
) -> Result<(Vec<usize>, TxComponents), ClientError> {
|
||||
let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap();
|
||||
let sig = dalek::sign(&self.key, serialized.as_slice()).map_err(ClientError::Dalek)?;
|
||||
|
||||
sig.verify(
|
||||
&DalekPublicKey::from_secret(&self.key),
|
||||
serialized.as_slice(),
|
||||
)
|
||||
.unwrap();
|
||||
Ok(self.mix_server.mix_outputs(&onions, &sig).unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-26
@@ -1,20 +1,21 @@
|
||||
use crate::crypto::dalek::DalekPublicKey;
|
||||
use crate::crypto::secp::SecretKey;
|
||||
|
||||
use core::num::NonZeroU32;
|
||||
use grin_core::global::ChainTypes;
|
||||
use grin_util::{file, ToHex, ZeroingString};
|
||||
use grin_wallet_util::OnionV3Address;
|
||||
use rand::{thread_rng, Rng};
|
||||
use ring::{aead, pbkdf2};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::result::Result;
|
||||
|
||||
use grin_core::global::ChainTypes;
|
||||
use grin_util::{file, ToHex, ZeroingString};
|
||||
use grin_wallet_util::OnionV3Address;
|
||||
use rand::{Rng, thread_rng};
|
||||
use ring::{aead, pbkdf2};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use grin_onion::crypto::dalek::DalekPublicKey;
|
||||
use grin_onion::crypto::secp::SecretKey;
|
||||
|
||||
const GRIN_HOME: &str = ".grin";
|
||||
const NODE_API_SECRET_FILE_NAME: &str = ".api_secret";
|
||||
const WALLET_OWNER_API_SECRET_FILE_NAME: &str = ".owner_api_secret";
|
||||
@@ -28,8 +29,6 @@ pub struct ServerConfig {
|
||||
pub interval_s: u32,
|
||||
/// socket address the server listener should bind to
|
||||
pub addr: SocketAddr,
|
||||
/// socket address the tor sender should bind to
|
||||
pub socks_proxy_addr: SocketAddr,
|
||||
/// foreign api address of the grin node
|
||||
pub grin_node_url: SocketAddr,
|
||||
/// path to file containing api secret for the grin node
|
||||
@@ -39,10 +38,10 @@ pub struct ServerConfig {
|
||||
/// path to file containing secret for the grin wallet's owner api
|
||||
pub wallet_owner_secret_path: Option<String>,
|
||||
/// public key of the previous mix/swap server (e.g. N_1 if this is N_2)
|
||||
#[serde(with = "crate::crypto::dalek::option_dalek_pubkey_serde", default)]
|
||||
#[serde(with = "grin_onion::crypto::dalek::option_dalek_pubkey_serde", default)]
|
||||
pub prev_server: Option<DalekPublicKey>,
|
||||
/// public key of the next mix server
|
||||
#[serde(with = "crate::crypto::dalek::option_dalek_pubkey_serde", default)]
|
||||
#[serde(with = "grin_onion::crypto::dalek::option_dalek_pubkey_serde", default)]
|
||||
pub next_server: Option<DalekPublicKey>,
|
||||
}
|
||||
|
||||
@@ -181,14 +180,13 @@ struct RawConfig {
|
||||
nonce: String,
|
||||
interval_s: u32,
|
||||
addr: SocketAddr,
|
||||
socks_proxy_addr: SocketAddr,
|
||||
grin_node_url: SocketAddr,
|
||||
grin_node_secret_path: Option<String>,
|
||||
wallet_owner_url: SocketAddr,
|
||||
wallet_owner_secret_path: Option<String>,
|
||||
#[serde(with = "crate::crypto::dalek::option_dalek_pubkey_serde", default)]
|
||||
#[serde(with = "grin_onion::crypto::dalek::option_dalek_pubkey_serde", default)]
|
||||
prev_server: Option<DalekPublicKey>,
|
||||
#[serde(with = "crate::crypto::dalek::option_dalek_pubkey_serde", default)]
|
||||
#[serde(with = "grin_onion::crypto::dalek::option_dalek_pubkey_serde", default)]
|
||||
next_server: Option<DalekPublicKey>,
|
||||
}
|
||||
|
||||
@@ -206,7 +204,6 @@ pub fn write_config(
|
||||
nonce: encrypted.nonce,
|
||||
interval_s: server_config.interval_s,
|
||||
addr: server_config.addr,
|
||||
socks_proxy_addr: server_config.socks_proxy_addr,
|
||||
grin_node_url: server_config.grin_node_url,
|
||||
grin_node_secret_path: server_config.grin_node_secret_path.clone(),
|
||||
wallet_owner_url: server_config.wallet_owner_url,
|
||||
@@ -243,7 +240,6 @@ pub fn load_config(
|
||||
key: secret_key,
|
||||
interval_s: raw_config.interval_s,
|
||||
addr: raw_config.addr,
|
||||
socks_proxy_addr: raw_config.socks_proxy_addr,
|
||||
grin_node_url: raw_config.grin_node_url,
|
||||
grin_node_secret_path: raw_config.grin_node_secret_path,
|
||||
wallet_owner_url: raw_config.wallet_owner_url,
|
||||
@@ -254,10 +250,7 @@ pub fn load_config(
|
||||
}
|
||||
|
||||
pub fn get_grin_path(chain_type: &ChainTypes) -> PathBuf {
|
||||
let mut grin_path = match dirs::home_dir() {
|
||||
Some(p) => p,
|
||||
None => PathBuf::new(),
|
||||
};
|
||||
let mut grin_path = dirs::home_dir().unwrap_or_else(|| PathBuf::new());
|
||||
grin_path.push(GRIN_HOME);
|
||||
grin_path.push(chain_type.shortname());
|
||||
grin_path
|
||||
@@ -289,10 +282,13 @@ pub fn wallet_owner_url(_chain_type: &ChainTypes) -> SocketAddr {
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test_util {
|
||||
use crate::{DalekPublicKey, ServerConfig};
|
||||
use secp256k1zkp::SecretKey;
|
||||
use std::net::TcpListener;
|
||||
|
||||
use grin_onion::crypto::dalek::DalekPublicKey;
|
||||
use secp256k1zkp::SecretKey;
|
||||
|
||||
use crate::config::ServerConfig;
|
||||
|
||||
pub fn local_config(
|
||||
server_key: &SecretKey,
|
||||
prev_server: &Option<DalekPublicKey>,
|
||||
@@ -302,7 +298,6 @@ pub mod test_util {
|
||||
key: server_key.clone(),
|
||||
interval_s: 1,
|
||||
addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?,
|
||||
socks_proxy_addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?,
|
||||
grin_node_url: "127.0.0.1:3413".parse()?,
|
||||
grin_node_secret_path: None,
|
||||
wallet_owner_url: "127.0.0.1:3420".parse()?,
|
||||
@@ -316,8 +311,9 @@ pub mod test_util {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use grin_onion::crypto::secp;
|
||||
|
||||
use super::*;
|
||||
use crate::crypto::secp;
|
||||
|
||||
#[test]
|
||||
fn server_key_encrypt() {
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use grin_api::json_rpc;
|
||||
use grin_util::to_base64;
|
||||
use grin_wallet_api::{EncryptedRequest, EncryptedResponse, JsonId};
|
||||
use hyper::body::Body as HyperBody;
|
||||
use hyper::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, USER_AGENT};
|
||||
use hyper::Request;
|
||||
use serde_json::json;
|
||||
use thiserror::Error;
|
||||
|
||||
use secp256k1zkp::SecretKey;
|
||||
|
||||
/// Error types for HTTP client connections
|
||||
#[derive(Error, Debug)]
|
||||
pub enum HttpError {
|
||||
#[error("Error decrypting response")]
|
||||
DecryptResponseError(),
|
||||
#[error("Hyper HTTP error: {0:?}")]
|
||||
HyperHttpError(hyper::http::Error),
|
||||
#[error("Hyper request failed with error: {0:?}")]
|
||||
RequestFailed(hyper::Error),
|
||||
#[error("Error with response body: {0:?}")]
|
||||
ResponseBodyError(hyper::Error),
|
||||
#[error("Error deserializing JSON response: {0:?}")]
|
||||
ResponseJsonError(serde_json::Error),
|
||||
#[error("Error decoding JSON-RPC response: {0:?}")]
|
||||
ResponseParseError(json_rpc::Error),
|
||||
#[error("Wrong response code: {0}")]
|
||||
ResponseStatusError(hyper::StatusCode),
|
||||
}
|
||||
|
||||
pub async fn async_send_enc_request<D: serde::de::DeserializeOwned>(
|
||||
url: &String,
|
||||
api_secret: &Option<String>,
|
||||
method: &str,
|
||||
params: &serde_json::Value,
|
||||
shared_key: &SecretKey,
|
||||
) -> Result<D, HttpError> {
|
||||
let req = json!({
|
||||
"method": method,
|
||||
"params": params,
|
||||
"id": JsonId::IntId(1),
|
||||
"jsonrpc": "2.0",
|
||||
});
|
||||
let enc_req = EncryptedRequest::from_json(&JsonId::IntId(1), &req, &shared_key).unwrap();
|
||||
let req = build_request(&url, &api_secret, serde_json::to_string(&enc_req).unwrap())?;
|
||||
let response_str = send_request_async(req).await?;
|
||||
let enc_res: EncryptedResponse =
|
||||
serde_json::from_str(&response_str).map_err(HttpError::ResponseJsonError)?;
|
||||
|
||||
let decrypted = enc_res
|
||||
.decrypt(&shared_key)
|
||||
.map_err(|_| HttpError::DecryptResponseError())?;
|
||||
|
||||
let response: json_rpc::Response =
|
||||
serde_json::from_value(decrypted).map_err(HttpError::ResponseJsonError)?;
|
||||
let parsed = response
|
||||
.clone()
|
||||
.into_result()
|
||||
.map_err(HttpError::ResponseParseError)?;
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
pub async fn async_send_json_request<D: serde::de::DeserializeOwned>(
|
||||
url: &String,
|
||||
api_secret: &Option<String>,
|
||||
method: &str,
|
||||
params: &serde_json::Value,
|
||||
) -> Result<D, HttpError> {
|
||||
let req_body = json!({
|
||||
"method": method,
|
||||
"params": params,
|
||||
"id": 1,
|
||||
"jsonrpc": "2.0",
|
||||
});
|
||||
let req = build_request(&url, &api_secret, serde_json::to_string(&req_body).unwrap())?;
|
||||
let data = send_request_async(req).await?;
|
||||
let ser: json_rpc::Response =
|
||||
serde_json::from_str(&data).map_err(HttpError::ResponseJsonError)?;
|
||||
let parsed = ser
|
||||
.clone()
|
||||
.into_result()
|
||||
.map_err(HttpError::ResponseParseError)?;
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
pub fn build_request(
|
||||
url: &String,
|
||||
api_secret: &Option<String>,
|
||||
req_body: String,
|
||||
) -> Result<Request<HyperBody>, HttpError> {
|
||||
let mut req_builder = hyper::Request::builder();
|
||||
if let Some(api_secret) = api_secret {
|
||||
let basic_auth = format!("Basic {}", to_base64(&format!("grin:{}", api_secret)));
|
||||
req_builder = req_builder.header(AUTHORIZATION, basic_auth);
|
||||
}
|
||||
|
||||
req_builder
|
||||
.method(hyper::Method::POST)
|
||||
.uri(url)
|
||||
.header(USER_AGENT, "grin-client")
|
||||
.header(ACCEPT, "application/json")
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.body(HyperBody::from(req_body))
|
||||
.map_err(HttpError::HyperHttpError)
|
||||
}
|
||||
|
||||
async fn send_request_async(req: Request<HyperBody>) -> Result<String, HttpError> {
|
||||
let client = hyper::Client::builder()
|
||||
.pool_idle_timeout(Duration::from_secs(30))
|
||||
.build_http();
|
||||
|
||||
let resp = client
|
||||
.request(req)
|
||||
.await
|
||||
.map_err(HttpError::RequestFailed)?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(HttpError::ResponseStatusError(resp.status()));
|
||||
}
|
||||
|
||||
let raw = hyper::body::to_bytes(resp)
|
||||
.await
|
||||
.map_err(HttpError::ResponseBodyError)?;
|
||||
|
||||
Ok(String::from_utf8_lossy(&raw).to_string())
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub mod config;
|
||||
pub mod http;
|
||||
pub mod mix_client;
|
||||
pub mod node;
|
||||
pub mod servers;
|
||||
pub mod store;
|
||||
pub mod tor;
|
||||
pub mod tx;
|
||||
pub mod wallet;
|
||||
|
||||
pub use config::ServerConfig;
|
||||
pub use mix_client::MixClient;
|
||||
pub use node::{GrinNode, HttpGrinNode, NodeError};
|
||||
pub use servers::mix::{MixError, MixServer};
|
||||
pub use servers::mix_rpc::listen as mix_listen;
|
||||
pub use servers::swap::{SwapError, SwapServer};
|
||||
pub use servers::swap_rpc::listen as swap_listen;
|
||||
pub use store::{StoreError, SwapStore};
|
||||
pub use wallet::{HttpWallet, Wallet, WalletError};
|
||||
@@ -0,0 +1,189 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use grin_api::json_rpc::{build_request, Response};
|
||||
use grin_core::ser;
|
||||
use grin_core::ser::ProtocolVersion;
|
||||
use grin_wallet_util::OnionV3Address;
|
||||
use serde_json;
|
||||
use serde_json::json;
|
||||
use thiserror::Error;
|
||||
use tor_rtcompat::Runtime;
|
||||
|
||||
use grin_onion::crypto::dalek::{self, DalekPublicKey};
|
||||
use grin_onion::onion::Onion;
|
||||
|
||||
use crate::config::ServerConfig;
|
||||
use crate::servers::mix_rpc::{MixReq, MixResp};
|
||||
use crate::tor::TorService;
|
||||
use crate::{http, tor};
|
||||
|
||||
/// Error types for interacting with nodes
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MixClientError {
|
||||
#[error("Tor Error: {0:?}")]
|
||||
Tor(tor::TorError),
|
||||
#[error("Communication Error: {0:?}")]
|
||||
CommError(http::HttpError),
|
||||
#[error("Dalek Error: {0:?}")]
|
||||
Dalek(dalek::DalekError),
|
||||
#[error("Error decoding JSON response: {0:?}")]
|
||||
DecodeResponseError(serde_json::Error),
|
||||
#[error("Error in JSON-RPC response: {0:?}")]
|
||||
ResponseError(grin_api::json_rpc::RpcError),
|
||||
#[error("Custom client error: {0:?}")]
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
/// A client for consuming a mix API
|
||||
#[async_trait]
|
||||
pub trait MixClient: Send + Sync {
|
||||
/// Swaps the outputs provided and returns the final swapped outputs and kernels.
|
||||
async fn mix_outputs(&self, onions: &Vec<Onion>) -> Result<MixResp, MixClientError>;
|
||||
}
|
||||
|
||||
pub struct MixClientImpl<R: Runtime> {
|
||||
config: ServerConfig,
|
||||
tor: Arc<grin_util::Mutex<TorService<R>>>,
|
||||
addr: OnionV3Address,
|
||||
}
|
||||
|
||||
impl<R: Runtime> MixClientImpl<R> {
|
||||
pub fn new(
|
||||
config: ServerConfig,
|
||||
tor: Arc<grin_util::Mutex<TorService<R>>>,
|
||||
next_pubkey: DalekPublicKey,
|
||||
) -> Self {
|
||||
let addr = OnionV3Address::from_bytes(next_pubkey.as_ref().to_bytes());
|
||||
MixClientImpl { config, tor, addr }
|
||||
}
|
||||
|
||||
async fn async_send_json_request<D: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
addr: &OnionV3Address,
|
||||
method: &str,
|
||||
params: &serde_json::Value,
|
||||
) -> Result<D, MixClientError> {
|
||||
let url = format!("{}/v1", addr.to_http_str());
|
||||
let request_str = serde_json::to_string(&build_request(method, params)).unwrap();
|
||||
let hyper_request =
|
||||
http::build_request(&url, &None, request_str).map_err(MixClientError::CommError)?;
|
||||
|
||||
let hyper_client = self.tor.lock().new_hyper_client();
|
||||
let res = hyper_client.request(hyper_request).await.unwrap();
|
||||
|
||||
let body_bytes = hyper::body::to_bytes(res.into_body()).await.unwrap();
|
||||
let res = String::from_utf8(body_bytes.to_vec()).unwrap();
|
||||
|
||||
let response: Response =
|
||||
serde_json::from_str(&res).map_err(MixClientError::DecodeResponseError)?;
|
||||
|
||||
if let Some(ref e) = response.error {
|
||||
return Err(MixClientError::ResponseError(e.clone()));
|
||||
}
|
||||
|
||||
let result = match response.result.clone() {
|
||||
Some(r) => serde_json::from_value(r).map_err(MixClientError::DecodeResponseError),
|
||||
None => serde_json::from_value(serde_json::Value::Null)
|
||||
.map_err(MixClientError::DecodeResponseError),
|
||||
}?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<R: Runtime> MixClient for MixClientImpl<R> {
|
||||
async fn mix_outputs(&self, onions: &Vec<Onion>) -> Result<MixResp, MixClientError> {
|
||||
let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap();
|
||||
let sig =
|
||||
dalek::sign(&self.config.key, serialized.as_slice()).map_err(MixClientError::Dalek)?;
|
||||
let mix = MixReq::new(onions.clone(), sig);
|
||||
|
||||
self.async_send_json_request::<MixResp>(&self.addr, "mix", &json!([mix]))
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod mock {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use grin_onion::onion::Onion;
|
||||
|
||||
use crate::servers::mix_rpc::MixResp;
|
||||
|
||||
use super::{MixClient, MixClientError};
|
||||
|
||||
pub struct MockMixClient {
|
||||
results: HashMap<Vec<Onion>, MixResp>,
|
||||
}
|
||||
|
||||
impl MockMixClient {
|
||||
pub fn new() -> MockMixClient {
|
||||
MockMixClient {
|
||||
results: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_response(&mut self, onions: &Vec<Onion>, r: MixResp) {
|
||||
self.results.insert(onions.clone(), r);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MixClient for MockMixClient {
|
||||
async fn mix_outputs(&self, onions: &Vec<Onion>) -> Result<MixResp, MixClientError> {
|
||||
self.results
|
||||
.get(onions)
|
||||
.map(|r| Ok(r.clone()))
|
||||
.unwrap_or(Err(MixClientError::Custom(
|
||||
"No response set for input".into(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test_util {
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use grin_core::ser;
|
||||
use grin_core::ser::ProtocolVersion;
|
||||
|
||||
use grin_onion::crypto::dalek::{self, DalekPublicKey};
|
||||
use grin_onion::crypto::secp::SecretKey;
|
||||
use grin_onion::onion::Onion;
|
||||
|
||||
use crate::servers::mix::MixServer;
|
||||
use crate::servers::mix_rpc::MixResp;
|
||||
|
||||
use super::{MixClient, MixClientError};
|
||||
|
||||
/// Implementation of the 'MixClient' trait that calls a mix server implementation directly.
|
||||
/// No JSON-RPC serialization or socket communication occurs.
|
||||
#[derive(Clone)]
|
||||
pub struct DirectMixClient {
|
||||
pub key: SecretKey,
|
||||
pub mix_server: Arc<dyn MixServer>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MixClient for DirectMixClient {
|
||||
async fn mix_outputs(&self, onions: &Vec<Onion>) -> Result<MixResp, MixClientError> {
|
||||
let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap();
|
||||
let sig =
|
||||
dalek::sign(&self.key, serialized.as_slice()).map_err(MixClientError::Dalek)?;
|
||||
|
||||
sig.verify(
|
||||
&DalekPublicKey::from_secret(&self.key),
|
||||
serialized.as_slice(),
|
||||
)
|
||||
.unwrap();
|
||||
Ok(self.mix_server.mix_outputs(&onions, &sig).await.unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
+142
-49
@@ -1,26 +1,44 @@
|
||||
use crate::crypto::secp::Commitment;
|
||||
|
||||
use grin_api::client;
|
||||
use grin_api::json_rpc::{build_request, Request, Response};
|
||||
use grin_api::{OutputPrintable, OutputType, Tip};
|
||||
use grin_core::consensus::COINBASE_MATURITY;
|
||||
use grin_core::core::{Input, OutputFeatures, Transaction};
|
||||
use grin_util::ToHex;
|
||||
|
||||
use serde_json::json;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use grin_api::LocatedTxKernel;
|
||||
use grin_api::{OutputPrintable, OutputType, Tip};
|
||||
use grin_core::consensus::COINBASE_MATURITY;
|
||||
use grin_core::core::hash::Hash;
|
||||
use grin_core::core::{Committed, Input, OutputFeatures, Transaction};
|
||||
use grin_util::ToHex;
|
||||
use serde_json::json;
|
||||
use thiserror::Error;
|
||||
|
||||
use grin_onion::crypto::secp::Commitment;
|
||||
|
||||
use crate::http;
|
||||
|
||||
#[async_trait]
|
||||
pub trait GrinNode: Send + Sync {
|
||||
/// Retrieves the unspent output with a matching commitment
|
||||
fn get_utxo(&self, output_commit: &Commitment) -> Result<Option<OutputPrintable>, NodeError>;
|
||||
async fn async_get_utxo(
|
||||
&self,
|
||||
output_commit: &Commitment,
|
||||
) -> Result<Option<OutputPrintable>, NodeError>;
|
||||
|
||||
/// Gets the height of the chain tip
|
||||
fn get_chain_height(&self) -> Result<u64, NodeError>;
|
||||
/// Gets the height and hash of the chain tip
|
||||
async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError>;
|
||||
|
||||
/// Posts a transaction to the grin node
|
||||
fn post_tx(&self, tx: &Transaction) -> Result<(), NodeError>;
|
||||
async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError>;
|
||||
|
||||
/// Returns a LocatedTxKernel based on the kernel excess.
|
||||
/// The min_height and max_height parameters are both optional.
|
||||
/// If not supplied, min_height will be set to 0 and max_height will be set to the head of the chain.
|
||||
/// The method will start at the block height max_height and traverse the kernel MMR backwards, until either the kernel is found or min_height is reached.
|
||||
async fn async_get_kernel(
|
||||
&self,
|
||||
excess: &Commitment,
|
||||
min_height: Option<u64>,
|
||||
max_height: Option<u64>,
|
||||
) -> Result<Option<LocatedTxKernel>, NodeError>;
|
||||
}
|
||||
|
||||
/// Error types for interacting with nodes
|
||||
@@ -30,23 +48,28 @@ pub enum NodeError {
|
||||
DecodeResponseError(serde_json::Error),
|
||||
#[error("JSON-RPC API communication error: {0:?}")]
|
||||
ApiCommError(grin_api::Error),
|
||||
#[error("Client error: {0:?}")]
|
||||
NodeCommError(http::HttpError),
|
||||
#[error("Error decoding JSON-RPC response: {0:?}")]
|
||||
ResponseParseError(grin_api::json_rpc::Error),
|
||||
}
|
||||
|
||||
/// Checks if a commitment is in the UTXO set
|
||||
pub fn is_unspent(node: &Arc<dyn GrinNode>, commit: &Commitment) -> Result<bool, NodeError> {
|
||||
let utxo = node.get_utxo(&commit)?;
|
||||
pub async fn async_is_unspent(
|
||||
node: &Arc<dyn GrinNode>,
|
||||
commit: &Commitment,
|
||||
) -> Result<bool, NodeError> {
|
||||
let utxo = node.async_get_utxo(&commit).await?;
|
||||
Ok(utxo.is_some())
|
||||
}
|
||||
|
||||
/// Checks whether a commitment is spendable at the block height provided
|
||||
pub fn is_spendable(
|
||||
pub async fn async_is_spendable(
|
||||
node: &Arc<dyn GrinNode>,
|
||||
commit: &Commitment,
|
||||
next_block_height: u64,
|
||||
) -> Result<bool, NodeError> {
|
||||
let output = node.get_utxo(&commit)?;
|
||||
let output = node.async_get_utxo(&commit).await?;
|
||||
if let Some(out) = output {
|
||||
let is_coinbase = match out.output_type {
|
||||
OutputType::Coinbase => true,
|
||||
@@ -70,11 +93,11 @@ pub fn is_spendable(
|
||||
}
|
||||
|
||||
/// Builds an input for an unspent output commitment
|
||||
pub fn build_input(
|
||||
pub async fn async_build_input(
|
||||
node: &Arc<dyn GrinNode>,
|
||||
output_commit: &Commitment,
|
||||
) -> Result<Option<Input>, NodeError> {
|
||||
let output = node.get_utxo(&output_commit)?;
|
||||
let output = node.async_get_utxo(&output_commit).await?;
|
||||
|
||||
if let Some(out) = output {
|
||||
let features = match out.output_type {
|
||||
@@ -89,6 +112,26 @@ pub fn build_input(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn async_is_tx_valid(
|
||||
node: &Arc<dyn GrinNode>,
|
||||
tx: &Transaction,
|
||||
) -> Result<bool, NodeError> {
|
||||
let next_block_height = node.async_get_chain_tip().await?.0 + 1;
|
||||
for input_commit in &tx.inputs_committed() {
|
||||
if !async_is_spendable(&node, &input_commit, next_block_height).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
for output_commit in &tx.outputs_committed() {
|
||||
if async_is_unspent(&node, &output_commit).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// HTTP (JSON-RPC) implementation of the 'GrinNode' trait
|
||||
#[derive(Clone)]
|
||||
pub struct HttpGrinNode {
|
||||
@@ -106,30 +149,25 @@ impl HttpGrinNode {
|
||||
}
|
||||
}
|
||||
|
||||
fn send_json_request<D: serde::de::DeserializeOwned>(
|
||||
async fn async_send_request<D: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: &serde_json::Value,
|
||||
) -> Result<D, NodeError> {
|
||||
let url = format!("http://{}{}", self.node_url, ENDPOINT);
|
||||
let req = build_request(method, params);
|
||||
let res =
|
||||
client::post::<Request, Response>(
|
||||
url.as_str(),
|
||||
self.node_api_secret.clone(),
|
||||
&req,
|
||||
client::TimeOut::default()
|
||||
).map_err(NodeError::ApiCommError)?;
|
||||
let parsed = res
|
||||
.clone()
|
||||
.into_result()
|
||||
.map_err(NodeError::ResponseParseError)?;
|
||||
let parsed = http::async_send_json_request(&url, &self.node_api_secret, &method, ¶ms)
|
||||
.await
|
||||
.map_err(NodeError::NodeCommError)?;
|
||||
Ok(parsed)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GrinNode for HttpGrinNode {
|
||||
fn get_utxo(&self, output_commit: &Commitment) -> Result<Option<OutputPrintable>, NodeError> {
|
||||
async fn async_get_utxo(
|
||||
&self,
|
||||
output_commit: &Commitment,
|
||||
) -> Result<Option<OutputPrintable>, NodeError> {
|
||||
let commits: Vec<String> = vec![output_commit.to_hex()];
|
||||
let start_height: Option<u64> = None;
|
||||
let end_height: Option<u64> = None;
|
||||
@@ -143,7 +181,9 @@ impl GrinNode for HttpGrinNode {
|
||||
include_proof,
|
||||
include_merkle_proof
|
||||
]);
|
||||
let outputs = self.send_json_request::<Vec<OutputPrintable>>("get_outputs", ¶ms)?;
|
||||
let outputs = self
|
||||
.async_send_request::<Vec<OutputPrintable>>("get_outputs", ¶ms)
|
||||
.await?;
|
||||
if outputs.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -151,37 +191,69 @@ impl GrinNode for HttpGrinNode {
|
||||
Ok(Some(outputs[0].clone()))
|
||||
}
|
||||
|
||||
fn get_chain_height(&self) -> Result<u64, NodeError> {
|
||||
async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError> {
|
||||
let params = json!([]);
|
||||
let tip_json = self.send_json_request::<serde_json::Value>("get_tip", ¶ms)?;
|
||||
let tip_json = self
|
||||
.async_send_request::<serde_json::Value>("get_tip", ¶ms)
|
||||
.await?;
|
||||
let tip =
|
||||
serde_json::from_value::<Tip>(tip_json).map_err(NodeError::DecodeResponseError)?;
|
||||
|
||||
Ok(tip.height)
|
||||
Ok((
|
||||
tip.height,
|
||||
Hash::from_hex(tip.last_block_pushed.as_str()).unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
fn post_tx(&self, tx: &Transaction) -> Result<(), NodeError> {
|
||||
async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError> {
|
||||
let params = json!([tx, true]);
|
||||
self.send_json_request::<serde_json::Value>("push_transaction", ¶ms)?;
|
||||
self.async_send_request::<serde_json::Value>("push_transaction", ¶ms)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn async_get_kernel(
|
||||
&self,
|
||||
excess: &Commitment,
|
||||
min_height: Option<u64>,
|
||||
max_height: Option<u64>,
|
||||
) -> Result<Option<LocatedTxKernel>, NodeError> {
|
||||
let params = json!([excess.0.as_ref().to_hex(), min_height, max_height]);
|
||||
let value = self
|
||||
.async_send_request::<serde_json::Value>("get_kernel", ¶ms)
|
||||
.await?;
|
||||
|
||||
let contents = format!("{:?}", value);
|
||||
if contents.contains("NotFound") {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let located_kernel = serde_json::from_value::<LocatedTxKernel>(value)
|
||||
.map_err(NodeError::DecodeResponseError)?;
|
||||
Ok(Some(located_kernel))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod mock {
|
||||
use super::{GrinNode, NodeError};
|
||||
use crate::crypto::secp::Commitment;
|
||||
|
||||
use grin_api::{OutputPrintable, OutputType};
|
||||
use grin_core::core::Transaction;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use grin_api::{LocatedTxKernel, OutputPrintable, OutputType};
|
||||
use grin_core::core::hash::Hash;
|
||||
use grin_core::core::Transaction;
|
||||
|
||||
use grin_onion::crypto::secp::Commitment;
|
||||
|
||||
use super::{GrinNode, NodeError};
|
||||
|
||||
/// Implementation of 'GrinNode' trait that mocks a grin node instance.
|
||||
/// Use only for testing purposes.
|
||||
pub struct MockGrinNode {
|
||||
utxos: HashMap<Commitment, OutputPrintable>,
|
||||
txns_posted: RwLock<Vec<Transaction>>,
|
||||
kernels: HashMap<Commitment, LocatedTxKernel>,
|
||||
}
|
||||
|
||||
impl MockGrinNode {
|
||||
@@ -189,6 +261,7 @@ pub mod mock {
|
||||
MockGrinNode {
|
||||
utxos: HashMap::new(),
|
||||
txns_posted: RwLock::new(Vec::new()),
|
||||
kernels: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +269,7 @@ pub mod mock {
|
||||
let mut node = MockGrinNode {
|
||||
utxos: HashMap::new(),
|
||||
txns_posted: RwLock::new(Vec::new()),
|
||||
kernels: HashMap::new(),
|
||||
};
|
||||
for utxo in utxos {
|
||||
node.add_default_utxo(utxo);
|
||||
@@ -226,10 +300,16 @@ pub mod mock {
|
||||
let read = self.txns_posted.read().unwrap();
|
||||
read.clone()
|
||||
}
|
||||
|
||||
pub fn add_kernel(&mut self, kernel: &LocatedTxKernel) {
|
||||
self.kernels
|
||||
.insert(kernel.tx_kernel.excess.clone(), kernel.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GrinNode for MockGrinNode {
|
||||
fn get_utxo(
|
||||
async fn async_get_utxo(
|
||||
&self,
|
||||
output_commit: &Commitment,
|
||||
) -> Result<Option<OutputPrintable>, NodeError> {
|
||||
@@ -240,14 +320,27 @@ pub mod mock {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn get_chain_height(&self) -> Result<u64, NodeError> {
|
||||
Ok(100)
|
||||
async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError> {
|
||||
Ok((100, Hash::default()))
|
||||
}
|
||||
|
||||
fn post_tx(&self, tx: &Transaction) -> Result<(), NodeError> {
|
||||
async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError> {
|
||||
let mut write = self.txns_posted.write().unwrap();
|
||||
write.push(tx.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn async_get_kernel(
|
||||
&self,
|
||||
excess: &Commitment,
|
||||
_min_height: Option<u64>,
|
||||
_max_height: Option<u64>,
|
||||
) -> Result<Option<LocatedTxKernel>, NodeError> {
|
||||
if let Some(kernel) = self.kernels.get(&excess) {
|
||||
return Ok(Some(kernel.clone()));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+86
-58
@@ -1,21 +1,26 @@
|
||||
use crate::client::MixClient;
|
||||
use crate::config::ServerConfig;
|
||||
use crate::tx::TxComponents;
|
||||
use crate::wallet::Wallet;
|
||||
use crate::{node, tx, GrinNode};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::stream::{self, StreamExt};
|
||||
use grin_core::core::{Output, OutputFeatures, TransactionBody};
|
||||
use grin_core::global::DEFAULT_ACCEPT_FEE_BASE;
|
||||
use grin_core::ser;
|
||||
use grin_core::ser::ProtocolVersion;
|
||||
use itertools::Itertools;
|
||||
use thiserror::Error;
|
||||
|
||||
use grin_onion::crypto::dalek::{self, DalekSignature};
|
||||
use grin_onion::onion::{Onion, OnionError, PeeledOnion};
|
||||
use itertools::Itertools;
|
||||
use secp256k1zkp::key::ZERO_KEY;
|
||||
use secp256k1zkp::Secp256k1;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::config::ServerConfig;
|
||||
use crate::mix_client::MixClient;
|
||||
use crate::node::{self, GrinNode};
|
||||
use crate::servers::mix_rpc::MixResp;
|
||||
use crate::tx::{self, TxComponents};
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
/// Mixer error types
|
||||
#[derive(Error, Debug)]
|
||||
@@ -43,17 +48,18 @@ pub enum MixError {
|
||||
#[error("Wallet error: {0:?}")]
|
||||
WalletError(crate::wallet::WalletError),
|
||||
#[error("Client comm error: {0:?}")]
|
||||
Client(crate::client::ClientError),
|
||||
Client(crate::mix_client::MixClientError),
|
||||
}
|
||||
|
||||
/// An internal MWixnet server - a "Mixer"
|
||||
#[async_trait]
|
||||
pub trait MixServer: Send + Sync {
|
||||
/// Swaps the outputs provided and returns the final swapped outputs and kernels.
|
||||
fn mix_outputs(
|
||||
async fn mix_outputs(
|
||||
&self,
|
||||
onions: &Vec<Onion>,
|
||||
sig: &DalekSignature,
|
||||
) -> Result<(Vec<usize>, TxComponents), MixError>;
|
||||
) -> Result<MixResp, MixError>;
|
||||
}
|
||||
|
||||
/// The standard MWixnet "Mixer" implementation
|
||||
@@ -130,15 +136,19 @@ impl MixServerImpl {
|
||||
Ok(peeled)
|
||||
}
|
||||
|
||||
fn build_final_outputs(
|
||||
async fn async_build_final_outputs(
|
||||
&self,
|
||||
peeled: &Vec<(usize, PeeledOnion)>,
|
||||
) -> Result<(Vec<usize>, TxComponents), MixError> {
|
||||
) -> Result<MixResp, MixError> {
|
||||
// Filter out commitments that already exist in the UTXO set
|
||||
let filtered: Vec<&(usize, PeeledOnion)> = peeled
|
||||
.iter()
|
||||
.filter(|(_, p)| !node::is_unspent(&self.node, &p.onion.commit).unwrap_or(true))
|
||||
.collect();
|
||||
let filtered: Vec<&(usize, PeeledOnion)> = stream::iter(peeled.iter())
|
||||
.filter(|(_, p)| async {
|
||||
!node::async_is_unspent(&self.node, &p.onion.commit)
|
||||
.await
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
// Build plain outputs for each mix entry
|
||||
let outputs: Vec<Output> = filtered
|
||||
@@ -158,7 +168,7 @@ impl MixServerImpl {
|
||||
.map(|(_, p)| p.payload.excess.clone())
|
||||
.collect();
|
||||
|
||||
let components = tx::assemble_components(
|
||||
let components = tx::async_assemble_components(
|
||||
&self.wallet,
|
||||
&TxComponents {
|
||||
offset: ZERO_KEY,
|
||||
@@ -169,17 +179,21 @@ impl MixServerImpl {
|
||||
self.get_fee_base(),
|
||||
fees_paid,
|
||||
)
|
||||
.await
|
||||
.map_err(MixError::TxError)?;
|
||||
|
||||
let indices = filtered.iter().map(|(i, _)| *i).collect();
|
||||
|
||||
Ok((indices, components))
|
||||
Ok(MixResp {
|
||||
indices,
|
||||
components,
|
||||
})
|
||||
}
|
||||
|
||||
fn call_next_mixer(
|
||||
async fn call_next_mixer(
|
||||
&self,
|
||||
peeled: &Vec<(usize, PeeledOnion)>,
|
||||
) -> Result<(Vec<usize>, TxComponents), MixError> {
|
||||
) -> Result<MixResp, MixError> {
|
||||
// Sort by commitment
|
||||
let mut onions_with_index = peeled.clone();
|
||||
onions_with_index
|
||||
@@ -191,15 +205,16 @@ impl MixServerImpl {
|
||||
|
||||
// Call next server
|
||||
let onions = peeled.iter().map(|(_, p)| p.onion.clone()).collect();
|
||||
let (mixed_indices, mixed_components) = self
|
||||
let mixed = self
|
||||
.mix_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.mix_outputs(&onions)
|
||||
.await
|
||||
.map_err(MixError::Client)?;
|
||||
|
||||
// Remove filtered entries
|
||||
let kept_next_indices = HashSet::<_>::from_iter(mixed_indices.clone());
|
||||
let kept_next_indices = HashSet::<_>::from_iter(mixed.indices.clone());
|
||||
let filtered_onions: Vec<&(usize, PeeledOnion)> = onions_with_index
|
||||
.iter()
|
||||
.filter(|(i, _)| {
|
||||
@@ -221,25 +236,30 @@ impl MixServerImpl {
|
||||
|
||||
let indices = kept_next_indices.into_iter().sorted().collect();
|
||||
|
||||
let components = tx::assemble_components(
|
||||
let components = tx::async_assemble_components(
|
||||
&self.wallet,
|
||||
&mixed_components,
|
||||
&mixed.components,
|
||||
&excesses,
|
||||
self.get_fee_base(),
|
||||
fees_paid,
|
||||
)
|
||||
.await
|
||||
.map_err(MixError::TxError)?;
|
||||
|
||||
Ok((indices, components))
|
||||
Ok(MixResp {
|
||||
indices,
|
||||
components,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MixServer for MixServerImpl {
|
||||
fn mix_outputs(
|
||||
async fn mix_outputs(
|
||||
&self,
|
||||
onions: &Vec<Onion>,
|
||||
sig: &DalekSignature,
|
||||
) -> Result<(Vec<usize>, TxComponents), MixError> {
|
||||
) -> Result<MixResp, MixError> {
|
||||
// Verify Signature
|
||||
let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap();
|
||||
sig.verify(
|
||||
@@ -252,10 +272,10 @@ impl MixServer for MixServerImpl {
|
||||
let mut peeled: Vec<(usize, PeeledOnion)> = onions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, o)| {
|
||||
if let Some(p) = self.peel_onion(&o).ok() {
|
||||
Some((i, p))
|
||||
} else {
|
||||
.filter_map(|(i, o)| match self.peel_onion(&o) {
|
||||
Ok(p) => Some((i, p)),
|
||||
Err(e) => {
|
||||
println!("Error peeling onion: {:?}", e);
|
||||
None
|
||||
}
|
||||
})
|
||||
@@ -271,24 +291,27 @@ impl MixServer for MixServerImpl {
|
||||
}
|
||||
|
||||
if self.server_config.next_server.is_some() {
|
||||
self.call_next_mixer(&peeled)
|
||||
self.call_next_mixer(&peeled).await
|
||||
} else {
|
||||
self.build_final_outputs(&peeled)
|
||||
self.async_build_final_outputs(&peeled).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_util {
|
||||
use crate::client::test_util::DirectMixClient;
|
||||
use crate::node::mock::MockGrinNode;
|
||||
use crate::wallet::mock::MockWallet;
|
||||
use crate::{config, DalekPublicKey, MixClient};
|
||||
|
||||
use crate::servers::mix::MixServerImpl;
|
||||
use secp256k1zkp::SecretKey;
|
||||
use std::sync::Arc;
|
||||
|
||||
use grin_onion::crypto::dalek::DalekPublicKey;
|
||||
use secp256k1zkp::SecretKey;
|
||||
|
||||
use crate::config;
|
||||
use crate::mix_client::MixClient;
|
||||
use crate::mix_client::test_util::DirectMixClient;
|
||||
use crate::node::mock::MockGrinNode;
|
||||
use crate::servers::mix::MixServerImpl;
|
||||
use crate::wallet::mock::MockWallet;
|
||||
|
||||
pub fn new_mixer(
|
||||
server_key: &SecretKey,
|
||||
prev_server: (&SecretKey, &DalekPublicKey),
|
||||
@@ -320,18 +343,21 @@ mod test_util {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::node::mock::MockGrinNode;
|
||||
use crate::{DalekPublicKey, MixClient};
|
||||
|
||||
use ::function_name::named;
|
||||
use grin_onion::crypto::secp::{self, Commitment};
|
||||
use grin_onion::test_util as onion_test_util;
|
||||
use grin_onion::{create_onion, new_hop, Hop};
|
||||
use secp256k1zkp::pedersen::RangeProof;
|
||||
use secp256k1zkp::SecretKey;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::function_name::named;
|
||||
|
||||
use grin_onion::{create_onion, Hop, new_hop};
|
||||
use grin_onion::crypto::dalek::DalekPublicKey;
|
||||
use grin_onion::crypto::secp::{self, Commitment};
|
||||
use grin_onion::test_util as onion_test_util;
|
||||
use secp256k1zkp::pedersen::RangeProof;
|
||||
use secp256k1zkp::SecretKey;
|
||||
|
||||
use crate::mix_client::MixClient;
|
||||
use crate::node::mock::MockGrinNode;
|
||||
|
||||
macro_rules! init_test {
|
||||
() => {{
|
||||
grin_core::global::set_local_chain_type(
|
||||
@@ -373,9 +399,9 @@ mod tests {
|
||||
/// * Swap Server - Simulated by test
|
||||
/// * Mixer 1 - Internal MixServerImpl directly called by test
|
||||
/// * Mixer 2 - Final MixServerImpl called by Mixer 1
|
||||
#[test]
|
||||
#[tokio::test]
|
||||
#[named]
|
||||
fn mix_lifecycle() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn mix_lifecycle() -> Result<(), Box<dyn std::error::Error>> {
|
||||
init_test!();
|
||||
|
||||
// Setup Input(s)
|
||||
@@ -426,13 +452,15 @@ mod tests {
|
||||
|
||||
// Simulate the swap server peeling the onion and then calling mix1
|
||||
let mix1_onion = onion.peel_layer(&swap_vars.sk)?;
|
||||
let (mixed_indices, mixed_components) =
|
||||
mixer1_client.mix_outputs(&vec![mix1_onion.onion.clone()])?;
|
||||
let mixed = mixer1_client
|
||||
.mix_outputs(&vec![mix1_onion.onion.clone()])
|
||||
.await?;
|
||||
|
||||
// Verify 3 outputs are returned: mixed output, mixer1's output, and mixer2's output
|
||||
assert_eq!(mixed_indices, vec![0 as usize]);
|
||||
assert_eq!(mixed_components.outputs.len(), 3);
|
||||
let output_commits: HashSet<Commitment> = mixed_components
|
||||
assert_eq!(mixed.indices, vec![0 as usize]);
|
||||
assert_eq!(mixed.components.outputs.len(), 3);
|
||||
let output_commits: HashSet<Commitment> = mixed
|
||||
.components
|
||||
.outputs
|
||||
.iter()
|
||||
.map(|o| o.identifier.commit.clone())
|
||||
|
||||
+47
-41
@@ -1,20 +1,21 @@
|
||||
use crate::client::MixClient;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::FutureExt;
|
||||
use jsonrpc_derive::rpc;
|
||||
use jsonrpc_http_server::{DomainsValidation, ServerBuilder};
|
||||
use jsonrpc_http_server::jsonrpc_core::{self, BoxFuture, IoHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use grin_onion::crypto::dalek::{self, DalekSignature};
|
||||
use grin_onion::onion::Onion;
|
||||
|
||||
use crate::config::ServerConfig;
|
||||
use crate::crypto::dalek::{self, DalekSignature};
|
||||
use crate::mix_client::MixClient;
|
||||
use crate::node::GrinNode;
|
||||
use crate::servers::mix::{MixError, MixServer, MixServerImpl};
|
||||
use crate::tx::TxComponents;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
use grin_onion::onion::Onion;
|
||||
use grin_util::StopState;
|
||||
use jsonrpc_derive::rpc;
|
||||
use jsonrpc_http_server::jsonrpc_core::{self as jsonrpc, IoHandler};
|
||||
use jsonrpc_http_server::{DomainsValidation, ServerBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::{sleep, spawn};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MixReq {
|
||||
onions: Vec<Onion>,
|
||||
@@ -22,6 +23,12 @@ pub struct MixReq {
|
||||
sig: DalekSignature,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct MixResp {
|
||||
pub indices: Vec<usize>,
|
||||
pub components: TxComponents,
|
||||
}
|
||||
|
||||
impl MixReq {
|
||||
pub fn new(onions: Vec<Onion>, sig: DalekSignature) -> Self {
|
||||
MixReq { onions, sig }
|
||||
@@ -31,22 +38,23 @@ impl MixReq {
|
||||
#[rpc(server)]
|
||||
pub trait MixAPI {
|
||||
#[rpc(name = "mix")]
|
||||
fn mix(&self, mix: MixReq) -> jsonrpc::Result<jsonrpc::Value>;
|
||||
fn mix(&self, mix: MixReq) -> BoxFuture<jsonrpc_core::Result<MixResp>>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RPCMixServer {
|
||||
server_config: ServerConfig,
|
||||
server: Arc<Mutex<dyn MixServer>>,
|
||||
server: Arc<tokio::sync::Mutex<dyn MixServer>>,
|
||||
}
|
||||
|
||||
impl RPCMixServer {
|
||||
/// Spin up an instance of the JSON-RPC HTTP server.
|
||||
fn start_http(&self) -> jsonrpc_http_server::Server {
|
||||
fn start_http(&self, runtime_handle: tokio::runtime::Handle) -> jsonrpc_http_server::Server {
|
||||
let mut io = IoHandler::new();
|
||||
io.extend_with(RPCMixServer::to_delegate(self.clone()));
|
||||
|
||||
ServerBuilder::new(io)
|
||||
.event_loop_executor(runtime_handle)
|
||||
.cors(DomainsValidation::Disabled)
|
||||
.request_middleware(|request: hyper::Request<hyper::Body>| {
|
||||
if request.uri() == "/v1" {
|
||||
@@ -60,57 +68,55 @@ impl RPCMixServer {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MixError> for jsonrpc::Error {
|
||||
impl From<MixError> for jsonrpc_core::Error {
|
||||
fn from(e: MixError) -> Self {
|
||||
jsonrpc::Error::invalid_params(e.to_string())
|
||||
jsonrpc_core::Error::invalid_params(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl MixAPI for RPCMixServer {
|
||||
fn mix(&self, mix: MixReq) -> jsonrpc::Result<jsonrpc::Value> {
|
||||
self.server
|
||||
.lock()
|
||||
.unwrap()
|
||||
.mix_outputs(&mix.onions, &mix.sig)?;
|
||||
Ok(jsonrpc::Value::String("success".into()))
|
||||
fn mix(&self, mix: MixReq) -> BoxFuture<jsonrpc_core::Result<MixResp>> {
|
||||
let server = self.server.clone();
|
||||
async move {
|
||||
let response = server
|
||||
.lock()
|
||||
.await
|
||||
.mix_outputs(&mix.onions, &mix.sig)
|
||||
.await?;
|
||||
Ok(response)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
/// Spin up the JSON-RPC web server
|
||||
pub fn listen(
|
||||
rt_handle: &tokio::runtime::Handle,
|
||||
server_config: ServerConfig,
|
||||
next_server: Option<Arc<dyn MixClient>>,
|
||||
wallet: Arc<dyn Wallet>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
stop_state: Arc<StopState>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
) -> Result<
|
||||
(
|
||||
Arc<tokio::sync::Mutex<dyn MixServer>>,
|
||||
jsonrpc_http_server::Server,
|
||||
),
|
||||
Box<dyn std::error::Error>,
|
||||
> {
|
||||
let server = MixServerImpl::new(
|
||||
server_config.clone(),
|
||||
next_server,
|
||||
wallet.clone(),
|
||||
node.clone(),
|
||||
);
|
||||
let server = Arc::new(Mutex::new(server));
|
||||
let server = Arc::new(tokio::sync::Mutex::new(server));
|
||||
|
||||
let rpc_server = RPCMixServer {
|
||||
server_config: server_config.clone(),
|
||||
server: server.clone(),
|
||||
};
|
||||
|
||||
let http_server = rpc_server.start_http();
|
||||
let http_server = rpc_server.start_http(rt_handle.clone());
|
||||
|
||||
let close_handle = http_server.close_handle();
|
||||
let round_handle = spawn(move || loop {
|
||||
if stop_state.is_stopped() {
|
||||
close_handle.close();
|
||||
break;
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100));
|
||||
});
|
||||
|
||||
http_server.wait();
|
||||
round_handle.join().unwrap();
|
||||
|
||||
Ok(())
|
||||
Ok((server, http_server))
|
||||
}
|
||||
|
||||
+333
-189
@@ -1,23 +1,25 @@
|
||||
use crate::client::MixClient;
|
||||
use crate::config::ServerConfig;
|
||||
use crate::crypto::comsig::ComSignature;
|
||||
use crate::crypto::secp::{Commitment, Secp256k1, SecretKey};
|
||||
use crate::node::{self, GrinNode};
|
||||
use crate::store::{StoreError, SwapData, SwapStatus, SwapStore};
|
||||
use crate::tx;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
use grin_core::core::hash::Hashed;
|
||||
use grin_core::core::{Input, Output, OutputFeatures, Transaction, TransactionBody};
|
||||
use grin_core::global::DEFAULT_ACCEPT_FEE_BASE;
|
||||
use grin_onion::onion::{Onion, OnionError};
|
||||
use itertools::Itertools;
|
||||
use secp256k1zkp::key::ZERO_KEY;
|
||||
use std::collections::HashSet;
|
||||
use std::result::Result;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use grin_core::core::{Committed, Input, Output, OutputFeatures, Transaction, TransactionBody};
|
||||
use grin_core::global::DEFAULT_ACCEPT_FEE_BASE;
|
||||
use itertools::Itertools;
|
||||
use secp256k1zkp::key::ZERO_KEY;
|
||||
use thiserror::Error;
|
||||
|
||||
use grin_onion::crypto::comsig::ComSignature;
|
||||
use grin_onion::crypto::secp::{Commitment, Secp256k1, SecretKey};
|
||||
use grin_onion::onion::{Onion, OnionError};
|
||||
|
||||
use crate::config::ServerConfig;
|
||||
use crate::mix_client::MixClient;
|
||||
use crate::node::{self, GrinNode};
|
||||
use crate::store::{StoreError, SwapData, SwapStatus, SwapStore, SwapTx};
|
||||
use crate::tx;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
/// Swap error types
|
||||
#[derive(Clone, Error, Debug, PartialEq)]
|
||||
pub enum SwapError {
|
||||
@@ -39,20 +41,53 @@ pub enum SwapError {
|
||||
FeeTooLow { minimum_fee: u64, actual_fee: u64 },
|
||||
#[error("Error saving swap to data store: {0}")]
|
||||
StoreError(StoreError),
|
||||
#[error("Error building transaction: {0}")]
|
||||
TxError(String),
|
||||
#[error("Node communication error: {0}")]
|
||||
NodeError(String),
|
||||
#[error("Client communication error: {0:?}")]
|
||||
ClientError(String),
|
||||
#[error("Swap transaction not found: {0:?}")]
|
||||
SwapTxNotFound(Commitment),
|
||||
#[error("{0}")]
|
||||
UnknownError(String),
|
||||
}
|
||||
|
||||
impl From<StoreError> for SwapError {
|
||||
fn from(e: StoreError) -> SwapError {
|
||||
SwapError::StoreError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tx::TxError> for SwapError {
|
||||
fn from(e: tx::TxError) -> SwapError {
|
||||
SwapError::TxError(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<node::NodeError> for SwapError {
|
||||
fn from(e: node::NodeError) -> SwapError {
|
||||
SwapError::NodeError(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// A public MWixnet server - the "Swap Server"
|
||||
#[async_trait]
|
||||
pub trait SwapServer: Send + Sync {
|
||||
/// Submit a new output to be swapped.
|
||||
fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError>;
|
||||
async fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError>;
|
||||
|
||||
/// Iterate through all saved submissions, filter out any inputs that are no longer spendable,
|
||||
/// and assemble the coinswap transaction, posting the transaction to the configured node.
|
||||
fn execute_round(&self) -> Result<Option<Transaction>, Box<dyn std::error::Error>>;
|
||||
async fn execute_round(&self) -> Result<Option<Arc<Transaction>>, SwapError>;
|
||||
|
||||
/// Verify the previous swap transaction is in the active chain or mempool.
|
||||
/// If it's not, rebroacast the transaction if it's still valid.
|
||||
/// If the transaction is no longer valid, perform the swap again.
|
||||
async fn check_reorg(
|
||||
&self,
|
||||
tx: &Arc<Transaction>,
|
||||
) -> Result<Option<Arc<Transaction>>, SwapError>;
|
||||
}
|
||||
|
||||
/// The standard MWixnet server implementation
|
||||
@@ -62,7 +97,7 @@ pub struct SwapServerImpl {
|
||||
next_server: Option<Arc<dyn MixClient>>,
|
||||
wallet: Arc<dyn Wallet>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
store: Arc<Mutex<SwapStore>>,
|
||||
store: Arc<tokio::sync::Mutex<SwapStore>>,
|
||||
}
|
||||
|
||||
impl SwapServerImpl {
|
||||
@@ -79,7 +114,7 @@ impl SwapServerImpl {
|
||||
next_server,
|
||||
wallet,
|
||||
node,
|
||||
store: Arc::new(Mutex::new(store)),
|
||||
store: Arc::new(tokio::sync::Mutex::new(store)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,10 +128,127 @@ impl SwapServerImpl {
|
||||
fn get_minimum_swap_fee(&self) -> u64 {
|
||||
TransactionBody::weight_by_iok(1, 1, 1) * self.get_fee_base()
|
||||
}
|
||||
|
||||
async fn async_is_spendable(&self, next_block_height: u64, swap: &SwapData) -> bool {
|
||||
if let SwapStatus::Unprocessed = swap.status {
|
||||
if node::async_is_spendable(&self.node, &swap.input.commit, next_block_height)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if !node::async_is_unspent(&self.node, &swap.output_commit)
|
||||
.await
|
||||
.unwrap_or(true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
async fn async_execute_round(
|
||||
&self,
|
||||
store: &SwapStore,
|
||||
mut swaps: Vec<SwapData>,
|
||||
) -> Result<Option<Arc<Transaction>>, SwapError> {
|
||||
swaps.sort_by(|a, b| a.output_commit.partial_cmp(&b.output_commit).unwrap());
|
||||
|
||||
if swaps.len() == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let (filtered, failed, offset, outputs, kernels) = if let Some(client) = &self.next_server {
|
||||
// Call next mix server
|
||||
let onions = swaps.iter().map(|s| s.onion.clone()).collect();
|
||||
let mixed = client
|
||||
.mix_outputs(&onions)
|
||||
.await
|
||||
.map_err(|e| SwapError::ClientError(e.to_string()))?;
|
||||
|
||||
// Filter out failed entries
|
||||
let kept_indices = HashSet::<_>::from_iter(mixed.indices.clone());
|
||||
let filtered = swaps
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| kept_indices.contains(i))
|
||||
.map(|(_, j)| j.clone())
|
||||
.collect();
|
||||
|
||||
let failed = swaps
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| !kept_indices.contains(i))
|
||||
.map(|(_, j)| j.clone())
|
||||
.collect();
|
||||
|
||||
(
|
||||
filtered,
|
||||
failed,
|
||||
mixed.components.offset,
|
||||
mixed.components.outputs,
|
||||
mixed.components.kernels,
|
||||
)
|
||||
} else {
|
||||
// Build plain outputs for each swap entry
|
||||
let outputs: Vec<Output> = swaps
|
||||
.iter()
|
||||
.map(|s| {
|
||||
Output::new(
|
||||
OutputFeatures::Plain,
|
||||
s.output_commit,
|
||||
s.rangeproof.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
(swaps, Vec::new(), ZERO_KEY, outputs, Vec::new())
|
||||
};
|
||||
|
||||
let fees_paid: u64 = filtered.iter().map(|s| s.fee).sum();
|
||||
let inputs: Vec<Input> = filtered.iter().map(|s| s.input).collect();
|
||||
let output_excesses: Vec<SecretKey> = filtered.iter().map(|s| s.excess.clone()).collect();
|
||||
|
||||
let tx = tx::async_assemble_tx(
|
||||
&self.wallet,
|
||||
&inputs,
|
||||
&outputs,
|
||||
&kernels,
|
||||
self.get_fee_base(),
|
||||
fees_paid,
|
||||
&offset,
|
||||
&output_excesses,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let chain_tip = self.node.async_get_chain_tip().await?;
|
||||
self.node.async_post_tx(&tx).await?;
|
||||
|
||||
store.save_swap_tx(&SwapTx {
|
||||
tx: tx.clone(),
|
||||
chain_tip,
|
||||
})?;
|
||||
|
||||
// Update status to in process
|
||||
let kernel_commit = tx.kernels().first().unwrap().excess;
|
||||
for mut swap in filtered {
|
||||
swap.status = SwapStatus::InProcess { kernel_commit };
|
||||
store.save_swap(&swap, true)?;
|
||||
}
|
||||
|
||||
// Update status of failed swaps
|
||||
for mut swap in failed {
|
||||
swap.status = SwapStatus::Failed;
|
||||
store.save_swap(&swap, true)?;
|
||||
}
|
||||
|
||||
Ok(Some(Arc::new(tx)))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SwapServer for SwapServerImpl {
|
||||
fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError> {
|
||||
async fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError> {
|
||||
// Verify that more than 1 payload exists when there's a next server,
|
||||
// or that exactly 1 payload exists when this is the final server
|
||||
if self.server_config.next_server.is_some() && onion.enc_payloads.len() <= 1
|
||||
@@ -114,7 +266,8 @@ impl SwapServer for SwapServerImpl {
|
||||
.map_err(|_| SwapError::InvalidComSignature)?;
|
||||
|
||||
// Verify that commitment is unspent
|
||||
let input = node::build_input(&self.node, &onion.commit)
|
||||
let input = node::async_build_input(&self.node, &onion.commit)
|
||||
.await
|
||||
.map_err(|e| SwapError::UnknownError(e.to_string()))?;
|
||||
let input = input.ok_or(SwapError::CoinNotFound {
|
||||
commit: onion.commit.clone(),
|
||||
@@ -144,7 +297,7 @@ impl SwapServer for SwapServerImpl {
|
||||
return Err(SwapError::MissingRangeproof);
|
||||
}
|
||||
|
||||
let locked = self.store.lock().unwrap();
|
||||
let locked = self.store.lock().await;
|
||||
|
||||
locked
|
||||
.save_swap(
|
||||
@@ -153,7 +306,7 @@ impl SwapServer for SwapServerImpl {
|
||||
output_commit: peeled.onion.commit,
|
||||
rangeproof: peeled.payload.rangeproof,
|
||||
input,
|
||||
fee: fee as u64,
|
||||
fee,
|
||||
onion: peeled.onion,
|
||||
status: SwapStatus::Unprocessed,
|
||||
},
|
||||
@@ -168,110 +321,77 @@ impl SwapServer for SwapServerImpl {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn execute_round(&self) -> Result<Option<Transaction>, Box<dyn std::error::Error>> {
|
||||
let locked_store = self.store.lock().unwrap();
|
||||
let next_block_height = self.node.get_chain_height()? + 1;
|
||||
async fn execute_round(&self) -> Result<Option<Arc<Transaction>>, SwapError> {
|
||||
let next_block_height = self.node.async_get_chain_tip().await?.0 + 1;
|
||||
|
||||
let spendable: Vec<SwapData> = locked_store
|
||||
let locked_store = self.store.lock().await;
|
||||
let swaps: Vec<SwapData> = locked_store
|
||||
.swaps_iter()?
|
||||
.unique_by(|s| s.output_commit)
|
||||
.filter(|s| match s.status {
|
||||
SwapStatus::Unprocessed => true,
|
||||
_ => false,
|
||||
})
|
||||
.filter(|s| {
|
||||
node::is_spendable(&self.node, &s.input.commit, next_block_height).unwrap_or(false)
|
||||
})
|
||||
.filter(|s| !node::is_unspent(&self.node, &s.output_commit).unwrap_or(true))
|
||||
.sorted_by(|a, b| a.output_commit.partial_cmp(&b.output_commit).unwrap())
|
||||
.collect();
|
||||
|
||||
if spendable.len() == 0 {
|
||||
return Ok(None);
|
||||
let mut spendable: Vec<SwapData> = vec![];
|
||||
for swap in &swaps {
|
||||
if self.async_is_spendable(next_block_height, &swap).await {
|
||||
spendable.push(swap.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let (filtered, failed, offset, outputs, kernels) = if let Some(client) = &self.next_server {
|
||||
// Call next mix server
|
||||
let onions = spendable.iter().map(|s| s.onion.clone()).collect();
|
||||
let (indices, mixed) = client
|
||||
.mix_outputs(&onions)
|
||||
.map_err(|e| SwapError::ClientError(e.to_string()))?;
|
||||
self.async_execute_round(&locked_store, swaps).await
|
||||
}
|
||||
|
||||
// Filter out failed entries
|
||||
let kept_indices = HashSet::<_>::from_iter(indices.clone());
|
||||
let filtered = spendable
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| kept_indices.contains(i))
|
||||
.map(|(_, j)| j.clone())
|
||||
.collect();
|
||||
async fn check_reorg(
|
||||
&self,
|
||||
tx: &Arc<Transaction>,
|
||||
) -> Result<Option<Arc<Transaction>>, SwapError> {
|
||||
let excess = tx.kernels().first().unwrap().excess;
|
||||
let locked_store = self.store.lock().await;
|
||||
if let Ok(swap_tx) = locked_store.get_swap_tx(&excess) {
|
||||
// If kernel is in active chain, return tx
|
||||
if self
|
||||
.node
|
||||
.async_get_kernel(&excess, Some(swap_tx.chain_tip.0), None)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Ok(Some(tx.clone()));
|
||||
}
|
||||
|
||||
let failed = spendable
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| !kept_indices.contains(i))
|
||||
.map(|(_, j)| j.clone())
|
||||
.collect();
|
||||
// If transaction is still valid, rebroadcast and return tx
|
||||
if node::async_is_tx_valid(&self.node, &tx).await? {
|
||||
self.node.async_post_tx(&tx).await?;
|
||||
return Ok(Some(tx.clone()));
|
||||
}
|
||||
|
||||
(filtered, failed, mixed.offset, mixed.outputs, mixed.kernels)
|
||||
// Collect all swaps based on tx's inputs, and execute_round with those swaps
|
||||
let next_block_height = self.node.async_get_chain_tip().await?.0 + 1;
|
||||
let mut swaps = Vec::new();
|
||||
for input_commit in &tx.inputs_committed() {
|
||||
if let Ok(swap) = locked_store.get_swap(&input_commit) {
|
||||
if self.async_is_spendable(next_block_height, &swap).await {
|
||||
swaps.push(swap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.async_execute_round(&locked_store, swaps).await
|
||||
} else {
|
||||
// Build plain outputs for each swap entry
|
||||
let outputs: Vec<Output> = spendable
|
||||
.iter()
|
||||
.map(|s| {
|
||||
Output::new(
|
||||
OutputFeatures::Plain,
|
||||
s.output_commit,
|
||||
s.rangeproof.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
(spendable, Vec::new(), ZERO_KEY, outputs, Vec::new())
|
||||
};
|
||||
|
||||
let fees_paid: u64 = filtered.iter().map(|s| s.fee).sum();
|
||||
let inputs: Vec<Input> = filtered.iter().map(|s| s.input).collect();
|
||||
let output_excesses: Vec<SecretKey> = filtered.iter().map(|s| s.excess.clone()).collect();
|
||||
|
||||
let tx = tx::assemble_tx(
|
||||
&self.wallet,
|
||||
&inputs,
|
||||
&outputs,
|
||||
&kernels,
|
||||
self.get_fee_base(),
|
||||
fees_paid,
|
||||
&offset,
|
||||
&output_excesses,
|
||||
)?;
|
||||
|
||||
self.node.post_tx(&tx)?;
|
||||
|
||||
// Update status to in process
|
||||
let kernel_hash = tx.kernels().first().unwrap().hash();
|
||||
for mut swap in filtered {
|
||||
swap.status = SwapStatus::InProcess { kernel_hash };
|
||||
locked_store.save_swap(&swap, true)?;
|
||||
Err(SwapError::SwapTxNotFound(excess))
|
||||
}
|
||||
|
||||
// Update status of failed swaps
|
||||
for mut swap in failed {
|
||||
swap.status = SwapStatus::Failed;
|
||||
locked_store.save_swap(&swap, true)?;
|
||||
}
|
||||
|
||||
Ok(Some(tx))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod mock {
|
||||
use super::{SwapError, SwapServer};
|
||||
use crate::crypto::comsig::ComSignature;
|
||||
|
||||
use grin_core::core::Transaction;
|
||||
use grin_onion::onion::Onion;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use grin_core::core::Transaction;
|
||||
|
||||
use grin_onion::crypto::comsig::ComSignature;
|
||||
use grin_onion::onion::Onion;
|
||||
|
||||
use super::{SwapError, SwapServer};
|
||||
|
||||
pub struct MockSwapServer {
|
||||
errors: HashMap<Onion, SwapError>,
|
||||
@@ -289,8 +409,9 @@ pub mod mock {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SwapServer for MockSwapServer {
|
||||
fn swap(&self, onion: &Onion, _comsig: &ComSignature) -> Result<(), SwapError> {
|
||||
async fn swap(&self, onion: &Onion, _comsig: &ComSignature) -> Result<(), SwapError> {
|
||||
if let Some(e) = self.errors.get(&onion) {
|
||||
return Err(e.clone());
|
||||
}
|
||||
@@ -298,21 +419,33 @@ pub mod mock {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn execute_round(&self) -> Result<Option<Transaction>, Box<dyn std::error::Error>> {
|
||||
async fn execute_round(&self) -> Result<Option<Arc<Transaction>>, SwapError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn check_reorg(
|
||||
&self,
|
||||
tx: &Arc<Transaction>,
|
||||
) -> Result<Option<Arc<Transaction>>, SwapError> {
|
||||
Ok(Some(tx.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test_util {
|
||||
use crate::crypto::dalek::DalekPublicKey;
|
||||
use crate::crypto::secp::SecretKey;
|
||||
use crate::servers::swap::SwapServerImpl;
|
||||
use crate::wallet::mock::MockWallet;
|
||||
use crate::{config, GrinNode, MixClient, SwapStore};
|
||||
use std::sync::Arc;
|
||||
|
||||
use grin_onion::crypto::dalek::DalekPublicKey;
|
||||
use grin_onion::crypto::secp::SecretKey;
|
||||
|
||||
use crate::config;
|
||||
use crate::mix_client::MixClient;
|
||||
use crate::node::GrinNode;
|
||||
use crate::servers::swap::SwapServerImpl;
|
||||
use crate::store::SwapStore;
|
||||
use crate::wallet::mock::MockWallet;
|
||||
|
||||
pub fn new_swapper(
|
||||
test_dir: &str,
|
||||
server_key: &SecretKey,
|
||||
@@ -339,23 +472,28 @@ pub mod test_util {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::node::mock::MockGrinNode;
|
||||
use crate::servers::swap::{SwapError, SwapServer};
|
||||
use crate::store::{SwapData, SwapStatus};
|
||||
use crate::tx::TxComponents;
|
||||
use crate::{client, tx, MixClient};
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::function_name::named;
|
||||
use grin_core::core::hash::Hashed;
|
||||
use grin_core::core::{Committed, Input, Output, OutputFeatures, Transaction, Weighting};
|
||||
use grin_core::core::{
|
||||
Committed, Input, Inputs, Output, OutputFeatures, Transaction, Weighting,
|
||||
};
|
||||
use secp256k1zkp::key::ZERO_KEY;
|
||||
use x25519_dalek::PublicKey as xPublicKey;
|
||||
|
||||
use grin_onion::crypto::comsig::ComSignature;
|
||||
use grin_onion::crypto::secp;
|
||||
use grin_onion::onion::Onion;
|
||||
use grin_onion::test_util as onion_test_util;
|
||||
use grin_onion::{create_onion, new_hop, Hop};
|
||||
use secp256k1zkp::key::ZERO_KEY;
|
||||
use std::sync::Arc;
|
||||
use x25519_dalek::PublicKey as xPublicKey;
|
||||
|
||||
use crate::mix_client::{self, MixClient};
|
||||
use crate::node::mock::MockGrinNode;
|
||||
use crate::servers::mix_rpc::MixResp;
|
||||
use crate::servers::swap::{SwapError, SwapServer};
|
||||
use crate::store::{SwapData, SwapStatus};
|
||||
use crate::tx;
|
||||
use crate::tx::TxComponents;
|
||||
|
||||
macro_rules! assert_error_type {
|
||||
($result:expr, $error_type:pat) => {
|
||||
@@ -380,9 +518,9 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Standalone swap server to demonstrate request validation and onion unwrapping.
|
||||
#[test]
|
||||
#[tokio::test]
|
||||
#[named]
|
||||
fn swap_standalone() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn swap_standalone() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
@@ -400,7 +538,7 @@ mod tests {
|
||||
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
server.swap(&onion, &comsig)?;
|
||||
server.swap(&onion, &comsig).await?;
|
||||
|
||||
// Make sure entry is added to server.
|
||||
let expected = SwapData {
|
||||
@@ -418,21 +556,21 @@ mod tests {
|
||||
};
|
||||
|
||||
{
|
||||
let store = server.store.lock().unwrap();
|
||||
let store = server.store.lock().await;
|
||||
assert_eq!(1, store.swaps_iter().unwrap().count());
|
||||
assert!(store.swap_exists(&input_commit).unwrap());
|
||||
assert_eq!(expected, store.get_swap(&input_commit).unwrap());
|
||||
}
|
||||
|
||||
let tx = server.execute_round()?;
|
||||
let tx = server.execute_round().await?;
|
||||
assert!(tx.is_some());
|
||||
|
||||
{
|
||||
// check that status was updated
|
||||
let store = server.store.lock().unwrap();
|
||||
let store = server.store.lock().await;
|
||||
assert!(match store.get_swap(&input_commit)?.status {
|
||||
SwapStatus::InProcess { kernel_hash } =>
|
||||
kernel_hash == tx.unwrap().kernels().first().unwrap().hash(),
|
||||
SwapStatus::InProcess { kernel_commit } =>
|
||||
kernel_commit == tx.unwrap().kernels().first().unwrap().excess,
|
||||
_ => false,
|
||||
});
|
||||
}
|
||||
@@ -451,9 +589,9 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Multi-server test to verify proper MixClient communication.
|
||||
#[test]
|
||||
#[tokio::test]
|
||||
#[named]
|
||||
fn swap_multiserver() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn swap_multiserver() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
// Setup input
|
||||
@@ -486,7 +624,7 @@ mod tests {
|
||||
|
||||
// Mock mixer
|
||||
let mixer_onion = onion.peel_layer(&swap_sk)?.onion;
|
||||
let mut mock_mixer = client::mock::MockMixClient::new();
|
||||
let mut mock_mixer = mix_client::mock::MockMixClient::new();
|
||||
let mixer_response = TxComponents {
|
||||
offset: ZERO_KEY,
|
||||
outputs: vec![Output::new(
|
||||
@@ -498,7 +636,10 @@ mod tests {
|
||||
};
|
||||
mock_mixer.set_response(
|
||||
&vec![mixer_onion.clone()],
|
||||
(vec![0 as usize], mixer_response),
|
||||
MixResp {
|
||||
indices: vec![0 as usize],
|
||||
components: mixer_response,
|
||||
},
|
||||
);
|
||||
|
||||
let mixer: Arc<dyn MixClient> = Arc::new(mock_mixer);
|
||||
@@ -508,9 +649,9 @@ mod tests {
|
||||
Some((&mixer_pk, &mixer)),
|
||||
node.clone(),
|
||||
);
|
||||
swapper.swap(&onion, &comsig)?;
|
||||
swapper.swap(&onion, &comsig).await?;
|
||||
|
||||
let tx = swapper.execute_round()?;
|
||||
let tx = swapper.execute_round().await?;
|
||||
assert!(tx.is_some());
|
||||
|
||||
// check that the transaction was posted
|
||||
@@ -527,9 +668,9 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Returns InvalidPayloadLength when too many payloads are provided.
|
||||
#[test]
|
||||
#[tokio::test]
|
||||
#[named]
|
||||
fn swap_too_many_payloads() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn swap_too_many_payloads() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
@@ -549,22 +690,19 @@ mod tests {
|
||||
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
let result = server.swap(&onion, &comsig);
|
||||
let result = server.swap(&onion, &comsig).await;
|
||||
assert_eq!(Err(SwapError::InvalidPayloadLength), result);
|
||||
|
||||
// Make sure no entry is added to the store
|
||||
assert_eq!(
|
||||
0,
|
||||
server.store.lock().unwrap().swaps_iter().unwrap().count()
|
||||
);
|
||||
assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns InvalidComSignature when ComSignature fails to verify.
|
||||
#[test]
|
||||
#[tokio::test]
|
||||
#[named]
|
||||
fn swap_invalid_com_signature() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn swap_invalid_com_signature() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
@@ -585,22 +723,19 @@ mod tests {
|
||||
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
let result = server.swap(&onion, &comsig);
|
||||
let result = server.swap(&onion, &comsig).await;
|
||||
assert_eq!(Err(SwapError::InvalidComSignature), result);
|
||||
|
||||
// Make sure no entry is added to the store
|
||||
assert_eq!(
|
||||
0,
|
||||
server.store.lock().unwrap().swaps_iter().unwrap().count()
|
||||
);
|
||||
assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns InvalidRangeProof when the rangeproof fails to verify for the commitment.
|
||||
#[test]
|
||||
#[tokio::test]
|
||||
#[named]
|
||||
fn swap_invalid_rangeproof() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn swap_invalid_rangeproof() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
@@ -620,22 +755,19 @@ mod tests {
|
||||
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
let result = server.swap(&onion, &comsig);
|
||||
let result = server.swap(&onion, &comsig).await;
|
||||
assert_eq!(Err(SwapError::InvalidRangeproof), result);
|
||||
|
||||
// Make sure no entry is added to the store
|
||||
assert_eq!(
|
||||
0,
|
||||
server.store.lock().unwrap().swaps_iter().unwrap().count()
|
||||
);
|
||||
assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns MissingRangeproof when no rangeproof is provided.
|
||||
#[test]
|
||||
#[tokio::test]
|
||||
#[named]
|
||||
fn swap_missing_rangeproof() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn swap_missing_rangeproof() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
@@ -652,22 +784,19 @@ mod tests {
|
||||
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
let result = server.swap(&onion, &comsig);
|
||||
let result = server.swap(&onion, &comsig).await;
|
||||
assert_eq!(Err(SwapError::MissingRangeproof), result);
|
||||
|
||||
// Make sure no entry is added to the store
|
||||
assert_eq!(
|
||||
0,
|
||||
server.store.lock().unwrap().swaps_iter().unwrap().count()
|
||||
);
|
||||
assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns CoinNotFound when there's no matching output in the UTXO set.
|
||||
#[test]
|
||||
#[tokio::test]
|
||||
#[named]
|
||||
fn swap_utxo_missing() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn swap_utxo_missing() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
@@ -686,7 +815,7 @@ mod tests {
|
||||
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new());
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
let result = server.swap(&onion, &comsig);
|
||||
let result = server.swap(&onion, &comsig).await;
|
||||
assert_eq!(
|
||||
Err(SwapError::CoinNotFound {
|
||||
commit: input_commit.clone()
|
||||
@@ -695,18 +824,15 @@ mod tests {
|
||||
);
|
||||
|
||||
// Make sure no entry is added to the store
|
||||
assert_eq!(
|
||||
0,
|
||||
server.store.lock().unwrap().swaps_iter().unwrap().count()
|
||||
);
|
||||
assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns AlreadySwapped when trying to swap the same commitment multiple times.
|
||||
#[test]
|
||||
#[tokio::test]
|
||||
#[named]
|
||||
fn swap_already_swapped() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn swap_already_swapped() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
@@ -725,10 +851,10 @@ mod tests {
|
||||
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
server.swap(&onion, &comsig)?;
|
||||
server.swap(&onion, &comsig).await?;
|
||||
|
||||
// Call swap a second time
|
||||
let result = server.swap(&onion, &comsig);
|
||||
let result = server.swap(&onion, &comsig).await;
|
||||
assert_eq!(
|
||||
Err(SwapError::AlreadySwapped {
|
||||
commit: input_commit.clone()
|
||||
@@ -739,10 +865,28 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns PeelOnionFailure when a failure occurs trying to decrypt the onion payload.
|
||||
#[test]
|
||||
/// Returns SwapTxNotFound when trying to check_reorg with a transaction not found in the store.
|
||||
#[tokio::test]
|
||||
#[named]
|
||||
fn swap_peel_onion_failure() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn swap_tx_not_found() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let server_key = secp::random_secret();
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new());
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
let kern = tx::build_kernel(&secp::random_secret(), 1000u64)?;
|
||||
let tx: Arc<Transaction> =
|
||||
Arc::new(Transaction::new(Inputs::default(), &[], &[kern.clone()]));
|
||||
let result = server.check_reorg(&tx).await;
|
||||
assert_eq!(Err(SwapError::SwapTxNotFound(kern.excess())), result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns PeelOnionFailure when a failure occurs trying to decrypt the onion payload.
|
||||
#[tokio::test]
|
||||
#[named]
|
||||
async fn swap_peel_onion_failure() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
@@ -763,7 +907,7 @@ mod tests {
|
||||
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
let result = server.swap(&onion, &comsig);
|
||||
let result = server.swap(&onion, &comsig).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_error_type!(result, SwapError::PeelOnionFailure(_));
|
||||
@@ -772,9 +916,9 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Returns FeeTooLow when the minimum fee is not met.
|
||||
#[test]
|
||||
#[tokio::test]
|
||||
#[named]
|
||||
fn swap_fee_too_low() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn swap_fee_too_low() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
@@ -793,11 +937,11 @@ mod tests {
|
||||
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
let result = server.swap(&onion, &comsig);
|
||||
let result = server.swap(&onion, &comsig).await;
|
||||
assert_eq!(
|
||||
Err(SwapError::FeeTooLow {
|
||||
minimum_fee: 12_500_000,
|
||||
actual_fee: fee as u64
|
||||
actual_fee: fee as u64,
|
||||
}),
|
||||
result
|
||||
);
|
||||
|
||||
+82
-89
@@ -1,22 +1,21 @@
|
||||
use crate::client::MixClient;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::FutureExt;
|
||||
use jsonrpc_core::{BoxFuture, Value};
|
||||
use jsonrpc_derive::rpc;
|
||||
use jsonrpc_http_server::{DomainsValidation, ServerBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use grin_onion::crypto::comsig::{self, ComSignature};
|
||||
use grin_onion::onion::Onion;
|
||||
|
||||
use crate::config::ServerConfig;
|
||||
use crate::mix_client::MixClient;
|
||||
use crate::node::GrinNode;
|
||||
use crate::servers::swap::{SwapError, SwapServer, SwapServerImpl};
|
||||
use crate::store::SwapStore;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
use grin_onion::crypto::comsig::{self, ComSignature};
|
||||
use grin_onion::onion::Onion;
|
||||
use grin_util::StopState;
|
||||
use jsonrpc_core::Value;
|
||||
use jsonrpc_derive::rpc;
|
||||
use jsonrpc_http_server::jsonrpc_core::*;
|
||||
use jsonrpc_http_server::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::{sleep, spawn};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SwapReq {
|
||||
onion: Onion,
|
||||
@@ -27,22 +26,23 @@ pub struct SwapReq {
|
||||
#[rpc(server)]
|
||||
pub trait SwapAPI {
|
||||
#[rpc(name = "swap")]
|
||||
fn swap(&self, swap: SwapReq) -> jsonrpc_core::Result<Value>;
|
||||
fn swap(&self, swap: SwapReq) -> BoxFuture<jsonrpc_core::Result<Value>>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RPCSwapServer {
|
||||
server_config: ServerConfig,
|
||||
server: Arc<Mutex<dyn SwapServer>>,
|
||||
server: Arc<tokio::sync::Mutex<dyn SwapServer>>,
|
||||
}
|
||||
|
||||
impl RPCSwapServer {
|
||||
/// Spin up an instance of the JSON-RPC HTTP server.
|
||||
fn start_http(&self) -> jsonrpc_http_server::Server {
|
||||
let mut io = IoHandler::new();
|
||||
fn start_http(&self, runtime_handle: tokio::runtime::Handle) -> jsonrpc_http_server::Server {
|
||||
let mut io = jsonrpc_core::IoHandler::new();
|
||||
io.extend_with(RPCSwapServer::to_delegate(self.clone()));
|
||||
|
||||
ServerBuilder::new(io)
|
||||
.event_loop_executor(runtime_handle)
|
||||
.cors(DomainsValidation::Disabled)
|
||||
.request_middleware(|request: hyper::Request<hyper::Body>| {
|
||||
if request.uri() == "/v1" {
|
||||
@@ -56,38 +56,45 @@ impl RPCSwapServer {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SwapError> for Error {
|
||||
impl From<SwapError> for jsonrpc_core::Error {
|
||||
fn from(e: SwapError) -> Self {
|
||||
match e {
|
||||
SwapError::UnknownError(_) => Error {
|
||||
SwapError::UnknownError(_) => jsonrpc_core::Error {
|
||||
message: e.to_string(),
|
||||
code: ErrorCode::InternalError,
|
||||
code: jsonrpc_core::ErrorCode::InternalError,
|
||||
data: None,
|
||||
},
|
||||
_ => Error::invalid_params(e.to_string()),
|
||||
_ => jsonrpc_core::Error::invalid_params(e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SwapAPI for RPCSwapServer {
|
||||
fn swap(&self, swap: SwapReq) -> jsonrpc_core::Result<Value> {
|
||||
self.server
|
||||
.lock()
|
||||
.unwrap()
|
||||
.swap(&swap.onion, &swap.comsig)?;
|
||||
Ok(Value::String("success".into()))
|
||||
fn swap(&self, swap: SwapReq) -> BoxFuture<jsonrpc_core::Result<Value>> {
|
||||
let server = self.server.clone();
|
||||
async move {
|
||||
server.lock().await.swap(&swap.onion, &swap.comsig).await?;
|
||||
Ok(Value::String("success".into()))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
/// Spin up the JSON-RPC web server
|
||||
pub fn listen(
|
||||
server_config: ServerConfig,
|
||||
rt_handle: &tokio::runtime::Handle,
|
||||
server_config: &ServerConfig,
|
||||
next_server: Option<Arc<dyn MixClient>>,
|
||||
wallet: Arc<dyn Wallet>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
store: SwapStore,
|
||||
stop_state: Arc<StopState>,
|
||||
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
) -> std::result::Result<
|
||||
(
|
||||
Arc<tokio::sync::Mutex<dyn SwapServer>>,
|
||||
jsonrpc_http_server::Server,
|
||||
),
|
||||
Box<dyn std::error::Error>,
|
||||
> {
|
||||
let server = SwapServerImpl::new(
|
||||
server_config.clone(),
|
||||
next_server,
|
||||
@@ -95,54 +102,34 @@ pub fn listen(
|
||||
node.clone(),
|
||||
store,
|
||||
);
|
||||
let server = Arc::new(Mutex::new(server));
|
||||
let server = Arc::new(tokio::sync::Mutex::new(server));
|
||||
|
||||
let rpc_server = RPCSwapServer {
|
||||
server_config: server_config.clone(),
|
||||
server: server.clone(),
|
||||
};
|
||||
|
||||
let http_server = rpc_server.start_http();
|
||||
let http_server = rpc_server.start_http(rt_handle.clone());
|
||||
|
||||
let close_handle = http_server.close_handle();
|
||||
let round_handle = spawn(move || {
|
||||
let mut secs = 0;
|
||||
loop {
|
||||
if stop_state.is_stopped() {
|
||||
close_handle.close();
|
||||
break;
|
||||
}
|
||||
|
||||
sleep(Duration::from_secs(1));
|
||||
secs = (secs + 1) % server_config.interval_s;
|
||||
|
||||
if secs == 0 {
|
||||
let _ = server.lock().unwrap().execute_round();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
http_server.wait();
|
||||
round_handle.join().unwrap();
|
||||
|
||||
Ok(())
|
||||
Ok((server, http_server))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::config::ServerConfig;
|
||||
use crate::crypto::comsig::ComSignature;
|
||||
use crate::crypto::secp;
|
||||
use crate::servers::swap::mock::MockSwapServer;
|
||||
use crate::servers::swap::{SwapError, SwapServer};
|
||||
use crate::servers::swap_rpc::{RPCSwapServer, SwapReq};
|
||||
|
||||
use grin_onion::create_onion;
|
||||
use std::net::TcpListener;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
|
||||
use hyper::{Body, Client, Request, Response};
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use grin_onion::create_onion;
|
||||
use grin_onion::crypto::comsig::ComSignature;
|
||||
use grin_onion::crypto::secp;
|
||||
|
||||
use crate::config::ServerConfig;
|
||||
use crate::servers::swap::{SwapError, SwapServer};
|
||||
use crate::servers::swap::mock::MockSwapServer;
|
||||
use crate::servers::swap_rpc::{RPCSwapServer, SwapReq};
|
||||
|
||||
async fn body_to_string(req: Response<Body>) -> String {
|
||||
let body_bytes = hyper::body::to_bytes(req.into_body()).await.unwrap();
|
||||
@@ -150,15 +137,15 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Spin up a temporary web service, query the API, then cleanup and return response
|
||||
fn make_request(
|
||||
server: Arc<Mutex<dyn SwapServer>>,
|
||||
async fn async_make_request(
|
||||
server: Arc<tokio::sync::Mutex<dyn SwapServer>>,
|
||||
req: String,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
runtime_handle: &tokio::runtime::Handle,
|
||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let server_config = ServerConfig {
|
||||
key: secp::random_secret(),
|
||||
interval_s: 1,
|
||||
addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?,
|
||||
socks_proxy_addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?,
|
||||
grin_node_url: "127.0.0.1:3413".parse()?,
|
||||
grin_node_secret_path: None,
|
||||
wallet_owner_url: "127.0.0.1:3420".parse()?,
|
||||
@@ -173,28 +160,21 @@ mod tests {
|
||||
};
|
||||
|
||||
// Start the JSON-RPC server
|
||||
let http_server = rpc_server.start_http();
|
||||
let http_server = rpc_server.start_http(runtime_handle.clone());
|
||||
|
||||
let uri = format!("http://{}/v1", server_config.addr);
|
||||
|
||||
let threaded_rt = Runtime::new()?;
|
||||
let do_request = async move {
|
||||
let request = Request::post(uri)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(req))
|
||||
.unwrap();
|
||||
let request = Request::post(uri)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(req))
|
||||
.unwrap();
|
||||
|
||||
Client::new().request(request).await
|
||||
};
|
||||
let response = Client::new().request(request).await?;
|
||||
|
||||
let response = threaded_rt.block_on(do_request)?;
|
||||
let response_str: String = threaded_rt.block_on(body_to_string(response));
|
||||
|
||||
// Wait for shutdown
|
||||
threaded_rt.shutdown_background();
|
||||
let response_str: String = body_to_string(response).await;
|
||||
|
||||
// Execute one round
|
||||
server.lock().unwrap().execute_round()?;
|
||||
server.lock().await.execute_round().await?;
|
||||
|
||||
// Stop the server
|
||||
http_server.close();
|
||||
@@ -206,7 +186,10 @@ mod tests {
|
||||
|
||||
/// Demonstrates a successful swap response
|
||||
#[test]
|
||||
fn swap_success() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn swap_success() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
let commitment = secp::commit(1234, &secp::random_secret())?;
|
||||
let onion = create_onion(&commitment, &vec![])?;
|
||||
let comsig = ComSignature::sign(1234, &secp::random_secret(), &onion.serialize()?)?;
|
||||
@@ -221,7 +204,8 @@ mod tests {
|
||||
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
|
||||
serde_json::json!(swap)
|
||||
);
|
||||
let response = make_request(server, req)?;
|
||||
let rt_handle = rt.handle().clone();
|
||||
let response = rt.block_on(async_make_request(server, req, &rt_handle))?;
|
||||
let expected = "{\"jsonrpc\":\"2.0\",\"result\":\"success\",\"id\":\"1\"}\n";
|
||||
assert_eq!(response, expected);
|
||||
|
||||
@@ -229,7 +213,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_bad_request() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn swap_bad_request() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
let server: Arc<Mutex<dyn SwapServer>> = Arc::new(Mutex::new(MockSwapServer::new()));
|
||||
|
||||
let params = "{ \"param\": \"Not a valid Swap request\" }";
|
||||
@@ -237,7 +224,8 @@ mod tests {
|
||||
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
|
||||
params
|
||||
);
|
||||
let response = make_request(server, req)?;
|
||||
let rt_handle = rt.handle().clone();
|
||||
let response = rt.block_on(async_make_request(server, req, &rt_handle))?;
|
||||
let expected = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"Invalid params: missing field `onion`.\"},\"id\":\"1\"}\n";
|
||||
assert_eq!(response, expected);
|
||||
Ok(())
|
||||
@@ -245,7 +233,11 @@ mod tests {
|
||||
|
||||
/// Returns "Commitment not found" when there's no matching output in the UTXO set.
|
||||
#[test]
|
||||
fn swap_utxo_missing() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn swap_utxo_missing() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
|
||||
let commitment = secp::commit(1234, &secp::random_secret())?;
|
||||
let onion = create_onion(&commitment, &vec![])?;
|
||||
let comsig = ComSignature::sign(1234, &secp::random_secret(), &onion.serialize()?)?;
|
||||
@@ -267,7 +259,8 @@ mod tests {
|
||||
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
|
||||
serde_json::json!(swap)
|
||||
);
|
||||
let response = make_request(server, req)?;
|
||||
let rt_handle = rt.handle().clone();
|
||||
let response = rt.block_on(async_make_request(server, req, &rt_handle))?;
|
||||
let expected = format!(
|
||||
"{{\"jsonrpc\":\"2.0\",\"error\":{{\"code\":-32602,\"message\":\"Output {:?} does not exist, or is already spent.\"}},\"id\":\"1\"}}\n",
|
||||
commitment
|
||||
|
||||
+92
-27
@@ -1,9 +1,5 @@
|
||||
use grin_core::core::{Input, Transaction};
|
||||
use grin_core::core::hash::Hash;
|
||||
use grin_onion::crypto::secp::{self, Commitment, RangeProof, SecretKey};
|
||||
use grin_onion::onion::Onion;
|
||||
use grin_onion::util::{read_optional, write_optional};
|
||||
|
||||
use grin_core::core::Input;
|
||||
use grin_core::ser::{
|
||||
self, DeserializationMode, ProtocolVersion, Readable, Reader, Writeable, Writer,
|
||||
};
|
||||
@@ -11,18 +7,30 @@ use grin_store::{self as store, Store};
|
||||
use grin_util::ToHex;
|
||||
use thiserror::Error;
|
||||
|
||||
use grin_onion::crypto::secp::{self, Commitment, RangeProof, SecretKey};
|
||||
use grin_onion::onion::Onion;
|
||||
use grin_onion::util::{read_optional, write_optional};
|
||||
|
||||
const DB_NAME: &str = "swap";
|
||||
const STORE_SUBPATH: &str = "swaps";
|
||||
|
||||
const CURRENT_VERSION: u8 = 0;
|
||||
const CURRENT_SWAP_VERSION: u8 = 0;
|
||||
const SWAP_PREFIX: u8 = b'S';
|
||||
|
||||
const CURRENT_TX_VERSION: u8 = 0;
|
||||
const TX_PREFIX: u8 = b'T';
|
||||
|
||||
/// Swap statuses
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SwapStatus {
|
||||
Unprocessed,
|
||||
InProcess { kernel_hash: Hash },
|
||||
Completed { kernel_hash: Hash, block_hash: Hash },
|
||||
InProcess {
|
||||
kernel_commit: Commitment,
|
||||
},
|
||||
Completed {
|
||||
kernel_commit: Commitment,
|
||||
block_hash: Hash,
|
||||
},
|
||||
Failed,
|
||||
}
|
||||
|
||||
@@ -32,16 +40,16 @@ impl Writeable for SwapStatus {
|
||||
SwapStatus::Unprocessed => {
|
||||
writer.write_u8(0)?;
|
||||
}
|
||||
SwapStatus::InProcess { kernel_hash } => {
|
||||
SwapStatus::InProcess { kernel_commit } => {
|
||||
writer.write_u8(1)?;
|
||||
kernel_hash.write(writer)?;
|
||||
kernel_commit.write(writer)?;
|
||||
}
|
||||
SwapStatus::Completed {
|
||||
kernel_hash,
|
||||
kernel_commit,
|
||||
block_hash,
|
||||
} => {
|
||||
writer.write_u8(2)?;
|
||||
kernel_hash.write(writer)?;
|
||||
kernel_commit.write(writer)?;
|
||||
block_hash.write(writer)?;
|
||||
}
|
||||
SwapStatus::Failed => {
|
||||
@@ -58,14 +66,14 @@ impl Readable for SwapStatus {
|
||||
let status = match reader.read_u8()? {
|
||||
0 => SwapStatus::Unprocessed,
|
||||
1 => {
|
||||
let kernel_hash = Hash::read(reader)?;
|
||||
SwapStatus::InProcess { kernel_hash }
|
||||
let kernel_commit = Commitment::read(reader)?;
|
||||
SwapStatus::InProcess { kernel_commit }
|
||||
}
|
||||
2 => {
|
||||
let kernel_hash = Hash::read(reader)?;
|
||||
let kernel_commit = Commitment::read(reader)?;
|
||||
let block_hash = Hash::read(reader)?;
|
||||
SwapStatus::Completed {
|
||||
kernel_hash,
|
||||
kernel_commit,
|
||||
block_hash,
|
||||
}
|
||||
}
|
||||
@@ -99,7 +107,7 @@ pub struct SwapData {
|
||||
|
||||
impl Writeable for SwapData {
|
||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
|
||||
writer.write_u8(CURRENT_VERSION)?;
|
||||
writer.write_u8(CURRENT_SWAP_VERSION)?;
|
||||
writer.write_fixed_bytes(&self.excess)?;
|
||||
writer.write_fixed_bytes(&self.output_commit)?;
|
||||
write_optional(writer, &self.rangeproof)?;
|
||||
@@ -115,7 +123,7 @@ impl Writeable for SwapData {
|
||||
impl Readable for SwapData {
|
||||
fn read<R: Reader>(reader: &mut R) -> Result<SwapData, ser::Error> {
|
||||
let version = reader.read_u8()?;
|
||||
if version != CURRENT_VERSION {
|
||||
if version != CURRENT_SWAP_VERSION {
|
||||
return Err(ser::Error::UnsupportedProtocolVersion);
|
||||
}
|
||||
|
||||
@@ -138,6 +146,41 @@ impl Readable for SwapData {
|
||||
}
|
||||
}
|
||||
|
||||
/// A transaction created as part of a swap round.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SwapTx {
|
||||
pub tx: Transaction,
|
||||
pub chain_tip: (u64, Hash),
|
||||
// TODO: Include status
|
||||
}
|
||||
|
||||
impl Writeable for SwapTx {
|
||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
|
||||
writer.write_u8(CURRENT_TX_VERSION)?;
|
||||
self.tx.write(writer)?;
|
||||
writer.write_u64(self.chain_tip.0)?;
|
||||
self.chain_tip.1.write(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Readable for SwapTx {
|
||||
fn read<R: Reader>(reader: &mut R) -> Result<SwapTx, ser::Error> {
|
||||
let version = reader.read_u8()?;
|
||||
if version != CURRENT_TX_VERSION {
|
||||
return Err(ser::Error::UnsupportedProtocolVersion);
|
||||
}
|
||||
|
||||
let tx = Transaction::read(reader)?;
|
||||
let height = reader.read_u64()?;
|
||||
let block_hash = Hash::read(reader)?;
|
||||
Ok(SwapTx {
|
||||
tx,
|
||||
chain_tip: (height, block_hash),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage facility for swap data.
|
||||
pub struct SwapStore {
|
||||
db: Store,
|
||||
@@ -148,6 +191,8 @@ pub struct SwapStore {
|
||||
pub enum StoreError {
|
||||
#[error("Swap entry already exists for '{0:?}'")]
|
||||
AlreadyExists(Commitment),
|
||||
#[error("Entry does not exist for '{0:?}'")]
|
||||
NotFound(Commitment),
|
||||
#[error("Error occurred while attempting to open db: {0}")]
|
||||
OpenError(store::lmdb::Error),
|
||||
#[error("Serialization error occurred: {0}")]
|
||||
@@ -193,7 +238,7 @@ impl SwapStore {
|
||||
|
||||
/// Reads a single value by key
|
||||
fn read<K: AsRef<[u8]> + Copy, V: Readable>(&self, prefix: u8, k: K) -> Result<V, StoreError> {
|
||||
store::option_to_not_found(self.db.get_ser(&store::to_key(prefix, k)[..], None), || {
|
||||
store::option_to_not_found(self.db.get_ser(&store::to_key(prefix, k), None), || {
|
||||
format!("{}:{}", prefix, k.to_hex())
|
||||
})
|
||||
.map_err(StoreError::ReadError)
|
||||
@@ -236,22 +281,42 @@ impl SwapStore {
|
||||
}
|
||||
|
||||
/// Reads a swap from the database
|
||||
#[allow(dead_code)]
|
||||
pub fn get_swap(&self, input_commit: &Commitment) -> Result<SwapData, StoreError> {
|
||||
self.read(SWAP_PREFIX, input_commit)
|
||||
}
|
||||
|
||||
/// Saves a swap transaction to the database
|
||||
pub fn save_swap_tx(&self, s: &SwapTx) -> Result<(), StoreError> {
|
||||
let data = ser::ser_vec(&s, ProtocolVersion::local())?;
|
||||
self.write(
|
||||
TX_PREFIX,
|
||||
&s.tx.kernels().first().unwrap().excess,
|
||||
&data,
|
||||
true,
|
||||
)
|
||||
.map_err(StoreError::WriteError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads a swap tx from the database
|
||||
pub fn get_swap_tx(&self, kernel_excess: &Commitment) -> Result<SwapTx, StoreError> {
|
||||
self.read(TX_PREFIX, kernel_excess)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::store::{SwapData, SwapStatus, SwapStore};
|
||||
use crate::StoreError;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use grin_core::core::{Input, OutputFeatures};
|
||||
use grin_core::global::{self, ChainTypes};
|
||||
use rand::RngCore;
|
||||
|
||||
use grin_onion::crypto::secp;
|
||||
use grin_onion::test_util as onion_test_util;
|
||||
use rand::RngCore;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::store::{StoreError, SwapData, SwapStatus, SwapStore};
|
||||
|
||||
fn new_store(test_name: &str) -> SwapStore {
|
||||
global::set_local_chain_type(ChainTypes::AutomatedTesting);
|
||||
@@ -278,11 +343,11 @@ mod tests {
|
||||
SwapStatus::Unprocessed
|
||||
} else if s == 1 {
|
||||
SwapStatus::InProcess {
|
||||
kernel_hash: onion_test_util::rand_hash(),
|
||||
kernel_commit: onion_test_util::rand_commit(),
|
||||
}
|
||||
} else {
|
||||
SwapStatus::Completed {
|
||||
kernel_hash: onion_test_util::rand_hash(),
|
||||
kernel_commit: onion_test_util::rand_commit(),
|
||||
block_hash: onion_test_util::rand_hash(),
|
||||
}
|
||||
};
|
||||
@@ -330,7 +395,7 @@ mod tests {
|
||||
assert!(store.swap_exists(&swap.input.commit)?);
|
||||
|
||||
swap.status = SwapStatus::InProcess {
|
||||
kernel_hash: onion_test_util::rand_hash(),
|
||||
kernel_commit: onion_test_util::rand_commit(),
|
||||
};
|
||||
let result = store.save_swap(&swap, false);
|
||||
assert_eq!(
|
||||
|
||||
+172
-56
@@ -1,83 +1,199 @@
|
||||
use crate::config::{self, ServerConfig};
|
||||
use std::sync::Arc;
|
||||
|
||||
use grin_core::global;
|
||||
use grin_wallet_impls::tor::config as tor_config;
|
||||
use grin_wallet_impls::tor::process::TorProcess;
|
||||
use std::collections::HashMap;
|
||||
use arti_client::config::TorClientConfigBuilder;
|
||||
use arti_client::{TorClient, TorClientConfig};
|
||||
use arti_hyper::ArtiHttpConnector;
|
||||
use curve25519_dalek::digest::Digest;
|
||||
use ed25519_dalek::hazmat::ExpandedSecretKey;
|
||||
use futures::task::SpawnExt;
|
||||
use sha2::Sha512;
|
||||
use thiserror::Error;
|
||||
use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder};
|
||||
use tls_api_native_tls::TlsConnector;
|
||||
use tor_hscrypto::pk::{HsIdKey, HsIdKeypair};
|
||||
use tor_hsrproxy::config::{
|
||||
Encapsulation, ProxyAction, ProxyConfigBuilder, ProxyPattern, ProxyRule, TargetAddr,
|
||||
};
|
||||
use tor_hsrproxy::OnionServiceReverseProxy;
|
||||
use tor_hsservice::config::OnionServiceConfigBuilder;
|
||||
use tor_hsservice::{
|
||||
HsIdKeypairSpecifier, HsIdPublicKeySpecifier, HsNickname, RunningOnionService,
|
||||
};
|
||||
use tor_keymgr::key_specifier_derive::internal;
|
||||
use tor_keymgr::{ArtiNativeKeystore, KeyMgrBuilder, KeystoreSelector};
|
||||
use tor_llcrypto::pk::ed25519::ExpandedKeypair;
|
||||
use tor_rtcompat::Runtime;
|
||||
|
||||
use secp256k1zkp::SecretKey;
|
||||
|
||||
use crate::config::ServerConfig;
|
||||
|
||||
/// Tor error types
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TorError {
|
||||
#[error("Error generating config: {0:?}")]
|
||||
ConfigError(String),
|
||||
ConfigError(grin_wallet_impls::Error),
|
||||
#[error("Error starting process: {0:?}")]
|
||||
ProcessError(grin_wallet_impls::tor::process::Error),
|
||||
}
|
||||
|
||||
pub fn init_tor_listener(server_config: &ServerConfig) -> Result<TorProcess, TorError> {
|
||||
println!("Initializing tor listener");
|
||||
pub struct TorService<R: Runtime> {
|
||||
tor_client: Option<TorClient<R>>,
|
||||
hidden_services: Vec<Arc<RunningOnionService>>,
|
||||
}
|
||||
|
||||
let mut tor_dir = config::get_grin_path(&global::get_chain_type());
|
||||
tor_dir.push("tor/listener");
|
||||
impl<R: Runtime> TorService<R> {
|
||||
/// Builds a hyper::Client with an ArtiHttpConnector over the TorClient.
|
||||
/// The returned Client makes HTTP requests through the TorClient directly, eliminating the need for a socks proxy.
|
||||
pub fn new_hyper_client(
|
||||
&self,
|
||||
) -> hyper::Client<ArtiHttpConnector<R, TlsConnector>, hyper::Body> {
|
||||
let tls_connector = TlsConnector::builder().unwrap().build().unwrap();
|
||||
let tor_connector = ArtiHttpConnector::new(self.tor_client.clone().unwrap(), tls_connector);
|
||||
|
||||
let mut torrc_dir = tor_dir.clone();
|
||||
torrc_dir.push("torrc");
|
||||
hyper::Client::builder().build::<_, hyper::Body>(tor_connector)
|
||||
}
|
||||
|
||||
tor_config::output_tor_listener_config(
|
||||
tor_dir.to_str().unwrap(),
|
||||
server_config.addr.to_string().as_str(),
|
||||
&vec![server_config.key.clone()],
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
)
|
||||
.map_err(|e| TorError::ConfigError(e.to_string()))?;
|
||||
pub fn stop(&mut self) {
|
||||
self.tor_client = None;
|
||||
self.hidden_services.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Start TOR process
|
||||
let mut process = TorProcess::new();
|
||||
process
|
||||
.torrc_path(torrc_dir.to_str().unwrap())
|
||||
.working_dir(tor_dir.to_str().unwrap())
|
||||
.timeout(20)
|
||||
.completion_percent(100)
|
||||
.launch()
|
||||
.map_err(TorError::ProcessError)?;
|
||||
pub async fn async_init_tor<R>(
|
||||
runtime: R,
|
||||
data_dir: &str,
|
||||
server_config: &ServerConfig,
|
||||
) -> Result<TorService<R>, TorError>
|
||||
where
|
||||
R: Runtime,
|
||||
{
|
||||
warn!("Initializing TOR");
|
||||
|
||||
println!(
|
||||
let state_dir = format!("{}/tor/state", &data_dir);
|
||||
let cache_dir = format!("{}/tor/cache", &data_dir);
|
||||
let hs_nickname = HsNickname::new("listener".to_string()).unwrap();
|
||||
|
||||
let mut client_config_builder =
|
||||
TorClientConfigBuilder::from_directories(state_dir.clone(), cache_dir.clone());
|
||||
client_config_builder
|
||||
.address_filter()
|
||||
.allow_onion_addrs(true);
|
||||
let client_config = client_config_builder.build().unwrap();
|
||||
|
||||
add_key_to_store(&client_config, &state_dir, &server_config.key, &hs_nickname)?;
|
||||
let tor_client = TorClient::with_runtime(runtime)
|
||||
.config(client_config)
|
||||
.create_bootstrapped()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let service =
|
||||
async_launch_hidden_service(hs_nickname.clone(), &tor_client, &server_config).await?;
|
||||
let tor_instance = TorService {
|
||||
tor_client: Some(tor_client),
|
||||
hidden_services: vec![service],
|
||||
};
|
||||
Ok(tor_instance)
|
||||
}
|
||||
|
||||
async fn async_launch_hidden_service<R>(
|
||||
hs_nickname: HsNickname,
|
||||
tor_client: &TorClient<R>,
|
||||
server_config: &ServerConfig,
|
||||
) -> Result<Arc<RunningOnionService>, TorError>
|
||||
where
|
||||
R: Runtime,
|
||||
{
|
||||
let svc_cfg = OnionServiceConfigBuilder::default()
|
||||
.nickname(hs_nickname.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let (service, request_stream) = tor_client.launch_onion_service(svc_cfg).unwrap();
|
||||
|
||||
let proxy_rule = ProxyRule::new(
|
||||
ProxyPattern::one_port(80).unwrap(),
|
||||
ProxyAction::Forward(Encapsulation::Simple, TargetAddr::Inet(server_config.addr)),
|
||||
);
|
||||
let mut proxy_cfg_builder = ProxyConfigBuilder::default();
|
||||
proxy_cfg_builder.set_proxy_ports(vec![proxy_rule]);
|
||||
let proxy = OnionServiceReverseProxy::new(proxy_cfg_builder.build().unwrap());
|
||||
|
||||
{
|
||||
let proxy = proxy.clone();
|
||||
let runtime_clone = tor_client.runtime().clone();
|
||||
tor_client
|
||||
.runtime()
|
||||
.spawn(async move {
|
||||
match proxy
|
||||
.handle_requests(runtime_clone, hs_nickname.clone(), request_stream)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
debug!("Onion service {} exited cleanly.", hs_nickname);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Onion service {} exited with an error: {}", hs_nickname, e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
warn!(
|
||||
"Server listening at http://{}.onion",
|
||||
server_config.onion_address().to_ov3_str()
|
||||
);
|
||||
Ok(process)
|
||||
Ok(service)
|
||||
}
|
||||
|
||||
pub fn init_tor_sender(server_config: &ServerConfig) -> Result<TorProcess, TorError> {
|
||||
println!(
|
||||
"Starting TOR Process for send at {:?}",
|
||||
server_config.socks_proxy_addr
|
||||
// TODO: Add proper error handling
|
||||
fn add_key_to_store(
|
||||
tor_config: &TorClientConfig,
|
||||
state_dir: &String,
|
||||
secret_key: &SecretKey,
|
||||
hs_nickname: &HsNickname,
|
||||
) -> Result<(), TorError> {
|
||||
let key_store_dir = format!("{}/keystore", &state_dir);
|
||||
let arti_store =
|
||||
ArtiNativeKeystore::from_path_and_mistrust(&key_store_dir, &tor_config.fs_mistrust())
|
||||
.unwrap();
|
||||
info!("Using keystore from {key_store_dir:?}");
|
||||
|
||||
let key_manager = KeyMgrBuilder::default()
|
||||
.default_store(Box::new(arti_store))
|
||||
.build()
|
||||
.map_err(|_| internal!("failed to build keymgr"))
|
||||
.unwrap();
|
||||
|
||||
let expanded_sk = ExpandedSecretKey::from_bytes(
|
||||
Sha512::default()
|
||||
.chain_update(secret_key)
|
||||
.finalize()
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
let mut tor_dir = config::get_grin_path(&global::get_chain_type());
|
||||
tor_dir.push("tor/sender");
|
||||
let mut sk_bytes = [0_u8; 64];
|
||||
sk_bytes[0..32].copy_from_slice(&expanded_sk.scalar.to_bytes());
|
||||
sk_bytes[32..64].copy_from_slice(&expanded_sk.hash_prefix);
|
||||
let expanded_kp = ExpandedKeypair::from_secret_key_bytes(sk_bytes).unwrap();
|
||||
|
||||
let mut torrc_dir = tor_dir.clone();
|
||||
torrc_dir.push("torrc");
|
||||
key_manager
|
||||
.insert(
|
||||
HsIdKey::from(expanded_kp.public().clone()),
|
||||
&HsIdPublicKeySpecifier::new(hs_nickname.clone()),
|
||||
KeystoreSelector::Default,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
tor_config::output_tor_sender_config(
|
||||
tor_dir.to_str().unwrap(),
|
||||
&server_config.socks_proxy_addr.to_string(),
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
)
|
||||
.map_err(|e| TorError::ConfigError(e.to_string()))?;
|
||||
key_manager
|
||||
.insert(
|
||||
HsIdKeypair::from(expanded_kp),
|
||||
&HsIdKeypairSpecifier::new(hs_nickname.clone()),
|
||||
KeystoreSelector::Default,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Start TOR process
|
||||
let mut tor_process = TorProcess::new();
|
||||
tor_process
|
||||
.torrc_path(torrc_dir.to_str().unwrap())
|
||||
.working_dir(tor_dir.to_str().unwrap())
|
||||
.timeout(20)
|
||||
.completion_percent(100)
|
||||
.launch()
|
||||
.map_err(TorError::ProcessError)?;
|
||||
Ok(tor_process)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::crypto::secp;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
use grin_core::core::{
|
||||
FeeFields, Input, Inputs, KernelFeatures, Output, Transaction, TransactionBody, TxKernel,
|
||||
};
|
||||
use grin_keychain::BlindingFactor;
|
||||
use grin_onion::crypto::secp;
|
||||
use secp256k1zkp::{ContextFlag, Secp256k1, SecretKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
@@ -43,7 +43,7 @@ pub struct TxComponents {
|
||||
}
|
||||
|
||||
/// Builds and verifies the finalized swap 'Transaction' using the provided components.
|
||||
pub fn assemble_tx(
|
||||
pub async fn async_assemble_tx(
|
||||
wallet: &Arc<dyn Wallet>,
|
||||
inputs: &Vec<Input>,
|
||||
outputs: &Vec<Output>,
|
||||
@@ -57,7 +57,7 @@ pub fn assemble_tx(
|
||||
let min_kernel_fee =
|
||||
TransactionBody::weight_by_iok(inputs.len() as u64, outputs.len() as u64, 1) * fee_base;
|
||||
|
||||
let components = add_kernel_and_collect_fees(
|
||||
let components = async_add_kernel_and_collect_fees(
|
||||
&wallet,
|
||||
&outputs,
|
||||
&kernels,
|
||||
@@ -66,7 +66,8 @@ pub fn assemble_tx(
|
||||
fees_paid,
|
||||
&prev_offset,
|
||||
&output_excesses,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
// assemble the transaction
|
||||
let tx = Transaction::new(
|
||||
@@ -79,7 +80,7 @@ pub fn assemble_tx(
|
||||
}
|
||||
|
||||
/// Adds a kernel and output to a collection of transaction components to consume fees and offset excesses.
|
||||
pub fn assemble_components(
|
||||
pub async fn async_assemble_components(
|
||||
wallet: &Arc<dyn Wallet>,
|
||||
components: &TxComponents,
|
||||
output_excesses: &Vec<SecretKey>,
|
||||
@@ -89,7 +90,7 @@ pub fn assemble_components(
|
||||
// calculate minimum fee required for the kernel
|
||||
let min_kernel_fee = TransactionBody::weight_by_iok(0, 0, 1) * fee_base;
|
||||
|
||||
add_kernel_and_collect_fees(
|
||||
async_add_kernel_and_collect_fees(
|
||||
&wallet,
|
||||
&components.outputs,
|
||||
&components.kernels,
|
||||
@@ -99,9 +100,10 @@ pub fn assemble_components(
|
||||
&components.offset,
|
||||
&output_excesses,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn add_kernel_and_collect_fees(
|
||||
async fn async_add_kernel_and_collect_fees(
|
||||
wallet: &Arc<dyn Wallet>,
|
||||
outputs: &Vec<Output>,
|
||||
kernels: &Vec<TxKernel>,
|
||||
@@ -128,7 +130,10 @@ fn add_kernel_and_collect_fees(
|
||||
let amount = fees_paid - (min_kernel_fee + fee_to_collect);
|
||||
kernel_fee -= amount;
|
||||
|
||||
let wallet_output = wallet.build_output(amount).map_err(TxError::WalletError)?;
|
||||
let wallet_output = wallet
|
||||
.async_build_output(amount)
|
||||
.await
|
||||
.map_err(TxError::WalletError)?;
|
||||
txn_outputs.push(wallet_output.1);
|
||||
|
||||
let output_excess = SecretKey::from_slice(&secp, &wallet_output.0.as_ref())
|
||||
@@ -202,11 +207,12 @@ fn add_kernel_and_collect_fees(
|
||||
///
|
||||
/// ```rust
|
||||
/// use secp256k1zkp::key::SecretKey;
|
||||
/// use crate::crypto::secp;
|
||||
/// use secp256k1zkp::rand::thread_rng;
|
||||
/// use grin_onion::crypto::secp;
|
||||
///
|
||||
/// let secret_key = SecretKey::new(&mut secp::rand::thread_rng());
|
||||
/// let secret_key = secp::random_secret();
|
||||
/// let fee = 10; // 10 nanogrin
|
||||
/// let kernel = build_kernel(&secret_key, fee);
|
||||
/// let kernel = mwixnet::tx::build_kernel(&secret_key, fee);
|
||||
/// ```
|
||||
pub fn build_kernel(excess: &SecretKey, fee: u64) -> Result<TxKernel, TxError> {
|
||||
let mut kernel = TxKernel::with_features(KernelFeatures::Plain {
|
||||
|
||||
+74
-88
@@ -1,36 +1,34 @@
|
||||
use crate::crypto::secp;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use grin_api::client;
|
||||
use grin_api::json_rpc::{build_request, Request, Response};
|
||||
use async_trait::async_trait;
|
||||
use grin_core::core::Output;
|
||||
use grin_core::libtx::secp_ser;
|
||||
use grin_keychain::BlindingFactor;
|
||||
use grin_util::{ToHex, ZeroingString};
|
||||
use grin_wallet_api::{EncryptedRequest, EncryptedResponse, JsonId, Token};
|
||||
use secp256k1zkp::{PublicKey, Secp256k1, SecretKey};
|
||||
use grin_wallet_api::Token;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::net::SocketAddr;
|
||||
use thiserror::Error;
|
||||
|
||||
use grin_onion::crypto::secp;
|
||||
use secp256k1zkp::{PublicKey, Secp256k1, SecretKey};
|
||||
|
||||
use crate::http;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Wallet: Send + Sync {
|
||||
/// Builds an output for the wallet with the provided amount.
|
||||
fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output), WalletError>;
|
||||
async fn async_build_output(
|
||||
&self,
|
||||
amount: u64,
|
||||
) -> Result<(BlindingFactor, Output), WalletError>;
|
||||
}
|
||||
|
||||
/// Error types for interacting with wallets
|
||||
#[derive(Error, Debug)]
|
||||
pub enum WalletError {
|
||||
#[error("Error encrypting request: {0:?}")]
|
||||
EncryptRequestError(grin_wallet_libwallet::Error),
|
||||
#[error("Error decrypting response: {0:?}")]
|
||||
DecryptResponseError(grin_wallet_libwallet::Error),
|
||||
#[error("Error decoding JSON response: {0:?}")]
|
||||
DecodeResponseError(serde_json::Error),
|
||||
#[error("JSON-RPC API communication error: {0:?}")]
|
||||
ApiCommError(grin_api::Error),
|
||||
#[error("Error decoding JSON-RPC response: {0:?}")]
|
||||
ResponseParseError(grin_api::json_rpc::Error),
|
||||
#[error("Error communication with wallet: {0:?}")]
|
||||
WalletCommError(http::HttpError),
|
||||
}
|
||||
|
||||
/// HTTP (JSONRPC) implementation of the 'Wallet' trait.
|
||||
@@ -55,26 +53,29 @@ pub struct ECDHPubkey {
|
||||
|
||||
impl HttpWallet {
|
||||
/// Calls the 'open_wallet' using the RPC API.
|
||||
pub fn open_wallet(
|
||||
pub async fn async_open_wallet(
|
||||
wallet_owner_url: &SocketAddr,
|
||||
wallet_owner_secret: &Option<String>,
|
||||
wallet_pass: &ZeroingString,
|
||||
) -> Result<HttpWallet, WalletError> {
|
||||
println!("Opening wallet at {}", wallet_owner_url);
|
||||
let shared_key = HttpWallet::init_secure_api(&wallet_owner_url, &wallet_owner_secret)?;
|
||||
|
||||
info!("Opening wallet at {}", wallet_owner_url);
|
||||
let shared_key =
|
||||
HttpWallet::async_init_secure_api(&wallet_owner_url, &wallet_owner_secret).await?;
|
||||
let open_wallet_params = json!({
|
||||
"name": null,
|
||||
"password": wallet_pass.to_string()
|
||||
});
|
||||
let token: Token = HttpWallet::send_enc_request(
|
||||
&wallet_owner_url,
|
||||
let url = format!("http://{}{}", wallet_owner_url, ENDPOINT);
|
||||
let token: Token = http::async_send_enc_request(
|
||||
&url,
|
||||
&wallet_owner_secret,
|
||||
"open_wallet",
|
||||
&open_wallet_params,
|
||||
&shared_key,
|
||||
)?;
|
||||
println!("Connected to wallet");
|
||||
)
|
||||
.await
|
||||
.map_err(WalletError::WalletCommError)?;
|
||||
info!("Connected to wallet");
|
||||
|
||||
Ok(HttpWallet {
|
||||
wallet_owner_url: wallet_owner_url.clone(),
|
||||
@@ -84,23 +85,27 @@ impl HttpWallet {
|
||||
})
|
||||
}
|
||||
|
||||
fn init_secure_api(
|
||||
async fn async_init_secure_api(
|
||||
wallet_owner_url: &SocketAddr,
|
||||
wallet_owner_secret: &Option<String>,
|
||||
) -> Result<SecretKey, WalletError> {
|
||||
let secp = Secp256k1::new();
|
||||
let ephemeral_sk = secp::random_secret();
|
||||
let ephemeral_pk = PublicKey::from_secret_key(&secp, &ephemeral_sk).unwrap();
|
||||
let ephemeral_pk_bytes = ephemeral_pk.serialize_vec(&secp, true);
|
||||
let init_params = json!({
|
||||
"ecdh_pubkey": ephemeral_pk.serialize_vec(&secp, true).to_hex()
|
||||
"ecdh_pubkey": ephemeral_pk_bytes.to_hex()
|
||||
});
|
||||
|
||||
let response_pk: ECDHPubkey = HttpWallet::send_json_request(
|
||||
&wallet_owner_url,
|
||||
let url = format!("http://{}{}", wallet_owner_url, ENDPOINT);
|
||||
let response_pk: ECDHPubkey = http::async_send_json_request(
|
||||
&url,
|
||||
&wallet_owner_secret,
|
||||
"init_secure_api",
|
||||
&init_params,
|
||||
)?;
|
||||
)
|
||||
.await
|
||||
.map_err(WalletError::WalletCommError)?;
|
||||
|
||||
let shared_key = {
|
||||
let mut shared_pubkey = response_pk.ecdh_pubkey.clone();
|
||||
@@ -113,59 +118,25 @@ impl HttpWallet {
|
||||
Ok(shared_key)
|
||||
}
|
||||
|
||||
fn send_enc_request<D: serde::de::DeserializeOwned>(
|
||||
wallet_owner_url: &SocketAddr,
|
||||
wallet_owner_secret: &Option<String>,
|
||||
pub async fn async_perform_request<D: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: &serde_json::Value,
|
||||
shared_key: &SecretKey,
|
||||
) -> Result<D, WalletError> {
|
||||
let url = format!("http://{}{}", wallet_owner_url, ENDPOINT);
|
||||
let req = json!({
|
||||
"method": method,
|
||||
"params": params,
|
||||
"id": JsonId::IntId(1),
|
||||
"jsonrpc": "2.0",
|
||||
});
|
||||
let enc_req = EncryptedRequest::from_json(&JsonId::IntId(1), &req, &shared_key)
|
||||
.map_err(WalletError::EncryptRequestError)?;
|
||||
let res = client::post::<EncryptedRequest, EncryptedResponse>(
|
||||
url.as_str(),
|
||||
wallet_owner_secret.clone(),
|
||||
&enc_req,
|
||||
client::TimeOut::default(),
|
||||
let url = format!("http://{}{}", self.wallet_owner_url, ENDPOINT);
|
||||
http::async_send_enc_request(
|
||||
&url,
|
||||
&self.wallet_owner_secret,
|
||||
method,
|
||||
params,
|
||||
&self.shared_key,
|
||||
)
|
||||
.map_err(WalletError::ApiCommError)?;
|
||||
let decrypted = res
|
||||
.decrypt(&shared_key)
|
||||
.map_err(WalletError::DecryptResponseError)?;
|
||||
let response: Response =
|
||||
serde_json::from_value(decrypted).map_err(WalletError::DecodeResponseError)?;
|
||||
let ok = response.result.unwrap().get("Ok").unwrap().clone();
|
||||
let parsed = serde_json::from_value(ok).map_err(WalletError::DecodeResponseError)?;
|
||||
Ok(parsed)
|
||||
.await
|
||||
.map_err(WalletError::WalletCommError)
|
||||
}
|
||||
|
||||
fn send_json_request<D: serde::de::DeserializeOwned>(
|
||||
wallet_owner_url: &SocketAddr,
|
||||
wallet_owner_secret: &Option<String>,
|
||||
method: &str,
|
||||
params: &serde_json::Value,
|
||||
) -> Result<D, WalletError> {
|
||||
let url = format!("http://{}{}", wallet_owner_url, ENDPOINT);
|
||||
let req = build_request(method, params);
|
||||
let res =
|
||||
client::post::<Request, Response>(
|
||||
url.as_str(),
|
||||
wallet_owner_secret.clone(),
|
||||
&req,
|
||||
client::TimeOut::default()
|
||||
).map_err(WalletError::ApiCommError)?;
|
||||
let parsed = res
|
||||
.clone()
|
||||
.into_result()
|
||||
.map_err(WalletError::ResponseParseError)?;
|
||||
Ok(parsed)
|
||||
pub fn get_token(&self) -> Token {
|
||||
self.token.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,36 +150,47 @@ pub struct OutputWithBlind {
|
||||
output: Output,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Wallet for HttpWallet {
|
||||
/// Builds an 'Output' for the wallet using the 'build_output' RPC API.
|
||||
fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output), WalletError> {
|
||||
let req_json = json!({
|
||||
"token": self.token.keychain_mask.clone().unwrap().0,
|
||||
async fn async_build_output(
|
||||
&self,
|
||||
amount: u64,
|
||||
) -> Result<(BlindingFactor, Output), WalletError> {
|
||||
let params = json!({
|
||||
"token": self.token,
|
||||
"features": "Plain",
|
||||
"amount": amount
|
||||
});
|
||||
let output: OutputWithBlind = HttpWallet::send_enc_request(
|
||||
&self.wallet_owner_url,
|
||||
|
||||
let url = format!("http://{}{}", self.wallet_owner_url, ENDPOINT);
|
||||
let output: OutputWithBlind = http::async_send_enc_request(
|
||||
&url,
|
||||
&self.wallet_owner_secret,
|
||||
"build_output",
|
||||
&req_json,
|
||||
¶ms,
|
||||
&self.shared_key,
|
||||
)?;
|
||||
)
|
||||
.await
|
||||
.map_err(WalletError::WalletCommError)?;
|
||||
Ok((output.blind, output.output))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod mock {
|
||||
use super::{Wallet, WalletError};
|
||||
use crate::crypto::secp;
|
||||
use std::borrow::BorrowMut;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use grin_core::core::{Output, OutputFeatures};
|
||||
use grin_keychain::BlindingFactor;
|
||||
|
||||
use grin_onion::crypto::secp;
|
||||
use secp256k1zkp::pedersen::Commitment;
|
||||
use secp256k1zkp::Secp256k1;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::{Wallet, WalletError};
|
||||
|
||||
/// Mock implementation of the 'Wallet' trait for unit-tests.
|
||||
#[derive(Clone)]
|
||||
@@ -230,9 +212,13 @@ pub mod mock {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Wallet for MockWallet {
|
||||
/// Builds an 'Output' for the wallet using the 'build_output' RPC API.
|
||||
fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output), WalletError> {
|
||||
async fn async_build_output(
|
||||
&self,
|
||||
amount: u64,
|
||||
) -> Result<(BlindingFactor, Output), WalletError> {
|
||||
let secp = Secp256k1::new();
|
||||
let blind = secp::random_secret();
|
||||
let commit = secp::commit(amount, &blind).unwrap();
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
// Copyright 2021 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Mining service, gets a block to mine, and based on mining configuration
|
||||
//! chooses a version of the cuckoo miner to mine the block and produce a valid
|
||||
//! header with its proof-of-work. Any valid mined blocks are submitted to the
|
||||
//! network.
|
||||
|
||||
use crate::common::types::BlockFees;
|
||||
use crate::common::wallet::IntegrationGrinWallet;
|
||||
use chrono::prelude::Utc;
|
||||
use chrono::{DateTime, NaiveDateTime};
|
||||
use grin_chain::Chain;
|
||||
use grin_core::core::hash::{Hash, Hashed};
|
||||
use grin_core::core::{Block, BlockHeader, Transaction};
|
||||
use grin_core::{consensus, global};
|
||||
use grin_keychain::Identifier;
|
||||
use grin_util::Mutex;
|
||||
use rand::{thread_rng, Rng};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct Miner {
|
||||
chain: Arc<Chain>,
|
||||
}
|
||||
|
||||
impl Miner {
|
||||
// Creates a new Miner. Needs references to the chain state and its
|
||||
/// storage.
|
||||
pub fn new(chain: Arc<Chain>) -> Miner {
|
||||
Miner { chain }
|
||||
}
|
||||
|
||||
pub async fn async_mine_empty_blocks(
|
||||
&self,
|
||||
wallet: &Arc<Mutex<IntegrationGrinWallet>>,
|
||||
num_blocks: usize,
|
||||
) {
|
||||
for _ in 0..num_blocks {
|
||||
self.async_mine_next_block(wallet, &vec![]).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a new block on top of the existing chain.
|
||||
pub async fn async_mine_next_block(
|
||||
&self,
|
||||
wallet: &Arc<Mutex<IntegrationGrinWallet>>,
|
||||
txs: &Vec<Transaction>,
|
||||
) {
|
||||
info!("Starting test miner loop.");
|
||||
|
||||
// iteration, we keep the returned derivation to provide it back when
|
||||
// nothing has changed. We only want to create a new key_id for each new block.
|
||||
let mut key_id = None;
|
||||
|
||||
loop {
|
||||
// get the latest chain state and build a block on top of it
|
||||
let head = self.chain.head_header().unwrap();
|
||||
let mut latest_hash = self.chain.head().unwrap().last_block_h;
|
||||
|
||||
let (mut b, block_fees) = self.async_get_block(wallet, txs, key_id.clone()).await;
|
||||
let sol = self.inner_mining_loop(&mut b, &head, &mut latest_hash);
|
||||
|
||||
// we found a solution, push our block through the chain processing pipeline
|
||||
if sol {
|
||||
info!(
|
||||
"Found valid proof of work, adding block {} (prev_root {}).",
|
||||
b.hash(),
|
||||
b.header.prev_root,
|
||||
);
|
||||
let res = self.chain.process_block(b, grin_chain::Options::MINE);
|
||||
if let Err(e) = res {
|
||||
error!("Error validating mined block: {:?}", e);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
key_id = None;
|
||||
} else {
|
||||
key_id = block_fees.key_id();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The inner part of mining loop for the internal miner
|
||||
/// kept around mostly for automated testing purposes
|
||||
fn inner_mining_loop(&self, b: &mut Block, head: &BlockHeader, latest_hash: &mut Hash) -> bool {
|
||||
while head.hash() == *latest_hash {
|
||||
let mut ctx = global::create_pow_context::<u32>(
|
||||
head.height,
|
||||
global::min_edge_bits(),
|
||||
global::proofsize(),
|
||||
10,
|
||||
)
|
||||
.unwrap();
|
||||
ctx.set_header_nonce(b.header.pre_pow(), None, true)
|
||||
.unwrap();
|
||||
if let Ok(proofs) = ctx.find_cycles() {
|
||||
b.header.pow.proof = proofs[0].clone();
|
||||
let proof_diff = b.header.pow.to_difficulty(b.header.height);
|
||||
if proof_diff >= (b.header.total_difficulty() - head.total_difficulty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
b.header.pow.nonce += 1;
|
||||
*latest_hash = self.chain.head().unwrap().last_block_h;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
// Ensure a block suitable for mining is built and returned
|
||||
// If a wallet listener URL is not provided the reward will be "burnt"
|
||||
// Warning: This call does not return until/unless a new block can be built
|
||||
async fn async_get_block(
|
||||
&self,
|
||||
wallet: &Arc<Mutex<IntegrationGrinWallet>>,
|
||||
txs: &Vec<Transaction>,
|
||||
key_id: Option<Identifier>,
|
||||
) -> (Block, BlockFees) {
|
||||
let wallet_retry_interval = 5;
|
||||
// get the latest chain state and build a block on top of it
|
||||
let mut result = self.async_build_block(wallet, txs, key_id.clone()).await;
|
||||
while let Err(e) = result {
|
||||
println!("Error: {:?}", &e);
|
||||
let mut new_key_id = key_id.to_owned();
|
||||
match e {
|
||||
grin_servers::common::types::Error::Chain(c) => match c {
|
||||
grin_chain::Error::DuplicateCommitment(_) => {
|
||||
debug!(
|
||||
"Duplicate commit for potential coinbase detected. Trying next derivation."
|
||||
);
|
||||
// use the next available key to generate a different coinbase commitment
|
||||
new_key_id = None;
|
||||
}
|
||||
_ => {
|
||||
error!("Chain Error: {}", c);
|
||||
}
|
||||
},
|
||||
grin_servers::common::types::Error::WalletComm(_) => {
|
||||
error!(
|
||||
"Error building new block: Can't connect to wallet listener; will retry"
|
||||
);
|
||||
async_std::task::sleep(Duration::from_secs(wallet_retry_interval)).await;
|
||||
}
|
||||
ae => {
|
||||
warn!("Error building new block: {:?}. Retrying.", ae);
|
||||
}
|
||||
}
|
||||
|
||||
// only wait if we are still using the same key: a different coinbase commitment is unlikely
|
||||
// to have duplication
|
||||
if new_key_id.is_some() {
|
||||
async_std::task::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
result = self.async_build_block(wallet, txs, new_key_id).await;
|
||||
}
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
/// Builds a new block with the chain head as previous and eligible
|
||||
/// transactions from the pool.
|
||||
async fn async_build_block(
|
||||
&self,
|
||||
wallet: &Arc<Mutex<IntegrationGrinWallet>>,
|
||||
txs: &Vec<Transaction>,
|
||||
key_id: Option<Identifier>,
|
||||
) -> Result<(Block, BlockFees), grin_servers::common::types::Error> {
|
||||
let head = self.chain.head_header()?;
|
||||
|
||||
// prepare the block header timestamp
|
||||
let mut now_sec = Utc::now().timestamp();
|
||||
let head_sec = head.timestamp.timestamp();
|
||||
if now_sec <= head_sec {
|
||||
now_sec = head_sec + 1;
|
||||
}
|
||||
|
||||
// Determine the difficulty our block should be at.
|
||||
// Note: do not keep the difficulty_iter in scope (it has an active batch).
|
||||
let difficulty = consensus::next_difficulty(head.height + 1, self.chain.difficulty_iter()?);
|
||||
|
||||
// build the coinbase and the block itself
|
||||
let fees = txs.iter().map(|tx| tx.fee()).sum();
|
||||
let height = head.height + 1;
|
||||
let block_fees = BlockFees {
|
||||
fees,
|
||||
key_id,
|
||||
height,
|
||||
};
|
||||
|
||||
let res = wallet.lock().async_create_coinbase(&block_fees).await?;
|
||||
let output = res.output;
|
||||
let kernel = res.kernel;
|
||||
let block_fees = BlockFees {
|
||||
key_id: res.key_id,
|
||||
..block_fees
|
||||
};
|
||||
let mut b = Block::from_reward(&head, &txs, output, kernel, difficulty.difficulty)?;
|
||||
|
||||
// making sure we're not spending time mining a useless block
|
||||
b.validate(&head.total_kernel_offset)?;
|
||||
|
||||
b.header.pow.nonce = thread_rng().gen();
|
||||
b.header.pow.secondary_scaling = difficulty.secondary_scaling;
|
||||
b.header.timestamp = DateTime::<Utc>::from_naive_utc_and_offset(
|
||||
NaiveDateTime::from_timestamp_opt(now_sec, 0).unwrap(),
|
||||
Utc,
|
||||
);
|
||||
|
||||
debug!(
|
||||
"Built new block with {} inputs and {} outputs, block difficulty: {}, cumulative difficulty {}",
|
||||
b.inputs().len(),
|
||||
b.outputs().len(),
|
||||
difficulty.difficulty,
|
||||
b.header.total_difficulty().to_num(),
|
||||
);
|
||||
|
||||
// Now set txhashset roots and sizes on the header of the block being built.
|
||||
match self.chain.set_txhashset_roots(&mut b) {
|
||||
Ok(_) => Ok((b, block_fees)),
|
||||
Err(e) => {
|
||||
match e {
|
||||
// If this is a duplicate commitment then likely trying to use
|
||||
// a key that hass already been derived but not in the wallet
|
||||
// for some reason, allow caller to retry.
|
||||
grin_chain::Error::DuplicateCommitment(e) => {
|
||||
Err(grin_servers::common::types::Error::Chain(
|
||||
grin_chain::Error::DuplicateCommitment(e),
|
||||
))
|
||||
}
|
||||
|
||||
// Some other issue, possibly duplicate kernel
|
||||
_ => {
|
||||
error!("Error setting txhashset root to build a block: {:?}", e);
|
||||
Err(grin_servers::common::types::Error::Chain(
|
||||
grin_chain::Error::Other(format!("{:?}", e)),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod miner;
|
||||
pub mod node;
|
||||
pub mod server;
|
||||
pub mod types;
|
||||
pub mod wallet;
|
||||
@@ -0,0 +1,122 @@
|
||||
extern crate grin_wallet_api as apiwallet;
|
||||
extern crate grin_wallet_config as wallet_config;
|
||||
extern crate grin_wallet_controller as wallet_controller;
|
||||
extern crate grin_wallet_impls as wallet;
|
||||
extern crate grin_wallet_libwallet as libwallet;
|
||||
|
||||
use futures::channel::oneshot;
|
||||
|
||||
use grin_core as core;
|
||||
|
||||
use grin_p2p as p2p;
|
||||
use grin_servers as servers;
|
||||
|
||||
use grin_util::logger::LogEntry;
|
||||
use grin_util::{Mutex, StopState};
|
||||
use std::default::Default;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use mwixnet::{GrinNode, HttpGrinNode};
|
||||
use std::sync::{mpsc, Arc};
|
||||
use std::thread;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct IntegrationGrinNode {
|
||||
server_config: servers::ServerConfig,
|
||||
stop_state: Arc<StopState>,
|
||||
server: Option<Arc<servers::Server>>,
|
||||
}
|
||||
|
||||
impl IntegrationGrinNode {
|
||||
pub fn start(&mut self) -> Arc<servers::Server> {
|
||||
let stop_state_thread = self.stop_state.clone();
|
||||
let server_config_thread = self.server_config.clone();
|
||||
|
||||
// Create a channel to communicate between threads
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
// Start the node in a new thread
|
||||
thread::spawn(move || {
|
||||
let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) =
|
||||
Box::leak(Box::new(oneshot::channel::<()>()));
|
||||
|
||||
servers::Server::start(
|
||||
server_config_thread.clone(),
|
||||
None,
|
||||
move |serv: servers::Server, _: Option<mpsc::Receiver<LogEntry>>| {
|
||||
// Signal that the callback has been called
|
||||
tx.send(serv).unwrap();
|
||||
// Do other necessary stuff here
|
||||
},
|
||||
Some(stop_state_thread.clone()),
|
||||
api_chan,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// Wait for the signal from the node-running thread
|
||||
let server = Arc::new(rx.recv().unwrap());
|
||||
self.server = Some(server.clone());
|
||||
|
||||
server
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
self.stop_state.stop();
|
||||
}
|
||||
|
||||
pub fn api_address(&self) -> SocketAddr {
|
||||
self.server_config.api_http_addr.parse().unwrap()
|
||||
}
|
||||
|
||||
pub fn to_client(&self) -> Arc<dyn GrinNode> {
|
||||
Arc::new(HttpGrinNode::new(&self.api_address(), &None))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct GrinNodeManager {
|
||||
// base directory for the server instance
|
||||
working_dir: String,
|
||||
|
||||
nodes: Vec<Arc<Mutex<IntegrationGrinNode>>>,
|
||||
}
|
||||
|
||||
impl GrinNodeManager {
|
||||
pub fn new(test_dir: &str) -> GrinNodeManager {
|
||||
GrinNodeManager {
|
||||
working_dir: String::from(test_dir),
|
||||
nodes: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_node(&mut self) -> Arc<Mutex<IntegrationGrinNode>> {
|
||||
let server_config = servers::ServerConfig {
|
||||
api_http_addr: format!("127.0.0.1:{}", 20000 + self.nodes.len()),
|
||||
api_secret_path: None,
|
||||
db_root: format!("{}/nodes/{}", self.working_dir, self.nodes.len()),
|
||||
p2p_config: p2p::P2PConfig {
|
||||
port: 13414,
|
||||
seeding_type: p2p::Seeding::None,
|
||||
..p2p::P2PConfig::default()
|
||||
},
|
||||
chain_type: core::global::ChainTypes::AutomatedTesting,
|
||||
skip_sync_wait: Some(true),
|
||||
stratum_mining_config: None,
|
||||
..Default::default()
|
||||
};
|
||||
let node = Arc::new(Mutex::new(IntegrationGrinNode {
|
||||
server_config,
|
||||
stop_state: Arc::new(StopState::new()),
|
||||
server: None,
|
||||
}));
|
||||
self.nodes.push(node.clone());
|
||||
node
|
||||
}
|
||||
|
||||
pub fn stop_all(&self) {
|
||||
for node in &self.nodes {
|
||||
node.lock().stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
use std::iter;
|
||||
use std::net::TcpListener;
|
||||
use std::sync::Arc;
|
||||
|
||||
use grin_core::core::Transaction;
|
||||
use tor_rtcompat::PreferredRuntime;
|
||||
use x25519_dalek::{PublicKey as xPublicKey, StaticSecret};
|
||||
|
||||
use grin_onion::crypto::comsig::ComSignature;
|
||||
use grin_onion::crypto::dalek::DalekPublicKey;
|
||||
use grin_onion::onion::Onion;
|
||||
use mwixnet::mix_client::MixClientImpl;
|
||||
use mwixnet::tor::TorService;
|
||||
use mwixnet::{tor, SwapError, SwapServer, SwapStore};
|
||||
use secp256k1zkp::SecretKey;
|
||||
|
||||
use crate::common::node::IntegrationGrinNode;
|
||||
use crate::common::wallet::{GrinWalletManager, IntegrationGrinWallet};
|
||||
|
||||
pub struct IntegrationSwapServer<R: tor_rtcompat::Runtime> {
|
||||
server_key: SecretKey,
|
||||
tor_instance: Arc<grin_util::Mutex<TorService<R>>>,
|
||||
swap_server: Arc<tokio::sync::Mutex<dyn SwapServer>>,
|
||||
rpc_server: jsonrpc_http_server::Server,
|
||||
_wallet: Arc<grin_util::Mutex<IntegrationGrinWallet>>,
|
||||
}
|
||||
|
||||
impl<R: tor_rtcompat::Runtime> IntegrationSwapServer<R> {
|
||||
pub async fn async_swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError> {
|
||||
self.swap_server.lock().await.swap(&onion, &comsig).await
|
||||
}
|
||||
|
||||
pub async fn async_execute_round(&self) -> Result<Option<Arc<Transaction>>, SwapError> {
|
||||
self.swap_server.lock().await.execute_round().await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IntegrationMixServer<R: tor_rtcompat::Runtime> {
|
||||
server_key: SecretKey,
|
||||
tor_instance: Arc<grin_util::Mutex<TorService<R>>>,
|
||||
rpc_server: jsonrpc_http_server::Server,
|
||||
_wallet: Arc<grin_util::Mutex<IntegrationGrinWallet>>,
|
||||
}
|
||||
|
||||
async fn async_new_swap_server<R>(
|
||||
data_dir: &str,
|
||||
rt_handle: &tokio::runtime::Handle,
|
||||
tor_runtime: R,
|
||||
wallets: &mut GrinWalletManager,
|
||||
server_key: &SecretKey,
|
||||
node: &Arc<grin_util::Mutex<IntegrationGrinNode>>,
|
||||
next_server: Option<&IntegrationMixServer<R>>,
|
||||
) -> IntegrationSwapServer<R>
|
||||
where
|
||||
R: tor_rtcompat::Runtime,
|
||||
{
|
||||
let wallet = wallets.async_new_wallet(&node.lock().api_address()).await;
|
||||
|
||||
let server_config = mwixnet::ServerConfig {
|
||||
key: server_key.clone(),
|
||||
interval_s: 15,
|
||||
addr: TcpListener::bind("127.0.0.1:0")
|
||||
.unwrap()
|
||||
.local_addr()
|
||||
.unwrap(),
|
||||
grin_node_url: node.lock().api_address(),
|
||||
grin_node_secret_path: None,
|
||||
wallet_owner_url: wallet.lock().owner_address(),
|
||||
wallet_owner_secret_path: None,
|
||||
prev_server: None,
|
||||
next_server: match next_server {
|
||||
Some(s) => Some(DalekPublicKey::from_secret(&s.server_key)),
|
||||
None => None,
|
||||
},
|
||||
};
|
||||
|
||||
// Open SwapStore
|
||||
let store = SwapStore::new(format!("{}/db", data_dir).as_str()).unwrap();
|
||||
let tor_instance = tor::async_init_tor(tor_runtime, &data_dir, &server_config)
|
||||
.await
|
||||
.unwrap();
|
||||
let tor_instance = Arc::new(grin_util::Mutex::new(tor_instance));
|
||||
|
||||
let (swap_server, rpc_server) = mwixnet::swap_listen(
|
||||
rt_handle,
|
||||
&server_config,
|
||||
match next_server {
|
||||
Some(s) => Some(Arc::new(MixClientImpl::new(
|
||||
server_config.clone(),
|
||||
tor_instance.clone(),
|
||||
DalekPublicKey::from_secret(&s.server_key),
|
||||
))),
|
||||
None => None,
|
||||
},
|
||||
wallet.lock().get_client(),
|
||||
node.lock().to_client(),
|
||||
store,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
IntegrationSwapServer {
|
||||
server_key: server_key.clone(),
|
||||
tor_instance,
|
||||
swap_server,
|
||||
rpc_server,
|
||||
_wallet: wallet,
|
||||
}
|
||||
}
|
||||
|
||||
async fn async_new_mix_server<R>(
|
||||
data_dir: &str,
|
||||
rt_handle: &tokio::runtime::Handle,
|
||||
tor_runtime: R,
|
||||
wallets: &mut GrinWalletManager,
|
||||
server_key: &SecretKey,
|
||||
node: &Arc<grin_util::Mutex<IntegrationGrinNode>>,
|
||||
prev_server: DalekPublicKey,
|
||||
next_server: Option<&IntegrationMixServer<R>>,
|
||||
) -> IntegrationMixServer<R>
|
||||
where
|
||||
R: tor_rtcompat::Runtime,
|
||||
{
|
||||
let wallet = wallets.async_new_wallet(&node.lock().api_address()).await;
|
||||
let server_config = mwixnet::ServerConfig {
|
||||
key: server_key.clone(),
|
||||
interval_s: 15,
|
||||
addr: TcpListener::bind("127.0.0.1:0")
|
||||
.unwrap()
|
||||
.local_addr()
|
||||
.unwrap(),
|
||||
grin_node_url: node.lock().api_address(),
|
||||
grin_node_secret_path: None,
|
||||
wallet_owner_url: wallet.lock().owner_address(),
|
||||
wallet_owner_secret_path: None,
|
||||
prev_server: Some(prev_server),
|
||||
next_server: match next_server {
|
||||
Some(s) => Some(DalekPublicKey::from_secret(&s.server_key)),
|
||||
None => None,
|
||||
},
|
||||
};
|
||||
|
||||
let tor_instance = tor::async_init_tor(tor_runtime, &data_dir, &server_config)
|
||||
.await
|
||||
.unwrap();
|
||||
let tor_instance = Arc::new(grin_util::Mutex::new(tor_instance));
|
||||
|
||||
let (_, rpc_server) = mwixnet::mix_listen(
|
||||
rt_handle,
|
||||
server_config.clone(),
|
||||
match next_server {
|
||||
Some(s) => Some(Arc::new(MixClientImpl::new(
|
||||
server_config.clone(),
|
||||
tor_instance.clone(),
|
||||
DalekPublicKey::from_secret(&s.server_key),
|
||||
))),
|
||||
None => None,
|
||||
},
|
||||
wallet.lock().get_client(),
|
||||
node.lock().to_client(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
IntegrationMixServer {
|
||||
server_key: server_key.clone(),
|
||||
tor_instance,
|
||||
rpc_server,
|
||||
_wallet: wallet,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Servers {
|
||||
pub swapper: IntegrationSwapServer<PreferredRuntime>,
|
||||
|
||||
pub mixers: Vec<IntegrationMixServer<PreferredRuntime>>,
|
||||
}
|
||||
|
||||
impl Servers {
|
||||
pub async fn async_setup(
|
||||
test_dir: &str,
|
||||
rt_handle: &tokio::runtime::Handle,
|
||||
wallets: &mut GrinWalletManager,
|
||||
node: &Arc<grin_util::Mutex<IntegrationGrinNode>>,
|
||||
num_mixers: usize,
|
||||
) -> Servers {
|
||||
// Pre-generate all server keys
|
||||
let server_keys: Vec<SecretKey> =
|
||||
iter::repeat_with(|| grin_onion::crypto::secp::random_secret())
|
||||
.take(num_mixers + 1)
|
||||
.collect();
|
||||
|
||||
// Setup mock tor network
|
||||
let tor_runtime = PreferredRuntime::current().unwrap();
|
||||
|
||||
// Build mixers in reverse order
|
||||
let mut mixers = Vec::new();
|
||||
for i in (0..num_mixers).rev() {
|
||||
let mix_server = async_new_mix_server(
|
||||
format!("{}/mixers/{}", test_dir, i).as_str(),
|
||||
rt_handle,
|
||||
tor_runtime.clone(),
|
||||
wallets,
|
||||
&server_keys[i + 1],
|
||||
&node,
|
||||
DalekPublicKey::from_secret(&server_keys[i]),
|
||||
mixers.last(),
|
||||
)
|
||||
.await;
|
||||
println!(
|
||||
"Mixer {}: server_key={}, prev_server={}, next_server={}",
|
||||
i,
|
||||
DalekPublicKey::from_secret(&server_keys[i + 1]).to_hex(),
|
||||
DalekPublicKey::from_secret(&server_keys[i]).to_hex(),
|
||||
match mixers.last() {
|
||||
Some(s) => DalekPublicKey::from_secret(&s.server_key).to_hex(),
|
||||
None => "NONE".to_string(),
|
||||
},
|
||||
);
|
||||
mixers.push(mix_server);
|
||||
}
|
||||
mixers.reverse();
|
||||
|
||||
let swapper = async_new_swap_server(
|
||||
format!("{}/swapper", test_dir).as_str(),
|
||||
rt_handle,
|
||||
tor_runtime.clone(),
|
||||
wallets,
|
||||
&server_keys[0],
|
||||
&node,
|
||||
mixers.first(),
|
||||
)
|
||||
.await;
|
||||
println!(
|
||||
"Swapper: server_key={}",
|
||||
DalekPublicKey::from_secret(&server_keys[0]).to_hex()
|
||||
);
|
||||
|
||||
Servers { swapper, mixers }
|
||||
}
|
||||
|
||||
pub fn get_pub_keys(&self) -> Vec<xPublicKey> {
|
||||
let mut pub_keys = vec![xPublicKey::from(&StaticSecret::from(
|
||||
self.swapper.server_key.0.clone(),
|
||||
))];
|
||||
for mixer in &self.mixers {
|
||||
pub_keys.push(xPublicKey::from(&StaticSecret::from(
|
||||
mixer.server_key.0.clone(),
|
||||
)))
|
||||
}
|
||||
pub_keys
|
||||
}
|
||||
|
||||
pub fn stop_all(&mut self) {
|
||||
self.swapper.rpc_server.close_handle().close();
|
||||
self.swapper.tor_instance.lock().stop();
|
||||
|
||||
self.mixers.iter_mut().for_each(|mixer| {
|
||||
mixer.rpc_server.close_handle().close();
|
||||
mixer.tor_instance.lock().stop();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use grin_core::libtx::secp_ser;
|
||||
use grin_keychain::Identifier;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Fees in block to use for coinbase amount calculation
|
||||
/// (Duplicated from Grin wallet project)
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct BlockFees {
|
||||
/// fees
|
||||
#[serde(with = "secp_ser::string_or_u64")]
|
||||
pub fees: u64,
|
||||
/// height
|
||||
#[serde(with = "secp_ser::string_or_u64")]
|
||||
pub height: u64,
|
||||
/// key id
|
||||
pub key_id: Option<Identifier>,
|
||||
}
|
||||
|
||||
impl BlockFees {
|
||||
/// return key id
|
||||
pub fn key_id(&self) -> Option<Identifier> {
|
||||
self.key_id.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
use grin_core::core::{FeeFields, Output, OutputFeatures, Transaction, TxKernel};
|
||||
use grin_core::global::ChainTypes;
|
||||
use grin_core::libtx::tx_fee;
|
||||
use grin_keychain::{BlindingFactor, ExtKeychain, Identifier, Keychain, SwitchCommitmentType};
|
||||
use grin_util::{Mutex, ZeroingString};
|
||||
use grin_wallet_api::Owner;
|
||||
use grin_wallet_config::WalletConfig;
|
||||
use grin_wallet_controller::controller;
|
||||
use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient};
|
||||
use grin_wallet_libwallet::{InitTxArgs, Slate, VersionedSlate, WalletInfo, WalletInst};
|
||||
use log::error;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use x25519_dalek::PublicKey as xPublicKey;
|
||||
|
||||
use grin_onion::crypto::comsig::ComSignature;
|
||||
use grin_onion::onion::Onion;
|
||||
use grin_onion::Hop;
|
||||
use mwixnet::http;
|
||||
use mwixnet::wallet::HttpWallet;
|
||||
use secp256k1zkp::pedersen::Commitment;
|
||||
use secp256k1zkp::{Secp256k1, SecretKey};
|
||||
|
||||
use crate::common::types::BlockFees;
|
||||
|
||||
/// Response to build a coinbase output.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct CbData {
|
||||
/// Output
|
||||
pub output: Output,
|
||||
/// Kernel
|
||||
pub kernel: TxKernel,
|
||||
/// Key Id
|
||||
pub key_id: Option<Identifier>,
|
||||
}
|
||||
|
||||
pub struct IntegrationGrinWallet {
|
||||
wallet: Arc<
|
||||
Mutex<
|
||||
Box<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<'static, HTTPNodeClient, ExtKeychain>,
|
||||
HTTPNodeClient,
|
||||
ExtKeychain,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
api_listen_port: u16,
|
||||
owner_api: Arc<
|
||||
Owner<DefaultLCProvider<'static, HTTPNodeClient, ExtKeychain>, HTTPNodeClient, ExtKeychain>,
|
||||
>,
|
||||
http_client: Arc<HttpWallet>,
|
||||
}
|
||||
|
||||
impl IntegrationGrinWallet {
|
||||
pub async fn async_new_wallet(
|
||||
wallet_dir: String,
|
||||
api_listen_port: u16,
|
||||
node_api: String,
|
||||
) -> IntegrationGrinWallet {
|
||||
let node_client = HTTPNodeClient::new(&node_api, None).unwrap();
|
||||
let mut wallet = Box::new(
|
||||
DefaultWalletImpl::<'static, HTTPNodeClient>::new(node_client.clone()).unwrap(),
|
||||
)
|
||||
as Box<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<HTTPNodeClient, ExtKeychain>,
|
||||
HTTPNodeClient,
|
||||
ExtKeychain,
|
||||
>,
|
||||
>;
|
||||
|
||||
// Wallet LifeCycle Provider provides all functions init wallet and work with seeds, etc...
|
||||
let lc = wallet.lc_provider().unwrap();
|
||||
|
||||
let mut wallet_config = WalletConfig::default();
|
||||
wallet_config.check_node_api_http_addr = node_api.clone();
|
||||
wallet_config.owner_api_listen_port = Some(api_listen_port);
|
||||
wallet_config.api_secret_path = None;
|
||||
wallet_config.data_file_dir = wallet_dir.clone();
|
||||
|
||||
// The top level wallet directory should be set manually (in the reference implementation,
|
||||
// this is provided in the WalletConfig)
|
||||
let _ = lc.set_top_level_directory(&wallet_config.data_file_dir);
|
||||
|
||||
lc.create_config(
|
||||
&ChainTypes::AutomatedTesting,
|
||||
"grin-wallet.toml",
|
||||
Some(wallet_config.clone()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
lc.create_wallet(None, None, 12, ZeroingString::from("pass"), false)
|
||||
.unwrap();
|
||||
|
||||
// Start owner API
|
||||
let km = Arc::new(Mutex::new(None));
|
||||
let wallet = Arc::new(Mutex::new(wallet));
|
||||
let owner_api = Arc::new(Owner::new(wallet.clone(), None));
|
||||
|
||||
let address_str = format!("127.0.0.1:{}", api_listen_port);
|
||||
let owner_addr: SocketAddr = address_str.parse().unwrap();
|
||||
let thr_wallet = wallet.clone();
|
||||
let _thread_handle = thread::spawn(move || {
|
||||
controller::owner_listener(
|
||||
thr_wallet,
|
||||
km,
|
||||
address_str.as_str(),
|
||||
None,
|
||||
None,
|
||||
Some(true),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let http_client = Arc::new(
|
||||
HttpWallet::async_open_wallet(&owner_addr, &None, &ZeroingString::from("pass"))
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
IntegrationGrinWallet {
|
||||
wallet,
|
||||
api_listen_port,
|
||||
owner_api,
|
||||
http_client,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn async_retrieve_summary_info(&self) -> Result<WalletInfo, mwixnet::WalletError> {
|
||||
let params = json!({
|
||||
"token": self.http_client.clone().get_token(),
|
||||
"refresh_from_node": true,
|
||||
"minimum_confirmations": 1
|
||||
});
|
||||
let (_, wallet_info): (bool, WalletInfo) = self
|
||||
.http_client
|
||||
.clone()
|
||||
.async_perform_request("retrieve_summary_info", ¶ms)
|
||||
.await?;
|
||||
Ok(wallet_info)
|
||||
}
|
||||
|
||||
pub async fn async_send(
|
||||
&self,
|
||||
receiving_wallet: &IntegrationGrinWallet,
|
||||
amount: u64,
|
||||
) -> Result<Transaction, mwixnet::WalletError> {
|
||||
let slate = self.async_init_send_tx(amount).await.unwrap();
|
||||
let slate = receiving_wallet.async_receive_tx(&slate).await.unwrap();
|
||||
let slate = self.async_finalize_tx(&slate).await.unwrap();
|
||||
let tx = Slate::from(slate).tx_or_err().unwrap().clone();
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
async fn async_init_send_tx(
|
||||
&self,
|
||||
amount: u64,
|
||||
) -> Result<VersionedSlate, mwixnet::WalletError> {
|
||||
let args = InitTxArgs {
|
||||
src_acct_name: None,
|
||||
amount,
|
||||
minimum_confirmations: 0,
|
||||
max_outputs: 10,
|
||||
num_change_outputs: 1,
|
||||
selection_strategy_is_use_all: false,
|
||||
..Default::default()
|
||||
};
|
||||
let params = json!({
|
||||
"token": self.http_client.clone().get_token(),
|
||||
"args": args
|
||||
});
|
||||
|
||||
let slate: VersionedSlate = self
|
||||
.http_client
|
||||
.clone()
|
||||
.async_perform_request("init_send_tx", ¶ms)
|
||||
.await?;
|
||||
|
||||
let params = json!({
|
||||
"token": self.http_client.clone().get_token(),
|
||||
"slate": &slate
|
||||
});
|
||||
self.http_client
|
||||
.clone()
|
||||
.async_perform_request("tx_lock_outputs", ¶ms)
|
||||
.await?;
|
||||
|
||||
Ok(slate)
|
||||
}
|
||||
|
||||
pub async fn async_receive_tx(
|
||||
&self,
|
||||
slate: &VersionedSlate,
|
||||
) -> Result<VersionedSlate, grin_servers::common::types::Error> {
|
||||
let params = json!([slate, null, null]);
|
||||
let response =
|
||||
http::async_send_json_request(&self.foreign_api(), &None, "receive_tx", ¶ms)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let report = format!("Failed to receive tx. Is the wallet listening? {}", e);
|
||||
error!("{}", report);
|
||||
grin_servers::common::types::Error::WalletComm(report)
|
||||
})?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn async_finalize_tx(
|
||||
&self,
|
||||
slate: &VersionedSlate,
|
||||
) -> Result<VersionedSlate, mwixnet::WalletError> {
|
||||
let params = json!({
|
||||
"token": self.http_client.clone().get_token(),
|
||||
"slate": slate
|
||||
});
|
||||
|
||||
self.http_client
|
||||
.clone()
|
||||
.async_perform_request("finalize_tx", ¶ms)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn async_post_tx(
|
||||
&self,
|
||||
finalized_slate: &VersionedSlate,
|
||||
fluff: bool,
|
||||
) -> Result<VersionedSlate, mwixnet::WalletError> {
|
||||
let params = json!({
|
||||
"token": self.http_client.clone().get_token(),
|
||||
"slate": finalized_slate,
|
||||
"fluff": fluff
|
||||
});
|
||||
|
||||
self.http_client
|
||||
.clone()
|
||||
.async_perform_request("post_tx", ¶ms)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Call the wallet API to create a coinbase output for the given block_fees.
|
||||
/// Will retry based on default "retry forever with backoff" behavior.
|
||||
pub async fn async_create_coinbase(
|
||||
&self,
|
||||
block_fees: &BlockFees,
|
||||
) -> Result<CbData, grin_servers::common::types::Error> {
|
||||
let params = json!({
|
||||
"block_fees": block_fees
|
||||
});
|
||||
let response =
|
||||
http::async_send_json_request(&self.foreign_api(), &None, "build_coinbase", ¶ms)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let report = format!("Failed to get coinbase. Is the wallet listening? {}", e);
|
||||
error!("{}", report);
|
||||
grin_servers::common::types::Error::WalletComm(report)
|
||||
})?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn build_onion(
|
||||
&self,
|
||||
commitment: &Commitment,
|
||||
server_pubkeys: &Vec<xPublicKey>,
|
||||
) -> Result<(Onion, ComSignature), grin_wallet_libwallet::Error> {
|
||||
let keychain = self
|
||||
.wallet
|
||||
.lock()
|
||||
.lc_provider()?
|
||||
.wallet_inst()?
|
||||
.keychain(self.keychain_mask().as_ref())?;
|
||||
let (_, outputs) =
|
||||
self.owner_api
|
||||
.retrieve_outputs(self.keychain_mask().as_ref(), false, false, None)?;
|
||||
|
||||
let mut output = None;
|
||||
for o in &outputs {
|
||||
if o.commit == *commitment {
|
||||
output = Some(o.output.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if output.is_none() {
|
||||
return Err(grin_wallet_libwallet::Error::GenericError(String::from(
|
||||
"output not found",
|
||||
)));
|
||||
}
|
||||
|
||||
let amount = output.clone().unwrap().value;
|
||||
let input_blind = keychain.derive_key(
|
||||
amount,
|
||||
&output.clone().unwrap().key_id,
|
||||
SwitchCommitmentType::Regular,
|
||||
)?;
|
||||
|
||||
let fee = tx_fee(1, 1, 1);
|
||||
let new_amount = amount - (fee * server_pubkeys.len() as u64);
|
||||
let new_output = self.owner_api.build_output(
|
||||
self.keychain_mask().as_ref(),
|
||||
OutputFeatures::Plain,
|
||||
new_amount,
|
||||
)?;
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
let mut blind_sum = new_output
|
||||
.blind
|
||||
.split(&BlindingFactor::from_secret_key(input_blind.clone()), &secp)?;
|
||||
|
||||
let hops = server_pubkeys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &p)| {
|
||||
if (i + 1) == server_pubkeys.len() {
|
||||
Hop {
|
||||
server_pubkey: p.clone(),
|
||||
excess: blind_sum.secret_key(&secp).unwrap(),
|
||||
fee: FeeFields::from(fee as u32),
|
||||
rangeproof: Some(new_output.output.proof.clone()),
|
||||
}
|
||||
} else {
|
||||
let hop_excess = BlindingFactor::rand(&secp);
|
||||
blind_sum = blind_sum.split(&hop_excess, &secp).unwrap();
|
||||
Hop {
|
||||
server_pubkey: p.clone(),
|
||||
excess: hop_excess.secret_key(&secp).unwrap(),
|
||||
fee: FeeFields::from(fee as u32),
|
||||
rangeproof: None,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let onion = grin_onion::create_onion(&commitment, &hops).unwrap();
|
||||
let comsig = ComSignature::sign(amount, &input_blind, &onion.serialize().unwrap()).unwrap();
|
||||
|
||||
Ok((onion, comsig))
|
||||
}
|
||||
|
||||
pub fn owner_api(
|
||||
&self,
|
||||
) -> Arc<
|
||||
Owner<DefaultLCProvider<'static, HTTPNodeClient, ExtKeychain>, HTTPNodeClient, ExtKeychain>,
|
||||
> {
|
||||
self.owner_api.clone()
|
||||
}
|
||||
|
||||
pub fn foreign_api(&self) -> String {
|
||||
format!("http://127.0.0.1:{}/v2/foreign", self.api_listen_port)
|
||||
}
|
||||
|
||||
pub fn owner_address(&self) -> SocketAddr {
|
||||
SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
|
||||
self.api_listen_port,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn keychain_mask(&self) -> Option<SecretKey> {
|
||||
self.http_client.as_ref().get_token().keychain_mask.clone()
|
||||
}
|
||||
|
||||
pub fn get_client(&self) -> Arc<HttpWallet> {
|
||||
self.http_client.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct GrinWalletManager {
|
||||
// base directory for the server instance
|
||||
working_dir: String,
|
||||
|
||||
wallets: Vec<Arc<Mutex<IntegrationGrinWallet>>>,
|
||||
}
|
||||
|
||||
impl GrinWalletManager {
|
||||
pub fn new(test_dir: &str) -> GrinWalletManager {
|
||||
GrinWalletManager {
|
||||
working_dir: String::from(test_dir),
|
||||
wallets: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn async_new_wallet(
|
||||
&mut self,
|
||||
node_api_addr: &SocketAddr,
|
||||
) -> Arc<Mutex<IntegrationGrinWallet>> {
|
||||
let wallet_dir = format!("{}/wallets/{}", self.working_dir, self.wallets.len());
|
||||
let wallet = Arc::new(Mutex::new(
|
||||
IntegrationGrinWallet::async_new_wallet(
|
||||
wallet_dir,
|
||||
21000 + self.wallets.len() as u16,
|
||||
format!("http://{}", node_api_addr),
|
||||
)
|
||||
.await,
|
||||
));
|
||||
self.wallets.push(wallet.clone());
|
||||
wallet
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
use function_name::named;
|
||||
use grin_core::global;
|
||||
use grin_util::logger::LoggingConfig;
|
||||
use log::Level;
|
||||
|
||||
use crate::common::miner::Miner;
|
||||
use crate::common::node::GrinNodeManager;
|
||||
use crate::common::server::Servers;
|
||||
use crate::common::wallet::GrinWalletManager;
|
||||
|
||||
mod common;
|
||||
|
||||
/// Just removes all results from previous runs
|
||||
fn clean_all_output(test_dir: &str) {
|
||||
if let Err(e) = remove_dir_all::remove_dir_all(test_dir) {
|
||||
println!("can't remove output from previous test :{}, may be ok", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_test(test_name: &str) -> (GrinNodeManager, GrinWalletManager, String) {
|
||||
let test_dir = format!("./target/tmp/.{}", test_name);
|
||||
clean_all_output(test_dir.as_str());
|
||||
|
||||
let mut logger = LoggingConfig::default();
|
||||
logger.log_to_file = false;
|
||||
logger.stdout_log_level = Level::Error;
|
||||
grin_util::init_logger(Some(logger), None);
|
||||
global::set_local_chain_type(global::ChainTypes::AutomatedTesting);
|
||||
global::set_local_accept_fee_base(50_000_000);
|
||||
global::init_global_chain_type(global::ChainTypes::AutomatedTesting);
|
||||
|
||||
let nodes = GrinNodeManager::new(test_dir.as_str());
|
||||
let wallets = GrinWalletManager::new(test_dir.as_str());
|
||||
|
||||
(nodes, wallets, test_dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[named]
|
||||
fn integration_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (mut nodes, mut wallets, test_dir) = setup_test(function_name!());
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
|
||||
// Create node
|
||||
let node1 = nodes.new_node();
|
||||
let node1_url = node1.lock().api_address();
|
||||
let node1_server = node1.lock().start();
|
||||
|
||||
// Setup swap & mix servers and their wallets
|
||||
let rt_handle = rt.handle().clone();
|
||||
let mut servers = rt.block_on(Servers::async_setup(
|
||||
test_dir.as_str(),
|
||||
&rt_handle,
|
||||
&mut wallets,
|
||||
&node1,
|
||||
2usize,
|
||||
));
|
||||
|
||||
rt.block_on(async {
|
||||
// Setup wallet to use with miner
|
||||
let mining_wallet = wallets.async_new_wallet(&node1_url).await;
|
||||
|
||||
// Mine enough blocks to have spendable coins
|
||||
let miner = Miner::new(node1_server.chain.clone());
|
||||
miner
|
||||
.async_mine_empty_blocks(&mining_wallet, 5 + global::coinbase_maturity() as usize)
|
||||
.await;
|
||||
|
||||
// Setup wallets for swap users
|
||||
let user1_wallet = wallets.async_new_wallet(&node1_url).await;
|
||||
let user2_wallet = wallets.async_new_wallet(&node1_url).await;
|
||||
|
||||
// Send from mining_wallet to user1_wallet
|
||||
let tx1 = mining_wallet
|
||||
.lock()
|
||||
.async_send(user1_wallet.lock().deref(), 10_000_000_000)
|
||||
.await
|
||||
.unwrap();
|
||||
let tx2 = mining_wallet
|
||||
.lock()
|
||||
.async_send(user2_wallet.lock().deref(), 20_000_000_000)
|
||||
.await
|
||||
.unwrap();
|
||||
miner
|
||||
.async_mine_next_block(&mining_wallet, &vec![tx1, tx2])
|
||||
.await;
|
||||
|
||||
let user1_km = user1_wallet.lock().keychain_mask();
|
||||
let (_, outputs) = user1_wallet
|
||||
.lock()
|
||||
.owner_api()
|
||||
.retrieve_outputs(user1_km.as_ref(), false, false, None)
|
||||
.unwrap();
|
||||
assert_eq!(outputs.len(), 1);
|
||||
for output in &outputs {
|
||||
let (onion, comsig) = user1_wallet
|
||||
.lock()
|
||||
.build_onion(&output.commit, &servers.get_pub_keys())
|
||||
.unwrap();
|
||||
servers.swapper.async_swap(&onion, &comsig).await.unwrap();
|
||||
}
|
||||
|
||||
let mining_wallet_info = mining_wallet
|
||||
.lock()
|
||||
.async_retrieve_summary_info()
|
||||
.await
|
||||
.unwrap();
|
||||
println!("Mining wallet: {:?}", mining_wallet_info);
|
||||
let user1_wallet_info = user1_wallet
|
||||
.lock()
|
||||
.async_retrieve_summary_info()
|
||||
.await
|
||||
.unwrap();
|
||||
println!("User1 wallet: {:?}", user1_wallet_info);
|
||||
let user2_wallet_info = user2_wallet
|
||||
.lock()
|
||||
.async_retrieve_summary_info()
|
||||
.await
|
||||
.unwrap();
|
||||
println!("User2 wallet: {:?}", user2_wallet_info);
|
||||
|
||||
let _tx = servers.swapper.async_execute_round().await.unwrap();
|
||||
});
|
||||
|
||||
servers.stop_all();
|
||||
nodes.stop_all();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user