converted to async/.await to support integration testing framework

(cherry picked from commit 26c129fa787ffec44f229ad8047d5c375fb4d257)
This commit is contained in:
scilio
2024-04-02 19:46:32 -04:00
parent f4e259f802
commit 389581d759
27 changed files with 3893 additions and 1534 deletions
+255
View File
@@ -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)),
))
}
}
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod miner;
pub mod node;
pub mod server;
pub mod types;
pub mod wallet;
+122
View File
@@ -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();
}
}
}
+244
View File
@@ -0,0 +1,244 @@
use crate::common::node::IntegrationGrinNode;
use crate::common::wallet::{GrinWalletManager, IntegrationGrinWallet};
use grin_core::core::Transaction;
use grin_onion::crypto::comsig::ComSignature;
use grin_onion::crypto::dalek::DalekPublicKey;
use grin_onion::onion::Onion;
use grin_wallet_impls::tor::process::TorProcess;
use mwixnet::client::MixClientImpl;
use mwixnet::{tor, SwapError, SwapServer, SwapStore};
use secp256k1zkp::SecretKey;
use std::iter;
use std::net::TcpListener;
use std::sync::Arc;
use x25519_dalek::{PublicKey as xPublicKey, StaticSecret};
pub struct IntegrationSwapServer {
server_key: SecretKey,
tor_process: TorProcess,
swap_server: Arc<tokio::sync::Mutex<dyn SwapServer>>,
rpc_server: jsonrpc_http_server::Server,
_wallet: Arc<grin_util::Mutex<IntegrationGrinWallet>>,
}
impl IntegrationSwapServer {
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 {
server_key: SecretKey,
tor_process: TorProcess,
rpc_server: jsonrpc_http_server::Server,
_wallet: Arc<grin_util::Mutex<IntegrationGrinWallet>>,
}
async fn async_new_swap_server(
data_dir: &str,
rt_handle: &tokio::runtime::Handle,
wallets: &mut GrinWalletManager,
server_key: &SecretKey,
node: &Arc<grin_util::Mutex<IntegrationGrinNode>>,
next_server: Option<&IntegrationMixServer>,
) -> IntegrationSwapServer {
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(),
socks_proxy_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_process = tor::init_tor_listener(&data_dir, &server_config).unwrap();
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(),
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_process,
swap_server,
rpc_server,
_wallet: wallet,
}
}
async fn async_new_mix_server(
data_dir: &str,
rt_handle: &tokio::runtime::Handle,
wallets: &mut GrinWalletManager,
server_key: &SecretKey,
node: &Arc<grin_util::Mutex<IntegrationGrinNode>>,
prev_server: DalekPublicKey,
next_server: Option<&IntegrationMixServer>,
) -> IntegrationMixServer {
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(),
socks_proxy_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_process = tor::init_tor_listener(&data_dir, &server_config).unwrap();
let (_, rpc_server) = mwixnet::mix_listen(
rt_handle,
server_config.clone(),
match next_server {
Some(s) => Some(Arc::new(MixClientImpl::new(
server_config.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_process,
rpc_server,
_wallet: wallet,
}
}
pub struct Servers {
pub swapper: IntegrationSwapServer,
pub mixers: Vec<IntegrationMixServer>,
}
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();
// 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,
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,
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_process.kill().unwrap();
self.mixers.iter_mut().for_each(|mixer| {
mixer.rpc_server.close_handle().close();
mixer.tor_process.kill().unwrap();
});
}
}
+24
View File
@@ -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()
}
}
+431
View File
@@ -0,0 +1,431 @@
use crate::common::types::BlockFees;
use grin_api::client;
use grin_api::json_rpc::Response;
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_onion::crypto::comsig::ComSignature;
use grin_onion::onion::Onion;
use grin_onion::Hop;
use grin_util::{Mutex, ToHex, 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 mwixnet::wallet::HttpWallet;
use secp256k1zkp::pedersen::Commitment;
use secp256k1zkp::{Secp256k1, SecretKey};
use serde_derive::{Deserialize, Serialize};
use serde_json::json;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::thread;
use x25519_dalek::PublicKey as xPublicKey;
/// 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", &params)
.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", &params)
.await?;
let params = json!({
"token": self.http_client.clone().get_token(),
"slate": &slate
});
self.http_client
.clone()
.async_perform_request("tx_lock_outputs", &params)
.await?;
Ok(slate)
}
pub async fn async_receive_tx(
&self,
slate: &VersionedSlate,
) -> Result<VersionedSlate, grin_servers::common::types::Error> {
let req_body = json!({
"jsonrpc": "2.0",
"method": "receive_tx",
"id": 1,
"params": [slate, null, null]
});
let res: Response = client::post_async(self.foreign_api().as_str(), &req_body, None)
.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)
})?;
let parsed: VersionedSlate = res.clone().into_result().map_err(|e| {
let report = format!("Error parsing result: {}", e);
error!("{}", report);
grin_servers::common::types::Error::WalletComm(report)
})?;
Ok(parsed)
}
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", &params)
.await
}
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", &params)
.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 req_body = json!({
"jsonrpc": "2.0",
"method": "build_coinbase",
"id": 1,
"params": {
"block_fees": block_fees
}
});
let res: Response = client::post_async(self.foreign_api().as_str(), &req_body, None)
.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)
})?;
let parsed: CbData = res.clone().into_result().map_err(|e| {
let report = format!("Error parsing result: {}", e);
error!("{}", report);
grin_servers::common::types::Error::WalletComm(report)
})?;
Ok(parsed)
}
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
}
}
+135
View File
@@ -0,0 +1,135 @@
use crate::common::miner::Miner;
use crate::common::node::GrinNodeManager;
use crate::common::server::Servers;
use crate::common::wallet::GrinWalletManager;
use function_name::named;
use grin_core::global;
use grin_util::logger::LoggingConfig;
use log::Level;
use std::ops::Deref;
mod common;
#[macro_use]
extern crate log;
/// 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 mut rt = tokio::runtime::Builder::new()
.threaded_scheduler()
.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(())
}