diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 783258e94..11255faae 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,12 +7,18 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - job_args: [api, config, controller, impls, libwallet, .] + job_args: [api, config, controller, impls, libwallet, integration, .] steps: - uses: actions/checkout@v3 - name: Test ${{ matrix.job_args }} working-directory: ${{ matrix.job_args }} - run: cargo test --release + # Multi-process node+wallet tests need serial execution (ports / RocksDB). + run: | + if [ "${{ matrix.job_args }}" = "integration" ]; then + cargo test --release -- --test-threads=1 + else + cargo test --release + fi macos-tests: name: macOS Tests diff --git a/Cargo.lock b/Cargo.lock index a342a8a18..738574cd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1029,6 +1029,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -1377,6 +1387,39 @@ dependencies = [ "zeroize", ] +[[package]] +name = "grin_servers" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c0fae501662eb0ea0804bd3fbfc02846612f512712073c18f8adbe7b908042" +dependencies = [ + "async-stream", + "chrono", + "fs2", + "futures 0.3.31", + "grin_api", + "grin_chain", + "grin_core", + "grin_keychain", + "grin_p2p", + "grin_pool", + "grin_store", + "grin_util", + "http", + "hyper 0.14.32", + "hyper-rustls 0.23.2", + "lmdb-zero", + "log", + "rand 0.6.5", + "rustls 0.20.9", + "serde", + "serde_derive", + "serde_json", + "tokio 1.42.1", + "tokio-util 0.7.17", + "walkdir", +] + [[package]] name = "grin_store" version = "5.4.1" @@ -1567,6 +1610,25 @@ dependencies = [ "x25519-dalek 0.6.0", ] +[[package]] +name = "grin_wallet_integration" +version = "5.4.1" +dependencies = [ + "futures 0.3.31", + "grin_api", + "grin_core", + "grin_keychain", + "grin_p2p", + "grin_servers", + "grin_util", + "grin_wallet_config", + "grin_wallet_controller", + "grin_wallet_impls", + "grin_wallet_libwallet", + "log", + "serde_json", +] + [[package]] name = "grin_wallet_libwallet" version = "5.4.1" diff --git a/Cargo.toml b/Cargo.toml index d47f3bc53..e7968b02d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,7 @@ name = "grin-wallet" path = "src/bin/grin-wallet.rs" [workspace] -members = ["api", "config", "controller", "impls", "libwallet", "util"] -exclude = ["integration"] +members = ["api", "config", "controller", "impls", "libwallet", "util", "integration"] [dependencies] clap = { version = "2.33", features = ["yaml"] } diff --git a/integration/Cargo.toml b/integration/Cargo.toml index 1230be2a3..255ab395b 100644 --- a/integration/Cargo.toml +++ b/integration/Cargo.toml @@ -1,39 +1,29 @@ [package] -name = "grin_integration" -version = "1.1.0" +name = "grin_wallet_integration" +version = "5.4.1" authors = ["Grin Developers "] -description = "Simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format." +description = "Node + wallet multi-process integration tests for grin-wallet" license = "Apache-2.0" -repository = "https://github.com/mimblewimble/grin" -keywords = [ "crypto", "grin", "mimblewimble" ] +repository = "https://github.com/mimblewimble/grin-wallet" +keywords = ["crypto", "grin", "mimblewimble"] workspace = ".." edition = "2021" +publish = false [dependencies] -hyper = "0.12" -futures = "0.1" -http = "0.1" -itertools = "0.7" -rand = "0.5" -serde = "1" +futures = "0.3" log = "0.4" -serde_derive = "1" serde_json = "1" -chrono = "0.4.4" -tokio = "0.1.11" -blake2-rfc = "0.2" -bufstream = "0.1" -#grin_apiwallet = { path = "../apiwallet", version = "1.1.0" } -#grin_libwallet = { path = "../libwallet", version = "1.1.0" } -#grin_refwallet = { path = "../refwallet", version = "1.1.0" } -#grin_wallet_config = { path = "../config", version = "1.1.0" } +grin_wallet_config = { path = "../config", version = "5.4.1" } +grin_wallet_controller = { path = "../controller", version = "5.4.1" } +grin_wallet_impls = { path = "../impls", version = "5.4.1" } +grin_wallet_libwallet = { path = "../libwallet", version = "5.4.1" } -#grin_core = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } -#grin_keychain = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } -#grin_chain = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } -#grin_util = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } -#grin_api = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } -#grin_store = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } -#grin_p2p = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } -#grin_servers = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } +# Align with workspace grin crates (node multi-server stack). +grin_core = "5.4.1" +grin_keychain = "5.4.1" +grin_util = "5.4.1" +grin_api = "5.4.1" +grin_p2p = "5.4.1" +grin_servers = "5.4.1" diff --git a/integration/README.md b/integration/README.md new file mode 100644 index 000000000..925b50ebf --- /dev/null +++ b/integration/README.md @@ -0,0 +1,20 @@ +# Wallet + node integration tests + +Multi-process tests that run a real Grin node (`grin_servers`) together with +`grin-wallet` foreign/owner HTTP listeners. + +**Node-only** multi-server suites (seeding, propagation, body sync, stratum) +live in [mimblewimble/grin](https://github.com/mimblewimble/grin) under +`integration/` ([#2957](https://github.com/mimblewimble/grin/issues/2957)). + +In-process wallet unit/integration coverage (mock node via `WalletProxy`) +remains under `controller/tests/`. + +## Run + +```bash +cd integration +cargo test --release -- --test-threads=1 +``` + +Serial execution avoids port and RocksDB races. diff --git a/integration/src/lib.rs b/integration/src/lib.rs index 608320163..f15473aef 100644 --- a/integration/src/lib.rs +++ b/integration/src/lib.rs @@ -12,10 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Grin integration test crate - -#![deny(non_upper_case_globals)] -#![deny(non_camel_case_types)] -#![deny(non_snake_case)] -#![deny(unused_mut)] -#![warn(missing_docs)] +//! Node + wallet multi-process integration tests. +//! +//! Node-only multi-server coverage lives in the [grin](https://github.com/mimblewimble/grin) +//! `integration` crate ([#2957](https://github.com/mimblewimble/grin/issues/2957)). +//! This crate exercises wallet listeners against a real `grin_servers` node. diff --git a/integration/tests/api.rs b/integration/tests/api.rs deleted file mode 100644 index 215880541..000000000 --- a/integration/tests/api.rs +++ /dev/null @@ -1,485 +0,0 @@ -// 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. - -#[macro_use] -extern crate log; - -mod framework; - -use self::core::global::{self, ChainTypes}; -use self::util::init_test_logger; -use self::util::Mutex; -use crate::framework::{LocalServerContainer, LocalServerContainerConfig}; -use grin_api as api; -use grin_core as core; -use grin_p2p as p2p; -use grin_util as util; -use std::sync::Arc; -use std::{thread, time}; - -#[test] -fn simple_server_wallet() { - init_test_logger(); - info!("starting simple_server_wallet"); - let _test_name_dir = "test_servers"; - core::global::set_local_chain_type(core::global::ChainTypes::AutomatedTesting); - - // Run a separate coinbase wallet for coinbase transactions - let coinbase_dir = "coinbase_wallet_api"; - framework::clean_all_output(coinbase_dir); - let mut coinbase_config = LocalServerContainerConfig::default(); - coinbase_config.name = String::from(coinbase_dir); - coinbase_config.wallet_validating_node_url = String::from("http://127.0.0.1:40001"); - coinbase_config.wallet_port = 50002; - let coinbase_wallet = Arc::new(Mutex::new( - LocalServerContainer::new(coinbase_config).unwrap(), - )); - - let _ = thread::spawn(move || { - let mut w = coinbase_wallet.lock(); - w.run_wallet(0); - }); - - // Wait for the wallet to start - thread::sleep(time::Duration::from_millis(1000)); - - let api_server_one_dir = "api_server_one"; - framework::clean_all_output(api_server_one_dir); - let mut server_config = LocalServerContainerConfig::default(); - server_config.name = String::from(api_server_one_dir); - server_config.p2p_server_port = 40000; - server_config.api_server_port = 40001; - server_config.start_miner = true; - server_config.start_wallet = false; - server_config.coinbase_wallet_address = - String::from(format!("http://{}:{}", server_config.base_addr, 50002)); - let mut server_one = LocalServerContainer::new(server_config.clone()).unwrap(); - - // Spawn server and let it run for a bit - let _ = thread::spawn(move || server_one.run_server(120)); - - //Wait for chain to build - thread::sleep(time::Duration::from_millis(5000)); - - // Starting tests - let base_addr = server_config.base_addr; - let api_server_port = server_config.api_server_port; - - warn!("Testing chain handler"); - let tip = get_tip(&base_addr, api_server_port); - assert!(tip.is_ok()); - - warn!("Testing status handler"); - let status = get_status(&base_addr, api_server_port); - assert!(status.is_ok()); - - // Be sure that at least a block is mined by Travis - let mut current_tip = get_tip(&base_addr, api_server_port).unwrap(); - while current_tip.height == 0 { - thread::sleep(time::Duration::from_millis(1000)); - current_tip = get_tip(&base_addr, api_server_port).unwrap(); - } - - warn!("Testing block handler"); - let last_block_by_height = get_block_by_height(&base_addr, api_server_port, current_tip.height); - assert!(last_block_by_height.is_ok()); - let last_block_by_height_compact = - get_block_by_height_compact(&base_addr, api_server_port, current_tip.height); - assert!(last_block_by_height_compact.is_ok()); - - let block_hash = current_tip.last_block_pushed; - let last_block_by_hash = get_block_by_hash(&base_addr, api_server_port, &block_hash); - assert!(last_block_by_hash.is_ok()); - let last_block_by_hash_compact = - get_block_by_hash_compact(&base_addr, api_server_port, &block_hash); - assert!(last_block_by_hash_compact.is_ok()); - - warn!("Testing chain output handler"); - let start_height = 0; - let end_height = current_tip.height; - let outputs_by_height = - get_outputs_by_height(&base_addr, api_server_port, start_height, end_height); - assert!(outputs_by_height.is_ok()); - let ids = get_ids_from_block_outputs(outputs_by_height.unwrap()); - let outputs_by_ids1 = get_outputs_by_ids1(&base_addr, api_server_port, ids.clone()); - assert!(outputs_by_ids1.is_ok()); - let outputs_by_ids2 = get_outputs_by_ids2(&base_addr, api_server_port, ids.clone()); - assert!(outputs_by_ids2.is_ok()); - - warn!("Testing txhashset handler"); - let roots = get_txhashset_roots(&base_addr, api_server_port); - assert!(roots.is_ok()); - let last_10_outputs = get_txhashset_lastoutputs(&base_addr, api_server_port, 0); - assert!(last_10_outputs.is_ok()); - let last_5_outputs = get_txhashset_lastoutputs(&base_addr, api_server_port, 5); - assert!(last_5_outputs.is_ok()); - let last_10_rangeproofs = get_txhashset_lastrangeproofs(&base_addr, api_server_port, 0); - assert!(last_10_rangeproofs.is_ok()); - let last_5_rangeproofs = get_txhashset_lastrangeproofs(&base_addr, api_server_port, 5); - assert!(last_5_rangeproofs.is_ok()); - let last_10_kernels = get_txhashset_lastkernels(&base_addr, api_server_port, 0); - assert!(last_10_kernels.is_ok()); - let last_5_kernels = get_txhashset_lastkernels(&base_addr, api_server_port, 5); - assert!(last_5_kernels.is_ok()); - - //let some more mining happen, make sure nothing pukes - thread::sleep(time::Duration::from_millis(5000)); -} - -/// Creates 2 servers and test P2P API -#[test] -fn test_p2p() { - init_test_logger(); - info!("starting test_p2p"); - global::set_local_chain_type(ChainTypes::AutomatedTesting); - - let _test_name_dir = "test_servers"; - - // Spawn server and let it run for a bit - let server_one_dir = "p2p_server_one"; - framework::clean_all_output(server_one_dir); - let mut server_config_one = LocalServerContainerConfig::default(); - server_config_one.name = String::from(server_one_dir); - server_config_one.p2p_server_port = 40002; - server_config_one.api_server_port = 40003; - server_config_one.start_miner = false; - server_config_one.start_wallet = false; - server_config_one.is_seeding = true; - let mut server_one = LocalServerContainer::new(server_config_one.clone()).unwrap(); - let _ = thread::spawn(move || server_one.run_server(120)); - - thread::sleep(time::Duration::from_millis(1000)); - - // Spawn server and let it run for a bit - let server_two_dir = "p2p_server_two"; - framework::clean_all_output(server_two_dir); - let mut server_config_two = LocalServerContainerConfig::default(); - server_config_two.name = String::from(server_two_dir); - server_config_two.p2p_server_port = 40004; - server_config_two.api_server_port = 40005; - server_config_two.start_miner = false; - server_config_two.start_wallet = false; - server_config_two.is_seeding = false; - let mut server_two = LocalServerContainer::new(server_config_two.clone()).unwrap(); - server_two.add_peer(format!( - "{}:{}", - server_config_one.base_addr, server_config_one.p2p_server_port - )); - let _ = thread::spawn(move || server_two.run_server(120)); - - // Let them do the handshake - thread::sleep(time::Duration::from_millis(2000)); - - // Starting tests - warn!("Starting P2P Tests"); - let base_addr = server_config_one.base_addr; - let api_server_port = server_config_one.api_server_port; - - // Check that peer all is also working - let mut peers_all = get_all_peers(&base_addr, api_server_port); - assert!(peers_all.is_ok()); - let pall = peers_all.unwrap(); - assert_eq!(pall.len(), 2); - - // Check that when we get peer connected the peer is here - let peers_connected = get_connected_peers(&base_addr, api_server_port); - assert!(peers_connected.is_ok()); - let pc = peers_connected.unwrap(); - assert_eq!(pc.len(), 1); - - // Check that the peer status is Healthy - let addr = format!( - "{}:{}", - server_config_two.base_addr, server_config_two.p2p_server_port - ); - let peer = get_peer(&base_addr, api_server_port, &addr); - assert!(peer.is_ok()); - assert_eq!(peer.unwrap().flags, p2p::State::Healthy); - - // Ban the peer - let ban_result = ban_peer(&base_addr, api_server_port, &addr); - assert!(ban_result.is_ok()); - thread::sleep(time::Duration::from_millis(2000)); - - // Check its status is banned with get peer - let peer = get_peer(&base_addr, api_server_port, &addr); - assert!(peer.is_ok()); - assert_eq!(peer.unwrap().flags, p2p::State::Banned); - - // Check from peer all - peers_all = get_all_peers(&base_addr, api_server_port); - assert!(peers_all.is_ok()); - assert_eq!(peers_all.unwrap().len(), 2); - - // Unban - let unban_result = unban_peer(&base_addr, api_server_port, &addr); - assert!(unban_result.is_ok()); - - // Check from peer connected - let peers_connected = get_connected_peers(&base_addr, api_server_port); - assert!(peers_connected.is_ok()); - assert_eq!(peers_connected.unwrap().len(), 0); - - // Check its status is healthy with get peer - let peer = get_peer(&base_addr, api_server_port, &addr); - assert!(peer.is_ok()); - assert_eq!(peer.unwrap().flags, p2p::State::Healthy); -} - -// Tip handler function -fn get_tip(base_addr: &String, api_server_port: u16) -> Result { - let url = format!("http://{}:{}/v1/chain", base_addr, api_server_port); - api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) -} - -// Status handler function -fn get_status(base_addr: &String, api_server_port: u16) -> Result { - let url = format!("http://{}:{}/v1/status", base_addr, api_server_port); - api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) -} - -// Block handler functions -fn get_block_by_height( - base_addr: &String, - api_server_port: u16, - height: u64, -) -> Result { - let url = format!( - "http://{}:{}/v1/blocks/{}", - base_addr, api_server_port, height - ); - api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) -} - -fn get_block_by_height_compact( - base_addr: &String, - api_server_port: u16, - height: u64, -) -> Result { - let url = format!( - "http://{}:{}/v1/blocks/{}?compact", - base_addr, api_server_port, height - ); - api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) -} - -fn get_block_by_hash( - base_addr: &String, - api_server_port: u16, - block_hash: &String, -) -> Result { - let url = format!( - "http://{}:{}/v1/blocks/{}", - base_addr, api_server_port, block_hash - ); - api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) -} - -fn get_block_by_hash_compact( - base_addr: &String, - api_server_port: u16, - block_hash: &String, -) -> Result { - let url = format!( - "http://{}:{}/v1/blocks/{}?compact", - base_addr, api_server_port, block_hash - ); - api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) -} - -// Chain output handler functions -fn get_outputs_by_ids1( - base_addr: &String, - api_server_port: u16, - ids: Vec, -) -> Result, Error> { - let url = format!( - "http://{}:{}/v1/chain/outputs/byids?id={}", - base_addr, - api_server_port, - ids.join(",") - ); - api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) -} - -fn get_outputs_by_ids2( - base_addr: &String, - api_server_port: u16, - ids: Vec, -) -> Result, Error> { - let mut ids_string: String = String::from(""); - for id in ids { - ids_string = ids_string + "?id=" + &id; - } - let ids_string = String::from(&ids_string[1..ids_string.len()]); - let url = format!( - "http://{}:{}/v1/chain/outputs/byids?{}", - base_addr, api_server_port, ids_string - ); - api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) -} - -fn get_outputs_by_height( - base_addr: &String, - api_server_port: u16, - start_height: u64, - end_height: u64, -) -> Result, Error> { - let url = format!( - "http://{}:{}/v1/chain/outputs/byheight?start_height={}&end_height={}", - base_addr, api_server_port, start_height, end_height - ); - api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) -} - -// TxHashSet handler functions -fn get_txhashset_roots(base_addr: &String, api_server_port: u16) -> Result { - let url = format!( - "http://{}:{}/v1/txhashset/roots", - base_addr, api_server_port - ); - api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) -} - -fn get_txhashset_lastoutputs( - base_addr: &String, - api_server_port: u16, - n: u64, -) -> Result, Error> { - let url: String; - if n == 0 { - url = format!( - "http://{}:{}/v1/txhashset/lastoutputs", - base_addr, api_server_port - ); - } else { - url = format!( - "http://{}:{}/v1/txhashset/lastoutputs?n={}", - base_addr, api_server_port, n - ); - } - api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) -} - -fn get_txhashset_lastrangeproofs( - base_addr: &String, - api_server_port: u16, - n: u64, -) -> Result, Error> { - let url: String; - if n == 0 { - url = format!( - "http://{}:{}/v1/txhashset/lastrangeproofs", - base_addr, api_server_port - ); - } else { - url = format!( - "http://{}:{}/v1/txhashset/lastrangeproofs?n={}", - base_addr, api_server_port, n - ); - } - api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) -} - -fn get_txhashset_lastkernels( - base_addr: &String, - api_server_port: u16, - n: u64, -) -> Result, Error> { - let url: String; - if n == 0 { - url = format!( - "http://{}:{}/v1/txhashset/lastkernels", - base_addr, api_server_port - ); - } else { - url = format!( - "http://{}:{}/v1/txhashset/lastkernels?n={}", - base_addr, api_server_port, n - ); - } - api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) -} - -// Helper function to get a vec of commitment output ids from a vec of block -// outputs -fn get_ids_from_block_outputs(block_outputs: Vec) -> Vec { - let mut ids: Vec = Vec::new(); - for block_output in block_outputs { - let outputs = &block_output.outputs; - for output in outputs { - ids.push(util::to_hex(output.clone().commit.0.to_vec())); - } - } - ids.into_iter().take(100).collect() -} - -pub fn ban_peer(base_addr: &String, api_server_port: u16, peer_addr: &String) -> Result<(), Error> { - let url = format!( - "http://{}:{}/v1/peers/{}/ban", - base_addr, api_server_port, peer_addr - ); - api::client::post_no_ret(url.as_str(), None, &"").map_err(|e| Error::API(e)) -} - -pub fn unban_peer( - base_addr: &String, - api_server_port: u16, - peer_addr: &String, -) -> Result<(), Error> { - let url = format!( - "http://{}:{}/v1/peers/{}/unban", - base_addr, api_server_port, peer_addr - ); - api::client::post_no_ret(url.as_str(), None, &"").map_err(|e| Error::API(e)) -} - -pub fn get_peer( - base_addr: &String, - api_server_port: u16, - peer_addr: &String, -) -> Result { - let url = format!( - "http://{}:{}/v1/peers/{}", - base_addr, api_server_port, peer_addr - ); - api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) -} - -pub fn get_connected_peers( - base_addr: &String, - api_server_port: u16, -) -> Result, Error> { - let url = format!( - "http://{}:{}/v1/peers/connected", - base_addr, api_server_port - ); - api::client::get::>(url.as_str(), None) - .map_err(|e| Error::API(e)) -} - -pub fn get_all_peers( - base_addr: &String, - api_server_port: u16, -) -> Result, Error> { - let url = format!("http://{}:{}/v1/peers/all", base_addr, api_server_port); - api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) -} - -/// Error type wrapping underlying module errors. -#[derive(Debug)] -pub enum Error { - /// Error originating from HTTP API calls. - API(api::Error), -} diff --git a/integration/tests/common/mod.rs b/integration/tests/common/mod.rs new file mode 100644 index 000000000..dbc770789 --- /dev/null +++ b/integration/tests/common/mod.rs @@ -0,0 +1,194 @@ +// Copyright 2026 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. + +//! Helpers for multi-process node + wallet integration tests. + +#![allow(dead_code)] + +use futures::channel::oneshot; +use grin_core::global::{self, ChainTypes}; +use grin_keychain::ExtKeychain; +use grin_p2p as p2p; +use grin_p2p::msg::PeerAddrs; +use grin_p2p::PeerAddr; +use grin_servers as servers; +use grin_util::{Mutex, StopState, ZeroingString}; +use grin_wallet_controller as controller; +use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient}; +use grin_wallet_libwallet::WalletInst; +use std::default::Default; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::Path; +use std::sync::Arc; +use std::{fs, thread, time}; + +/// Configure AutomatedTesting for the test thread and server worker threads. +pub fn init_chain() { + global::set_local_chain_type(ChainTypes::AutomatedTesting); + global::set_global_chain_type(ChainTypes::AutomatedTesting); +} + +/// Remove leftover data from a previous run. +pub fn clean_all_output(test_name_dir: &str) { + let target_dir = format!("target/tmp/{}", test_name_dir); + if let Err(e) = fs::remove_dir_all(&target_dir) { + if Path::new(&target_dir).exists() { + println!( + "can't remove output from previous test {}: {}, may be ok", + target_dir, e + ); + } + } +} + +pub fn leak_api_chan() -> &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) { + Box::leak(Box::new(oneshot::channel::<()>())) +} + +pub fn settle() { + thread::sleep(time::Duration::from_millis(500)); +} + +/// Build a node `ServerConfig` with unique ports derived from `n`. +pub fn node_config(n: u16, test_name_dir: &str) -> servers::ServerConfig { + servers::ServerConfig { + api_http_addr: format!("127.0.0.1:{}", 20000 + n), + api_secret_path: None, + foreign_api_secret_path: None, + db_root: format!("target/tmp/{}/grin-node-{}", test_name_dir, n), + p2p_config: p2p::P2PConfig { + host: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + port: 10000 + n, + seeding_type: p2p::Seeding::None, + seeds: None, + ..p2p::P2PConfig::default() + }, + chain_type: ChainTypes::AutomatedTesting, + archive_mode: Some(true), + skip_sync_wait: Some(true), + run_tui: Some(false), + run_test_miner: Some(false), + stratum_mining_config: None, + ..Default::default() + } +} + +pub fn start_node(cfg: servers::ServerConfig) -> servers::Server { + // grin_servers 5.4.1: (config, stop_state, api_chan) + servers::Server::new(cfg, None, leak_api_chan()).expect("node starts") +} + +pub type WalletHandle = Arc< + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider<'static, HTTPNodeClient, ExtKeychain>, + HTTPNodeClient, + ExtKeychain, + >, + >, + >, +>; + +/// Create and open a local wallet pointed at `node_url`. +pub fn create_wallet( + test_dir: &str, + name: &str, + node_url: &str, +) -> (WalletHandle, Option) { + let dir = format!("target/tmp/{}/{}", test_dir, name); + fs::create_dir_all(&dir).unwrap(); + + let client = HTTPNodeClient::new(node_url, None).expect("HTTPNodeClient"); + let mut wallet = Box::new(DefaultWalletImpl::::new(client).unwrap()) + as Box< + dyn WalletInst< + DefaultLCProvider<'static, HTTPNodeClient, ExtKeychain>, + HTTPNodeClient, + ExtKeychain, + >, + >; + let lc = wallet.lc_provider().unwrap(); + lc.set_top_level_directory(&dir).unwrap(); + lc.create_wallet(None, None, 32, ZeroingString::from(""), false) + .unwrap(); + let mask = lc + .open_wallet(None, ZeroingString::from(""), false, false) + .unwrap(); + (Arc::new(Mutex::new(wallet)), mask) +} + +/// Start foreign HTTP listener (coinbase / slate receive) on a background thread. +pub fn start_foreign_listener(wallet: WalletHandle, mask: Option, addr: &str) { + let keychain_mask = Arc::new(Mutex::new(mask)); + let listen = addr.to_string(); + thread::spawn(move || { + let _ = controller::controller::foreign_listener( + wallet, + keychain_mask, + &listen, + None, + false, + true, + None, + ); + }); + // Give the listener time to bind. + thread::sleep(time::Duration::from_millis(800)); +} + +/// Start owner HTTP listener (optional foreign) on a background thread. +pub fn start_owner_listener( + wallet: WalletHandle, + mask: Option, + addr: &str, + include_foreign: bool, +) { + let keychain_mask = Arc::new(Mutex::new(mask)); + let listen = addr.to_string(); + thread::spawn(move || { + let _ = controller::controller::owner_listener( + wallet, + keychain_mask, + &listen, + None, + None, + Some(include_foreign), + None, + true, + ); + }); + thread::sleep(time::Duration::from_millis(800)); +} + +/// Mine with the internal test miner, sending coinbase to `wallet_url` if set. +pub fn start_test_miner( + server: &servers::Server, + wallet_url: Option, +) -> Arc { + let stop = Arc::new(StopState::new()); + server.start_test_miner(wallet_url, stop.clone()); + stop +} + +pub fn peer_addr(addr: &str) -> PeerAddr { + PeerAddr(addr.parse::().expect("peer addr")) +} + +pub fn seeds(addrs: &[&str]) -> Option { + Some(PeerAddrs { + peers: addrs.iter().map(|a| peer_addr(a)).collect(), + }) +} diff --git a/integration/tests/dandelion.rs b/integration/tests/dandelion.rs deleted file mode 100644 index c80e3b648..000000000 --- a/integration/tests/dandelion.rs +++ /dev/null @@ -1,157 +0,0 @@ -// 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. - -#[macro_use] -extern crate log; - -mod framework; - -use self::util::Mutex; -use crate::framework::{LocalServerContainer, LocalServerContainerConfig}; -use grin_core as core; -use grin_util as util; -use std::sync::Arc; -use std::{thread, time}; - -/// Start 1 node mining, 1 non mining node and two wallets. -/// Then send a transaction from one wallet to another and propagate it a stem -/// transaction but without stem relay and check if the transaction is still -/// broadcasted. -#[test] -#[ignore] -fn test_dandelion_timeout() { - let test_name_dir = "test_dandelion_timeout"; - core::global::set_local_chain_type(core::global::ChainTypes::AutomatedTesting); - framework::clean_all_output(test_name_dir); - let mut log_config = util::LoggingConfig::default(); - //log_config.stdout_log_level = util::LogLevel::Trace; - log_config.stdout_log_level = util::LogLevel::Info; - //init_logger(Some(log_config)); - util::init_test_logger(); - - // Run a separate coinbase wallet for coinbase transactions - let mut coinbase_config = LocalServerContainerConfig::default(); - coinbase_config.name = String::from("coinbase_wallet"); - coinbase_config.wallet_validating_node_url = String::from("http://127.0.0.1:30001"); - coinbase_config.wallet_port = 10002; - let coinbase_wallet = Arc::new(Mutex::new( - LocalServerContainer::new(coinbase_config).unwrap(), - )); - let coinbase_wallet_config = { coinbase_wallet.lock().wallet_config.clone() }; - - let coinbase_seed = LocalServerContainer::get_wallet_seed(&coinbase_wallet_config); - - let _ = thread::spawn(move || { - let mut w = coinbase_wallet.lock(); - w.run_wallet(0); - }); - - let mut recp_config = LocalServerContainerConfig::default(); - recp_config.name = String::from("target_wallet"); - recp_config.wallet_validating_node_url = String::from("http://127.0.0.1:30001"); - recp_config.wallet_port = 20002; - let target_wallet = Arc::new(Mutex::new(LocalServerContainer::new(recp_config).unwrap())); - let target_wallet_cloned = target_wallet.clone(); - let recp_wallet_config = { target_wallet.lock().wallet_config.clone() }; - - let recp_seed = LocalServerContainer::get_wallet_seed(&recp_wallet_config); - //Start up a second wallet, to receive - let _ = thread::spawn(move || { - let mut w = target_wallet_cloned.lock(); - w.run_wallet(0); - }); - - // Spawn server and let it run for a bit - let mut server_one_config = LocalServerContainerConfig::default(); - server_one_config.name = String::from("server_one"); - server_one_config.p2p_server_port = 30000; - server_one_config.api_server_port = 30001; - server_one_config.start_miner = true; - server_one_config.start_wallet = false; - server_one_config.is_seeding = false; - server_one_config.coinbase_wallet_address = - String::from(format!("http://{}:{}", server_one_config.base_addr, 10002)); - let mut server_one = LocalServerContainer::new(server_one_config).unwrap(); - - let mut server_two_config = LocalServerContainerConfig::default(); - server_two_config.name = String::from("server_two"); - server_two_config.p2p_server_port = 40000; - server_two_config.api_server_port = 40001; - server_two_config.start_miner = false; - server_two_config.start_wallet = false; - server_two_config.is_seeding = true; - let mut server_two = LocalServerContainer::new(server_two_config.clone()).unwrap(); - - server_one.add_peer(format!( - "{}:{}", - server_two_config.base_addr, server_two_config.p2p_server_port - )); - - // Spawn servers and let them run for a bit - let _ = thread::spawn(move || { - server_two.run_server(120); - }); - - // Wait for the first server to start - thread::sleep(time::Duration::from_millis(5000)); - - let _ = thread::spawn(move || { - server_one.run_server(120); - }); - - // Let them do a handshake and properly update their peer relay - thread::sleep(time::Duration::from_millis(30000)); - - //Wait until we have some funds to send - let mut coinbase_info = - LocalServerContainer::get_wallet_info(&coinbase_wallet_config, &coinbase_seed); - let mut slept_time = 0; - while coinbase_info.amount_currently_spendable < 100000000000 { - thread::sleep(time::Duration::from_millis(500)); - slept_time += 500; - if slept_time > 10000 { - panic!("Coinbase not confirming in time"); - } - coinbase_info = - LocalServerContainer::get_wallet_info(&coinbase_wallet_config, &coinbase_seed); - } - - warn!("Sending 50 Grins to recipient wallet"); - - // Sending stem transaction - LocalServerContainer::send_amount_to( - &coinbase_wallet_config, - "50.00", - 1, - "not_all", - "http://127.0.0.1:20002", - false, - ); - - let coinbase_info = - LocalServerContainer::get_wallet_info(&coinbase_wallet_config, &coinbase_seed); - println!("Coinbase wallet info: {:?}", coinbase_info); - - let recipient_info = LocalServerContainer::get_wallet_info(&recp_wallet_config, &recp_seed); - - // The transaction should be waiting in the node stempool thus cannot be mined. - println!("Recipient wallet info: {:?}", recipient_info); - assert!(recipient_info.amount_awaiting_confirmation == 50000000000); - - // Wait for stem timeout - thread::sleep(time::Duration::from_millis(35000)); - println!("Recipient wallet info: {:?}", recipient_info); - let recipient_info = LocalServerContainer::get_wallet_info(&recp_wallet_config, &recp_seed); - assert!(recipient_info.amount_currently_spendable == 50000000000); -} diff --git a/integration/tests/framework.rs b/integration/tests/framework.rs deleted file mode 100644 index ffade00c0..000000000 --- a/integration/tests/framework.rs +++ /dev/null @@ -1,682 +0,0 @@ -// 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. - -extern crate grin_apiwallet as apiwallet; -extern crate grin_libwallet as libwallet; -extern crate grin_refwallet as wallet; -extern crate grin_wallet_config as wallet_config; - -use self::keychain::Keychain; -use self::util::Mutex; -use self::wallet::{HTTPNodeClient, HTTPWalletCommAdapter, LMDBBackend}; -use self::wallet_config::WalletConfig; -use blake2_rfc as blake2; -use grin_api as api; -use grin_core as core; -use grin_keychain as keychain; -use grin_p2p as p2p; -use grin_servers as servers; -use grin_util as util; -use p2p::PeerAddr; -use std::default::Default; -use std::ops::Deref; -use std::sync::Arc; -use std::{fs, thread, time}; - -/// Just removes all results from previous runs -pub fn clean_all_output(test_name_dir: &str) { - let target_dir = format!("target/tmp/{}", test_name_dir); - if let Err(e) = remove_dir_all::remove_dir_all(target_dir) { - println!("can't remove output from previous test :{}, may be ok", e); - } -} - -/// Errors that can be returned by LocalServerContainer -#[derive(Debug)] -#[allow(dead_code)] -pub enum Error { - Internal(String), - Argument(String), - NotFound, -} - -/// All-in-one server configuration struct, for convenience -/// -#[derive(Clone)] -pub struct LocalServerContainerConfig { - // user friendly name for the server, also denotes what dir - // the data files will appear in - pub name: String, - - // Base IP address - pub base_addr: String, - - // Port the server (p2p) is running on - pub p2p_server_port: u16, - - // Port the API server is running on - pub api_server_port: u16, - - // Port the wallet server is running on - pub wallet_port: u16, - - // Port the wallet owner API is running on - pub owner_port: u16, - - // Whether to include the foreign API endpoints in the owner API - pub owner_api_include_foreign: bool, - - // Whether we're going to mine - pub start_miner: bool, - - // time in millis by which to artificially slow down the mining loop - // in this container - pub miner_slowdown_in_millis: u64, - - // Whether we're going to run a wallet as well, - // can use same server instance as a validating node for convenience - pub start_wallet: bool, - - // address of a server to use as a seed - pub seed_addr: String, - - // keep track of whether this server is supposed to be seeding - pub is_seeding: bool, - - // Whether to burn mining rewards - pub burn_mining_rewards: bool, - - // full address to send coinbase rewards to - pub coinbase_wallet_address: String, - - // When running a wallet, the address to check inputs and send - // finalised transactions to, - pub wallet_validating_node_url: String, -} - -/// Default server config -impl Default for LocalServerContainerConfig { - fn default() -> LocalServerContainerConfig { - LocalServerContainerConfig { - name: String::from("test_host"), - base_addr: String::from("127.0.0.1"), - api_server_port: 13413, - p2p_server_port: 13414, - wallet_port: 13415, - owner_port: 13420, - owner_api_include_foreign: false, - seed_addr: String::from(""), - is_seeding: false, - start_miner: false, - start_wallet: false, - burn_mining_rewards: false, - coinbase_wallet_address: String::from(""), - wallet_validating_node_url: String::from(""), - miner_slowdown_in_millis: 0, - } - } -} - -/// A top-level container to hold everything that might be running -/// on a server, i.e. server, wallet in send or receive mode - -#[allow(dead_code)] -pub struct LocalServerContainer { - // Configuration - config: LocalServerContainerConfig, - - // Structure of references to the - // internal server data - pub p2p_server_stats: Option, - - // The API server instance - api_server: Option, - - // whether the server is running - pub server_is_running: bool, - - // Whether the server is mining - pub server_is_mining: bool, - - // Whether the server is also running a wallet - // Not used if running wallet without server - pub wallet_is_running: bool, - - // the list of peers to connect to - pub peer_list: Vec, - - // base directory for the server instance - pub working_dir: String, - - // Wallet configuration - pub wallet_config: WalletConfig, -} - -impl LocalServerContainer { - /// Create a new local server container with defaults, with the given name - /// all related files will be created in the directory - /// target/tmp/{name} - - pub fn new(config: LocalServerContainerConfig) -> Result { - let working_dir = format!("target/tmp/{}", config.name); - let mut wallet_config = WalletConfig::default(); - - wallet_config.api_listen_port = config.wallet_port; - wallet_config.check_node_api_http_addr = config.wallet_validating_node_url.clone(); - wallet_config.owner_api_include_foreign = Some(config.owner_api_include_foreign); - wallet_config.data_file_dir = working_dir.clone(); - Ok(LocalServerContainer { - config: config, - p2p_server_stats: None, - api_server: None, - server_is_running: false, - server_is_mining: false, - wallet_is_running: false, - working_dir: working_dir, - peer_list: Vec::new(), - wallet_config: wallet_config, - }) - } - - pub fn run_server(&mut self, duration_in_seconds: u64) -> servers::Server { - let api_addr = format!("{}:{}", self.config.base_addr, self.config.api_server_port); - - let mut seeding_type = p2p::Seeding::None; - let mut seeds = Vec::new(); - - if self.config.seed_addr.len() > 0 { - seeding_type = p2p::Seeding::List; - seeds = vec![PeerAddr::from_ip( - self.config.seed_addr.to_string().parse().unwrap(), - )]; - } - - let s = servers::Server::new(servers::ServerConfig { - api_http_addr: api_addr, - api_secret_path: None, - db_root: format!("{}/.grin", self.working_dir), - p2p_config: p2p::P2PConfig { - port: self.config.p2p_server_port, - seeds: Some(seeds), - seeding_type: seeding_type, - ..p2p::P2PConfig::default() - }, - chain_type: core::global::ChainTypes::AutomatedTesting, - skip_sync_wait: Some(true), - stratum_mining_config: None, - ..Default::default() - }) - .unwrap(); - - self.p2p_server_stats = Some(s.get_server_stats().unwrap()); - - let mut wallet_url = None; - - if self.config.start_wallet == true { - self.run_wallet(duration_in_seconds + 5); - // give a second to start wallet before continuing - thread::sleep(time::Duration::from_millis(1000)); - wallet_url = Some(format!( - "http://{}:{}", - self.config.base_addr, self.config.wallet_port - )); - } - - if self.config.start_miner == true { - println!( - "starting test Miner on port {}", - self.config.p2p_server_port - ); - s.start_test_miner(wallet_url, s.stop_state.clone()); - } - - for p in &mut self.peer_list { - println!("{} connecting to peer: {}", self.config.p2p_server_port, p); - let _ = s.connect_peer(PeerAddr::from_ip(p.parse().unwrap())); - } - - if self.wallet_is_running { - self.stop_wallet(); - } - - s - } - - /// Make a wallet for use in test endpoints (run_wallet and run_owner). - fn make_wallet_for_tests( - &mut self, - ) -> Arc>> { - // URL on which to start the wallet listener (i.e. api server) - let _url = format!("{}:{}", self.config.base_addr, self.config.wallet_port); - - // Just use the name of the server for a seed for now - let seed = format!("{}", self.config.name); - - let _seed = blake2::blake2b::blake2b(32, &[], seed.as_bytes()); - - println!( - "Starting the Grin wallet receiving daemon on {} ", - self.config.wallet_port - ); - - self.wallet_config = WalletConfig::default(); - - self.wallet_config.api_listen_port = self.config.wallet_port; - self.wallet_config.check_node_api_http_addr = - self.config.wallet_validating_node_url.clone(); - self.wallet_config.data_file_dir = self.working_dir.clone(); - self.wallet_config.owner_api_include_foreign = Some(self.config.owner_api_include_foreign); - - let _ = fs::create_dir_all(self.wallet_config.clone().data_file_dir); - let r = wallet::WalletSeed::init_file(&self.wallet_config, 32, None, ""); - - let client_n = HTTPNodeClient::new(&self.wallet_config.check_node_api_http_addr, None); - - if let Err(_e) = r { - //panic!("Error initializing wallet seed: {}", e); - } - - let wallet: LMDBBackend = - LMDBBackend::new(self.wallet_config.clone(), "", client_n).unwrap_or_else(|e| { - panic!( - "Error creating wallet: {:?} Config: {:?}", - e, self.wallet_config - ) - }); - - Arc::new(Mutex::new(wallet)) - } - - /// Starts a wallet daemon to receive - pub fn run_wallet(&mut self, _duration_in_mills: u64) { - let wallet = self.make_wallet_for_tests(); - - wallet::controller::foreign_listener(wallet, &self.wallet_config.api_listen_addr(), None) - .unwrap_or_else(|e| { - panic!( - "Error creating wallet listener: {:?} Config: {:?}", - e, self.wallet_config - ) - }); - - self.wallet_is_running = true; - } - - /// Starts a wallet owner daemon - #[allow(dead_code)] - pub fn run_owner(&mut self) { - let wallet = self.make_wallet_for_tests(); - - // WalletConfig doesn't allow changing the owner API path, so we build - // the path ourselves - let owner_listen_addr = format!("127.0.0.1:{}", self.config.owner_port); - - wallet::controller::owner_listener( - wallet, - &owner_listen_addr, - None, - None, - self.wallet_config.owner_api_include_foreign.clone(), - ) - .unwrap_or_else(|e| { - panic!( - "Error creating wallet owner listener: {:?} Config: {:?}", - e, self.wallet_config - ) - }); - } - - #[allow(dead_code)] - pub fn get_wallet_seed(config: &WalletConfig) -> wallet::WalletSeed { - let _ = fs::create_dir_all(config.clone().data_file_dir); - wallet::WalletSeed::init_file(config, 32, None, "").unwrap(); - let wallet_seed = - wallet::WalletSeed::from_file(config, "").expect("Failed to read wallet seed file."); - wallet_seed - } - - #[allow(dead_code)] - pub fn get_wallet_info( - config: &WalletConfig, - wallet_seed: &wallet::WalletSeed, - ) -> wallet::WalletInfo { - let keychain: keychain::ExtKeychain = wallet_seed - .derive_keychain(false) - .expect("Failed to derive keychain from seed file and passphrase."); - let client_n = HTTPNodeClient::new(&config.check_node_api_http_addr, None); - let mut wallet = LMDBBackend::new(config.clone(), "", client_n) - .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config)); - wallet.keychain = Some(keychain); - let parent_id = keychain::ExtKeychain::derive_key_id(2, 0, 0, 0, 0); - let _ = libwallet::internal::updater::refresh_outputs(&mut wallet, &parent_id, false); - libwallet::internal::updater::retrieve_info(&mut wallet, &parent_id, 1).unwrap() - } - - #[allow(dead_code)] - pub fn send_amount_to( - config: &WalletConfig, - amount: &str, - minimum_confirmations: u64, - selection_strategy: &str, - dest: &str, - _fluff: bool, - ) { - let amount = core::core::amount_from_hr_string(amount) - .expect("Could not parse amount as a number with optional decimal point."); - - let wallet_seed = - wallet::WalletSeed::from_file(config, "").expect("Failed to read wallet seed file."); - - let keychain: keychain::ExtKeychain = wallet_seed - .derive_keychain(false) - .expect("Failed to derive keychain from seed file and passphrase."); - - let client_n = HTTPNodeClient::new(&config.check_node_api_http_addr, None); - let client_w = HTTPWalletCommAdapter::new(); - - let max_outputs = 500; - let change_outputs = 1; - - let mut wallet = LMDBBackend::new(config.clone(), "", client_n) - .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config)); - wallet.keychain = Some(keychain); - let _ = wallet::controller::owner_single_use(Arc::new(Mutex::new(wallet)), |api| { - let (mut slate, lock_fn) = api.initiate_tx( - None, - amount, - minimum_confirmations, - max_outputs, - change_outputs, - selection_strategy == "all", - None, - )?; - slate = client_w.send_tx_sync(dest, &slate)?; - slate = api.finalize_tx(&slate)?; - api.tx_lock_outputs(&slate, lock_fn)?; - println!( - "Tx sent: {} grin to {} (strategy '{}')", - core::core::amount_to_hr_string(amount, false), - dest, - selection_strategy, - ); - Ok(()) - }) - .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config)); - } - - /// Stops the running wallet server - pub fn stop_wallet(&mut self) { - println!("Stop wallet!"); - let api_server = self.api_server.as_mut().unwrap(); - api_server.stop(); - } - - /// Adds a peer to this server to connect to upon running - - #[allow(dead_code)] - pub fn add_peer(&mut self, addr: String) { - self.peer_list.push(addr); - } -} - -/// Configuration values for container pool - -pub struct LocalServerContainerPoolConfig { - // Base name to append to all the servers in this pool - pub base_name: String, - - // Base http address for all of the servers in this pool - pub base_http_addr: String, - - // Base port server for all of the servers in this pool - // Increment the number by 1 for each new server - pub base_p2p_port: u16, - - // Base api port for all of the servers in this pool - // Increment this number by 1 for each new server - pub base_api_port: u16, - - // Base wallet port for this server - // - pub base_wallet_port: u16, - - // Base wallet owner port for this server - // - pub base_owner_port: u16, - - // How long the servers in the pool are going to run - pub run_length_in_seconds: u64, -} - -/// Default server config -/// -impl Default for LocalServerContainerPoolConfig { - fn default() -> LocalServerContainerPoolConfig { - LocalServerContainerPoolConfig { - base_name: String::from("test_pool"), - base_http_addr: String::from("127.0.0.1"), - base_p2p_port: 10000, - base_api_port: 11000, - base_wallet_port: 12000, - base_owner_port: 13000, - run_length_in_seconds: 30, - } - } -} - -/// A convenience pool for running many servers simultaneously -/// without necessarily having to configure each one manually - -#[allow(dead_code)] -pub struct LocalServerContainerPool { - // configuration - pub config: LocalServerContainerPoolConfig, - - // keep ahold of all the created servers thread-safely - server_containers: Vec, - - // Keep track of what the last ports a server was opened on - next_p2p_port: u16, - - next_api_port: u16, - - next_wallet_port: u16, - - next_owner_port: u16, - - // keep track of whether a seed exists, and pause a bit if so - is_seeding: bool, -} - -#[allow(dead_code)] -impl LocalServerContainerPool { - pub fn new(config: LocalServerContainerPoolConfig) -> LocalServerContainerPool { - (LocalServerContainerPool { - next_api_port: config.base_api_port, - next_p2p_port: config.base_p2p_port, - next_wallet_port: config.base_wallet_port, - next_owner_port: config.base_owner_port, - config: config, - server_containers: Vec::new(), - is_seeding: false, - }) - } - - /// adds a single server on the next available port - /// overriding passed-in values as necessary. Config object is an OUT value - /// with - /// ports/addresses filled in - /// - - #[allow(dead_code)] - pub fn create_server(&mut self, server_config: &mut LocalServerContainerConfig) { - // If we're calling it this way, need to override these - server_config.p2p_server_port = self.next_p2p_port; - server_config.api_server_port = self.next_api_port; - server_config.wallet_port = self.next_wallet_port; - server_config.owner_port = self.next_owner_port; - - server_config.name = String::from(format!( - "{}/{}-{}", - self.config.base_name, self.config.base_name, server_config.p2p_server_port - )); - - // Use self as coinbase wallet - server_config.coinbase_wallet_address = String::from(format!( - "http://{}:{}", - server_config.base_addr, server_config.wallet_port - )); - - self.next_p2p_port += 1; - self.next_api_port += 1; - self.next_wallet_port += 1; - self.next_owner_port += 1; - - if server_config.is_seeding { - self.is_seeding = true; - } - - let _server_address = format!( - "{}:{}", - server_config.base_addr, server_config.p2p_server_port - ); - - let server_container = LocalServerContainer::new(server_config.clone()).unwrap(); - // self.server_containers.push(server_arc); - - // Create a future that runs the server for however many seconds - // collect them all and run them in the run_all_servers - let _run_time = self.config.run_length_in_seconds; - - self.server_containers.push(server_container); - } - - /// adds n servers, ready to run - /// - /// - #[allow(dead_code)] - pub fn create_servers(&mut self, number: u16) { - for _ in 0..number { - // self.create_server(); - } - } - - /// runs all servers, and returns a vector of references to the servers - /// once they've all been run - /// - - #[allow(dead_code)] - pub fn run_all_servers(self) -> Arc>> { - let run_length = self.config.run_length_in_seconds; - let mut handles = vec![]; - - // return handles to all of the servers, wrapped in mutexes, handles, etc - let return_containers = Arc::new(Mutex::new(Vec::new())); - - let is_seeding = self.is_seeding.clone(); - - for mut s in self.server_containers { - let return_container_ref = return_containers.clone(); - let handle = thread::spawn(move || { - if is_seeding && !s.config.is_seeding { - // there's a seed and we're not it, so hang around longer and give the seed - // a chance to start - thread::sleep(time::Duration::from_millis(2000)); - } - let server_ref = s.run_server(run_length); - return_container_ref.lock().push(server_ref); - }); - // Not a big fan of sleeping hack here, but there appears to be a - // concurrency issue when creating files in rocksdb that causes - // failure if we don't pause a bit before starting the next server - thread::sleep(time::Duration::from_millis(500)); - handles.push(handle); - } - - for handle in handles { - match handle.join() { - Ok(_) => {} - Err(e) => { - println!("Error starting server thread: {:?}", e); - panic!(e); - } - } - } - - // return a much simplified version of the results - return_containers.clone() - } - - #[allow(dead_code)] - pub fn connect_all_peers(&mut self) { - // just pull out all currently active servers, build a list, - // and feed into all servers - let mut server_addresses: Vec = Vec::new(); - for s in &self.server_containers { - let server_address = format!("{}:{}", s.config.base_addr, s.config.p2p_server_port); - server_addresses.push(server_address); - } - - for a in server_addresses { - for s in &mut self.server_containers { - if format!("{}:{}", s.config.base_addr, s.config.p2p_server_port) != a { - s.add_peer(a.clone()); - } - } - } - } -} - -#[allow(dead_code)] -pub fn stop_all_servers(servers: Arc>>) { - let locked_servs = servers.lock(); - for s in locked_servs.deref() { - s.stop(); - } -} - -/// Create and return a ServerConfig -#[allow(dead_code)] -pub fn config(n: u16, test_name_dir: &str, seed_n: u16) -> servers::ServerConfig { - servers::ServerConfig { - api_http_addr: format!("127.0.0.1:{}", 20000 + n), - api_secret_path: None, - db_root: format!("target/tmp/{}/grin-sync-{}", test_name_dir, n), - p2p_config: p2p::P2PConfig { - port: 10000 + n, - seeding_type: p2p::Seeding::List, - seeds: Some(vec![PeerAddr::from_ip( - format!("127.0.0.1:{}", 10000 + seed_n).parse().unwrap(), - )]), - ..p2p::P2PConfig::default() - }, - chain_type: core::global::ChainTypes::AutomatedTesting, - archive_mode: Some(true), - skip_sync_wait: Some(true), - ..Default::default() - } -} - -/// return stratum mining config -#[allow(dead_code)] -pub fn stratum_config() -> servers::common::types::StratumServerConfig { - servers::common::types::StratumServerConfig { - enable_stratum_server: Some(true), - stratum_server_addr: Some(String::from("127.0.0.1:13416")), - attempt_time_per_block: 60, - minimum_share_difficulty: 1, - wallet_listener_url: String::from("http://127.0.0.1:13415"), - burn_reward: false, - } -} diff --git a/integration/tests/simulnet.rs b/integration/tests/simulnet.rs deleted file mode 100644 index df407ec5d..000000000 --- a/integration/tests/simulnet.rs +++ /dev/null @@ -1,1007 +0,0 @@ -// 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. - -extern crate grin_apiwallet as apiwallet; -extern crate grin_libwallet as libwallet; -extern crate grin_refwallet as wallet; -extern crate grin_wallet_config as wallet_config; -#[macro_use] -extern crate log; - -mod framework; - -use self::core::core::hash::Hashed; -use self::core::global::{self, ChainTypes}; -use self::libwallet::types::{WalletBackend, WalletInst}; -use self::util::{Mutex, StopState}; -use self::wallet::controller; -use self::wallet::lmdb_wallet::LMDBBackend; -use self::wallet::{HTTPNodeClient, HTTPWalletCommAdapter}; -use self::wallet_config::WalletConfig; -use grin_api as api; -use grin_core as core; -use grin_keychain as keychain; -use grin_p2p as p2p; -use grin_servers as servers; -use grin_util as util; -use p2p::PeerAddr; -use std::cmp; -use std::default::Default; -use std::process::exit; -use std::sync::Arc; -use std::{thread, time}; - -use crate::framework::{ - config, stop_all_servers, LocalServerContainerConfig, LocalServerContainerPool, - LocalServerContainerPoolConfig, -}; - -/// Testing the frameworks by starting a fresh server, creating a genesis -/// Block and mining into a wallet for a bit -#[test] -fn basic_genesis_mine() { - util::init_test_logger(); - global::set_local_chain_type(ChainTypes::AutomatedTesting); - - let test_name_dir = "genesis_mine"; - framework::clean_all_output(test_name_dir); - - // Create a server pool - let mut pool_config = LocalServerContainerPoolConfig::default(); - pool_config.base_name = String::from(test_name_dir); - pool_config.run_length_in_seconds = 10; - - pool_config.base_api_port = 30000; - pool_config.base_p2p_port = 31000; - pool_config.base_wallet_port = 32000; - - let mut pool = LocalServerContainerPool::new(pool_config); - - // Create a server to add into the pool - let mut server_config = LocalServerContainerConfig::default(); - server_config.start_miner = true; - server_config.start_wallet = false; - server_config.burn_mining_rewards = true; - - pool.create_server(&mut server_config); - let servers = pool.run_all_servers(); - stop_all_servers(servers); -} - -/// Creates 5 servers, first being a seed and check that through peer address -/// messages they all end up connected. -#[test] -fn simulate_seeding() { - util::init_test_logger(); - global::set_local_chain_type(ChainTypes::AutomatedTesting); - - let test_name_dir = "simulate_seeding"; - framework::clean_all_output(test_name_dir); - - // Create a server pool - let mut pool_config = LocalServerContainerPoolConfig::default(); - pool_config.base_name = test_name_dir.to_string(); - pool_config.run_length_in_seconds = 30; - - // have to use different ports because of tests being run in parallel - pool_config.base_api_port = 30020; - pool_config.base_p2p_port = 31020; - pool_config.base_wallet_port = 32020; - - let mut pool = LocalServerContainerPool::new(pool_config); - - // Create a first seed server to add into the pool - let mut server_config = LocalServerContainerConfig::default(); - // server_config.start_miner = true; - server_config.start_wallet = false; - server_config.burn_mining_rewards = true; - server_config.is_seeding = true; - - pool.create_server(&mut server_config); - - // wait the seed server fully start up before start remaining servers - thread::sleep(time::Duration::from_millis(1_000)); - - // point next servers at first seed - server_config.is_seeding = false; - server_config.seed_addr = format!( - "{}:{}", - server_config.base_addr, server_config.p2p_server_port - ); - - for _ in 0..4 { - pool.create_server(&mut server_config); - } - - let servers = pool.run_all_servers(); - thread::sleep(time::Duration::from_secs(5)); - - // Check they all end up connected. - let url = format!( - "http://{}:{}/v1/peers/connected", - &server_config.base_addr, 30020 - ); - let peers_all = api::client::get::>(url.as_str(), None); - assert!(peers_all.is_ok()); - assert_eq!(peers_all.unwrap().len(), 4); - - stop_all_servers(servers); - - // wait servers fully stop before start next automated test - thread::sleep(time::Duration::from_millis(1_000)); -} - -/// Create 1 server, start it mining, then connect 4 other peers mining and -/// using the first as a seed. Meant to test the evolution of mining difficulty with miners -/// running at different rates. -/// -/// TODO: Just going to comment this out as an automatically run test for the time -/// being, As it's more for actively testing and hurts CI a lot -#[ignore] -#[test] -fn simulate_parallel_mining() { - global::set_local_chain_type(ChainTypes::AutomatedTesting); - - let test_name_dir = "simulate_parallel_mining"; - // framework::clean_all_output(test_name_dir); - - // Create a server pool - let mut pool_config = LocalServerContainerPoolConfig::default(); - pool_config.base_name = test_name_dir.to_string(); - pool_config.run_length_in_seconds = 60; - // have to use different ports because of tests being run in parallel - pool_config.base_api_port = 30040; - pool_config.base_p2p_port = 31040; - pool_config.base_wallet_port = 32040; - - let mut pool = LocalServerContainerPool::new(pool_config); - - // Create a first seed server to add into the pool - let mut server_config = LocalServerContainerConfig::default(); - server_config.start_miner = true; - server_config.start_wallet = true; - server_config.is_seeding = true; - - pool.create_server(&mut server_config); - - // point next servers at first seed - server_config.is_seeding = false; - server_config.seed_addr = format!( - "{}:{}", - server_config.base_addr, server_config.p2p_server_port - ); - - // And create 4 more, then let them run for a while - for i in 1..4 { - // fudge in some slowdown - server_config.miner_slowdown_in_millis = i * 2; - pool.create_server(&mut server_config); - } - - // pool.connect_all_peers(); - - let servers = pool.run_all_servers(); - stop_all_servers(servers); - - // Check mining difficulty here?, though I'd think it's more valuable - // to simply output it. Can at least see the evolution of the difficulty target - // in the debug log output for now -} - -// TODO: Convert these tests to newer framework format -/// Create a network of 5 servers and mine a block, verifying that the block -/// gets propagated to all. -#[test] -fn simulate_block_propagation() { - util::init_test_logger(); - - // we actually set the chain_type in the ServerConfig below - // TODO - avoid needing to set it in two places? - global::set_local_chain_type(ChainTypes::AutomatedTesting); - - let test_name_dir = "grin-prop"; - framework::clean_all_output(test_name_dir); - - // instantiates 5 servers on different ports - let mut servers = vec![]; - for n in 0..5 { - let s = servers::Server::new(framework::config(10 * n, test_name_dir, 0)).unwrap(); - servers.push(s); - thread::sleep(time::Duration::from_millis(100)); - } - - // start mining - let stop = Arc::new(Mutex::new(StopState::new())); - servers[0].start_test_miner(None, stop.clone()); - - // monitor for a change of head on a different server and check whether - // chain height has changed - let mut success = false; - let mut time_spent = 0; - loop { - let mut count = 0; - for n in 0..5 { - if servers[n].head().height > 3 { - count += 1; - } - } - if count == 5 { - success = true; - break; - } - thread::sleep(time::Duration::from_millis(1_000)); - time_spent += 1; - if time_spent >= 30 { - info!("simulate_block_propagation - fail on timeout",); - break; - } - - // stop mining after 8s - if time_spent == 8 { - servers[0].stop_test_miner(stop.clone()); - } - } - for n in 0..5 { - servers[n].stop(); - } - assert_eq!(true, success); - - // wait servers fully stop before start next automated test - thread::sleep(time::Duration::from_millis(1_000)); -} - -/// Creates 2 different disconnected servers, mine a few blocks on one, connect -/// them and check that the 2nd gets all the blocks -#[test] -fn simulate_full_sync() { - util::init_test_logger(); - - // we actually set the chain_type in the ServerConfig below - global::set_local_chain_type(ChainTypes::AutomatedTesting); - - let test_name_dir = "grin-sync"; - framework::clean_all_output(test_name_dir); - - let s1 = servers::Server::new(framework::config(1000, "grin-sync", 1000)).unwrap(); - // mine a few blocks on server 1 - let stop = Arc::new(Mutex::new(StopState::new())); - s1.start_test_miner(None, stop.clone()); - thread::sleep(time::Duration::from_secs(8)); - s1.stop_test_miner(stop); - - let s2 = servers::Server::new(framework::config(1001, "grin-sync", 1000)).unwrap(); - - // Get the current header from s1. - let s1_header = s1.chain.head_header().unwrap(); - info!( - "simulate_full_sync - s1 header head: {} at {}", - s1_header.hash(), - s1_header.height - ); - - // Wait for s2 to sync up to and including the header from s1. - let mut time_spent = 0; - while s2.head().height < s1_header.height { - thread::sleep(time::Duration::from_millis(1_000)); - time_spent += 1; - if time_spent >= 30 { - info!( - "sync fail. s2.head().height: {}, s1_header.height: {}", - s2.head().height, - s1_header.height - ); - break; - } - } - - // Confirm both s1 and s2 see a consistent header at that height. - let s2_header = s2.chain.get_block_header(&s1_header.hash()).unwrap(); - assert_eq!(s1_header, s2_header); - - // Stop our servers cleanly. - s1.stop(); - s2.stop(); - - // wait servers fully stop before start next automated test - thread::sleep(time::Duration::from_millis(1_000)); -} - -/// Creates 2 different disconnected servers, mine a few blocks on one, connect -/// them and check that the 2nd gets all using fast sync algo -#[test] -fn simulate_fast_sync() { - util::init_test_logger(); - - // we actually set the chain_type in the ServerConfig below - global::set_local_chain_type(ChainTypes::AutomatedTesting); - - let test_name_dir = "grin-fast"; - framework::clean_all_output(test_name_dir); - - // start s1 and mine enough blocks to get beyond the fast sync horizon - let s1 = servers::Server::new(framework::config(2000, "grin-fast", 2000)).unwrap(); - let stop = Arc::new(Mutex::new(StopState::new())); - s1.start_test_miner(None, stop.clone()); - - while s1.head().height < 20 { - thread::sleep(time::Duration::from_millis(1_000)); - } - s1.stop_test_miner(stop); - - let mut conf = config(2001, "grin-fast", 2000); - conf.archive_mode = Some(false); - - let s2 = servers::Server::new(conf).unwrap(); - - // Get the current header from s1. - let s1_header = s1.chain.head_header().unwrap(); - - // Wait for s2 to sync up to and including the header from s1. - let mut total_wait = 0; - while s2.head().height < s1_header.height { - thread::sleep(time::Duration::from_millis(1_000)); - total_wait += 1; - if total_wait >= 30 { - error!( - "simulate_fast_sync test fail on timeout! s2 height: {}, s1 height: {}", - s2.head().height, - s1_header.height, - ); - break; - } - } - - // Confirm both s1 and s2 see a consistent header at that height. - let s2_header = s2.chain.get_block_header(&s1_header.hash()).unwrap(); - assert_eq!(s1_header, s2_header); - - // Stop our servers cleanly. - s1.stop(); - s2.stop(); - - // wait servers fully stop before start next automated test - thread::sleep(time::Duration::from_millis(1_000)); -} - -/// Preparation: -/// Creates 6 disconnected servers: A, B, C, D, E and F, mine 80 blocks on A, -/// Compact server A. -/// Connect all servers, check all get state_sync_threshold full blocks using fast sync. -/// Disconnect all servers from each other. -/// -/// Test case 1: nodes that just synced is able to handle forks of up to state_sync_threshold -/// Mine state_sync_threshold-7 blocks on A -/// Mine state_sync_threshold-1 blocks on C (long fork), connect C to server A -/// check server A can sync to C without txhashset download. -/// -/// Test case 2: nodes with history in between state_sync_threshold and cut_through_horizon will -/// be able to handle forks larger than state_sync_threshold but not as large as cut_through_horizon. -/// Mine 20 blocks on A (then A has 59 blocks in local chain) -/// Mine cut_through_horizon-1 blocks on D (longer fork), connect D to servers A, then fork point -/// is at A's body head.height - 39, and 20 < 39 < 70. -/// check server A can sync without txhashset download. -/// -/// Test case 3: nodes that have enough history is able to handle forks of up to cut_through_horizon -/// Mine cut_through_horizon+10 blocks on E, connect E to servers A and B -/// check server A can sync to E without txhashset download. -/// check server B can sync to E but need txhashset download. -/// -/// Test case 4: nodes which had a success state sync can have a new state sync if needed. -/// Mine cut_through_horizon+20 blocks on F (longer fork than E), connect F to servers B -/// check server B can sync to F with txhashset download. -/// -/// Test case 5: normal sync (not a fork) should not trigger a txhashset download -/// Mine cut_through_horizon-10 blocks on F, connect F to servers B -/// check server B can sync to F without txhashset download. -/// -/// Test case 6: far behind sync (not a fork) should trigger a txhashset download -/// Mine cut_through_horizon+1 blocks on F, connect F to servers B -/// check server B can sync to F with txhashset download. -/// -/// -#[ignore] -#[test] -fn simulate_long_fork() { - util::init_test_logger(); - println!("starting simulate_long_fork"); - - // we actually set the chain_type in the ServerConfig below - global::set_local_chain_type(ChainTypes::AutomatedTesting); - - let test_name_dir = "grin-long-fork"; - framework::clean_all_output(test_name_dir); - - let s = long_fork_test_preparation(); - for si in &s { - si.pause(); - } - thread::sleep(time::Duration::from_millis(1_000)); - - long_fork_test_case_1(&s); - thread::sleep(time::Duration::from_millis(1_000)); - - long_fork_test_case_2(&s); - thread::sleep(time::Duration::from_millis(1_000)); - - long_fork_test_case_3(&s); - thread::sleep(time::Duration::from_millis(1_000)); - - long_fork_test_case_4(&s); - thread::sleep(time::Duration::from_millis(1_000)); - - long_fork_test_case_5(&s); - - // Clean up - for si in &s { - si.stop(); - } - - // wait servers fully stop before start next automated test - thread::sleep(time::Duration::from_millis(1_000)); -} - -fn long_fork_test_preparation() -> Vec { - println!("preparation: mine 80 blocks, create 6 servers and sync all of them"); - - let mut s: Vec = vec![]; - - // start server A and mine 80 blocks to get beyond the fast sync horizon - let mut conf = framework::config(2100, "grin-long-fork", 2100); - conf.archive_mode = Some(false); - conf.api_secret_path = None; - let s0 = servers::Server::new(conf).unwrap(); - thread::sleep(time::Duration::from_millis(1_000)); - s.push(s0); - let stop = Arc::new(Mutex::new(StopState::new())); - s[0].start_test_miner(None, stop.clone()); - - while s[0].head().height < global::cut_through_horizon() as u64 + 10 { - thread::sleep(time::Duration::from_millis(1_000)); - } - s[0].stop_test_miner(stop); - thread::sleep(time::Duration::from_millis(1_000)); - - // Get the current header from s0. - let s0_header = s[0].chain.head().unwrap(); - - // check the tail after compacting - let _ = s[0].chain.compact(); - let s0_tail = s[0].chain.tail().unwrap(); - assert_eq!( - s0_header.height - global::cut_through_horizon() as u64, - s0_tail.height - ); - - for i in 1..6 { - let mut conf = config(2100 + i, "grin-long-fork", 2100); - conf.archive_mode = Some(false); - conf.api_secret_path = None; - let si = servers::Server::new(conf).unwrap(); - s.push(si); - } - thread::sleep(time::Duration::from_millis(1_000)); - - // Wait for s[1..5] to sync up to and including the header from s0. - let mut total_wait = 0; - let mut min_height = 0; - while min_height < s0_header.height { - thread::sleep(time::Duration::from_millis(1_000)); - total_wait += 1; - if total_wait >= 60 { - println!( - "simulate_long_fork (preparation) test fail on timeout! minimum height: {}, s0 height: {}", - min_height, - s0_header.height, - ); - exit(1); - } - min_height = s0_header.height; - for i in 1..6 { - min_height = cmp::min(s[i].head().height, min_height); - } - } - - // Confirm both s0 and s1 see a consistent header at that height. - let s1_header = s[1].chain.head().unwrap(); - assert_eq!(s0_header, s1_header); - println!( - "preparation done. all 5 servers head.height: {}", - s0_header.height - ); - - // Wait for peers fully connection - let mut total_wait = 0; - let mut min_peers = 0; - while min_peers < 4 { - thread::sleep(time::Duration::from_millis(1_000)); - total_wait += 1; - if total_wait >= 60 { - println!( - "simulate_long_fork (preparation) test fail on timeout! minimum connected peers: {}", - min_peers, - ); - exit(1); - } - min_peers = 4; - for i in 0..5 { - let peers_connected = get_connected_peers(&"127.0.0.1".to_owned(), 22100 + i); - min_peers = cmp::min(min_peers, peers_connected.len()); - } - } - - return s; -} - -fn long_fork_test_mining(blocks: u64, n: u16, s: &servers::Server) { - // Get the current header from node. - let sn_header = s.chain.head().unwrap(); - - // Mining - let stop = Arc::new(Mutex::new(StopState::new())); - s.start_test_miner(None, stop.clone()); - - while s.head().height < sn_header.height + blocks { - thread::sleep(time::Duration::from_millis(1)); - } - s.stop_test_miner(stop); - thread::sleep(time::Duration::from_millis(1_000)); - println!( - "{} blocks mined on s{}. s{}.height: {} (old height: {})", - s.head().height - sn_header.height, - n, - n, - s.head().height, - sn_header.height, - ); - - let _ = s.chain.compact(); - let sn_header = s.chain.head().unwrap(); - let sn_tail = s.chain.tail().unwrap(); - println!( - "after compacting, s{}.head().height: {}, s{}.tail().height: {}", - n, sn_header.height, n, sn_tail.height, - ); -} - -fn long_fork_test_case_1(s: &[servers::Server]) { - println!("\ntest case 1 start"); - - // Mine state_sync_threshold-7 blocks on s0 - long_fork_test_mining(global::state_sync_threshold() as u64 - 7, 0, &s[0]); - - // Mine state_sync_threshold-1 blocks on s2 (long fork), a fork with more work than s0 chain - long_fork_test_mining(global::state_sync_threshold() as u64 - 1, 2, &s[2]); - - let s2_header = s[2].chain.head().unwrap(); - let s0_header = s[0].chain.head().unwrap(); - let s0_tail = s[0].chain.tail().unwrap(); - println!( - "test case 1: s0 start syncing with s2... s0.head().height: {}, s2.head().height: {}", - s0_header.height, s2_header.height, - ); - s[0].resume(); - s[2].resume(); - - // Check server s0 can sync to s2 without txhashset download. - let mut total_wait = 0; - while s[0].head().height < s2_header.height { - thread::sleep(time::Duration::from_millis(1_000)); - total_wait += 1; - if total_wait >= 120 { - println!( - "test case 1: test fail on timeout! s0 height: {}, s2 height: {}", - s[0].head().height, - s2_header.height, - ); - exit(1); - } - } - let s0_tail_new = s[0].chain.tail().unwrap(); - assert_eq!(s0_tail_new.height, s0_tail.height); - println!( - "test case 1: s0.head().height: {}, s2_header.height: {}", - s[0].head().height, - s2_header.height, - ); - assert_eq!(s[0].head().last_block_h, s2_header.last_block_h); - - s[0].pause(); - s[2].stop(); - println!("test case 1 passed") -} - -fn long_fork_test_case_2(s: &[servers::Server]) { - println!("\ntest case 2 start"); - - // Mine 20 blocks on s0 - long_fork_test_mining(20, 0, &s[0]); - - // Mine cut_through_horizon-1 blocks on s3 (longer fork) - long_fork_test_mining(global::cut_through_horizon() as u64 - 1, 3, &s[3]); - let s3_header = s[3].chain.head().unwrap(); - let s0_header = s[0].chain.head().unwrap(); - let s0_tail = s[0].chain.tail().unwrap(); - println!( - "test case 2: s0 start syncing with s3. s0.head().height: {}, s3.head().height: {}", - s0_header.height, s3_header.height, - ); - s[0].resume(); - s[3].resume(); - - // Check server s0 can sync to s3 without txhashset download. - let mut total_wait = 0; - while s[0].head().height < s3_header.height { - thread::sleep(time::Duration::from_millis(1_000)); - total_wait += 1; - if total_wait >= 120 { - println!( - "test case 2: test fail on timeout! s0 height: {}, s3 height: {}", - s[0].head().height, - s3_header.height, - ); - exit(1); - } - } - let s0_tail_new = s[0].chain.tail().unwrap(); - assert_eq!(s0_tail_new.height, s0_tail.height); - assert_eq!(s[0].head().hash(), s3_header.hash()); - - let _ = s[0].chain.compact(); - let s0_header = s[0].chain.head().unwrap(); - let s0_tail = s[0].chain.tail().unwrap(); - println!( - "test case 2: after compacting, s0.head().height: {}, s0.tail().height: {}", - s0_header.height, s0_tail.height, - ); - - s[0].pause(); - s[3].stop(); - println!("test case 2 passed") -} - -fn long_fork_test_case_3(s: &[servers::Server]) { - println!("\ntest case 3 start"); - - // Mine cut_through_horizon+1 blocks on s4 - long_fork_test_mining(global::cut_through_horizon() as u64 + 10, 4, &s[4]); - - let s4_header = s[4].chain.head().unwrap(); - let s0_header = s[0].chain.head().unwrap(); - let s0_tail = s[0].chain.tail().unwrap(); - let s1_header = s[1].chain.head().unwrap(); - let s1_tail = s[1].chain.tail().unwrap(); - println!( - "test case 3: s0/1 start syncing with s4. s0.head().height: {}, s0.tail().height: {}, s1.head().height: {}, s1.tail().height: {}, s4.head().height: {}", - s0_header.height, s0_tail.height, - s1_header.height, s1_tail.height, - s4_header.height, - ); - s[0].resume(); - s[4].resume(); - - // Check server s0 can sync to s4. - let mut total_wait = 0; - while s[0].head().height < s4_header.height { - thread::sleep(time::Duration::from_millis(1_000)); - total_wait += 1; - if total_wait >= 120 { - println!( - "test case 3: test fail on timeout! s0 height: {}, s4 height: {}", - s[0].head().height, - s4_header.height, - ); - exit(1); - } - } - assert_eq!(s[0].head().hash(), s4_header.hash()); - - s[0].stop(); - s[1].resume(); - - // Check server s1 can sync to s4 but with txhashset download. - let mut total_wait = 0; - while s[1].head().height < s4_header.height { - thread::sleep(time::Duration::from_millis(1_000)); - total_wait += 1; - if total_wait >= 120 { - println!( - "test case 3: test fail on timeout! s1 height: {}, s4 height: {}", - s[1].head().height, - s4_header.height, - ); - exit(1); - } - } - let s1_tail_new = s[1].chain.tail().unwrap(); - println!( - "test case 3: s[1].tail().height: {}, old height: {}", - s1_tail_new.height, s1_tail.height - ); - assert_ne!(s1_tail_new.height, s1_tail.height); - assert_eq!(s[1].head().hash(), s4_header.hash()); - - s[1].pause(); - s[4].pause(); - println!("test case 3 passed") -} - -fn long_fork_test_case_4(s: &[servers::Server]) { - println!("\ntest case 4 start"); - - let _ = s[1].chain.compact(); - - // Mine cut_through_horizon+20 blocks on s5 (longer fork than s4) - long_fork_test_mining(global::cut_through_horizon() as u64 + 20, 5, &s[5]); - - let s5_header = s[5].chain.head().unwrap(); - let s1_header = s[1].chain.head().unwrap(); - let s1_tail = s[1].chain.tail().unwrap(); - println!( - "test case 4: s1 start syncing with s5. s1.head().height: {}, s1.tail().height: {}, s5.head().height: {}", - s1_header.height, s1_tail.height, - s5_header.height, - ); - s[1].resume(); - s[5].resume(); - - // Check server s1 can sync to s5 with a new txhashset download. - let mut total_wait = 0; - while s[1].head().height < s5_header.height { - thread::sleep(time::Duration::from_millis(1_000)); - total_wait += 1; - if total_wait >= 120 { - println!( - "test case 4: test fail on timeout! s1 height: {}, s5 height: {}", - s[1].head().height, - s5_header.height, - ); - exit(1); - } - } - let s1_tail_new = s[1].chain.tail().unwrap(); - println!( - "test case 4: s[1].tail().height: {}, old height: {}", - s1_tail_new.height, s1_tail.height - ); - assert_ne!(s1_tail_new.height, s1_tail.height); - assert_eq!(s[1].head().hash(), s5_header.hash()); - - s[1].pause(); - s[5].pause(); - - println!("test case 4 passed") -} - -fn long_fork_test_case_5(s: &[servers::Server]) { - println!("\ntest case 5 start"); - - let _ = s[1].chain.compact(); - - // Mine cut_through_horizon-10 blocks on s5 - long_fork_test_mining(global::cut_through_horizon() as u64 - 10, 5, &s[5]); - - let s5_header = s[5].chain.head().unwrap(); - let s1_header = s[1].chain.head().unwrap(); - let s1_tail = s[1].chain.tail().unwrap(); - println!( - "test case 5: s1 start syncing with s5. s1.head().height: {}, s1.tail().height: {}, s5.head().height: {}", - s1_header.height, s1_tail.height, - s5_header.height, - ); - s[1].resume(); - s[5].resume(); - - // Check server s1 can sync to s5 without a txhashset download (normal body sync) - let mut total_wait = 0; - while s[1].head().height < s5_header.height { - thread::sleep(time::Duration::from_millis(1_000)); - total_wait += 1; - if total_wait >= 120 { - println!( - "test case 5: test fail on timeout! s1 height: {}, s5 height: {}", - s[1].head().height, - s5_header.height, - ); - exit(1); - } - } - let s1_tail_new = s[1].chain.tail().unwrap(); - println!( - "test case 5: s[1].tail().height: {}, old height: {}", - s1_tail_new.height, s1_tail.height - ); - assert_eq!(s1_tail_new.height, s1_tail.height); - assert_eq!(s[1].head().hash(), s5_header.hash()); - - s[1].pause(); - s[5].pause(); - - println!("test case 5 passed") -} - -#[allow(dead_code)] -fn long_fork_test_case_6(s: &[servers::Server]) { - println!("\ntest case 6 start"); - - let _ = s[1].chain.compact(); - - // Mine cut_through_horizon+1 blocks on s5 - long_fork_test_mining(global::cut_through_horizon() as u64 + 1, 5, &s[5]); - - let s5_header = s[5].chain.head().unwrap(); - let s1_header = s[1].chain.head().unwrap(); - let s1_tail = s[1].chain.tail().unwrap(); - println!( - "test case 6: s1 start syncing with s5. s1.head().height: {}, s1.tail().height: {}, s5.head().height: {}", - s1_header.height, s1_tail.height, - s5_header.height, - ); - s[1].resume(); - s[5].resume(); - - // Check server s1 can sync to s5 without a txhashset download (normal body sync) - let mut total_wait = 0; - while s[1].head().height < s5_header.height { - thread::sleep(time::Duration::from_millis(1_000)); - total_wait += 1; - if total_wait >= 120 { - println!( - "test case 6: test fail on timeout! s1 height: {}, s5 height: {}", - s[1].head().height, - s5_header.height, - ); - exit(1); - } - } - let s1_tail_new = s[1].chain.tail().unwrap(); - println!( - "test case 6: s[1].tail().height: {}, old height: {}", - s1_tail_new.height, s1_tail.height - ); - assert_eq!(s1_tail_new.height, s1_tail.height); - assert_eq!(s[1].head().hash(), s5_header.hash()); - - s[1].pause(); - s[5].pause(); - - println!("test case 6 passed") -} - -pub fn create_wallet( - dir: &str, - client_n: HTTPNodeClient, -) -> Arc>> { - let mut wallet_config = WalletConfig::default(); - wallet_config.data_file_dir = String::from(dir); - let _ = wallet::WalletSeed::init_file(&wallet_config, 32, None, ""); - let mut wallet: LMDBBackend = - LMDBBackend::new(wallet_config.clone(), "", client_n).unwrap_or_else(|e| { - panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config) - }); - wallet.open_with_credentials().unwrap_or_else(|e| { - panic!( - "Error initializing wallet: {:?} Config: {:?}", - e, wallet_config - ) - }); - Arc::new(Mutex::new(wallet)) -} - -/// Intended to replicate https://github.com/mimblewimble/grin/issues/1325 -#[ignore] -#[test] -fn replicate_tx_fluff_failure() { - util::init_test_logger(); - global::set_local_chain_type(ChainTypes::UserTesting); - framework::clean_all_output("tx_fluff"); - - // Create Wallet 1 (Mining Input) and start it listening - // Wallet 1 post to another node, just for fun - let client1 = HTTPNodeClient::new("http://127.0.0.1:23003", None); - let client1_w = HTTPWalletCommAdapter::new(); - let wallet1 = create_wallet("target/tmp/tx_fluff/wallet1", client1.clone()); - let _wallet1_handle = thread::spawn(move || { - controller::foreign_listener(wallet1, "127.0.0.1:33000", None) - .unwrap_or_else(|e| panic!("Error creating wallet1 listener: {:?}", e,)); - }); - - // Create Wallet 2 (Recipient) and launch - let client2 = HTTPNodeClient::new("http://127.0.0.1:23001", None); - let wallet2 = create_wallet("target/tmp/tx_fluff/wallet2", client2.clone()); - let _wallet2_handle = thread::spawn(move || { - controller::foreign_listener(wallet2, "127.0.0.1:33001", None) - .unwrap_or_else(|e| panic!("Error creating wallet2 listener: {:?}", e,)); - }); - - // Server 1 (mines into wallet 1) - let mut s1_config = framework::config(3000, "tx_fluff", 3000); - s1_config.test_miner_wallet_url = Some("http://127.0.0.1:33000".to_owned()); - s1_config.dandelion_config.embargo_secs = Some(10); - s1_config.dandelion_config.patience_secs = Some(1); - s1_config.dandelion_config.relay_secs = Some(1); - let s1 = servers::Server::new(s1_config.clone()).unwrap(); - // Mine off of server 1 - s1.start_test_miner(s1_config.test_miner_wallet_url, s1.stop_state.clone()); - thread::sleep(time::Duration::from_secs(5)); - - // Server 2 (another node) - let mut s2_config = framework::config(3001, "tx_fluff", 3001); - s2_config.p2p_config.seeds = Some(vec![PeerAddr::from_ip("127.0.0.1:13000".parse().unwrap())]); - s2_config.dandelion_config.embargo_secs = Some(10); - s2_config.dandelion_config.patience_secs = Some(1); - s2_config.dandelion_config.relay_secs = Some(1); - let _s2 = servers::Server::new(s2_config.clone()).unwrap(); - - let dl_nodes = 5; - - for i in 0..dl_nodes { - // (create some stem nodes) - let mut s_config = framework::config(3002 + i, "tx_fluff", 3002 + i); - s_config.p2p_config.seeds = - Some(vec![PeerAddr::from_ip("127.0.0.1:13000".parse().unwrap())]); - s_config.dandelion_config.embargo_secs = Some(10); - s_config.dandelion_config.patience_secs = Some(1); - s_config.dandelion_config.relay_secs = Some(1); - let _ = servers::Server::new(s_config.clone()).unwrap(); - } - - thread::sleep(time::Duration::from_secs(10)); - - // get another instance of wallet1 (to update contents and perform a send) - let wallet1 = create_wallet("target/tmp/tx_fluff/wallet1", client1.clone()); - - let amount = 30_000_000_000; - let dest = "http://127.0.0.1:33001"; - - wallet::controller::owner_single_use(wallet1, |api| { - let (mut slate, lock_fn) = api.initiate_tx( - None, amount, // amount - 2, // minimum confirmations - 500, // max outputs - 1000, // num change outputs - true, // select all outputs - None, - )?; - slate = client1_w.send_tx_sync(dest, &slate)?; - slate = api.finalize_tx(&slate)?; - api.tx_lock_outputs(&slate, lock_fn)?; - api.post_tx(&slate.tx, false)?; - Ok(()) - }) - .unwrap(); - - // Give some time for propagation and mining - thread::sleep(time::Duration::from_secs(200)); - - // get another instance of wallet (to check contents) - let wallet2 = create_wallet("target/tmp/tx_fluff/wallet2", client2.clone()); - - wallet::controller::owner_single_use(wallet2, |api| { - let res = api.retrieve_summary_info(true, 1).unwrap(); - assert_eq!(res.1.amount_currently_spendable, amount); - Ok(()) - }) - .unwrap(); -} - -fn get_connected_peers( - base_addr: &String, - api_server_port: u16, -) -> Vec { - let url = format!( - "http://{}:{}/v1/peers/connected", - base_addr, api_server_port - ); - api::client::get::>(url.as_str(), None).unwrap() -} diff --git a/integration/tests/stratum.rs b/integration/tests/stratum.rs deleted file mode 100644 index ac19e790c..000000000 --- a/integration/tests/stratum.rs +++ /dev/null @@ -1,177 +0,0 @@ -// 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. - -#[macro_use] -extern crate log; - -mod framework; - -use self::core::global::{self, ChainTypes}; -use crate::framework::{config, stratum_config}; -use bufstream::BufStream; -use grin_core as core; -use grin_servers as servers; -use grin_util as util; -use grin_util::{Mutex, StopState}; -use serde_json::Value; -use std::io::prelude::{BufRead, Write}; -use std::net::TcpStream; -use std::process; -use std::sync::Arc; -use std::{thread, time}; - -// Create a grin server, and a stratum server. -// Simulate a few JSONRpc requests and verify the results. -// Validate disconnected workers -// Validate broadcasting new jobs -#[test] -fn basic_stratum_server() { - util::init_test_logger(); - global::set_local_chain_type(ChainTypes::AutomatedTesting); - - let test_name_dir = "stratum_server"; - framework::clean_all_output(test_name_dir); - - // Create a server - let s = servers::Server::new(config(4000, test_name_dir, 0)).unwrap(); - - // Get mining config with stratumserver enabled - let mut stratum_cfg = stratum_config(); - stratum_cfg.burn_reward = true; - stratum_cfg.attempt_time_per_block = 999; - stratum_cfg.enable_stratum_server = Some(true); - stratum_cfg.stratum_server_addr = Some(String::from("127.0.0.1:11101")); - - // Start stratum server - s.start_stratum_server(stratum_cfg); - - // Wait for stratum server to start and - // Verify stratum server accepts connections - loop { - if let Ok(_stream) = TcpStream::connect("127.0.0.1:11101") { - break; - } else { - thread::sleep(time::Duration::from_millis(500)); - } - // As this stream falls out of scope it will be disconnected - } - info!("stratum server connected"); - - // Create a few new worker connections - let mut workers = vec![]; - for _n in 0..5 { - let w = TcpStream::connect("127.0.0.1:11101").unwrap(); - w.set_nonblocking(true) - .expect("Failed to set TcpStream to non-blocking"); - let stream = BufStream::new(w); - workers.push(stream); - } - assert!(workers.len() == 5); - info!("workers length verification ok"); - - // Simulate a worker lost connection - workers.remove(4); - - // Swallow the genesis block - thread::sleep(time::Duration::from_secs(5)); // Wait for the server to broadcast - let mut response = String::new(); - for n in 0..workers.len() { - let _result = workers[n].read_line(&mut response); - } - - // Verify a few stratum JSONRpc commands - // getjobtemplate - expected block template result - let mut response = String::new(); - let job_req = "{\"id\": \"Stratum\", \"jsonrpc\": \"2.0\", \"method\": \"getjobtemplate\"}\n"; - workers[2].write(job_req.as_bytes()).unwrap(); - workers[2].flush().unwrap(); - thread::sleep(time::Duration::from_secs(1)); // Wait for the server to reply - match workers[2].read_line(&mut response) { - Ok(_) => { - let r: Value = serde_json::from_str(&response).unwrap(); - assert_eq!(r["error"], serde_json::Value::Null); - assert_ne!(r["result"], serde_json::Value::Null); - } - Err(_e) => { - assert!(false); - } - } - info!("a few stratum JSONRpc commands verification ok"); - - // keepalive - expected "ok" result - let mut response = String::new(); - let job_req = "{\"id\":\"3\",\"jsonrpc\":\"2.0\",\"method\":\"keepalive\"}\n"; - let ok_resp = "{\"id\":\"3\",\"jsonrpc\":\"2.0\",\"method\":\"keepalive\",\"result\":\"ok\",\"error\":null}\n"; - workers[2].write(job_req.as_bytes()).unwrap(); - workers[2].flush().unwrap(); - thread::sleep(time::Duration::from_secs(1)); // Wait for the server to reply - let _st = workers[2].read_line(&mut response); - assert_eq!(response.as_str(), ok_resp); - info!("keepalive test ok"); - - // "doesnotexist" - error expected - let mut response = String::new(); - let job_req = "{\"id\":\"4\",\"jsonrpc\":\"2.0\",\"method\":\"doesnotexist\"}\n"; - let ok_resp = "{\"id\":\"4\",\"jsonrpc\":\"2.0\",\"method\":\"doesnotexist\",\"result\":null,\"error\":{\"code\":-32601,\"message\":\"Method not found\"}}\n"; - workers[3].write(job_req.as_bytes()).unwrap(); - workers[3].flush().unwrap(); - thread::sleep(time::Duration::from_secs(1)); // Wait for the server to reply - let _st = workers[3].read_line(&mut response); - assert_eq!(response.as_str(), ok_resp); - info!("worker doesnotexist test ok"); - - // Verify stratum server and worker stats - let stats = s.get_server_stats().unwrap(); - assert_eq!(stats.stratum_stats.block_height, 1); // just 1 genesis block - assert_eq!(stats.stratum_stats.num_workers, 4); // 5 - 1 = 4 - assert_eq!(stats.stratum_stats.worker_stats[5].is_connected, false); // worker was removed - assert_eq!(stats.stratum_stats.worker_stats[1].is_connected, true); - info!("stratum server and worker stats verification ok"); - - // Start mining blocks - let stop = Arc::new(Mutex::new(StopState::new())); - s.start_test_miner(None, stop.clone()); - info!("test miner started"); - - // This test is supposed to complete in 3 seconds, - // so let's set a timeout on 10s to avoid infinite waiting happened in Travis-CI. - let _handler = thread::spawn(|| { - thread::sleep(time::Duration::from_secs(10)); - error!("basic_stratum_server test fail on timeout!"); - thread::sleep(time::Duration::from_millis(100)); - process::exit(1); - }); - - // Simulate a worker lost connection - workers.remove(1); - - // Wait for a few mined blocks - thread::sleep(time::Duration::from_secs(3)); - s.stop_test_miner(stop); - - // Verify blocks are being broadcast to workers - let expected = String::from("job"); - let mut jobtemplate = String::new(); - let _st = workers[2].read_line(&mut jobtemplate); - let job_template: Value = serde_json::from_str(&jobtemplate).unwrap(); - assert_eq!(job_template["method"], expected); - info!("blocks broadcasting to workers test ok"); - - // Verify stratum server and worker stats - let stats = s.get_server_stats().unwrap(); - assert_eq!(stats.stratum_stats.num_workers, 3); // 5 - 2 = 3 - assert_eq!(stats.stratum_stats.worker_stats[2].is_connected, false); // worker was removed - assert_ne!(stats.stratum_stats.block_height, 1); - info!("basic_stratum_server test done and ok."); -} diff --git a/integration/tests/wallet_node.rs b/integration/tests/wallet_node.rs new file mode 100644 index 000000000..da5a4c078 --- /dev/null +++ b/integration/tests/wallet_node.rs @@ -0,0 +1,191 @@ +// 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. + +//! Live node + wallet: mine coinbase into a foreign listener and read summary info. + +#[macro_use] +extern crate log; + +mod common; + +use crate::common::{ + clean_all_output, create_wallet, init_chain, node_config, settle, start_foreign_listener, + start_node, start_test_miner, +}; +use grin_core::consensus; +use grin_core::global; +use grin_util as util; +use grin_wallet_controller as controller; +use std::{thread, time}; + +/// Start a node, attach a foreign wallet for coinbase, mine, then assert balance. +#[test] +fn mine_to_wallet_and_summary() { + util::init_test_logger(); + init_chain(); + + let test_dir = "mine_to_wallet"; + clean_all_output(test_dir); + + // Node on ports derived from n=5000 → p2p 15000, api 25000 + let n = 5000u16; + let node = start_node(node_config(n, test_dir)); + let node_url = format!("http://127.0.0.1:{}", 20000 + n); + let wallet_port = 35000u16; + let wallet_addr = format!("127.0.0.1:{}", wallet_port); + let wallet_url = format!("http://{}", wallet_addr); + + let (wallet, mask) = create_wallet(test_dir, "coinbase", &node_url); + start_foreign_listener(wallet.clone(), mask.clone(), &wallet_addr); + + let miner_stop = start_test_miner(&node, Some(wallet_url)); + + // Mine until we have several coinbase outputs. + let mut waited = 0; + while node.head().unwrap().height < 5 { + thread::sleep(time::Duration::from_secs(1)); + waited += 1; + assert!(waited < 60, "node did not mine enough blocks"); + } + node.stop_test_miner(miner_stop); + thread::sleep(time::Duration::from_secs(2)); + + let height = node.head().unwrap().height; + info!("mined to height {}", height); + assert!(height >= 5); + + // Wallet should see coinbases (maturity rules still apply for spendable). + let mask_ref = mask.as_ref(); + controller::controller::owner_single_use(Some(wallet.clone()), mask_ref, None, |api, m| { + // Force a node refresh. + let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed, "wallet should refresh against live node"); + assert_eq!(info.last_confirmed_height, height); + // All coinbase from mining goes to this wallet. + assert_eq!(info.total, height * consensus::REWARD); + assert!( + info.amount_currently_spendable < info.total + || height > global::coinbase_maturity(), + "immature coinbase expected until maturity" + ); + Ok(()) + }) + .expect("owner summary"); + + node.stop(); + settle(); +} + +/// Two wallets against one node: mine to A, send slate to B via foreign API, post tx. +#[test] +fn wallet_send_receive_via_http() { + util::init_test_logger(); + init_chain(); + + let test_dir = "wallet_http_send"; + clean_all_output(test_dir); + + let n = 5100u16; + let node = start_node(node_config(n, test_dir)); + let node_url = format!("http://127.0.0.1:{}", 20000 + n); + + let sender_port = 35100u16; + let rec_port = 35101u16; + let sender_addr = format!("127.0.0.1:{}", sender_port); + let rec_addr = format!("127.0.0.1:{}", rec_port); + + let (wallet_a, mask_a) = create_wallet(test_dir, "wallet_a", &node_url); + let (wallet_b, mask_b) = create_wallet(test_dir, "wallet_b", &node_url); + + // Foreign on A for coinbase; foreign on B for receive. + start_foreign_listener(wallet_a.clone(), mask_a.clone(), &sender_addr); + start_foreign_listener(wallet_b.clone(), mask_b.clone(), &rec_addr); + + let miner_stop = start_test_miner(&node, Some(format!("http://{}", sender_addr))); + + // Need mature coinbase: AutomatedTesting maturity is 3 blocks. + let target = global::coinbase_maturity() + 3; + let mut waited = 0; + while node.head().unwrap().height < target { + thread::sleep(time::Duration::from_secs(1)); + waited += 1; + assert!(waited < 60, "timeout mining for mature coinbase"); + } + node.stop_test_miner(miner_stop); + thread::sleep(time::Duration::from_secs(2)); + + let amount = consensus::REWARD; + let mask_a_ref = mask_a.as_ref(); + let mask_b_clone = mask_b.clone(); + + controller::controller::owner_single_use(Some(wallet_a.clone()), mask_a_ref, None, |api, m| { + let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed); + assert!(info.amount_currently_spendable >= amount); + + use grin_wallet_libwallet::InitTxArgs; + let args = InitTxArgs { + src_acct_name: None, + amount, + minimum_confirmations: 1, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + ..Default::default() + }; + let mut slate = api.init_send_tx(m, args)?; + api.tx_lock_outputs(m, &slate)?; + + // Receive on B (in-process foreign; same process as HTTP listener but direct call). + controller::controller::foreign_single_use(wallet_b.clone(), mask_b_clone.clone(), |fapi| { + slate = fapi.receive_tx(&slate, None, None)?; + Ok(()) + })?; + + slate = api.finalize_tx(m, &slate)?; + api.post_tx(m, &slate, false)?; + Ok(()) + }) + .expect("send/receive"); + + // Mine a confirmation block (burn rewards — tx already in pool/chain via post_tx). + let miner_stop = start_test_miner(&node, None); + let h0 = node.head().unwrap().height; + let mut waited = 0; + while node.head().unwrap().height <= h0 { + thread::sleep(time::Duration::from_secs(1)); + waited += 1; + assert!(waited < 30, "timeout mining confirmation"); + } + node.stop_test_miner(miner_stop); + thread::sleep(time::Duration::from_secs(2)); + + // B should hold the received amount (may still be immature depending on path). + let mask_b_ref = mask_b.as_ref(); + controller::controller::owner_single_use(Some(wallet_b.clone()), mask_b_ref, None, |api, m| { + let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed); + assert!( + info.total >= amount, + "receiver total {} < {}", + info.total, + amount + ); + Ok(()) + }) + .expect("receiver summary"); + + node.stop(); + settle(); +}