diff --git a/.gitignore b/.gitignore index 08666063a4..241c3a428f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ target .vscode/ *.code-workspace tags +null # Ignore OS files .DS_Store diff --git a/Cargo.lock b/Cargo.lock index da40b76979..62b5681df7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1308,16 +1308,16 @@ dependencies = [ [[package]] name = "crossterm" -version = "0.25.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "crossterm_winapi 0.9.1", "futures-core", - "libc", - "mio 0.8.11", + "mio 1.0.3", "parking_lot 0.12.3", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -3906,7 +3906,7 @@ dependencies = [ [[package]] name = "minotari_app_grpc" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "argon2 0.4.1", "base64 0.13.1", @@ -3935,7 +3935,7 @@ dependencies = [ [[package]] name = "minotari_app_utilities" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "clap 3.2.25", "dialoguer 0.10.4", @@ -3957,7 +3957,7 @@ dependencies = [ [[package]] name = "minotari_chat_ffi" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "cbindgen", "chrono", @@ -3982,14 +3982,14 @@ dependencies = [ [[package]] name = "minotari_console_wallet" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "blake2", "chrono", "clap 3.2.25", "config", "console-subscriber", - "crossterm 0.25.0", + "crossterm 0.28.1", "digest 0.10.7", "dirs-next 2.0.0", "futures 0.3.31", @@ -4038,14 +4038,14 @@ dependencies = [ [[package]] name = "minotari_ledger_wallet_common" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "bs58 0.5.1", ] [[package]] name = "minotari_ledger_wallet_comms" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "borsh", "dialoguer 0.11.0", @@ -4067,7 +4067,7 @@ dependencies = [ [[package]] name = "minotari_merge_mining_proxy" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "anyhow", "bincode", @@ -4076,7 +4076,7 @@ dependencies = [ "chrono", "clap 3.2.25", "config", - "crossterm 0.25.0", + "crossterm 0.28.1", "futures 0.3.31", "hex", "hyper 0.14.31", @@ -4088,6 +4088,7 @@ dependencies = [ "minotari_node_grpc_client", "minotari_wallet_grpc_client", "monero", + "regex", "reqwest", "scraper", "serde", @@ -4101,6 +4102,7 @@ dependencies = [ "tari_utilities", "thiserror 1.0.69", "tokio", + "toml 0.8.19", "tonic 0.12.3", "tracing", "url", @@ -4108,7 +4110,7 @@ dependencies = [ [[package]] name = "minotari_miner" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "base64 0.13.1", "borsh", @@ -4117,7 +4119,7 @@ dependencies = [ "clap 3.2.25", "config", "crossbeam", - "crossterm 0.25.0", + "crossterm 0.28.1", "derivative", "futures 0.3.31", "hex", @@ -4144,7 +4146,7 @@ dependencies = [ [[package]] name = "minotari_mining_helper_ffi" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "borsh", "cbindgen", @@ -4163,7 +4165,7 @@ dependencies = [ [[package]] name = "minotari_node" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "anyhow", "async-trait", @@ -4173,7 +4175,7 @@ dependencies = [ "clap 3.2.25", "config", "console-subscriber", - "crossterm 0.25.0", + "crossterm 0.28.1", "derive_more 0.99.18", "either", "futures 0.3.31", @@ -4217,7 +4219,7 @@ dependencies = [ [[package]] name = "minotari_wallet" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "argon2 0.4.1", "async-trait", @@ -4268,7 +4270,7 @@ dependencies = [ [[package]] name = "minotari_wallet_ffi" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "borsh", "cbindgen", @@ -4331,18 +4333,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - [[package]] name = "mio" version = "1.0.3" @@ -4350,6 +4340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.52.0", ] @@ -4376,6 +4367,7 @@ dependencies = [ "hex-literal 0.4.1", "sealed", "serde", + "serde-big-array", "thiserror 1.0.69", "tiny-keccak", ] @@ -4700,9 +4692,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.68" +version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -4741,9 +4733,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.104" +version = "0.9.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" dependencies = [ "cc", "libc", @@ -6239,6 +6231,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde-value" version = "0.7.0" @@ -6449,7 +6450,7 @@ checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio 0.7.14", - "mio 0.8.11", + "mio 1.0.3", "signal-hook", ] @@ -6852,7 +6853,7 @@ dependencies = [ [[package]] name = "tari_chat_client" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "anyhow", "async-trait", @@ -6879,7 +6880,7 @@ dependencies = [ [[package]] name = "tari_common" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "anyhow", "config", @@ -6904,7 +6905,7 @@ dependencies = [ [[package]] name = "tari_common_sqlite" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "diesel", "diesel_migrations", @@ -6918,7 +6919,7 @@ dependencies = [ [[package]] name = "tari_common_types" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "base64 0.21.7", "bitflags 2.6.0", @@ -6943,7 +6944,7 @@ dependencies = [ [[package]] name = "tari_comms" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "anyhow", "async-trait", @@ -6992,7 +6993,7 @@ dependencies = [ [[package]] name = "tari_comms_dht" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "anyhow", "bitflags 2.6.0", @@ -7035,7 +7036,7 @@ dependencies = [ [[package]] name = "tari_comms_rpc_macros" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "futures 0.3.31", "proc-macro2", @@ -7050,7 +7051,7 @@ dependencies = [ [[package]] name = "tari_contacts" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "chrono", "diesel", @@ -7084,7 +7085,7 @@ dependencies = [ [[package]] name = "tari_core" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "anyhow", "async-trait", @@ -7187,11 +7188,11 @@ dependencies = [ [[package]] name = "tari_features" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" [[package]] name = "tari_hashing" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "blake2", "borsh", @@ -7248,7 +7249,7 @@ dependencies = [ [[package]] name = "tari_key_manager" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "argon2 0.4.1", "async-trait", @@ -7283,7 +7284,7 @@ dependencies = [ [[package]] name = "tari_libtor" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "derivative", "libtor", @@ -7291,13 +7292,12 @@ dependencies = [ "rand", "tari_common", "tari_p2p", - "tempfile", "tor-hash-passwd", ] [[package]] name = "tari_max_size" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "borsh", "serde", @@ -7307,7 +7307,7 @@ dependencies = [ [[package]] name = "tari_metrics" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "anyhow", "futures 0.3.31", @@ -7322,7 +7322,7 @@ dependencies = [ [[package]] name = "tari_mmr" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "bincode", "blake2", @@ -7340,7 +7340,7 @@ dependencies = [ [[package]] name = "tari_p2p" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "anyhow", "clap 3.2.25", @@ -7378,7 +7378,7 @@ dependencies = [ [[package]] name = "tari_script" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "blake2", "borsh", @@ -7396,7 +7396,7 @@ dependencies = [ [[package]] name = "tari_service_framework" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "anyhow", "async-trait", @@ -7413,7 +7413,7 @@ dependencies = [ [[package]] name = "tari_shutdown" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "futures 0.3.31", "tokio", @@ -7421,7 +7421,7 @@ dependencies = [ [[package]] name = "tari_storage" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "bincode", "lmdb-zero", @@ -7434,7 +7434,7 @@ dependencies = [ [[package]] name = "tari_test_utils" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" dependencies = [ "futures 0.3.31", "futures-test", diff --git a/README.md b/README.md index 4bdc713376..f7a1dd2c44 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ The recommended running versions of each network are: | Network | Version | |-----------|----------------| | Stagenet | 1.0.0-alpha.0a | -| Nextnet | 1.11.1-rc.1 | -| Esmeralda | 1.11.1-pre.1 | +| Nextnet | 1.11.2-rc.0 | +| Esmeralda | 1.11.2-pre.0 | For more detail about versioning, see [Release Ideology](https://github.com/tari-project/tari/blob/development/docs/src/branching_releases.md). diff --git a/applications/minotari_app_grpc/Cargo.toml b/applications/minotari_app_grpc/Cargo.toml index 780eb4d72c..3202e1207a 100644 --- a/applications/minotari_app_grpc/Cargo.toml +++ b/applications/minotari_app_grpc/Cargo.toml @@ -4,7 +4,7 @@ authors = ["The Tari Development Community"] description = "This crate is to provide a single source for all cross application grpc files and conversions to and from tari::core" repository = "https://github.com/tari-project/tari" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2021" [dependencies] @@ -32,7 +32,7 @@ tonic = { version = "0.12.3", features = ["tls"] } zeroize = "1" [build-dependencies] -tari_features = { path = "../../common/tari_features", version = "1.11.1-pre.1" } +tari_features = { path = "../../common/tari_features", version = "1.11.3-pre.0" } tonic-build = "0.12.3" [package.metadata.cargo-machete] diff --git a/applications/minotari_app_utilities/Cargo.toml b/applications/minotari_app_utilities/Cargo.toml index e34717799c..0545879e97 100644 --- a/applications/minotari_app_utilities/Cargo.toml +++ b/applications/minotari_app_utilities/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "minotari_app_utilities" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" authors = ["The Tari Development Community"] edition = "2018" license = "BSD-3-Clause" @@ -30,7 +30,7 @@ tari_common = { path = "../../common", features = [ "build", "static-application-info", ] } -tari_features = { path = "../../common/tari_features", version = "1.11.1-pre.1" } +tari_features = { path = "../../common/tari_features", version = "1.11.3-pre.0" } [features] miner_input = ["minotari_app_grpc"] diff --git a/applications/minotari_console_wallet/Cargo.toml b/applications/minotari_console_wallet/Cargo.toml index cdabf7e2ea..81d15d1338 100644 --- a/applications/minotari_console_wallet/Cargo.toml +++ b/applications/minotari_console_wallet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "minotari_console_wallet" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" authors = ["The Tari Development Community"] edition = "2018" license = "BSD-3-Clause" @@ -8,7 +8,7 @@ license = "BSD-3-Clause" [dependencies] minotari_app_grpc = { path = "../minotari_app_grpc" } minotari_app_utilities = { path = "../minotari_app_utilities" } -minotari_ledger_wallet_comms = { path = "../../applications/minotari_ledger_wallet/comms", version = "1.11.1-pre.1", optional = true } +minotari_ledger_wallet_comms = { path = "../../applications/minotari_ledger_wallet/comms", version = "1.11.3-pre.0", optional = true } tari_common = { path = "../../common" } tari_common_types = { path = "../../base_layer/common_types" } tari_comms = { path = "../../comms/core" } @@ -37,7 +37,7 @@ blake2 = "0.10" chrono = { version = "0.4.39", default-features = false } clap = { version = "3.2", features = ["derive", "env"] } config = "0.14.0" -crossterm = { version = "0.25.0" } +crossterm = { version = "0.28" } digest = "0.10" dirs-next = "2.0" futures = { version = "^0.3.16", default-features = false, features = [ @@ -84,7 +84,7 @@ default-features = false features = ["crossterm"] [build-dependencies] -tari_features = { path = "../../common/tari_features", version = "1.11.1-pre.1" } +tari_features = { path = "../../common/tari_features", version = "1.11.3-pre.0" } [features] default = ["libtor", "ledger"] diff --git a/applications/minotari_console_wallet/src/cli.rs b/applications/minotari_console_wallet/src/cli.rs index fd81288c88..45be632dcf 100644 --- a/applications/minotari_console_wallet/src/cli.rs +++ b/applications/minotari_console_wallet/src/cli.rs @@ -94,6 +94,9 @@ pub struct Cli { pub view_private_key: Option, #[clap(long)] pub spend_key: Option, + /// Path to the libtor data directory + #[clap(short, long, parse(from_os_str))] + pub libtor_data_dir: Option, } impl ConfigOverrideProvider for Cli { diff --git a/applications/minotari_console_wallet/src/lib.rs b/applications/minotari_console_wallet/src/lib.rs index ce4aa9722e..4eaca951cf 100644 --- a/applications/minotari_console_wallet/src/lib.rs +++ b/applications/minotari_console_wallet/src/lib.rs @@ -33,7 +33,6 @@ mod recovery; mod ui; mod utils; mod wallet_modes; - pub use cli::{ BurnMinotariArgs, Cli, @@ -108,6 +107,7 @@ pub fn run_wallet(shutdown: &mut Shutdown, runtime: Runtime, config: &mut Applic profile_with_tokio_console: false, view_private_key: None, spend_key: None, + libtor_data_dir: None, }; run_wallet_with_cli(shutdown, runtime, config, cli) @@ -165,7 +165,12 @@ pub fn run_wallet_with_cli( // This is currently only possible on linux/macos #[cfg(all(unix, feature = "libtor"))] if config.wallet.use_libtor && config.wallet.p2p.transport.is_tor() { - let tor = Tor::initialize()?; + let data_dir = if let Some(dir) = cli.libtor_data_dir.clone() { + dir.join("libtor").join("wallet") + } else { + cli.common.get_base_path().join("libtor").join("wallet") + }; + let tor = Tor::initialize(data_dir)?; tor.update_comms_transport(&mut config.wallet.p2p.transport)?; tor.run_background(); debug!( @@ -265,6 +270,7 @@ pub fn run_wallet_with_cli( print!("\nShutting down wallet... "); shutdown.trigger(); runtime.block_on(wallet.wait_until_shutdown()); + println!("Done."); result diff --git a/applications/minotari_console_wallet/src/ui/app.rs b/applications/minotari_console_wallet/src/ui/app.rs index ce6dc2ee57..e853f04467 100644 --- a/applications/minotari_console_wallet/src/ui/app.rs +++ b/applications/minotari_console_wallet/src/ui/app.rs @@ -20,6 +20,7 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use log::trace; use minotari_wallet::{error::WalletError, util::wallet_identity::WalletIdentity, WalletConfig, WalletSqlite}; use tari_common::exit_codes::{ExitCode, ExitError}; use tari_comms::peer_manager::Peer; @@ -136,6 +137,7 @@ impl App { } pub fn on_control_key(&mut self, c: char) { + trace!(target: LOG_TARGET, "on_control_key: {}", c); match c { 'q' | 'c' => { self.should_quit = true; @@ -145,6 +147,7 @@ impl App { } pub fn on_key(&mut self, c: char) { + trace!(target: LOG_TARGET, "on_key: {}", c); match c { '\t' => { self.tabs.next(); @@ -154,34 +157,42 @@ impl App { } pub fn on_backtab(&mut self) { + trace!(target: LOG_TARGET, "on_backtab"); self.tabs.previous(); } pub fn on_up(&mut self) { + trace!(target: LOG_TARGET, "on_up"); self.tabs.on_up(&mut self.app_state); } pub fn on_down(&mut self) { + trace!(target: LOG_TARGET, "on_down"); self.tabs.on_down(&mut self.app_state); } pub fn on_f10(&mut self) { + trace!(target: LOG_TARGET, "on_f10"); self.should_quit = true; } pub fn on_right(&mut self) { + trace!(target: LOG_TARGET, "on_right"); self.tabs.next(); } pub fn on_left(&mut self) { + trace!(target: LOG_TARGET, "on_left"); self.tabs.previous(); } pub fn on_esc(&mut self) { + trace!(target: LOG_TARGET, "on_esc"); self.tabs.on_esc(&mut self.app_state); } pub fn on_backspace(&mut self) { + trace!(target: LOG_TARGET, "on_backspace"); self.tabs.on_backspace(&mut self.app_state); } diff --git a/applications/minotari_console_wallet/src/ui/mod.rs b/applications/minotari_console_wallet/src/ui/mod.rs index a1b343e6d6..999bc5f38b 100644 --- a/applications/minotari_console_wallet/src/ui/mod.rs +++ b/applications/minotari_console_wallet/src/ui/mod.rs @@ -38,7 +38,7 @@ use std::io::{stdout, Stdout}; pub use app::*; use crossterm::{ - event::{KeyCode, KeyModifiers}, + event::{KeyCode, KeyEventState, KeyModifiers}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; @@ -74,7 +74,9 @@ pub fn run(app: App>) -> Result<(), ExitError> { .map_err(|e| ExitError::new(ExitCode::WalletError, e))?; crossterm_loop(app) } + /// This is the main loop of the application UI using Crossterm based events +#[allow(clippy::too_many_lines)] fn crossterm_loop(mut app: App>) -> Result<(), ExitError> { let events = CrosstermEvents::new(); enable_raw_mode().map_err(|e| { @@ -108,25 +110,52 @@ fn crossterm_loop(mut app: App>) -> Result<(), ExitErro error!(target: LOG_TARGET, "Error drawing interface. {}", e); ExitCode::InterfaceError })?; + let event = events.next(); #[allow(clippy::blocks_in_conditions)] - match events.next().map_err(|e| { + match event.map_err(|e| { error!(target: LOG_TARGET, "Error reading input event: {}", e); ExitCode::InterfaceError })? { - Event::Input(event) => match (event.code, event.modifiers) { - (KeyCode::Char(c), KeyModifiers::CONTROL) => app.on_control_key(c), - (KeyCode::Char(c), _) => app.on_key(c), - (KeyCode::Left, _) => app.on_left(), - (KeyCode::Up, _) => app.on_up(), - (KeyCode::Right, _) => app.on_right(), - (KeyCode::Down, _) => app.on_down(), - (KeyCode::Esc, _) => app.on_esc(), - (KeyCode::Backspace, _) => app.on_backspace(), - (KeyCode::Enter, _) => app.on_key('\n'), - (KeyCode::Tab, _) => app.on_key('\t'), - (KeyCode::BackTab, _) => app.on_backtab(), - (KeyCode::F(10), _) => app.on_f10(), - _ => {}, + Event::Input(event) => { + trace!(target: LOG_TARGET, "event: '{:?}' '{}' '{:?}' '{}'", + event.code, + event.modifiers, + event.kind, + match event.state { + KeyEventState::KEYPAD => "KEYPAD", + KeyEventState::CAPS_LOCK => "CAPS_LOCK", + KeyEventState::NUM_LOCK => "NUM_LOCK", + _ => "NONE", + } + ); + #[cfg(target_os = "windows")] + let action_now = { + use crossterm::event::KeyEventKind; + match (event.kind, event.modifiers, event.code) { + (KeyEventKind::Press, KeyModifiers::CONTROL, KeyCode::Char(c)) => c == 'q' || c == 'c', + (KeyEventKind::Press, _, KeyCode::F(c)) => c == 10, + (KeyEventKind::Release, _, _) => true, + (..) => false, + } + }; + #[cfg(not(target_os = "windows"))] + let action_now = true; + match (event.code, event.modifiers, action_now) { + (_, _, false) => {}, + (KeyCode::Char(c), KeyModifiers::CONTROL, _) => app.on_control_key(c), + (KeyCode::Char(c), _, _) => app.on_key(c), + (KeyCode::Left, _, _) => app.on_left(), + (KeyCode::Up, _, _) => app.on_up(), + (KeyCode::Right, _, _) => app.on_right(), + (KeyCode::Down, _, _) => app.on_down(), + (KeyCode::Esc, _, _) => app.on_esc(), + (KeyCode::Backspace, _, _) => app.on_backspace(), + (KeyCode::Enter, _, _) => app.on_key('\n'), + (KeyCode::Tab, _, _) => app.on_key('\t'), + (KeyCode::BackTab, _, _) => app.on_backtab(), + (KeyCode::F(10), _, _) => app.on_f10(), + _ => {}, + } }, Event::Tick => { app.on_tick(); diff --git a/applications/minotari_ledger_wallet/common/Cargo.toml b/applications/minotari_ledger_wallet/common/Cargo.toml index 55ae227a3e..9546c4af63 100644 --- a/applications/minotari_ledger_wallet/common/Cargo.toml +++ b/applications/minotari_ledger_wallet/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "minotari_ledger_wallet_common" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" authors = ["The Tari Development Community"] license = "BSD-3-Clause" edition = "2021" diff --git a/applications/minotari_ledger_wallet/comms/Cargo.toml b/applications/minotari_ledger_wallet/comms/Cargo.toml index 97d2df550f..992dda2244 100644 --- a/applications/minotari_ledger_wallet/comms/Cargo.toml +++ b/applications/minotari_ledger_wallet/comms/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "minotari_ledger_wallet_comms" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" authors = ["The Tari Development Community"] license = "BSD-3-Clause" edition = "2021" diff --git a/applications/minotari_ledger_wallet/wallet/Cargo.toml b/applications/minotari_ledger_wallet/wallet/Cargo.toml index 9561a51fb5..3b684a2446 100644 --- a/applications/minotari_ledger_wallet/wallet/Cargo.toml +++ b/applications/minotari_ledger_wallet/wallet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "minotari_ledger_wallet" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" authors = ["The Tari Development Community"] license = "BSD-3-Clause" edition = "2021" @@ -9,7 +9,7 @@ edition = "2021" tari_crypto = { version = "0.22.0", default-features = false, features = [ "borsh", ] } -tari_hashing = { path = "../../../hashing", version = "1.11.1-pre.1" } +tari_hashing = { path = "../../../hashing", version = "1.11.3-pre.0" } minotari_ledger_wallet_common = { path = "../common" } diff --git a/applications/minotari_merge_mining_proxy/Cargo.toml b/applications/minotari_merge_mining_proxy/Cargo.toml index 9315b01337..1cc589dbd6 100644 --- a/applications/minotari_merge_mining_proxy/Cargo.toml +++ b/applications/minotari_merge_mining_proxy/Cargo.toml @@ -4,7 +4,7 @@ authors = ["The Tari Development Community"] description = "The Tari merge mining proxy for xmrig" repository = "https://github.com/tari-project/tari" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" [features] @@ -33,14 +33,14 @@ bytes = "1.1" chrono = { version = "0.4.39", default-features = false } clap = { version = "3.2", features = ["derive", "env"] } config = { version = "0.14.0" } -crossterm = { version = "0.25.0" } +crossterm = { version = "0.28" } futures = { version = "^0.3.16", features = ["async-await"] } hex = "0.4.2" hyper = { version = "0.14.12", features = ["default"] } jsonrpc = "0.12.0" log = { version = "0.4.8", features = ["std"] } markup5ever = "0.12.1" -monero = { version = "0.21.0" } +monero = { version = "0.21.0" , features = ["serde"] } reqwest = { version = "0.11.4", features = ["json"] } serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.57" @@ -50,9 +50,11 @@ tonic = "0.12.3" tracing = "0.1" url = "2.1.1" scraper = "0.19.0" +toml = "0.8.19" +regex = "1.11.1" [build-dependencies] -tari_features = { path = "../../common/tari_features", version = "1.11.1-pre.1" } +tari_features = { path = "../../common/tari_features", version = "1.11.3-pre.0" } [dev-dependencies] hyper = { version = "0.14.12", features = ["full"] } diff --git a/applications/minotari_merge_mining_proxy/src/block_template_protocol.rs b/applications/minotari_merge_mining_proxy/src/block_template_protocol.rs index e44f6a6078..a46e2a2fd7 100644 --- a/applications/minotari_merge_mining_proxy/src/block_template_protocol.rs +++ b/applications/minotari_merge_mining_proxy/src/block_template_protocol.rs @@ -83,7 +83,7 @@ impl<'a> BlockTemplateProtocol<'a> { #[allow(clippy::too_many_lines)] impl BlockTemplateProtocol<'_> { /// Create [FinalBlockTemplateData] with [MoneroMiningData]. - pub async fn get_next_block_template( + pub async fn get_next_tari_block_template( mut self, monero_mining_data: MoneroMiningData, block_templates: &BlockTemplateRepository, diff --git a/applications/minotari_merge_mining_proxy/src/config.rs b/applications/minotari_merge_mining_proxy/src/config.rs index 3e9268e752..e217f1c47a 100644 --- a/applications/minotari_merge_mining_proxy/src/config.rs +++ b/applications/minotari_merge_mining_proxy/src/config.rs @@ -20,12 +20,15 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + time::Duration, +}; use minotari_wallet_grpc_client::GrpcAuthentication; use serde::{Deserialize, Serialize}; use tari_common::{ - configuration::{Network, StringList}, + configuration::{serializers, Network, StringList}, SubConfigPath, }; use tari_common_types::tari_address::TariAddress; @@ -94,8 +97,22 @@ pub struct MergeMiningProxyConfig { pub wallet_payment_address: String, /// Range proof type - revealed_value or bullet_proof_plus: (default = revealed_value) pub range_proof_type: RangeProofType, - /// Use p2pool to submit and get block templates + /// Use p2pool to submit and get block templates (default = false) pub p2pool_enabled: bool, + /// Monero fallback strategy(default = MonerodFallback::MonerodOnly) + pub monerod_fallback: MonerodFallback, + /// The timeout duration for connecting to monerod (default = 2s) + #[serde(with = "serializers::seconds")] + pub monerod_connection_timeout: Duration, +} + +#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub(crate) enum MonerodFallback { + MonerodOnly, + #[default] + StaticWhenMonerodFails, + StaticOnly, } impl Default for MergeMiningProxyConfig { @@ -157,6 +174,8 @@ impl Default for MergeMiningProxyConfig { wallet_payment_address: TariAddress::default().to_base58(), range_proof_type: RangeProofType::RevealedValue, p2pool_enabled: false, + monerod_fallback: Default::default(), + monerod_connection_timeout: Duration::from_secs(2), } } } @@ -179,10 +198,11 @@ impl SubConfigPath for MergeMiningProxyConfig { mod test { use std::str::FromStr; + use serde::{Deserialize, Serialize}; use tari_common::DefaultConfigLoader; use tari_comms::multiaddr::Multiaddr; - use crate::config::MergeMiningProxyConfig; + use crate::config::{MergeMiningProxyConfig, MonerodFallback}; fn get_config(override_from: &str) -> config::Config { let s = r#" @@ -241,4 +261,35 @@ mod test { assert!(!config.monerod_use_auth); assert!(config.submit_to_origin); } + + #[derive(Clone, Serialize, Deserialize, Debug)] + #[allow(clippy::struct_excessive_bools)] + struct TestConfig { + name: String, + inner_config_1: TestInnerConfig, + inner_config_2: TestInnerConfig, + inner_config_3: TestInnerConfig, + } + + #[derive(Clone, Serialize, Deserialize, Debug)] + #[allow(clippy::struct_excessive_bools)] + struct TestInnerConfig { + monerod: MonerodFallback, + } + + #[test] + fn it_deserializes_enums() { + let config_str = r#" + name = "blockchain champion" + inner_config_1.monerod = "monerod_only" + inner_config_2.monerod = "static_when_monerod_fails" + inner_config_3.monerod = "static_only" + "#; + let config = toml::from_str::(config_str).unwrap(); + + // Enums in the config + assert_eq!(config.inner_config_1.monerod, MonerodFallback::MonerodOnly); + assert_eq!(config.inner_config_2.monerod, MonerodFallback::StaticWhenMonerodFails); + assert_eq!(config.inner_config_3.monerod, MonerodFallback::StaticOnly); + } } diff --git a/applications/minotari_merge_mining_proxy/src/proxy.rs b/applications/minotari_merge_mining_proxy/src/proxy/inner.rs similarity index 56% rename from applications/minotari_merge_mining_proxy/src/proxy.rs rename to applications/minotari_merge_mining_proxy/src/proxy/inner.rs index 00b3b68252..1f4c98ab02 100644 --- a/applications/minotari_merge_mining_proxy/src/proxy.rs +++ b/applications/minotari_merge_mining_proxy/src/proxy/inner.rs @@ -23,27 +23,24 @@ use std::{ cmp, convert::TryInto, - future::Future, - pin::Pin, + str::FromStr, sync::{ atomic::{AtomicBool, Ordering}, Arc, RwLock, }, - task::{Context, Poll}, - time::{Duration, Instant}, + time::Instant, }; use borsh::BorshSerialize; use bytes::Bytes; -use hyper::{header::HeaderValue, service::Service, Body, Method, Request, Response, StatusCode, Uri}; -use json::json; -use jsonrpc::error::StandardError; -use minotari_app_grpc::tari_rpc::SubmitBlockRequest; +use hyper::{header::HeaderValue, Body, Request, Response, StatusCode, Uri}; +use log::error; +use minotari_app_grpc::{tari_rpc, tari_rpc::SubmitBlockRequest}; use minotari_app_utilities::parse_miner_input::{BaseNodeGrpcClient, ShaP2PoolGrpcClient}; -use minotari_node_grpc_client::grpc; -use reqwest::{ResponseBuilderExt, Url}; +use monero::Hash; use serde_json as json; +use serde_json::json; use tari_common_types::tari_address::TariAddress; use tari_core::{ consensus::ConsensusManager, @@ -51,132 +48,60 @@ use tari_core::{ }; use tari_utilities::hex::Hex; use tokio::time::timeout; -use tracing::{debug, error, info, trace, warn}; +use tracing::{debug, info, trace, warn}; +use url::Url; use crate::{ block_template_data::BlockTemplateRepository, block_template_protocol::{BlockTemplateProtocol, MoneroMiningData}, common::{json_rpc, monero_rpc::CoreRpcErrorCode, proxy, proxy::convert_json_to_hyper_json_response}, - config::MergeMiningProxyConfig, + config::{MergeMiningProxyConfig, MonerodFallback}, error::MmProxyError, + proxy::{ + monerod_method::{parse_monerod_rpc_method, MonerodMethod}, + static_responses::{ + convert_static_monerod_response_to_hyper_response, + self_select_submit_block_monerod_response, + static_json_rpc_url, + }, + utils::{convert_reqwest_response_to_hyper_json_response, request_bytes_to_value}, + }, }; -const LOG_TARGET: &str = "minotari_mm_proxy::proxy"; -/// The JSON object key name used for merge mining proxy response extensions -pub(crate) const MMPROXY_AUX_KEY_NAME: &str = "_aux"; +const LOG_TARGET: &str = "minotari_mm_proxy::proxy::inner"; /// The identifier used to identify the tari aux chain data const TARI_CHAIN_ID: &str = "xtr"; -/// The timeout duration for connecting to monerod -pub(crate) const MONEROD_CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); -pub(crate) const NUMBER_OF_MONEROD_SERVERS: usize = 15; +const BUSY_QUALIFYING: &str = "BusyQualifyingMonerodUrl"; #[derive(Debug, Clone)] -pub struct MergeMiningProxyService { - inner: InnerService, -} - -impl MergeMiningProxyService { - pub fn new( - config: MergeMiningProxyConfig, - http_client: reqwest::Client, - base_node_client: BaseNodeGrpcClient, - p2pool_client: Option, - block_templates: BlockTemplateRepository, - randomx_factory: RandomXFactory, - wallet_payment_address: TariAddress, - ) -> Result { - trace!(target: LOG_TARGET, "Config: {:?}", config); - let consensus_manager = ConsensusManager::builder(config.network).build()?; - Ok(Self { - inner: InnerService { - config: Arc::new(config), - block_templates, - http_client, - base_node_client, - p2pool_client, - initial_sync_achieved: Arc::new(AtomicBool::new(false)), - current_monerod_server: Arc::new(RwLock::new(None)), - last_assigned_monerod_url: Arc::new(RwLock::new(None)), - randomx_factory, - consensus_manager, - wallet_payment_address, - }, - }) - } -} - -#[allow(clippy::type_complexity)] -impl Service> for MergeMiningProxyService { - type Error = hyper::Error; - type Future = Pin> + Send>>; - type Response = Response; - - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, mut request: Request) -> Self::Future { - let inner = self.inner.clone(); - let future = async move { - let bytes = match proxy::read_body_until_end(request.body_mut()).await { - Ok(b) => b, - Err(err) => { - warn!(target: LOG_TARGET, "Method: Unknown, Failed to read request: {:?}", err); - let resp = proxy::json_response( - StatusCode::BAD_REQUEST, - &json_rpc::standard_error_response( - None, - StandardError::InvalidRequest, - Some(json!({"details": err.to_string()})), - ), - ) - .expect("unexpected failure"); - return Ok(resp); - }, - }; - let request = request.map(|_| bytes.freeze()); - let method_name = parse_method_name(&request); - match inner.handle(&method_name, request).await { - Ok(resp) => Ok(resp), - Err(err) => { - error!(target: LOG_TARGET, "Method \"{}\" failed handling request: {:?}", method_name, err); - Ok(proxy::json_response( - StatusCode::INTERNAL_SERVER_ERROR, - &json_rpc::standard_error_response( - None, - StandardError::InternalError, - Some(json!({"details": err.to_string()})), - ), - ) - .expect("unexpected failure")) - }, - } - }; - - Box::pin(future) - } +pub struct InnerService { + pub(crate) config: Arc, + pub(crate) block_templates: BlockTemplateRepository, + pub(crate) http_client: reqwest::Client, + pub(crate) base_node_client: BaseNodeGrpcClient, + pub(crate) p2pool_client: Option, + pub(crate) initial_sync_achieved: Arc, + pub(crate) current_monerod_server: Arc>>, + pub(crate) last_assigned_monerod_url: Arc>>, + pub(crate) monerod_cache_values: Arc>>, + pub(crate) randomx_factory: RandomXFactory, + pub(crate) consensus_manager: ConsensusManager, + pub(crate) wallet_payment_address: TariAddress, } -#[derive(Debug, Clone)] -struct InnerService { - config: Arc, - block_templates: BlockTemplateRepository, - http_client: reqwest::Client, - base_node_client: BaseNodeGrpcClient, - p2pool_client: Option, - initial_sync_achieved: Arc, - current_monerod_server: Arc>>, - last_assigned_monerod_url: Arc>>, - randomx_factory: RandomXFactory, - consensus_manager: ConsensusManager, - wallet_payment_address: TariAddress, +#[derive(Debug, Clone, Default)] +pub(crate) struct MonerodCacheValues { + pub(crate) height: u64, + pub(crate) prev_hash: Hash, + pub(crate) timestamp: Option, + pub(crate) seed_height: Option, + pub(crate) seed_hash: Option, } -const BUSY_QUALIFYING: &str = "BusyQualifyingMonerodUrl"; - impl InnerService { #[allow(clippy::cast_possible_wrap)] async fn handle_get_height(&self, monerod_resp: Response) -> Result, MmProxyError> { + trace!(target: LOG_TARGET, "handle_get_height monerod_resp body: {}", monerod_resp.body()); let (parts, mut json) = monerod_resp.into_parts(); if json["height"].is_null() { warn!(target: LOG_TARGET, r#"Monerod response was invalid: "height" is null"#); @@ -191,7 +116,7 @@ impl InnerService { let result = base_node_client - .get_tip_info(grpc::Empty {}) + .get_tip_info(tari_rpc::Empty {}) .await .map_err(|err| MmProxyError::GrpcRequestError { status: err, @@ -231,7 +156,7 @@ impl InnerService { let request = request.body(); let (parts, mut json_resp) = monerod_resp.into_parts(); - info!(target: LOG_TARGET, "Block submited: submit request #{}", request); + info!(target: LOG_TARGET, "Block submit request #{}", request); let params = match request["params"].as_array() { Some(v) => v, None => { @@ -247,25 +172,21 @@ impl InnerService { }, }; - for param in params.iter().filter_map(|p| p.as_str()) { + for (i, param) in params.iter().filter_map(|p| p.as_str()).enumerate() { + trace!(target: LOG_TARGET, "handle_submit_block, param {} of {}", i, params.len()); let monero_block = monero_rx::deserialize_monero_block_from_hex(param)?; - debug!(target: LOG_TARGET, "Monero block: {}", monero_block); + trace!(target: LOG_TARGET, "Monero block: {}", monero_block); let hash = monero_rx::extract_aux_merkle_root_from_block(&monero_block)?.ok_or_else(|| { MmProxyError::MissingDataError("Could not find Minotari header in coinbase".to_string()) })?; - - debug!( - target: LOG_TARGET, - "Minotari Hash found in Monero block: {}", - hex::encode(hash) - ); + debug!(target: LOG_TARGET, "Minotari Hash found in Monero block: {}", hex::encode(hash)); let mut block_data = match self.block_templates.get_final_template(&hash).await { Some(d) => d, None => { info!( target: LOG_TARGET, - "Block `{}` submitted but no matching block template was found, possible duplicate submission", + "Could not submit block `{}`, no matching block template found, possible duplicate submission", hex::encode(hash) ); continue; @@ -349,7 +270,7 @@ impl InnerService { json!({ "status": "OK", "untrusted": !self.initial_sync_achieved.load(Ordering::SeqCst) }), ); let resp = resp.into_inner(); - json_resp = append_aux_chain_data( + json_resp = crate::proxy::utils::append_aux_chain_data( json_resp, json!({"id": TARI_CHAIN_ID, "block_hash": resp.block_hash.to_hex()}), ); @@ -447,11 +368,11 @@ impl InnerService { // Add merge mining tag on blocktemplate request if !self.initial_sync_achieved.load(Ordering::SeqCst) { - let grpc::TipInfoResponse { + let tari_rpc::TipInfoResponse { initial_sync_achieved, metadata, .. - } = grpc_client.get_tip_info(grpc::Empty {}).await?.into_inner(); + } = grpc_client.get_tip_info(tari_rpc::Empty {}).await?.into_inner(); if initial_sync_achieved { self.initial_sync_achieved.store(true, Ordering::SeqCst); @@ -498,7 +419,7 @@ impl InnerService { }; let final_block_template_data = new_block_protocol - .get_next_block_template(monero_mining_data, &self.block_templates) + .get_next_tari_block_template(monero_mining_data, &self.block_templates) .await?; monerod_resp["result"]["blocktemplate_blob"] = final_block_template_data.blocktemplate_blob.clone().into(); @@ -516,11 +437,11 @@ impl InnerService { let aux_chain_mr = hex::encode(final_block_template_data.aux_chain_mr.clone()); let block_reward = final_block_template_data.template.tari_miner_data.reward; let total_fees = final_block_template_data.template.tari_miner_data.total_fees; - let monerod_resp = add_aux_data( + let monerod_resp = crate::proxy::utils::add_aux_data( monerod_resp, json!({ "base_difficulty": final_block_template_data.template.monero_difficulty }), ); - let monerod_resp = append_aux_chain_data( + let monerod_resp = crate::proxy::utils::append_aux_chain_data( monerod_resp, json!({ "id": TARI_CHAIN_ID, @@ -580,10 +501,12 @@ impl InnerService { ); let mut client = self.base_node_client.clone(); - let resp = client.get_header_by_hash(grpc::GetHeaderByHashRequest { hash }).await; + let resp = client + .get_header_by_hash(tari_rpc::GetHeaderByHashRequest { hash }) + .await; match resp { Ok(resp) => { - let json_block_header = try_into_json_block_header(resp.into_inner())?; + let json_block_header = crate::proxy::utils::try_into_json_block_header(resp.into_inner())?; debug!( target: LOG_TARGET, @@ -592,7 +515,7 @@ impl InnerService { let json_resp = json_rpc::success_response(request["id"].as_i64(), json!({ "block_header": json_block_header })); - let json_resp = append_aux_chain_data(json_resp, json!({ "id": TARI_CHAIN_ID })); + let json_resp = crate::proxy::utils::append_aux_chain_data(json_resp, json!({ "id": TARI_CHAIN_ID })); Ok(proxy::into_response(parts, &json_resp)) }, @@ -620,21 +543,21 @@ impl InnerService { } let mut client = self.base_node_client.clone(); - let tip_info = client.get_tip_info(grpc::Empty {}).await?; + let tip_info = client.get_tip_info(tari_rpc::Empty {}).await?; let tip_info = tip_info.into_inner(); let chain_metadata = tip_info.metadata.ok_or_else(|| { MmProxyError::UnexpectedTariBaseNodeResponse("get_tip_info returned no chain metadata".into()) })?; let tip_header = client - .get_header_by_hash(grpc::GetHeaderByHashRequest { + .get_header_by_hash(tari_rpc::GetHeaderByHashRequest { hash: chain_metadata.best_block_hash, }) .await?; let tip_header = tip_header.into_inner(); - let json_block_header = try_into_json_block_header(tip_header)?; - let resp = append_aux_chain_data( + let json_block_header = crate::proxy::utils::try_into_json_block_header(tip_header)?; + let resp = crate::proxy::utils::append_aux_chain_data( monero_resp, json!({ "id": TARI_CHAIN_ID, @@ -644,9 +567,18 @@ impl InnerService { Ok(proxy::into_response(parts, &resp)) } - fn clear_current_monerod_server_lock(&self) { + fn clear_current_monerod_server_lock(&self, last_assigned_server: Option<&str>) { + // Current let mut lock = self.current_monerod_server.write().expect("Write lock should not fail"); *lock = None; + // Last assigned + if let Some(server) = last_assigned_server { + let mut lock = self + .last_assigned_monerod_url + .write() + .expect("Write lock should not fail"); + *lock = Some(server.to_string()); + } trace!( target: LOG_TARGET, "Monerod status - Current: 'None', Last assigned: {}", self.last_assigned_monerod_url.read().expect("Read lock should not fail").clone().unwrap_or_default() @@ -664,8 +596,10 @@ impl InnerService { } fn update_monerod_server_locks(&self, server: &str) { + // Current let mut lock = self.current_monerod_server.write().expect("Write lock should not fail"); *lock = Some(server.to_string()); + // Last assigned let mut lock = self .last_assigned_monerod_url .write() @@ -674,7 +608,10 @@ impl InnerService { trace!(target: LOG_TARGET, "Monerod status - Current: {}, Last assigned: {}", server, server); } - async fn get_fully_qualified_monerod_url(&self, request_uri: &Uri) -> Result { + async fn get_monerod_url(&self, request_uri: &Uri) -> Result, MmProxyError> { + if self.config.monerod_fallback == MonerodFallback::StaticOnly { + return Ok(None); + } // Return the previously qualified monerod URL if it exists let mut parse_error = None; { @@ -688,13 +625,13 @@ impl InnerService { return Err(MmProxyError::ServersUnavailable(BUSY_QUALIFYING.to_string())); } match format!("{}{}", server, request_uri.path()).parse::() { - Ok(url) => return Ok(url), + Ok(url) => return Ok(Some(url)), Err(e) => parse_error = Some(e), } } } if let Some(e) = parse_error { - self.clear_current_monerod_server_lock(); + self.clear_current_monerod_server_lock(None); return Err(e.into()); } @@ -717,7 +654,7 @@ impl InnerService { .position(|x| x == &last_used_url) .unwrap_or(0); let (left, right) = self.config.monerod_url.split_at_checked(pos).ok_or_else(|| { - self.clear_current_monerod_server_lock(); + self.clear_current_monerod_server_lock(None); MmProxyError::ConversionError("Invalid utf 8 url".to_string()) })?; let left = left.to_vec(); @@ -728,9 +665,9 @@ impl InnerService { for server in iter { let start = Instant::now(); let url = match format!("{}{}", server, request_uri.path()).parse::() { - Ok(uri) => uri, + Ok(val) => val, Err(e) => { - self.clear_current_monerod_server_lock(); + self.clear_current_monerod_server_lock(Some(server)); return Err(e.into()); }, }; @@ -739,7 +676,7 @@ impl InnerService { target: LOG_TARGET, "Trying to connect to Monerod server at: {} (entry {} of {})", url.as_str(), pos + 1, self.config.monerod_url.len() ); - match timeout(MONEROD_CONNECTION_TIMEOUT, reqwest::get(url.clone())).await { + match timeout(self.config.monerod_connection_timeout, reqwest::get(url.clone())).await { Ok(response) => { self.update_monerod_server_locks(server); let data_len = match response { @@ -751,7 +688,7 @@ impl InnerService { "Monerod server available (response in {:.2?}, {} bytes): {}", start.elapsed(), data_len, url.as_str() ); - return Ok(url); + return Ok(Some(url)); }, Err(_) => { warn!( @@ -759,12 +696,16 @@ impl InnerService { "Monerod server unavailable (timeout in {:.2?}): {}", start.elapsed(), url.as_str() ); + self.clear_current_monerod_server_lock(Some(server)); + if self.config.monerod_fallback == MonerodFallback::StaticWhenMonerodFails { + return Ok(None); + } }, } } // Clear the "busy qualifying" state - self.clear_current_monerod_server_lock(); + self.clear_current_monerod_server_lock(None); Err(MmProxyError::ServersUnavailable(format!("{}", self.config.monerod_url))) } @@ -772,94 +713,79 @@ impl InnerService { async fn proxy_request_to_monerod( &self, request: Request, + monerod_method: MonerodMethod, ) -> Result<(Request, Response), MmProxyError> { - let monerod_uri = self.get_fully_qualified_monerod_url(request.uri()).await?; - - let mut headers = request.headers().clone(); - // Some public monerod setups (e.g. those that are reverse proxied by nginx) require the Host header. - // The mmproxy is the direct client of monerod and so is responsible for setting this header. - if let Some(host) = monerod_uri.host_str() { - let host: HeaderValue = match monerod_uri.port_or_known_default() { - Some(port) => format!("{}:{}", host, port).parse()?, - None => host.parse()?, - }; - headers.insert("host", host); - debug!( - target: LOG_TARGET, - "Host header updated to match monerod_uri. Request headers: {:?}", headers - ); - } - let mut builder = self - .http_client - .request(request.method().clone(), monerod_uri.clone()) - .headers(headers); - - if self.config.monerod_use_auth { - // Use HTTP basic auth. This is the only reason we are using `reqwest` over the standard hyper client. - builder = builder.basic_auth(&self.config.monerod_username, Some(&self.config.monerod_password)); - } - - debug!( - target: LOG_TARGET, - "[monerod] request: {} {}", - request.method(), - monerod_uri, - ); + trace!(target: LOG_TARGET, "proxy_request_to_monerod: '{}'", monerod_method); - let mut submit_block = false; + // This is a cheap clone of the request body let body: Bytes = request.body().clone(); let json = json::from_slice::(&body[..]).unwrap_or_default(); - if let Some(method) = json["method"].as_str() { - trace!(target: LOG_TARGET, "json[\"method\"]: {}", method); - match method { - "submitblock" | "submit_block" => { - submit_block = true; - }, - _ => {}, + let request_id = json["id"].as_i64(); + let self_select_response = monerod_method == MonerodMethod::SubmitBlock && !self.config.submit_to_origin; + + let json_response = if let Some(monerod_url) = self.get_monerod_url(request.uri()).await? { + let mut headers = request.headers().clone(); + // Some public monerod setups (e.g. those that are reverse proxied by nginx) require the Host header. + // The mmproxy is the direct client of monerod and so is responsible for setting this header. + if let Some(host) = monerod_url.host_str() { + let host: HeaderValue = match monerod_url.port_or_known_default() { + Some(port) => format!("{}:{}", host, port).parse()?, + None => host.parse()?, + }; + headers.insert("host", host); + debug!( + target: LOG_TARGET, + "Host header updated to match monerod_uri. Request headers: {:?}", headers + ); + } + let mut builder = self + .http_client + .request(request.method().clone(), monerod_url.clone()) + .headers(headers.clone()); + + if self.config.monerod_use_auth { + // Use HTTP basic auth. This is the only reason we are using `reqwest` over the standard hyper client. + builder = builder.basic_auth(&self.config.monerod_username, Some(&self.config.monerod_password)); } - trace!( - target: LOG_TARGET, - "submitblock({}), proxy_submit_to_origin({})", - submit_block, - self.config.submit_to_origin - ); - } - // If the request is a block submission and we are not submitting blocks - // to the origin (self-select mode, see next comment for a full explanation) - let json_response = if submit_block && !self.config.submit_to_origin { debug!( target: LOG_TARGET, - "[monerod] skip: Proxy configured for self-select mode. Pool will submit to MoneroD, submitting to \ - Minotari.", + "[monerod] request: {} {}", + request.method(), + monerod_url, ); - // This is required for self-select configuration. - // We are not submitting the block to Monero here (the pool does this), - // we are only interested in intercepting the request for the purposes of - // submitting the block to Tari which will only happen if the accept response - // (which normally would occur for normal mining) is provided here. - // There is no point in trying to submit the block to Monero here since the - // share submitted by XMRig is only guaranteed to meet the difficulty of - // min(Tari,Monero) since that is what was returned with the original template. - // So it would otherwise be a duplicate submission of what the pool will do - // itself (whether the miner submits directly to monerod or the pool does, - // the pool is the only one being paid out here due to the nature - // of self-select). Furthermore, discussions with devs from Monero and XMRig are - // very much against spamming the nodes unnecessarily. - // NB!: This is by design, do not change this without understanding - // it's implications. - let accept_response = json_rpc::default_block_accept_response(json["id"].as_i64()); - - convert_json_to_hyper_json_response(accept_response, StatusCode::OK, monerod_uri.clone()).await? + if self_select_response { + let accept_response = self_select_submit_block_monerod_response(request_id); + convert_json_to_hyper_json_response(accept_response, StatusCode::OK, monerod_url.clone()).await? + } else { + let resp = match builder + .body(body.clone()) + .send() + .await + .map_err(MmProxyError::MonerodRequestFailed) + { + Ok(val) => val, + Err(e) => { + debug!(target: LOG_TARGET, "[monerod] request '{}' response error '{}'", monerod_method, e); + return Err(e); + }, + }; + + let hyper_json_response = convert_reqwest_response_to_hyper_json_response(resp).await?; + self.update_monerod_cache_values(monerod_method, hyper_json_response.body())?; + hyper_json_response + } + } else if self_select_response { + let accept_response = self_select_submit_block_monerod_response(request_id); + convert_json_to_hyper_json_response(accept_response, StatusCode::OK, static_json_rpc_url()).await? } else { - let resp = builder - // This is a cheap clone of the request body - .body(body) - .send() - .await - .map_err(MmProxyError::MonerodRequestFailed)?; - convert_reqwest_response_to_hyper_json_response(resp).await? + let cache_values = self + .monerod_cache_values + .read() + .expect("Read lock should not fail") + .clone(); + convert_static_monerod_response_to_hyper_response(monerod_method, request_id, cache_values)? }; let rpc_status = if json_response.body()["error"].is_null() { @@ -875,47 +801,140 @@ impl InnerService { json_response.status(), rpc_status ); + trace!(target: LOG_TARGET, "[monerod] '{}' response '{:?}'", monerod_method, json_response); Ok((request, json_response)) } + fn update_monerod_cache_values( + &self, + monerod_method: MonerodMethod, + json: &json::Value, + ) -> Result<(), MmProxyError> { + let (timestamp, seed_height, seed_hash) = { + if let Some(cache) = self + .monerod_cache_values + .read() + .expect("Read lock should not fail") + .clone() + { + (cache.timestamp, cache.seed_height, cache.seed_hash) + } else { + (None, None, None) + } + }; + let mut lock = self.monerod_cache_values.write().expect("Write lock should not fail"); + match monerod_method { + MonerodMethod::GetHeight => { + *lock = Some(MonerodCacheValues { + height: json["height"] + .as_u64() + .ok_or(MmProxyError::InvalidMonerodResponse("height".to_string()))?, + prev_hash: Hash::from_str( + json["hash"] + .as_str() + .ok_or(MmProxyError::InvalidMonerodResponse("hash".to_string()))?, + ) + .map_err(|e| MmProxyError::InvalidMonerodResponse(e.to_string()))?, + timestamp, + seed_height, + seed_hash, + }); + }, + MonerodMethod::GetBlockTemplate => { + *lock = Some(MonerodCacheValues { + height: json["result"]["height"] + .as_u64() + .ok_or(MmProxyError::InvalidMonerodResponse("height".to_string()))?, + prev_hash: Hash::from_str( + json["result"]["prev_hash"] + .as_str() + .ok_or(MmProxyError::InvalidMonerodResponse("prev_hash".to_string()))?, + ) + .map_err(|e| MmProxyError::InvalidMonerodResponse(e.to_string()))?, + timestamp, + seed_height: Some( + json["result"]["seed_height"] + .as_u64() + .ok_or(MmProxyError::InvalidMonerodResponse("seed_height".to_string()))?, + ), + seed_hash: Some( + Hash::from_str( + json["result"]["seed_hash"] + .as_str() + .ok_or(MmProxyError::InvalidMonerodResponse("seed_hash".to_string()))?, + ) + .map_err(|e| MmProxyError::InvalidMonerodResponse(e.to_string()))?, + ), + }); + }, + MonerodMethod::GetLastBlockHeader => { + *lock = Some(MonerodCacheValues { + height: json["result"]["block_header"]["height"] + .as_u64() + .ok_or(MmProxyError::InvalidMonerodResponse("height".to_string()))?, + prev_hash: Hash::from_str( + json["result"]["block_header"]["prev_hash"] + .as_str() + .ok_or(MmProxyError::InvalidMonerodResponse("prev_hash".to_string()))?, + ) + .map_err(|e| MmProxyError::InvalidMonerodResponse(e.to_string()))?, + timestamp: Some( + json["result"]["block_header"]["timestamp"] + .as_u64() + .ok_or(MmProxyError::InvalidMonerodResponse("timestamp".to_string()))?, + ), + seed_height: Some( + json["result"]["block_header"]["seed_height"] + .as_u64() + .ok_or(MmProxyError::InvalidMonerodResponse("seed_height".to_string()))?, + ), + seed_hash: Some( + Hash::from_str( + json["result"]["block_header"]["seed_hash"] + .as_str() + .ok_or(MmProxyError::InvalidMonerodResponse("seed_hash".to_string()))?, + ) + .map_err(|e| MmProxyError::InvalidMonerodResponse(e.to_string()))?, + ), + }); + }, + _ => {}, + } + + Ok(()) + } + async fn get_proxy_response( &self, request: Request, monerod_resp: Response, + monerod_method: MonerodMethod, ) -> Result, MmProxyError> { - match request.method().clone() { - Method::GET => { - // All get requests go to /request_name, methods do not have a body, optionally could have query params - // if applicable. - match request.uri().path() { - "/get_height" | "/getheight" => self.handle_get_height(monerod_resp).await, - _ => Ok(proxy::into_body_from_response(monerod_resp)), - } + trace!(target: LOG_TARGET, "get_proxy_response: '{}'", monerod_method); + match monerod_method { + MonerodMethod::GetHeight => self.handle_get_height(monerod_resp).await, + MonerodMethod::GetBlockTemplate => self.handle_get_block_template(monerod_resp).await, + MonerodMethod::SubmitBlock => { + self.handle_submit_block(request_bytes_to_value(request)?, monerod_resp) + .await }, - Method::POST => { - // All post requests go to /json_rpc, body of request contains a field `method` to indicate which call - // takes place. - let json = json::from_slice::(request.body())?; - let request = request.map(move |_| json); - match request.body()["method"].as_str().unwrap_or_default() { - "submitblock" | "submit_block" => self.handle_submit_block(request, monerod_resp).await, - "getblocktemplate" | "get_block_template" => self.handle_get_block_template(monerod_resp).await, - "getblockheaderbyhash" | "get_block_header_by_hash" => { - self.handle_get_block_header_by_hash(request, monerod_resp).await - }, - "getlastblockheader" | "get_last_block_header" => { - self.handle_get_last_block_header(monerod_resp).await - }, - - _ => Ok(proxy::into_body_from_response(monerod_resp)), - } + MonerodMethod::GetBlockHeaderByHash => { + self.handle_get_block_header_by_hash(request_bytes_to_value(request)?, monerod_resp) + .await + }, + MonerodMethod::GetLastBlockHeader => self.handle_get_last_block_header(monerod_resp).await, + _ => { + // Simply return the response "as is" + Ok(proxy::into_body_from_response(monerod_resp)) }, - // Simply return the response "as is" - _ => Ok(proxy::into_body_from_response(monerod_resp)), } } - async fn handle(self, method_name: &str, request: Request) -> Result, MmProxyError> { + pub(crate) async fn handle( + self, + method_name: &str, + request: Request, + ) -> Result, MmProxyError> { let start = Instant::now(); debug!( @@ -926,18 +945,15 @@ impl InnerService { request.headers(), String::from_utf8_lossy(&request.body().clone()[..]), ); + let monerod_method = parse_monerod_rpc_method(request.method(), request.uri(), request.body()); - match self.proxy_request_to_monerod(request).await { + match self.proxy_request_to_monerod(request, monerod_method).await { Ok((request, monerod_resp)) => { // Any failed (!= 200 OK) responses from Monero are immediately returned to the requester let monerod_status = monerod_resp.status(); if !monerod_status.is_success() { // we dont break on monerod returning an error code. - warn!( - target: LOG_TARGET, - "Monerod returned an error: {}", - monerod_resp.status() - ); + warn!(target: LOG_TARGET, "Monerod returned an error: {}", monerod_resp.status()); debug!( "Method: {}, MoneroD Status: {}, Proxy Status: N/A, Response Time: {}ms", method_name, @@ -947,7 +963,7 @@ impl InnerService { return Ok(monerod_resp.map(|json| json.to_string().into())); } - match self.get_proxy_response(request, monerod_resp).await { + match self.get_proxy_response(request, monerod_resp, monerod_method).await { Ok(response) => { debug!( "Method: {}, MoneroD Status: {}, Proxy Status: {}, Response Time: {}ms", @@ -959,308 +975,19 @@ impl InnerService { Ok(response) }, Err(e) => { + error!(target: LOG_TARGET, "get_proxy_response: {}", e); // Monero Server encountered a problem processing the request, reset the current monerod server - self.clear_current_monerod_server_lock(); + self.clear_current_monerod_server_lock(None); Err(e) }, } }, Err(e) => { + error!(target: LOG_TARGET, "proxy_request_to_monerod: {}", e); // Monero Server encountered a problem processing the request, reset the current monerod server - self.clear_current_monerod_server_lock(); + self.clear_current_monerod_server_lock(None); Err(e) }, } } } - -async fn convert_reqwest_response_to_hyper_json_response( - resp: reqwest::Response, -) -> Result, MmProxyError> { - let mut builder = Response::builder(); - - let headers = builder - .headers_mut() - .expect("headers_mut errors only when the builder has an error (e.g invalid header value)"); - headers.extend(resp.headers().iter().map(|(name, value)| (name.clone(), value.clone()))); - - builder = builder - .version(resp.version()) - .status(resp.status()) - .url(resp.url().clone()); - - let body = resp.json().await.map_err(MmProxyError::MonerodRequestFailed)?; - let resp = builder.body(body)?; - Ok(resp) -} - -/// Add mmproxy extensions object to JSON RPC success response -pub fn add_aux_data(mut response: json::Value, mut ext: json::Value) -> json::Value { - if response["result"].is_null() { - return response; - } - match response["result"][MMPROXY_AUX_KEY_NAME].as_object_mut() { - Some(obj_mut) => { - let ext_mut = ext - .as_object_mut() - .expect("invalid parameter: expected `ext: json::Value` to be an object but it was not"); - obj_mut.append(ext_mut); - }, - None => { - response["result"][MMPROXY_AUX_KEY_NAME] = ext; - }, - } - response -} - -/// Append chain data to the result object. If the result object is null, a JSON object is created. -/// -/// ## Panics -/// -/// If response["result"] is not a JSON object type or null. -pub fn append_aux_chain_data(mut response: json::Value, chain_data: json::Value) -> json::Value { - let result = &mut response["result"]; - if result.is_null() { - *result = json!({}); - } - let chains = match result[MMPROXY_AUX_KEY_NAME]["chains"].as_array_mut() { - Some(arr_mut) => arr_mut, - None => { - result[MMPROXY_AUX_KEY_NAME]["chains"] = json!([]); - result[MMPROXY_AUX_KEY_NAME]["chains"].as_array_mut().unwrap() - }, - }; - - chains.push(chain_data); - response -} - -fn try_into_json_block_header(header: grpc::BlockHeaderResponse) -> Result { - let grpc::BlockHeaderResponse { - header, - reward, - confirmations, - difficulty, - num_transactions, - } = header; - let header = header.ok_or_else(|| { - MmProxyError::UnexpectedTariBaseNodeResponse( - "Base node GRPC returned an empty header field when calling get_header_by_hash".into(), - ) - })?; - - Ok(json!({ - "block_size": 0, - "depth": confirmations, - "difficulty": difficulty, - "hash": header.hash.to_hex(), - "height": header.height, - "major_version": header.version, - "minor_version": 0, - "nonce": header.nonce, - "num_txes": num_transactions, - // Cannot be an orphan - "orphan_status": false, - "prev_hash": header.prev_hash.to_hex(), - "reward": reward, - "timestamp": header.timestamp - })) -} - -fn parse_method_name(request: &Request) -> String { - match *request.method() { - Method::GET => { - let mut chars = request.uri().path().chars(); - chars.next(); - chars.as_str().to_string() - }, - Method::POST => { - let json = json::from_slice::(request.body()).unwrap_or_default(); - str::replace(json["method"].as_str().unwrap_or_default(), "\"", "") - }, - _ => "unsupported".to_string(), - } -} - -#[cfg(test)] -mod test { - use std::{fmt::Display, time::Instant}; - - use anyhow::{anyhow, Error}; - use chrono::{Local, Timelike}; - use reqwest::Client; - use serde_json::{json, Value}; - - #[allow(clippy::enum_variant_names)] - #[derive(Clone, Copy)] - enum Method { - GetHeight, - GetBlockTemplate, - GetVersion, - } - - impl Display for Method { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let str = match self { - Method::GetHeight => "get_height".to_string(), - Method::GetBlockTemplate => "get_block_template".to_string(), - Method::GetVersion => "get_version".to_string(), - }; - write!(f, "{}", str) - } - } - - async fn get_json_rpc(method: Method, json_rpc_port: u16) -> Result { - match method { - Method::GetHeight => get_response(json_rpc_port, method).await, - Method::GetBlockTemplate | Method::GetVersion => json_rpc_request(method, json_rpc_port).await, - } - } - - async fn get_response(json_rpc_port: u16, method: Method) -> Result { - let full_address = format!("http://127.0.0.1:{}", json_rpc_port); - match reqwest::get(format!("{}/{}", full_address, method)) - .await - .unwrap() - .json::() - .await - { - Ok(val) => Ok(val.to_string()), - Err(e) => Err(e.into()), - } - } - - async fn json_rpc_request(method: Method, json_rpc_port: u16) -> Result { - let rpc_method = format!("{}", method); - let request_body = match method { - Method::GetBlockTemplate => json!({ - "jsonrpc": "2.0", - "id": "0", - "method": rpc_method, - "params": { - "wallet_address": "489r43gR8bDMJNBf4Q6sL9CNERvZQrTqjRCSESqgWQEWWq2UGAfj2voaw3zBtD7U8CQ391Nc1PDHUHiN85yhbZnCDasqzyX", - } - }), - Method::GetVersion => json!({ - "jsonrpc": "2.0", - "id": "0", - "method": rpc_method, - "params": {} - }), - _ => return Err(anyhow!("'{}' not supported", method)), - }; - let rpc_url = format!("http://127.0.0.1:{}/json_rpc", json_rpc_port); - - // Create an HTTP client - let client = Client::new(); - - // Send the POST request - let response = client.post(rpc_url).json(&request_body).send().await?; - - // Parse the response body - if response.status().is_success() { - let response_text = response.text().await?; - let response_json: serde_json::Value = serde_json::from_str(&response_text)?; - if response_json.get("error").is_some() { - return Err(anyhow!("'{}' failed ({})", method, response_text)); - } - if response_json.get("result").is_none() { - return Err(anyhow!("'{}' failed ({})", method, response_text)); - } - Ok(response_text) - } else { - Err(anyhow!("{} failed({})", method, response.status())) - } - } - - fn time_now() -> String { - let now = Local::now(); - format!( - "{:02}:{:02}:{:02}.{:03}", - now.hour(), - now.minute(), - now.second(), - now.timestamp_subsec_millis() - ) - } - - async fn inner_json_rpc_loop(method: Method, json_rpc_port: u16, responses: &mut Vec, count: usize) { - let start = Instant::now(); - let response = get_json_rpc(method, json_rpc_port).await; - match response { - Ok(val) => { - responses.push(format!( - " {}: method: {}; time now: {}; duration: {:.2?}, response length: {}", - count, - method, - time_now(), - start.elapsed(), - val.len(), - )); - }, - Err(err) => { - responses.push(format!( - " {}: method: {}; time now: {}; duration: {:.2?}, response length: {}, Error: {}", - count, - method, - time_now(), - start.elapsed(), - err.to_string().len(), - err - )); - }, - } - } - - // To execute this test a merge mining proxy must be running (just verify the port, default used), ideally when - // RandomX mining with XMRig is taking place. - #[tokio::test] - #[ignore] - async fn test_get_monerod_info() { - let json_rpc_port = 18081; - let tick = tokio::time::Duration::from_secs(2); - let mut interval = tokio::time::interval(tick); - let mut responses = Vec::with_capacity(50); - for method in [Method::GetHeight, Method::GetVersion, Method::GetBlockTemplate] { - let mut count = 0; - responses.push(format!("method: {}, tick: {:.2?}", method, tick)); - loop { - interval.tick().await; - count += 1; - inner_json_rpc_loop(method, json_rpc_port, &mut responses, count).await; - if count >= 5 { - break; - } - } - } - for response in responses { - println!("{}", response); - } - } - - // To execute this test a merge mining proxy must be running (just verify the port, default used), ideally when - // RandomX mining with XMRig is taking place. - #[tokio::test] - #[ignore] - async fn stress_test_get_monerod_info() { - let json_rpc_port = 18081; - let tick = tokio::time::Duration::from_millis(1000); - let mut interval = tokio::time::interval(tick); - let mut responses = Vec::with_capacity(3010); - for method in [Method::GetHeight, Method::GetVersion, Method::GetBlockTemplate] { - let mut count = 0; - responses.push(format!("method: {}, tick: {:.2?}", method, tick)); - loop { - interval.tick().await; - count += 1; - inner_json_rpc_loop(method, json_rpc_port, &mut responses, count).await; - if count >= 500 { - break; - } - } - } - for response in responses { - println!("{}", response); - } - } -} diff --git a/applications/minotari_merge_mining_proxy/src/proxy/mod.rs b/applications/minotari_merge_mining_proxy/src/proxy/mod.rs new file mode 100644 index 0000000000..956102daa4 --- /dev/null +++ b/applications/minotari_merge_mining_proxy/src/proxy/mod.rs @@ -0,0 +1,312 @@ +// Copyright 2020, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub(crate) mod inner; +mod monerod_method; +pub(crate) mod service; +pub(crate) mod static_responses; +pub(crate) mod utils; + +#[cfg(test)] +mod test { + use std::{str::FromStr, time::Instant}; + + use anyhow::{anyhow, Error}; + use chrono::{Local, Timelike}; + use monero::{blockdata::transaction::SubField::MergeMining, Hash, VarInt}; + use reqwest::Client; + use serde_json::json; + use tari_core::proof_of_work::monero_rx; + + use crate::proxy::{ + monerod_method::MonerodMethod, + static_responses::{get_empty_monero_block, BLOCK_HASH_AT_3336491, BLOCK_HEIGHT_3336491, TIMESTAMP_AT_3336491}, + utils::convert_reqwest_response_to_hyper_json_response, + }; + + /// This is the public Monero primary donation address for their general fund + /// (See https://www.getmonero.org/get-started/contributing/) + const DEFAULT_MONERO_ADDRESS: &str = + "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A"; + + async fn get_json_rpc( + method: MonerodMethod, + json_rpc_port: u16, + hyper_json_response: bool, + hash: Option, + block_blob: Option, + ) -> Result { + match method { + MonerodMethod::GetHeight => get_response(json_rpc_port, method).await, + MonerodMethod::GetBlockTemplate | + MonerodMethod::GetVersion | + MonerodMethod::SubmitBlock | + MonerodMethod::GetBlockHeaderByHash | + MonerodMethod::GetLastBlockHeader | + MonerodMethod::GetBlock => { + json_rpc_request_body(method, json_rpc_port, hyper_json_response, hash, block_blob).await + }, + _ => Err(anyhow!("'{}' not supported", method)), + } + } + + async fn get_response(json_rpc_port: u16, method: MonerodMethod) -> Result { + let full_address = format!("http://127.0.0.1:{}", json_rpc_port); + match reqwest::get(format!("{}/{}", full_address, method)) + .await + .unwrap() + .json::() + .await + { + Ok(val) => { + println!("{}: {}", method, val); + Ok(val.to_string()) + }, + Err(e) => Err(e.into()), + } + } + + // The static XMRig request bodies in this test helper function are for testing purposes only. Their content can be + // verified for similarity with the actual request bodies that are generated by XMRig as the first log entry in + // `applications/minotari_merge_mining_proxy/src/proxy/inner.rs` method `pub(crate) async fn handle(`. + async fn json_rpc_request_body( + method: MonerodMethod, + json_rpc_port: u16, + hyper_json_response: bool, + hash: Option, + block_blob: Option, + ) -> Result { + let rpc_method = format!("{}", method); + let request_body = match method { + MonerodMethod::GetBlockTemplate => json!({ + "jsonrpc": "2.0", + "id": "0", + "method": rpc_method, + "params": { + "wallet_address": DEFAULT_MONERO_ADDRESS, + } + }), + MonerodMethod::GetVersion | MonerodMethod::GetLastBlockHeader => json!({ + "jsonrpc": "2.0", + "id": "0", + "method": rpc_method, + "params": {} + }), + MonerodMethod::SubmitBlock => { + let block_blob = if let Some(blob) = block_blob { + blob + } else { + const ARBITRARY_MERGE_MINING_HASH: &str = + "8e6dab82d22909b40bda27ec0e96aa0c6c0012023d353f3be941eb6ef1793cad"; + let merge_mining_tag = Some(MergeMining( + VarInt(0), + Hash::from_str(ARBITRARY_MERGE_MINING_HASH).expect("will not fail"), + )); + let block = get_empty_monero_block( + Hash::from_str(BLOCK_HASH_AT_3336491)?, + TIMESTAMP_AT_3336491, + BLOCK_HEIGHT_3336491, + merge_mining_tag, + ); + monero_rx::serialize_monero_block_to_hex(&block)? + }; + assert!(monero_rx::deserialize_monero_block_from_hex(&block_blob).is_ok()); + + json!({ + "jsonrpc": "2.0", + "id": "0", + "method": rpc_method, + // These request params were generated by XMRig when submitting a block that was solved during merge + // mining on esmeralda. + "params": [block_blob] + }) + }, + MonerodMethod::GetBlockHeaderByHash | MonerodMethod::GetBlock => json!({ + "jsonrpc": "2.0", + "id": "0", + "method": rpc_method, + "params": { + "hash": hash.unwrap_or(BLOCK_HASH_AT_3336491.to_string()), + } + }), + _ => return Err(anyhow!("'{}' not supported", method)), + }; + let rpc_url = format!("http://127.0.0.1:{}/json_rpc", json_rpc_port); + + // Create an HTTP client + let client = Client::new(); + + // Send the POST request + let response = client.post(rpc_url).json(&request_body).send().await?; + + // Parse the response body + if response.status().is_success() { + if hyper_json_response { + let hyper_json_response = convert_reqwest_response_to_hyper_json_response(response).await?; + + println!(); + println!("{} - response: {:?}", method, hyper_json_response); + println!("{} - status: {:?}", method, hyper_json_response.status()); + println!("{} - version: {:?}", method, hyper_json_response.version()); + println!("{} - headers: {:?}", method, hyper_json_response.headers()); + println!("{} - extensions: {:?}", method, hyper_json_response.extensions()); + println!("{} - body: {:?}", method, hyper_json_response.body()); + + let response_json = hyper_json_response.body(); + if response_json.get("error").is_some() { + return Err(anyhow!("'{}' failed ({})", method, response_json)); + } + if response_json.get("result").is_none() { + return Err(anyhow!("'{}' failed ({})", method, response_json)); + } + + Ok(response_json.to_string()) + } else { + let response_text = response.text().await?; + let response_json: serde_json::Value = serde_json::from_str(&response_text)?; + if response_json.get("error").is_some() { + return Err(anyhow!("'{}' failed ({})", method, response_text)); + } + if response_json.get("result").is_none() { + return Err(anyhow!("'{}' failed ({})", method, response_text)); + } + Ok(response_text) + } + } else { + Err(anyhow!("{} failed({})", method, response.status())) + } + } + + fn time_now() -> String { + let now = Local::now(); + format!( + "{:02}:{:02}:{:02}.{:03}", + now.hour(), + now.minute(), + now.second(), + now.timestamp_subsec_millis() + ) + } + + pub(crate) async fn inner_json_rpc( + method: MonerodMethod, + json_rpc_port: u16, + responses: &mut Vec, + count: usize, + hyper_json_response: bool, + hash: Option, + block_blob: Option, + ) { + let start = Instant::now(); + let response = get_json_rpc(method, json_rpc_port, hyper_json_response, hash, block_blob).await; + match response { + Ok(val) => { + responses.push(format!( + " {}: method: {}; time now: {}; duration: {:.2?}, response length: {}, response body: {}", + count, + method, + time_now(), + start.elapsed(), + val.len(), + val + )); + }, + Err(err) => { + responses.push(format!( + " {}: method: {}; time now: {}; duration: {:.2?}, response length: {}, Error: {}", + count, + method, + time_now(), + start.elapsed(), + err.to_string().len(), + err + )); + }, + } + } + + // To execute this test a merge mining proxy must be running (just verify the port, default used), ideally when + // RandomX mining with XMRig is taking place. + #[tokio::test] + #[ignore] + async fn test_get_monerod_info() { + let json_rpc_port = 18081; + let tick = tokio::time::Duration::from_secs(2); + let mut interval = tokio::time::interval(tick); + let mut responses = Vec::with_capacity(50); + for method in [ + MonerodMethod::GetHeight, + MonerodMethod::GetVersion, + MonerodMethod::GetBlockTemplate, + MonerodMethod::SubmitBlock, + MonerodMethod::GetBlockHeaderByHash, + MonerodMethod::GetLastBlockHeader, + ] { + let mut count = 0; + responses.push(format!("method: {}, tick: {:.2?}", method, tick)); + loop { + interval.tick().await; + count += 1; + inner_json_rpc(method, json_rpc_port, &mut responses, count, false, None, None).await; + if count >= 2 { + break; + } + } + } + for response in responses { + println!("{}", response); + } + } + + // To execute this test a merge mining proxy must be running (just verify the port, default used), ideally when + // RandomX mining with XMRig is taking place. + #[tokio::test] + #[ignore] + async fn stress_test_get_monerod_info() { + let json_rpc_port = 18081; + let tick = tokio::time::Duration::from_millis(1000); + let mut interval = tokio::time::interval(tick); + let mut responses = Vec::with_capacity(3010); + for method in [ + MonerodMethod::GetHeight, + MonerodMethod::GetVersion, + MonerodMethod::GetBlockTemplate, + MonerodMethod::SubmitBlock, + MonerodMethod::GetBlockHeaderByHash, + MonerodMethod::GetLastBlockHeader, + ] { + let mut count = 0; + responses.push(format!("method: {}, tick: {:.2?}", method, tick)); + loop { + interval.tick().await; + count += 1; + inner_json_rpc(method, json_rpc_port, &mut responses, count, false, None, None).await; + if count >= 500 { + break; + } + } + } + for response in responses { + println!("{}", response); + } + } +} diff --git a/applications/minotari_merge_mining_proxy/src/proxy/monerod_method.rs b/applications/minotari_merge_mining_proxy/src/proxy/monerod_method.rs new file mode 100644 index 0000000000..776604de7a --- /dev/null +++ b/applications/minotari_merge_mining_proxy/src/proxy/monerod_method.rs @@ -0,0 +1,103 @@ +// Copyright 2020, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{fmt::Display, str::FromStr}; + +use bytes::Bytes; +use hyper::{Method, Uri}; +use log::warn; + +use crate::error::MmProxyError; + +const LOG_TARGET: &str = "minotari_mm_proxy::proxy::monerod_method"; + +#[allow(clippy::enum_variant_names)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub(crate) enum MonerodMethod { + GetHeight, + GetVersion, + GetBlockTemplate, + SubmitBlock, + GetBlockHeaderByHash, + GetLastBlockHeader, + #[allow(dead_code)] + GetBlock, + RpcMethodNotDefined, +} + +impl FromStr for MonerodMethod { + type Err = MmProxyError; + + fn from_str(s: &str) -> Result { + match s { + "/get_height" | "/getheight" => Ok(MonerodMethod::GetHeight), + "get_height" | "getheight" => Ok(MonerodMethod::GetHeight), + "get_version" | "getversion" => Ok(MonerodMethod::GetVersion), + "get_block_template" | "getblocktemplate" => Ok(MonerodMethod::GetBlockTemplate), + "submit_block" | "submitblock" => Ok(MonerodMethod::SubmitBlock), + "get_block_header_by_hash" | "getblockheaderbyhash" => Ok(MonerodMethod::GetBlockHeaderByHash), + "get_last_block_header" | "getlastblockheader" => Ok(MonerodMethod::GetLastBlockHeader), + "get_block" | "getblovk" => Ok(MonerodMethod::GetLastBlockHeader), + _ => { + let msg = format!("Unknown monerod rpc method: '{}'", s); + warn!(target: LOG_TARGET, "{}", msg); + Err(MmProxyError::ConversionError(msg)) + }, + } + } +} + +impl Display for MonerodMethod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + MonerodMethod::GetHeight => "get_height".to_string(), + MonerodMethod::GetVersion => "get_version".to_string(), + MonerodMethod::GetBlockTemplate => "get_block_template".to_string(), + MonerodMethod::SubmitBlock => "submit_block".to_string(), + MonerodMethod::GetBlockHeaderByHash => "get_block_header_by_hash".to_string(), + MonerodMethod::GetLastBlockHeader => "get_last_block_header".to_string(), + MonerodMethod::GetBlock => "get_block".to_string(), + MonerodMethod::RpcMethodNotDefined => "rpc_method_not_defined".to_string(), + }; + write!(f, "{}", str) + } +} + +/// Parse the monerod RPC method from the request components +pub fn parse_monerod_rpc_method(request_method: &Method, request_uri: &Uri, request_body: &Bytes) -> MonerodMethod { + match *request_method { + // All get requests go to /request_name, methods do not have a body, optionally could have query params + // if applicable. + Method::GET => MonerodMethod::from_str(request_uri.path()).unwrap_or(MonerodMethod::RpcMethodNotDefined), + // All post requests go to /json_rpc, body of request contains a field `method` to indicate which call + // takes place. + Method::POST => { + let json = serde_json::from_slice::(&request_body[..]).unwrap_or_default(); + if let Some(method) = json["method"].as_str() { + MonerodMethod::from_str(method).unwrap_or(MonerodMethod::RpcMethodNotDefined) + } else { + MonerodMethod::RpcMethodNotDefined + } + }, + _ => MonerodMethod::RpcMethodNotDefined, + } +} diff --git a/applications/minotari_merge_mining_proxy/src/proxy/service.rs b/applications/minotari_merge_mining_proxy/src/proxy/service.rs new file mode 100644 index 0000000000..39a2ebd666 --- /dev/null +++ b/applications/minotari_merge_mining_proxy/src/proxy/service.rs @@ -0,0 +1,135 @@ +// Copyright 2020, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{ + future::Future, + pin::Pin, + sync::{atomic::AtomicBool, Arc, RwLock}, + task::{Context, Poll}, +}; + +use hyper::{Body, Request, Response, StatusCode}; +use jsonrpc::error::StandardError; +use minotari_app_utilities::parse_miner_input::{BaseNodeGrpcClient, ShaP2PoolGrpcClient}; +use serde_json::json; +use tari_common_types::tari_address::TariAddress; +use tari_comms::protocol::rpc::__macro_reexports::Service; +use tari_core::{consensus::ConsensusManager, proof_of_work::randomx_factory::RandomXFactory}; +use tracing::{error, trace, warn}; + +use crate::{ + block_template_data::BlockTemplateRepository, + common::{json_rpc, proxy}, + config::MergeMiningProxyConfig, + error::MmProxyError, + proxy::{inner::InnerService, utils::parse_method_name}, +}; + +const LOG_TARGET: &str = "minotari_mm_proxy::proxy::service"; + +#[derive(Debug, Clone)] +pub struct MergeMiningProxyService { + inner: InnerService, +} + +impl MergeMiningProxyService { + pub fn new( + config: MergeMiningProxyConfig, + http_client: reqwest::Client, + base_node_client: BaseNodeGrpcClient, + p2pool_client: Option, + block_templates: BlockTemplateRepository, + randomx_factory: RandomXFactory, + wallet_payment_address: TariAddress, + ) -> Result { + trace!(target: LOG_TARGET, "Config: {:?}", config); + let consensus_manager = ConsensusManager::builder(config.network).build()?; + Ok(Self { + inner: InnerService { + config: Arc::new(config), + block_templates, + http_client, + base_node_client, + p2pool_client, + initial_sync_achieved: Arc::new(AtomicBool::new(false)), + current_monerod_server: Arc::new(RwLock::new(None)), + last_assigned_monerod_url: Arc::new(RwLock::new(None)), + monerod_cache_values: Arc::new(RwLock::new(None)), + randomx_factory, + consensus_manager, + wallet_payment_address, + }, + }) + } +} + +#[allow(clippy::type_complexity)] +impl Service> for MergeMiningProxyService { + type Error = hyper::Error; + type Future = Pin> + Send>>; + type Response = Response; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, mut request: Request) -> Self::Future { + let inner = self.inner.clone(); + let future = async move { + let bytes = match proxy::read_body_until_end(request.body_mut()).await { + Ok(b) => b, + Err(err) => { + warn!(target: LOG_TARGET, "Method: Unknown, Failed to read request: {:?}", err); + let resp = proxy::json_response( + StatusCode::BAD_REQUEST, + &json_rpc::standard_error_response( + None, + StandardError::InvalidRequest, + Some(json!({"details": err.to_string()})), + ), + ) + .expect("unexpected failure"); + return Ok(resp); + }, + }; + let request = request.map(|_| bytes.freeze()); + let method_name = parse_method_name(&request); + match inner.handle(&method_name, request).await { + Ok(resp) => Ok(resp), + Err(err) => { + error!(target: LOG_TARGET, "Method \"{}\" failed handling request: {:?}", method_name, err); + Ok(proxy::json_response( + StatusCode::INTERNAL_SERVER_ERROR, + &json_rpc::standard_error_response( + None, + StandardError::InternalError, + Some(json!({"details": err.to_string()})), + ), + ) + .expect("unexpected failure")) + }, + } + }; + + Box::pin(future) + } +} diff --git a/applications/minotari_merge_mining_proxy/src/proxy/static_responses.rs b/applications/minotari_merge_mining_proxy/src/proxy/static_responses.rs new file mode 100644 index 0000000000..288c3f56c4 --- /dev/null +++ b/applications/minotari_merge_mining_proxy/src/proxy/static_responses.rs @@ -0,0 +1,694 @@ +// Copyright 2020, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::str::FromStr; + +use log::trace; +use monero::{ + blockdata::{ + transaction, + transaction::{ + ExtraField, + RawExtraField, + SubField::{Nonce, TxPublicKey}, + }, + }, + util::ringct::Key, + Block, + BlockHeader, + Hash, + PublicKey, + Transaction, + TransactionPrefix, + VarInt, +}; +use tari_core::proof_of_work::monero_rx; +use tracing::debug; +use url::Url; + +use crate::{ + common::json_rpc, + error::MmProxyError, + proxy::{inner::MonerodCacheValues, monerod_method::MonerodMethod}, +}; + +const LOG_TARGET: &str = "minotari_mm_proxy::proxy::static_responses"; + +struct StaticResponse { + headers: hyper::HeaderMap, + version: hyper::Version, + status: hyper::StatusCode, + body: serde_json::Value, +} + +// Default metadata for Monero block at height 3336491 +pub(crate) const BLOCK_HEIGHT_3336491: u64 = 3336491; +pub(crate) const BLOCK_HASH_AT_3336491: &str = "a1b9f62c45d67d5e2acb21efcaf8804d3674005190152d11b6f04d80acf8013c"; +pub(crate) const TIMESTAMP_AT_3336491: u64 = 1738223139; +const SEED_HEIGHT_AT_3336491: u64 = 3336192; +const SEED_HASH_AT_3336491: &str = "91ef83186cefaa646dc4c6e950e68e4debab52b4f4a9b7f465891e91fe5f6ce4"; +const DIFFICULTY_AT_3336491: u64 = 490520097899; +const REWARD_AT_3336491: u64 = 600741780000; +const WIDE_DIFFICULTY_AT_3336491: &str = "0x6df86346d3"; +const MAX_HF_VERSION: u64 = 16; + +// These static monerod responses can be captured using the merge mining proxy connected to XMRig and monerod as the +// last log entry in `applications/minotari_merge_mining_proxy/src/proxy/inner.rs` method +// `async fn proxy_request_to_monerod(`. Also see `get_new_monerod_static_responses()` test function in this file. +#[allow(clippy::too_many_lines)] +fn get_static_monerod_response( + method: MonerodMethod, + req_id: Option, + monerod_cache_values: Option, +) -> Result { + let req_id = req_id.unwrap_or(-1); + let (height, hash, timestamp, seed_height, seed_hash) = if let Some(cache_values) = monerod_cache_values { + ( + cache_values.height, + cache_values.prev_hash, + cache_values.timestamp.unwrap_or(TIMESTAMP_AT_3336491), + cache_values.seed_height.unwrap_or(SEED_HEIGHT_AT_3336491), + if let Some(hash) = cache_values.seed_hash { + &hex::encode(hash) + } else { + SEED_HASH_AT_3336491 + }, + ) + } else { + ( + BLOCK_HEIGHT_3336491, + Hash::from_str(BLOCK_HASH_AT_3336491).expect("will not fail"), + TIMESTAMP_AT_3336491, + SEED_HEIGHT_AT_3336491, + SEED_HASH_AT_3336491, + ) + }; + + let response = match method { + // If hash and height are not provided, this will return the Monero hash for block 3336491 + MonerodMethod::GetHeight => StaticResponse { + headers: { + let mut headers = hyper::HeaderMap::new(); + headers.insert("content-type", "application/json".parse().unwrap()); + headers + }, + version: hyper::Version::HTTP_11, + status: hyper::StatusCode::OK, + body: serde_json::json!({ + // The monero hash for blockchain at height 3331664 + "hash": hex::encode(hash), + "height": height, + "status": "OK", + "untrusted": false + }), + }, + // This was the 'get_version' response for monero blockchain height 3336491. A custom 'height' can be provided, + // but hard fork and version information will go out of date at the next hard fork. + MonerodMethod::GetVersion => StaticResponse { + headers: { + let mut headers = hyper::HeaderMap::new(); + headers.insert("content-type", "application/json".parse().unwrap()); + headers + }, + version: hyper::Version::HTTP_11, + status: hyper::StatusCode::OK, + body: serde_json::json!({ + "id": req_id, + "jsonrpc": "2.0", + "result": { + "current_height": height, + "hard_forks": [ + {"height": 1, "hf_version": 1}, + {"height": 1009827, "hf_version": 2}, + {"height": 1141317, "hf_version": 3}, + {"height": 1220516, "hf_version": 4}, + {"height": 1288616, "hf_version": 5}, + {"height": 1400000, "hf_version": 6}, + {"height": 1546000, "hf_version": 7}, + {"height": 1685555, "hf_version": 8}, + {"height": 1686275, "hf_version": 9}, + {"height": 1788000, "hf_version": 10}, + {"height": 1788720, "hf_version": 11}, + {"height": 1978433, "hf_version": 12}, + {"height": 2210000, "hf_version": 13}, + {"height": 2210720, "hf_version": 14}, + {"height": 2688888, "hf_version": 15}, + {"height": 2689608, "hf_version": 16} + ], + "release": true, + "status": "OK", + "untrusted": false, + "version": 196622 + } + }), + }, + // This will return an empty 'get_block_template' response for monero blockchain height 'height + 1'. Dynamic + // values are used as far as possible, for example 'hash' and 'timestamp' must be supplied. + MonerodMethod::GetBlockTemplate => { + let monero_block = get_empty_monero_block(hash, timestamp, height + 1, None); + let blockhashing_blob = monero_rx::create_blockhashing_blob_from_block(&monero_block)?; + let blocktemplate_blob = monero_rx::serialize_monero_block_to_hex(&monero_block)?; + + StaticResponse { + headers: { + let mut headers = hyper::HeaderMap::new(); + headers.insert("content-type", "application/json".parse().unwrap()); + headers + }, + version: hyper::Version::HTTP_11, + status: hyper::StatusCode::OK, + body: serde_json::json!({ + "id": req_id, + "jsonrpc": "2.0", + // The 'get_block_template' response for monero blockchain height 3334284 + "result": { + "blockhashing_blob": blockhashing_blob, + "blocktemplate_blob": blocktemplate_blob, + "difficulty": DIFFICULTY_AT_3336491, + "difficulty_top64": 0, + "expected_reward": REWARD_AT_3336491, + "height": height, + "next_seed_hash": "", + "prev_hash": hex::encode(monero_block.header.prev_id), + "reserved_offset": 0, + "seed_hash": seed_hash, + "seed_height": seed_height, + "status": "OK", + "untrusted": false, + "wide_difficulty": WIDE_DIFFICULTY_AT_3336491 + } + }), + } + }, + // This return an error response by design, as we can never construct a static block that will be accepted, and + // most if not all cases a block solved while merge mining will not be accepted by the Monero network. + MonerodMethod::SubmitBlock => StaticResponse { + headers: { + let mut headers = hyper::HeaderMap::new(); + headers.insert("content-type", "application/json".parse().unwrap()); + headers + }, + version: hyper::Version::HTTP_11, + status: hyper::StatusCode::OK, + body: serde_json::json!({ + "error": { + "code": -7, + "message": "Block not accepted" + }, + "id": req_id, + "jsonrpc": "2.0" + }), + }, + // This return an error response by design, as it is impossible to return the correct header or block + // corresponding to 1 of millions of correct answers while offline. + MonerodMethod::GetBlockHeaderByHash | MonerodMethod::GetBlock => StaticResponse { + headers: { + let mut headers = hyper::HeaderMap::new(); + headers.insert("content-type", "application/json".parse().unwrap()); + headers + }, + version: hyper::Version::HTTP_11, + status: hyper::StatusCode::OK, + body: serde_json::json!({ + "error": { + "code": -5, + "message": &format!("Internal error: can't get block by hash '{}'.", method) + }, + "id": req_id, + "jsonrpc": "2.0" + }), + }, + // This return a fixed header by design, as it is impossible to return the correct header corresponding to + // the last block while offline. + MonerodMethod::GetLastBlockHeader => StaticResponse { + headers: { + let mut headers = hyper::HeaderMap::new(); + headers.insert("content-type", "application/json".parse().unwrap()); + headers + }, + version: hyper::Version::HTTP_11, + status: hyper::StatusCode::OK, + body: serde_json::json!({ + "id": req_id, + "jsonrpc": "2.0", + "result": { + "block_header": { + "block_size": 3865, + "block_weight": 3865, + "cumulative_difficulty": 418129042015270429u64, + "cumulative_difficulty_top64": 0, + "depth": 0, + "difficulty": DIFFICULTY_AT_3336491, + "difficulty_top64": 0, + "hash": BLOCK_HASH_AT_3336491, + "height": height, + "long_term_weight": 176470, + "major_version": MAX_HF_VERSION, + "miner_tx_hash": "8112cdbbd21a99a347386d03e0798d095a356ddde84ebb574011cb8cc33c200f", + "minor_version": MAX_HF_VERSION, + "nonce": 67153, + "num_txes": 38, + "orphan_status": false, + "pow_hash": "", + "prev_hash": &hex::encode(hash), + "reward": REWARD_AT_3336491, + "timestamp": timestamp, + "wide_cumulative_difficulty": "0x5cd7e25fb98361d", + "wide_difficulty": WIDE_DIFFICULTY_AT_3336491 + }, + "credits": 0, + "status": "OK", + "top_hash": "", + "untrusted": false + } + }), + }, + MonerodMethod::RpcMethodNotDefined => StaticResponse { + headers: hyper::HeaderMap::new(), + version: hyper::Version::HTTP_11, + status: hyper::StatusCode::BAD_REQUEST, + body: serde_json::json!({"error": "Unknown method"}), + }, + }; + + Ok(response) +} + +// Monero block with only the miner transaction and no other transactions - miner data correspond to block 3336491 +pub(crate) fn get_empty_monero_block( + prev_id: Hash, + timestamp: u64, + height: u64, + merge_mining_tag: Option, +) -> Block { + // Miner transaction data for block 3336491 + const TX_KEY: &str = "67399a3d8caf949713cf3aae1f4027b29a8df626a167ed84aef2c011e3a9ff5f"; + const PUBLIC_KEY: &str = "9785629f62f7688cd7fc7025d1c6837a818fc5d09c0a2adb4f77545cfe57fb6b"; + const NONCE: &str = "115fe80c4c8a36c100000000000000000000"; + + let key = Key::from(Hash::from_str(TX_KEY).unwrap().to_bytes()).key; + + let mut sub_fields = vec![ + TxPublicKey(PublicKey::from_str(PUBLIC_KEY).unwrap()), + Nonce(hex::decode(NONCE).unwrap()), + ]; + if let Some(tag) = merge_mining_tag { + sub_fields.insert(0, tag); + } + let extra = RawExtraField::from(ExtraField(sub_fields)); + + Block { + header: BlockHeader { + major_version: VarInt(MAX_HF_VERSION), + minor_version: VarInt(MAX_HF_VERSION), + timestamp: VarInt(timestamp), + prev_id, + nonce: 0, + }, + // This is an arbitrary miner transaction + miner_tx: Transaction { + prefix: TransactionPrefix { + version: VarInt(2), + unlock_time: VarInt(height + 60), + inputs: vec![transaction::TxIn::Gen { height: VarInt(height) }], + outputs: vec![transaction::TxOut { + amount: VarInt(600741780000), + target: transaction::TxOutTarget::ToTaggedKey { key, view_tag: 223 }, + }], + extra, + }, + signatures: vec![], + rct_signatures: monero::util::ringct::RctSig { + sig: Some(monero::util::ringct::RctSigBase { + rct_type: monero::util::ringct::RctType::Null, + txn_fee: monero::util::amount::Amount::ZERO, + pseudo_outs: vec![], + ecdh_info: vec![], + out_pk: vec![], + }), + p: None, + }, + }, + tx_hashes: vec![ + Hash::from_str("6893e92efa26b95975f96c493de78600e2aac40b833552421ebe579d67b7b6ec").expect("will not fail"), + Hash::from_str("ddbeb9bc923255a3117c1483c14449bf459fea824ab13917516d3863d89e5d6a").expect("will not fail"), + ], + } +} + +pub(crate) fn convert_static_monerod_response_to_hyper_response( + method: MonerodMethod, + req_id: Option, + monerod_cache_values: Option, +) -> Result, MmProxyError> { + if let Some(cache_values) = monerod_cache_values.clone() { + trace!( + target: LOG_TARGET, + "[monerod] use static response for {}, req_id: {:?}, height: {:?}, prev_hash: {:?}, timestamp: {:?}, \ + seed_height: {:?}, seed_hash: {:?}", + method, req_id, + cache_values.height, + cache_values.prev_hash, + cache_values.timestamp, + cache_values.seed_height, + cache_values.seed_hash.map(hex::encode), + ); + } else { + trace!(target: LOG_TARGET, "[monerod] use static response for {}, req_id: {:?}", method, req_id); + } + let static_response = get_static_monerod_response(method, req_id, monerod_cache_values)?; + + let mut builder = hyper::Response::builder(); + + let headers = builder + .headers_mut() + .expect("headers_mut errors only when the builder has an error (e.g invalid header value)"); + headers.extend( + static_response + .headers + .iter() + .map(|(name, value)| (name.clone(), value.clone())), + ); + + builder = builder.version(static_response.version).status(static_response.status); + + let resp = builder.body(static_response.body)?; + Ok(resp) +} + +/// This is required for self-select configuration if the request is a block submission and we are not submitting blocks +/// to the origin (self-select mode) +pub(crate) fn self_select_submit_block_monerod_response(request_id: Option) -> serde_json::Value { + debug!( + target: LOG_TARGET, + "[monerod] skip: Proxy configured for self-select mode. Pool will submit to MoneroD, submitting to \ + Minotari.", + ); + + // We are not submitting the block to Monero here (the pool does this), + // we are only interested in intercepting the request for the purposes of + // submitting the block to Tari which will only happen if the accept response + // (which normally would occur for normal mining) is provided here. + // There is no point in trying to submit the block to Monero here since the + // share submitted by XMRig is only guaranteed to meet the difficulty of + // min(Tari,Monero) since that is what was returned with the original template. + // So it would otherwise be a duplicate submission of what the pool will do + // itself (whether the miner submits directly to monerod or the pool does, + // the pool is the only one being paid out here due to the nature + // of self-select). Furthermore, discussions with devs from Monero and XMRig are + // very much against spamming the nodes unnecessarily. + // NB!: This is by design, do not change this without understanding + // it's implications. + json_rpc::default_block_accept_response(request_id) +} + +pub(crate) fn static_json_rpc_url() -> Url { + Url::parse("http://82.64.166.200:18081/json_rpc").expect("Invalid URL") +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use hyper::HeaderMap; + use monero::{blockdata::transaction::SubField, Hash, VarInt}; + use regex::Regex; + use serde_json::json; + use tari_core::proof_of_work::monero_rx; + use url::Url; + + use crate::proxy::{ + inner::MonerodCacheValues, + monerod_method::MonerodMethod, + static_responses::{ + convert_static_monerod_response_to_hyper_response, + get_empty_monero_block, + get_static_monerod_response, + self_select_submit_block_monerod_response, + static_json_rpc_url, + BLOCK_HASH_AT_3336491, + MAX_HF_VERSION, + }, + test, + }; + + fn extract_error_json(response: &str) -> Option { + let re = Regex::new(r"Error: '.*?' failed \((\{.*\})\)").unwrap(); + if let Some(captures) = re.captures(response) { + if let Some(json_str) = captures.get(1) { + let json_value: serde_json::Value = serde_json::from_str(json_str.as_str()).unwrap(); + return Some(json_value); + } + } + None + } + + // To execute this test a merge mining proxy must be running (just verify the port, default used), together with a + // base node. This function can also be used to capture monerod responses. + // Note: `config.monerod_fallback` must be `MonerodOnly` or `StaticWhenMonerodFails` + #[tokio::test] + #[ignore] + async fn get_monerod_dynamic_responses() { + let json_rpc_port = 18081; + let mut responses = Vec::with_capacity(50); + + println!(); + for method in [ + MonerodMethod::GetHeight, + MonerodMethod::GetVersion, + MonerodMethod::GetBlockTemplate, + MonerodMethod::SubmitBlock, + MonerodMethod::GetBlockHeaderByHash, + MonerodMethod::GetLastBlockHeader, + MonerodMethod::GetBlock, + ] { + let block_hash = if method == MonerodMethod::GetBlockHeaderByHash || method == MonerodMethod::GetBlock { + Some(BLOCK_HASH_AT_3336491.to_string()) + } else { + None + }; + test::inner_json_rpc(method, json_rpc_port, &mut responses, 1, true, block_hash.clone(), None).await; + // Investigate the responses + let json: serde_json::Value = if responses[responses.len() - 1].contains(" response body: ") { + let response = responses[responses.len() - 1] + .split(" response body: ") + .collect::>()[1]; + serde_json::from_str(response).unwrap_or(serde_json::Value::Null) + } else { + extract_error_json(&responses[responses.len() - 1]).unwrap_or(serde_json::Value::Null) + }; + match method { + MonerodMethod::GetVersion => { + // Assert no error + assert_eq!(json["error"], json!(null)); + // Verify the hard fork version information is still up to date + let max_hf_version = get_max_hf_version(json); + assert_eq!(max_hf_version, MAX_HF_VERSION); + }, + MonerodMethod::GetHeight | + MonerodMethod::GetBlockTemplate | + MonerodMethod::GetBlockHeaderByHash | + MonerodMethod::GetLastBlockHeader | + MonerodMethod::GetBlock => { + // Assert no error + assert_eq!(json["error"], json!(null)); + }, + MonerodMethod::SubmitBlock => { + // Assert error + assert_ne!(json["error"], json!(null)); + }, + MonerodMethod::RpcMethodNotDefined => {}, + } + } + + // Manipulate the first numeric character of the hash that is less than 9 to get a different hash (we want + // "get_block_header_by_hash" to fail) + let mut hash = BLOCK_HASH_AT_3336491.to_string(); + if let Some(pos) = hash + .chars() + .position(|c| c.is_ascii_digit() && c.to_digit(10).unwrap() < 9) + { + let mut chars: Vec = hash.chars().collect(); + chars[pos] = std::char::from_digit(chars[pos].to_digit(10).unwrap() + 1, 10).unwrap(); + hash = chars.into_iter().collect(); + } + let method = MonerodMethod::GetBlockHeaderByHash; + test::inner_json_rpc(method, json_rpc_port, &mut responses, 1, true, Some(hash), None).await; + if let Some(json) = extract_error_json(&responses[responses.len() - 1]) { + // Assert error + assert_ne!(json["error"], json!(null)); + } else { + panic!("Expected error response"); + } + + println!(); + for response in responses { + println!("{}", response); + } + } + + fn headers_to_json(headers: &HeaderMap) -> serde_json::Value { + let mut map = serde_json::Map::new(); + for (key, value) in headers { + map.insert( + key.to_string(), + serde_json::Value::String(value.to_str().unwrap().to_string()), + ); + } + serde_json::Value::Object(map) + } + + #[test] + fn test_monerod_static_responses() { + for method in [ + MonerodMethod::GetHeight, + MonerodMethod::GetVersion, + MonerodMethod::GetBlockTemplate, + MonerodMethod::SubmitBlock, + MonerodMethod::GetBlockHeaderByHash, + MonerodMethod::GetLastBlockHeader, + MonerodMethod::GetBlock, + MonerodMethod::RpcMethodNotDefined, + ] { + let static_hyper_response = convert_static_monerod_response_to_hyper_response( + method, + Some(123), + Some(MonerodCacheValues { + height: 3331664, + prev_hash: Hash::from_str("98f83f921a006ccb8ab14ec7e7245e4a4350471027b4d490c41e8d84e4b8a196") + .unwrap(), + timestamp: Some(12345678), + seed_height: None, + seed_hash: None, + }), + ) + .unwrap(); + let static_response = get_static_monerod_response( + method, + Some(123), + Some(MonerodCacheValues { + height: 3331664, + prev_hash: Hash::from_str("98f83f921a006ccb8ab14ec7e7245e4a4350471027b4d490c41e8d84e4b8a196") + .unwrap(), + timestamp: Some(12345678), + seed_height: None, + seed_hash: None, + }), + ) + .unwrap(); + + // Version + assert_eq!(static_hyper_response.version(), static_response.version); + + // Status + assert_eq!(static_hyper_response.status(), static_response.status); + + // Headers + assert_eq!( + headers_to_json(static_hyper_response.headers()), + headers_to_json(&static_response.headers) + ); + + // Body + assert_eq!(static_hyper_response.body(), &static_response.body); + + if method == MonerodMethod::GetBlockTemplate { + let (_parts, monerod_resp) = static_hyper_response.into_parts(); + let blocktemplate_blob = monerod_resp["result"]["blocktemplate_blob"] + .to_string() + .replace('\"', ""); + assert!(monero_rx::deserialize_monero_block_from_hex(&blocktemplate_blob).is_ok()); + } + } + + let monerod_response = self_select_submit_block_monerod_response(Some(123)); + assert_eq!( + monerod_response, + json!({ + "id": 123, + "jsonrpc": "2.0", + "result": "{}", + "status": "OK", + "untrusted": false, + }) + ); + + assert_eq!( + static_json_rpc_url(), + Url::parse("http://82.64.166.200:18081/json_rpc").unwrap() + ); + } + + #[test] + fn test_get_empty_block() { + let prev_id = Hash::from_str("840915066009f63da3bf1160ce0ac3b2a57865d0b9329dcbf9ae1627200987d7").unwrap(); + let timestamp = 1234567890; + let height = 123456; + let block = get_empty_monero_block(prev_id, timestamp, height, None); + let extra = block.miner_tx.prefix.extra.try_parse(); + for field in &extra.0 { + if let SubField::MergeMining(..) = field { + panic!("Merge mining tag should not be present"); + } + } + + assert_eq!(block.header.prev_id, prev_id); + assert_eq!(block.header.timestamp, VarInt(timestamp)); + assert_eq!(block.header.major_version, VarInt(MAX_HF_VERSION)); + assert_eq!(block.header.minor_version, VarInt(MAX_HF_VERSION)); + + let blocktemplate_blob = monero_rx::serialize_monero_block_to_hex(&block).unwrap(); + assert!(monero_rx::deserialize_monero_block_from_hex(&blocktemplate_blob).is_ok()); + + const ARBITRARY_MERGE_MINING_HASH: &str = "8e6dab82d22909b40bda27ec0e96aa0c6c0012023d353f3be941eb6ef1793cad"; + let merge_mining_tag = Some(SubField::MergeMining( + VarInt(0), + Hash::from_str(ARBITRARY_MERGE_MINING_HASH).expect("will not fail"), + )); + let block = get_empty_monero_block(prev_id, timestamp, height, merge_mining_tag); + let extra = block.miner_tx.prefix.extra.try_parse(); + let mut found_merge_mining_tag = false; + for field in &extra.0 { + if let SubField::MergeMining(..) = field { + found_merge_mining_tag = true; + } + } + assert!(found_merge_mining_tag); + } + + fn get_max_hf_version(json: serde_json::Value) -> u64 { + json["result"]["hard_forks"] + .as_array() + .unwrap() + .iter() + .map(|hf| hf["hf_version"].as_u64().unwrap()) + .max() + .unwrap() + } + + #[test] + fn test_hf_version() { + let get_static = get_static_monerod_response(MonerodMethod::GetVersion, Some(123), None).unwrap(); + let max_hf_version = get_max_hf_version(get_static.body); + assert_eq!(max_hf_version, MAX_HF_VERSION); + } +} diff --git a/applications/minotari_merge_mining_proxy/src/proxy/utils.rs b/applications/minotari_merge_mining_proxy/src/proxy/utils.rs new file mode 100644 index 0000000000..c32158316a --- /dev/null +++ b/applications/minotari_merge_mining_proxy/src/proxy/utils.rs @@ -0,0 +1,149 @@ +// Copyright 2020, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bytes::Bytes; +use hyper::{Method, Request, Response}; +use minotari_app_grpc::tari_rpc; +use reqwest::ResponseBuilderExt; +use serde_json as json; +use serde_json::json; +use tari_utilities::hex::Hex; + +use crate::error::MmProxyError; + +/// The JSON object key name used for merge mining proxy response extensions +pub(crate) const MMPROXY_AUX_KEY_NAME: &str = "_aux"; + +pub async fn convert_reqwest_response_to_hyper_json_response( + resp: reqwest::Response, +) -> Result, MmProxyError> { + let mut builder = Response::builder(); + + let headers = builder + .headers_mut() + .expect("headers_mut errors only when the builder has an error (e.g invalid header value)"); + headers.extend(resp.headers().iter().map(|(name, value)| (name.clone(), value.clone()))); + + builder = builder + .version(resp.version()) + .status(resp.status()) + .url(resp.url().clone()); + + let body = resp.json().await.map_err(MmProxyError::MonerodRequestFailed)?; + let resp = builder.body(body)?; + Ok(resp) +} + +/// Add mmproxy extensions object to JSON RPC success response +pub fn add_aux_data(mut response: json::Value, mut ext: json::Value) -> json::Value { + if response["result"].is_null() { + return response; + } + match response["result"][MMPROXY_AUX_KEY_NAME].as_object_mut() { + Some(obj_mut) => { + let ext_mut = ext + .as_object_mut() + .expect("invalid parameter: expected `ext: json::Value` to be an object but it was not"); + obj_mut.append(ext_mut); + }, + None => { + response["result"][MMPROXY_AUX_KEY_NAME] = ext; + }, + } + response +} + +/// Append chain data to the result object. If the result object is null, a JSON object is created. +/// +/// ## Panics +/// +/// If response["result"] is not a JSON object type or null. +pub fn append_aux_chain_data(mut response: json::Value, chain_data: json::Value) -> json::Value { + let result = &mut response["result"]; + if result.is_null() { + *result = json!({}); + } + let chains = match result[MMPROXY_AUX_KEY_NAME]["chains"].as_array_mut() { + Some(arr_mut) => arr_mut, + None => { + result[MMPROXY_AUX_KEY_NAME]["chains"] = json!([]); + result[MMPROXY_AUX_KEY_NAME]["chains"].as_array_mut().unwrap() + }, + }; + + chains.push(chain_data); + response +} + +pub fn try_into_json_block_header(header: tari_rpc::BlockHeaderResponse) -> Result { + let tari_rpc::BlockHeaderResponse { + header, + reward, + confirmations, + difficulty, + num_transactions, + } = header; + let header = header.ok_or_else(|| { + MmProxyError::UnexpectedTariBaseNodeResponse( + "Base node GRPC returned an empty header field when calling get_header_by_hash".into(), + ) + })?; + + Ok(json!({ + "block_size": 0, + "depth": confirmations, + "difficulty": difficulty, + "hash": header.hash.to_hex(), + "height": header.height, + "major_version": header.version, + "minor_version": 0, + "nonce": header.nonce, + "num_txes": num_transactions, + // Cannot be an orphan + "orphan_status": false, + "prev_hash": header.prev_hash.to_hex(), + "reward": reward, + "timestamp": header.timestamp + })) +} + +/// Parse the method name from the request +pub fn parse_method_name(request: &Request) -> String { + match *request.method() { + Method::GET => { + let mut chars = request.uri().path().chars(); + chars.next(); + chars.as_str().to_string() + }, + Method::POST => { + let json = json::from_slice::(request.body()).unwrap_or_default(); + str::replace(json["method"].as_str().unwrap_or_default(), "\"", "") + }, + _ => "unsupported".to_string(), + } +} + +/// Convert a request with a Bytes body to a request with a json Value body +pub fn request_bytes_to_value(request: Request) -> Result, MmProxyError> { + let json = json::from_slice::(request.body())?; + Ok(request.map(move |_| json)) +} diff --git a/applications/minotari_merge_mining_proxy/src/run_merge_miner.rs b/applications/minotari_merge_mining_proxy/src/run_merge_miner.rs index d2b60d6ebc..1ad8d1d1bf 100644 --- a/applications/minotari_merge_mining_proxy/src/run_merge_miner.rs +++ b/applications/minotari_merge_mining_proxy/src/run_merge_miner.rs @@ -43,10 +43,10 @@ use tonic::transport::{Certificate, ClientTlsConfig, Endpoint}; use crate::{ block_template_data::BlockTemplateRepository, - config::MergeMiningProxyConfig, + config::{MergeMiningProxyConfig, MonerodFallback}, error::MmProxyError, monero_fail::{get_monerod_info, order_and_select_monerod_info, MonerodEntry}, - proxy::{MergeMiningProxyService, MONEROD_CONNECTION_TIMEOUT, NUMBER_OF_MONEROD_SERVERS}, + proxy::service::MergeMiningProxyService, Cli, }; @@ -59,47 +59,52 @@ pub async fn start_merge_miner(cli: Cli) -> Result<(), anyhow::Error> { let mut config = MergeMiningProxyConfig::load_from(&cfg)?; config.set_base_path(cli.common.get_base_path()); - // Get reputable monerod URLs - let mut assigned_dynamic_fail = false; - if config.use_dynamic_fail_data { - if let Ok(entries) = get_monerod_info( - NUMBER_OF_MONEROD_SERVERS, - MONEROD_CONNECTION_TIMEOUT, - &config.monero_fail_url, - ) - .await - { - if !entries.is_empty() { - let entries_len = entries.len(); - config.monerod_url = StringList::from(entries.into_iter().map(|entry| entry.url).collect::>()); - assigned_dynamic_fail = true; - debug!( - target: LOG_TARGET, - "Using {} vetted monerod servers from the Monero website at '{}'", - entries_len, config.monero_fail_url - ); + if config.monerod_fallback != MonerodFallback::StaticOnly { + // Get reputable monerod URLs + let mut assigned_dynamic_fail = false; + if config.use_dynamic_fail_data { + if let Ok(entries) = get_monerod_info( + NUMBER_OF_MONEROD_SERVERS, + config.monerod_connection_timeout, + &config.monero_fail_url, + ) + .await + { + if !entries.is_empty() { + let entries_len = entries.len(); + config.monerod_url = + StringList::from(entries.into_iter().map(|entry| entry.url).collect::>()); + assigned_dynamic_fail = true; + debug!( + target: LOG_TARGET, + "Using {} vetted monerod servers from the Monero website at '{}'", + entries_len, config.monero_fail_url + ); + } } } - } - if !assigned_dynamic_fail { - let mut entries = Vec::new(); - for url in config.monerod_url.clone().into_vec() { - entries.push(MonerodEntry { - url, - ..Default::default() - }); - } - if let Ok(entries) = - order_and_select_monerod_info(NUMBER_OF_MONEROD_SERVERS, MONEROD_CONNECTION_TIMEOUT, &entries).await - { - if !entries.is_empty() { - let entries_len = entries.len(); - config.monerod_url = StringList::from(entries.into_iter().map(|entry| entry.url).collect::>()); - debug!( - target: LOG_TARGET, - "Using {} vetted monerod servers from the config list'", - entries_len - ); + if !assigned_dynamic_fail { + let mut entries = Vec::new(); + for url in config.monerod_url.clone().into_vec() { + entries.push(MonerodEntry { + url, + ..Default::default() + }); + } + if let Ok(entries) = + order_and_select_monerod_info(NUMBER_OF_MONEROD_SERVERS, config.monerod_connection_timeout, &entries) + .await + { + if !entries.is_empty() { + let entries_len = entries.len(); + config.monerod_url = + StringList::from(entries.into_iter().map(|entry| entry.url).collect::>()); + debug!( + target: LOG_TARGET, + "Using {} vetted monerod servers from the config list'", + entries_len + ); + } } } } @@ -265,3 +270,5 @@ async fn connect_sha_p2pool(config: &MergeMiningProxyConfig) -> Result, } impl ConfigOverrideProvider for Cli { diff --git a/applications/minotari_node/src/commands/command/test_peer_liveness.rs b/applications/minotari_node/src/commands/command/test_peer_liveness.rs index bda5c3a30a..b933cf8916 100644 --- a/applications/minotari_node/src/commands/command/test_peer_liveness.rs +++ b/applications/minotari_node/src/commands/command/test_peer_liveness.rs @@ -119,6 +119,7 @@ impl HandleCommand for CommandContext { } // Wait for the liveness test to complete + let mut count = 0; loop { tokio::select! { _ = rx.changed() => { @@ -153,7 +154,20 @@ impl HandleCommand for CommandContext { break; }, - _ = tokio::time::sleep(Duration::from_secs(1)) => {}, + _ = tokio::time::sleep(Duration::from_secs(1)) => { + count += 1; + if count >= 180 { + if let Some(true) = args.exit { + println!(" >> The liveness test failed to complete and base node will now exit\n"); + self.shutdown.trigger(); + tokio::time::sleep(Duration::from_secs(1)).await; + process::exit(1) + } else { + println!(" >> The liveness test failed to complete\n"); + break; + } + } + }, } } diff --git a/applications/minotari_node/src/grpc/base_node_grpc_server.rs b/applications/minotari_node/src/grpc/base_node_grpc_server.rs index c6e9a5f869..27a3b2f1c1 100644 --- a/applications/minotari_node/src/grpc/base_node_grpc_server.rs +++ b/applications/minotari_node/src/grpc/base_node_grpc_server.rs @@ -188,7 +188,12 @@ impl BaseNodeGrpcServer { fn check_method_enabled(&self, method: GrpcMethod) -> Result<(), Status> { if !self.is_method_enabled(method) { - warn!(target: LOG_TARGET, "`{}` method called but it is not allowed. Allow it in the config file or start the node with a different set of CLI options", method); + warn!( + target: LOG_TARGET, + "`{}` method called but it is not allowed. Allow it in the config file or start the node with a \ + different set of CLI options", + method + ); return Err(Status::permission_denied(format!( "`{}` method not made available", method diff --git a/applications/minotari_node/src/lib.rs b/applications/minotari_node/src/lib.rs index 681c4a6bb0..f45da918db 100644 --- a/applications/minotari_node/src/lib.rs +++ b/applications/minotari_node/src/lib.rs @@ -92,6 +92,7 @@ pub async fn run_base_node( mining_enabled: false, second_layer_grpc_enabled: false, disable_splash_screen: false, + libtor_data_dir: None, }; run_base_node_with_cli(node_identity, config, cli, shutdown).await @@ -169,7 +170,6 @@ pub async fn run_base_node_with_cli( info!(target: LOG_TARGET, "Minotari base node has STARTED"); main_loop.cli_loop(cli.disable_splash_screen).await; - ctx.wait_for_shutdown().await; println!("Goodbye!"); diff --git a/applications/minotari_node/src/main.rs b/applications/minotari_node/src/main.rs index 3f76c71abd..272ad865bc 100644 --- a/applications/minotari_node/src/main.rs +++ b/applications/minotari_node/src/main.rs @@ -152,7 +152,12 @@ fn main_inner() -> Result<(), ExitError> { // This is currently only possible on linux/macos #[cfg(all(unix, feature = "libtor"))] if config.base_node.use_libtor && config.base_node.p2p.transport.is_tor() { - let tor = Tor::initialize()?; + let data_dir = if let Some(dir) = cli.libtor_data_dir.clone() { + dir.join("libtor").join("base_node") + } else { + cli.common.get_base_path().join("libtor").join("base_node") + }; + let tor = Tor::initialize(data_dir)?; tor.update_comms_transport(&mut config.base_node.p2p.transport)?; tor.run_background(); debug!( diff --git a/base_layer/chat_ffi/Cargo.toml b/base_layer/chat_ffi/Cargo.toml index b56c7bb1fe..aaf1938e0c 100644 --- a/base_layer/chat_ffi/Cargo.toml +++ b/base_layer/chat_ffi/Cargo.toml @@ -3,7 +3,7 @@ name = "minotari_chat_ffi" authors = ["The Tari Development Community"] description = "Tari cryptocurrency chat C FFI bindings" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" [dependencies] @@ -28,7 +28,7 @@ thiserror = "1.0.26" tokio = "1.23" [target.'cfg(target_os="android")'.dependencies] -openssl = { version = "0.10.66", features = ["vendored"] } +openssl = { version = "0.10.70", features = ["vendored"] } [lib] crate-type = ["staticlib", "cdylib"] diff --git a/base_layer/common_types/Cargo.toml b/base_layer/common_types/Cargo.toml index 00c8596e5c..99c30690d5 100644 --- a/base_layer/common_types/Cargo.toml +++ b/base_layer/common_types/Cargo.toml @@ -3,13 +3,13 @@ name = "tari_common_types" authors = ["The Tari Development Community"] description = "Tari cryptocurrency common types" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" [dependencies] tari_crypto = { version = "0.22.0" } tari_utilities = { version = "0.8" } -tari_common = { path = "../../common", version = "1.11.1-pre.1" } +tari_common = { path = "../../common", version = "1.11.3-pre.0" } minotari_ledger_wallet_common = { path = "../../applications/minotari_ledger_wallet/common" } chacha20poly1305 = "0.10.1" bitflags = { version = "2.4", features = ["serde"] } diff --git a/base_layer/contacts/Cargo.toml b/base_layer/contacts/Cargo.toml index a937f77ab1..945fe7e4d6 100644 --- a/base_layer/contacts/Cargo.toml +++ b/base_layer/contacts/Cargo.toml @@ -3,22 +3,22 @@ name = "tari_contacts" authors = ["The Tari Development Community"] description = "Tari contacts library" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" [dependencies] -tari_common = { path = "../../common", version = "1.11.1-pre.1" } -tari_common_sqlite = { path = "../../common_sqlite", version = "1.11.1-pre.1" } -tari_common_types = { path = "../../base_layer/common_types", version = "1.11.1-pre.1" } -tari_comms = { path = "../../comms/core", version = "1.11.1-pre.1" } -tari_comms_dht = { path = "../../comms/dht", version = "1.11.1-pre.1" } +tari_common = { path = "../../common", version = "1.11.3-pre.0" } +tari_common_sqlite = { path = "../../common_sqlite", version = "1.11.3-pre.0" } +tari_common_types = { path = "../../base_layer/common_types", version = "1.11.3-pre.0" } +tari_comms = { path = "../../comms/core", version = "1.11.3-pre.0" } +tari_comms_dht = { path = "../../comms/dht", version = "1.11.3-pre.0" } tari_crypto = { version = "0.22.0" } tari_max_size = { path = "../../infrastructure/max_size" } tari_p2p = { path = "../p2p", features = [ "auto-update", -], version = "1.11.1-pre.1" } -tari_service_framework = { path = "../service_framework", version = "1.11.1-pre.1" } -tari_shutdown = { path = "../../infrastructure/shutdown", version = "1.11.1-pre.1" } +], version = "1.11.3-pre.0" } +tari_service_framework = { path = "../service_framework", version = "1.11.3-pre.0" } +tari_shutdown = { path = "../../infrastructure/shutdown", version = "1.11.3-pre.0" } tari_utilities = { version = "0.8" } chrono = { version = "0.4.39", default-features = false, features = ["serde"] } @@ -48,7 +48,7 @@ tari_test_utils = { path = "../../infrastructure/test_utils" } tempfile = "3.1.0" [build-dependencies] -tari_common = { path = "../../common", version = "1.11.1-pre.1" } +tari_common = { path = "../../common", version = "1.11.3-pre.0" } [package.metadata.cargo-machete] ignored = [ diff --git a/base_layer/contacts/src/chat_client/Cargo.toml b/base_layer/contacts/src/chat_client/Cargo.toml index 3207d721a1..6f16fdee8d 100644 --- a/base_layer/contacts/src/chat_client/Cargo.toml +++ b/base_layer/contacts/src/chat_client/Cargo.toml @@ -3,7 +3,7 @@ name = "tari_chat_client" authors = ["The Tari Development Community"] description = "Tari cucumber chat client" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" diff --git a/base_layer/core/Cargo.toml b/base_layer/core/Cargo.toml index 19b2d50881..39b81a8250 100644 --- a/base_layer/core/Cargo.toml +++ b/base_layer/core/Cargo.toml @@ -6,7 +6,7 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" [features] @@ -28,26 +28,26 @@ ledger = ["minotari_ledger_wallet_comms"] metrics = ["tari_metrics"] [dependencies] -minotari_ledger_wallet_comms = { path = "../../applications/minotari_ledger_wallet/comms", version = "1.11.1-pre.1", optional = true } -tari_common = { path = "../../common", version = "1.11.1-pre.1" } -tari_common_types = { path = "../../base_layer/common_types", version = "1.11.1-pre.1" } -tari_comms = { path = "../../comms/core", version = "1.11.1-pre.1" } -tari_comms_dht = { path = "../../comms/dht", version = "1.11.1-pre.1" } -tari_comms_rpc_macros = { path = "../../comms/rpc_macros", version = "1.11.1-pre.1" } +minotari_ledger_wallet_comms = { path = "../../applications/minotari_ledger_wallet/comms", version = "1.11.3-pre.0", optional = true } +tari_common = { path = "../../common", version = "1.11.3-pre.0" } +tari_common_types = { path = "../../base_layer/common_types", version = "1.11.3-pre.0" } +tari_comms = { path = "../../comms/core", version = "1.11.3-pre.0" } +tari_comms_dht = { path = "../../comms/dht", version = "1.11.3-pre.0" } +tari_comms_rpc_macros = { path = "../../comms/rpc_macros", version = "1.11.3-pre.0" } tari_crypto = { version = "0.22.0", features = ["borsh"] } tari_max_size = { path = "../../infrastructure/max_size" } -tari_metrics = { path = "../../infrastructure/metrics", optional = true, version = "1.11.1-pre.1" } -tari_mmr = { path = "../../base_layer/mmr", optional = true, version = "1.11.1-pre.1" } -tari_p2p = { path = "../../base_layer/p2p", version = "1.11.1-pre.1" } -tari_script = { path = "../../infrastructure/tari_script", version = "1.11.1-pre.1" } -tari_service_framework = { path = "../service_framework", version = "1.11.1-pre.1" } -tari_shutdown = { path = "../../infrastructure/shutdown", version = "1.11.1-pre.1" } -tari_storage = { path = "../../infrastructure/storage", version = "1.11.1-pre.1" } -tari_test_utils = { path = "../../infrastructure/test_utils", version = "1.11.1-pre.1" } +tari_metrics = { path = "../../infrastructure/metrics", optional = true, version = "1.11.3-pre.0" } +tari_mmr = { path = "../../base_layer/mmr", optional = true, version = "1.11.3-pre.0" } +tari_p2p = { path = "../../base_layer/p2p", version = "1.11.3-pre.0" } +tari_script = { path = "../../infrastructure/tari_script", version = "1.11.3-pre.0" } +tari_service_framework = { path = "../service_framework", version = "1.11.3-pre.0" } +tari_shutdown = { path = "../../infrastructure/shutdown", version = "1.11.3-pre.0" } +tari_storage = { path = "../../infrastructure/storage", version = "1.11.3-pre.0" } +tari_test_utils = { path = "../../infrastructure/test_utils", version = "1.11.3-pre.0" } tari_utilities = { version = "0.8", features = ["borsh"] } tari_key_manager = { path = "../key_manager", features = [ "key_manager_service", -], version = "1.11.1-pre.1" } +], version = "1.11.3-pre.0" } tari_common_sqlite = { path = "../../common_sqlite" } tari_hashing = { path = "../../hashing" } @@ -123,8 +123,8 @@ static_assertions = "1.1.0" [build-dependencies] tari_common = { path = "../../common", features = [ "build", -], version = "1.11.1-pre.1" } -tari_features = { path = "../../common/tari_features", version = "1.11.1-pre.1" } +], version = "1.11.3-pre.0" } +tari_features = { path = "../../common/tari_features", version = "1.11.3-pre.0" } [[bench]] name = "mempool" diff --git a/base_layer/core/src/base_node/rpc/service.rs b/base_layer/core/src/base_node/rpc/service.rs index 34b8a30d46..a07d47e7f3 100644 --- a/base_layer/core/src/base_node/rpc/service.rs +++ b/base_layer/core/src/base_node/rpc/service.rs @@ -575,7 +575,7 @@ impl BaseNodeWalletService for BaseNodeWalletRpc async fn get_height_at_time(&self, request: Request) -> Result, RpcStatus> { let requested_epoch_time: u64 = request.into_message(); - + trace!(target: LOG_TARGET, "requested_epoch_time: {}", requested_epoch_time); let tip_header = self .db() .fetch_tip_header() @@ -584,6 +584,13 @@ impl BaseNodeWalletService for BaseNodeWalletRpc let mut left_height = 0u64; let mut right_height = tip_header.height(); + trace!( + target: LOG_TARGET, + "requested_epoch_time: {}, left: {}, right: {}", + requested_epoch_time, + left_height, + right_height + ); while left_height <= right_height { let mut mid_height = (left_height + right_height) / 2; @@ -612,11 +619,32 @@ impl BaseNodeWalletService for BaseNodeWalletRpc .ok_or_else(|| { RpcStatus::not_found(&format!("Header not found during search at height {}", mid_height - 1)) })?; + trace!( + target: LOG_TARGET, + "requested_epoch_time: {}, left: {}, mid: {}/{} ({}/{}), right: {}", + requested_epoch_time, + left_height, + mid_height, + mid_height-1, + mid_header.timestamp.as_u64(), + before_mid_header.timestamp.as_u64(), + right_height + ); if requested_epoch_time < mid_header.timestamp.as_u64() && requested_epoch_time >= before_mid_header.timestamp.as_u64() { + trace!( + target: LOG_TARGET, + "requested_epoch_time: {}, selected height: {}", + requested_epoch_time, before_mid_header.height + ); return Ok(Response::new(before_mid_header.height)); } else if mid_height == right_height { + trace!( + target: LOG_TARGET, + "requested_epoch_time: {}, selected height: {}", + requested_epoch_time, right_height + ); return Ok(Response::new(right_height)); } else if requested_epoch_time <= mid_header.timestamp.as_u64() { right_height = mid_height; diff --git a/base_layer/core/src/proof_of_work/monero_rx/helpers.rs b/base_layer/core/src/proof_of_work/monero_rx/helpers.rs index 7f57275e38..fb13e96aef 100644 --- a/base_layer/core/src/proof_of_work/monero_rx/helpers.rs +++ b/base_layer/core/src/proof_of_work/monero_rx/helpers.rs @@ -221,7 +221,7 @@ pub fn deserialize_monero_block_from_hex(data: T) -> Result { let bytes = hex::decode(data).map_err(|_| HexError::HexConversionError {})?; let obj = consensus::deserialize::(&bytes) - .map_err(|_| MergeMineError::ValidationError("blocktemplate blob invalid".to_string()))?; + .map_err(|e| MergeMineError::ValidationError(format!("blocktemplate blob invalid: {}", e)))?; Ok(obj) } diff --git a/base_layer/key_manager/Cargo.toml b/base_layer/key_manager/Cargo.toml index 3b2f2ac2b2..db1c8420b2 100644 --- a/base_layer/key_manager/Cargo.toml +++ b/base_layer/key_manager/Cargo.toml @@ -4,7 +4,7 @@ authors = ["The Tari Development Community"] description = "Tari cryptocurrency wallet key management" repository = "https://github.com/tari-project/tari" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2021" [lib] @@ -13,9 +13,9 @@ crate-type = ["lib", "cdylib"] [dependencies] tari_crypto = { version = "0.22.0" } tari_utilities = { version = "0.8" } -tari_common_sqlite = { path = "../../common_sqlite", version = "1.11.1-pre.1" } -tari_common_types = { path = "../../base_layer/common_types", version = "1.11.1-pre.1" } -tari_service_framework = { path = "../service_framework", version = "1.11.1-pre.1" } +tari_common_sqlite = { path = "../../common_sqlite", version = "1.11.3-pre.0" } +tari_common_types = { path = "../../base_layer/common_types", version = "1.11.3-pre.0" } +tari_service_framework = { path = "../service_framework", version = "1.11.3-pre.0" } async-trait = { version = "0.1.50" } chrono = { version = "0.4.39", default-features = false, features = ["serde"] } diff --git a/base_layer/key_manager/src/cipher_seed.rs b/base_layer/key_manager/src/cipher_seed.rs index 469b765417..845f8b0b11 100644 --- a/base_layer/key_manager/src/cipher_seed.rs +++ b/base_layer/key_manager/src/cipher_seed.rs @@ -70,6 +70,7 @@ pub const CIPHER_SEED_MAC_BYTES: usize = 5; pub const CIPHER_SEED_ENCRYPTION_KEY_BYTES: usize = 32; pub const CIPHER_SEED_MAC_KEY_BYTES: usize = 32; pub const CIPHER_SEED_CHECKSUM_BYTES: usize = 4; +pub const SECONDS_PER_DAY: u64 = 24 * 60 * 60; /// This is an implementation of a Cipher Seed based on the `aezeed` encoding scheme: /// https://github.com/lightningnetwork/lnd/tree/master/aezeed @@ -134,7 +135,6 @@ impl CipherSeed { /// Generate a new seed pub fn new() -> Self { use std::time::{Duration, SystemTime, UNIX_EPOCH}; - const SECONDS_PER_DAY: u64 = 24 * 60 * 60; let birthday_genesis_date = UNIX_EPOCH + Duration::from_secs(BIRTHDAY_GENESIS_FROM_UNIX_EPOCH); let days = SystemTime::now() .duration_since(birthday_genesis_date) @@ -148,7 +148,7 @@ impl CipherSeed { #[cfg(target_arch = "wasm32")] /// Generate a new seed pub fn new() -> Self { - const MILLISECONDS_PER_DAY: u64 = 24 * 60 * 60 * 1000; + const MILLISECONDS_PER_DAY: u64 = SECONDS_PER_DAY * 1000; let millis = js_sys::Date::now() as u64; let days = millis / MILLISECONDS_PER_DAY; let birthday = u16::try_from(days).unwrap_or(0u16); // default to the epoch on error @@ -447,10 +447,11 @@ impl Mnemonic for CipherSeed { mod test { use std::str::FromStr; + use chrono::{DateTime, TimeZone, Utc}; use crc32fast::Hasher as CrcHasher; use tari_utilities::{Hidden, SafePassword}; - use super::BIRTHDAY_GENESIS_FROM_UNIX_EPOCH; + use super::{BIRTHDAY_GENESIS_FROM_UNIX_EPOCH, SECONDS_PER_DAY}; use crate::{ cipher_seed::{ CipherSeed, @@ -663,7 +664,7 @@ mod test { let birthday_genesis_time_in_seconds = get_birthday_from_unix_epoch_in_seconds(birthday, to_days); assert_eq!( birthday_genesis_time_in_seconds, - BIRTHDAY_GENESIS_FROM_UNIX_EPOCH + u64::from(birthday - to_days) * 24 * 60 * 60 + BIRTHDAY_GENESIS_FROM_UNIX_EPOCH + u64::from(birthday - to_days) * SECONDS_PER_DAY ); } @@ -678,4 +679,56 @@ mod test { // to 3th July 2022 00:00:00 assert_eq!(birthday_from_unix_epoch, 1656806400); } + + // This test simulates a wallet that was created on the 29th January 2025, one day after the birthday genesis date + // of 28th January 2025. The birthday is 1124 days after the genesis date of 1st January 2022. The wallet will + // request blocks from the base node 14 days before its birthday, which is 15th January 2025. + #[test] + fn verify_birthday_genesis_from_unix_epoch_constant() { + let birthday = 1124u16; + let to_days = 14u16; + let year = 2025; + let month = 1; + let day = 29; + let day_less_to_days = day - u32::from(to_days); + + // Verify birthday genesis from unix epoch constant + let date = Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0); + let seconds_since_epoch = date.unwrap().timestamp(); + assert_eq!( + BIRTHDAY_GENESIS_FROM_UNIX_EPOCH, + u64::try_from(seconds_since_epoch).unwrap() + ); + + // The wallet birthday is 1124 days after the genesis date + let birthday_epoch_time = i64::try_from(BIRTHDAY_GENESIS_FROM_UNIX_EPOCH).unwrap() + + i64::from(birthday) * i64::try_from(SECONDS_PER_DAY).unwrap(); + assert_eq!(birthday_epoch_time, 1738108800i64); + let birthday_epoch_time_date = Utc.timestamp_opt(birthday_epoch_time, 0); + assert_eq!( + birthday_epoch_time_date.unwrap(), + Utc.with_ymd_and_hms(year, month, day, 0, 0, 0).unwrap() + ); + + // The wallet will request blocks from the base node 14 days before its birthday + let birthday_epoch_time_less_to_days = + birthday_epoch_time - i64::from(to_days) * i64::try_from(SECONDS_PER_DAY).unwrap(); + assert_eq!( + birthday_epoch_time_less_to_days, + i64::try_from(get_birthday_from_unix_epoch_in_seconds(birthday, to_days)).unwrap() + ); + assert_eq!(birthday_epoch_time_less_to_days, 1736899200i64); + let birthday_epoch_time_less_to_days_date = Utc.timestamp_opt(birthday_epoch_time_less_to_days, 0); + assert_eq!( + birthday_epoch_time_less_to_days_date.unwrap(), + Utc.with_ymd_and_hms(year, month, day_less_to_days, 0, 0, 0).unwrap() + ); + + // The base node will return the genesis block as the first block to scan from + let genesis_date_time = DateTime::parse_from_rfc2822("28 Jan 2025 08:00:00 +0200").unwrap(); + let genesis_epoch_time = genesis_date_time.timestamp(); + assert_eq!(genesis_epoch_time, 1738044000i64); + assert!(genesis_epoch_time > birthday_epoch_time_less_to_days); + assert!(genesis_epoch_time < birthday_epoch_time); + } } diff --git a/base_layer/mmr/Cargo.toml b/base_layer/mmr/Cargo.toml index dfea4cdfbe..795b8c451c 100644 --- a/base_layer/mmr/Cargo.toml +++ b/base_layer/mmr/Cargo.toml @@ -4,7 +4,7 @@ authors = ["The Tari Development Community"] description = "A Merkle Mountain Range implementation" repository = "https://github.com/tari-project/tari" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" [features] diff --git a/base_layer/p2p/Cargo.toml b/base_layer/p2p/Cargo.toml index a1b0e474a4..e053f6462d 100644 --- a/base_layer/p2p/Cargo.toml +++ b/base_layer/p2p/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tari_p2p" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" authors = ["The Tari Development community"] description = "Tari base layer-specific peer-to-peer communication features" repository = "https://github.com/tari-project/tari" @@ -10,12 +10,12 @@ license = "BSD-3-Clause" edition = "2021" [dependencies] -tari_comms = { path = "../../comms/core", version = "1.11.1-pre.1" } -tari_comms_dht = { path = "../../comms/dht", version = "1.11.1-pre.1" } -tari_common = { path = "../../common", version = "1.11.1-pre.1" } -tari_service_framework = { path = "../service_framework", version = "1.11.1-pre.1" } -tari_shutdown = { path = "../../infrastructure/shutdown", version = "1.11.1-pre.1" } -tari_storage = { path = "../../infrastructure/storage", version = "1.11.1-pre.1" } +tari_comms = { path = "../../comms/core", version = "1.11.3-pre.0" } +tari_comms_dht = { path = "../../comms/dht", version = "1.11.3-pre.0" } +tari_common = { path = "../../common", version = "1.11.3-pre.0" } +tari_service_framework = { path = "../service_framework", version = "1.11.3-pre.0" } +tari_shutdown = { path = "../../infrastructure/shutdown", version = "1.11.3-pre.0" } +tari_storage = { path = "../../infrastructure/storage", version = "1.11.3-pre.0" } tari_utilities = { version = "0.8" } anyhow = "1.0.53" @@ -56,7 +56,7 @@ tempfile = "3.1.0" [build-dependencies] tari_common = { path = "../../common", features = [ "build", -], version = "1.11.1-pre.1" } +], version = "1.11.3-pre.0" } [features] test-mocks = [] diff --git a/base_layer/service_framework/Cargo.toml b/base_layer/service_framework/Cargo.toml index 3237aab33d..2dd54458b2 100644 --- a/base_layer/service_framework/Cargo.toml +++ b/base_layer/service_framework/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tari_service_framework" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" authors = ["The Tari Development Community"] description = "The Tari communication stack service framework" repository = "https://github.com/tari-project/tari" @@ -10,7 +10,7 @@ license = "BSD-3-Clause" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tari_shutdown = { path = "../../infrastructure/shutdown", version = "1.11.1-pre.1" } +tari_shutdown = { path = "../../infrastructure/shutdown", version = "1.11.3-pre.0" } anyhow = "1.0.53" async-trait = "0.1.50" diff --git a/base_layer/tari_mining_helper_ffi/Cargo.toml b/base_layer/tari_mining_helper_ffi/Cargo.toml index 1140676248..6fc54de1c3 100644 --- a/base_layer/tari_mining_helper_ffi/Cargo.toml +++ b/base_layer/tari_mining_helper_ffi/Cargo.toml @@ -3,7 +3,7 @@ name = "minotari_mining_helper_ffi" authors = ["The Tari Development Community"] description = "Tari cryptocurrency miningcore C FFI bindings" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" [dependencies] @@ -15,7 +15,7 @@ tari_core = { path = "../core", default-features = false, features = [ "base_node_proto", "base_node", ] } -tari_common_types = { path = "../../base_layer/common_types", version = "1.11.1-pre.1" } +tari_common_types = { path = "../../base_layer/common_types", version = "1.11.3-pre.0" } tari_utilities = { version = "0.8" } libc = "0.2.65" thiserror = "1.0.26" @@ -27,7 +27,7 @@ tari_core = { path = "../core", features = ["transactions", "base_node"] } rand = "0.8" [build-dependencies] -tari_features = { path = "../../common/tari_features", version = "1.11.1-pre.1" } +tari_features = { path = "../../common/tari_features", version = "1.11.3-pre.0" } cbindgen = "0.24.3" tari_common = { path = "../../common", features = [ "build", diff --git a/base_layer/wallet/Cargo.toml b/base_layer/wallet/Cargo.toml index f5b62ceb4f..838f47ab0b 100644 --- a/base_layer/wallet/Cargo.toml +++ b/base_layer/wallet/Cargo.toml @@ -3,32 +3,32 @@ name = "minotari_wallet" authors = ["The Tari Development Community"] description = "Tari cryptocurrency wallet library" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" [dependencies] -tari_common = { path = "../../common", version = "1.11.1-pre.1" } -tari_common_sqlite = { path = "../../common_sqlite", version = "1.11.1-pre.1" } -tari_common_types = { path = "../../base_layer/common_types", version = "1.11.1-pre.1" } -tari_comms = { path = "../../comms/core", version = "1.11.1-pre.1" } -tari_comms_dht = { path = "../../comms/dht", version = "1.11.1-pre.1" } -tari_contacts = { path = "../../base_layer/contacts", version = "1.11.1-pre.1" } +tari_common = { path = "../../common", version = "1.11.3-pre.0" } +tari_common_sqlite = { path = "../../common_sqlite", version = "1.11.3-pre.0" } +tari_common_types = { path = "../../base_layer/common_types", version = "1.11.3-pre.0" } +tari_comms = { path = "../../comms/core", version = "1.11.3-pre.0" } +tari_comms_dht = { path = "../../comms/dht", version = "1.11.3-pre.0" } +tari_contacts = { path = "../../base_layer/contacts", version = "1.11.3-pre.0" } tari_core = { path = "../../base_layer/core", default-features = false, features = [ "transactions", "mempool_proto", "base_node_proto", -], version = "1.11.1-pre.1" } +], version = "1.11.3-pre.0" } tari_crypto = { version = "0.22.0" } tari_max_size = { path = "../../infrastructure/max_size" } tari_key_manager = { path = "../key_manager", features = [ "key_manager_service", -], version = "1.11.1-pre.1" } +], version = "1.11.3-pre.0" } tari_p2p = { path = "../p2p", features = [ "auto-update", -], version = "1.11.1-pre.1" } -tari_script = { path = "../../infrastructure/tari_script", version = "1.11.1-pre.1" } -tari_service_framework = { path = "../service_framework", version = "1.11.1-pre.1" } -tari_shutdown = { path = "../../infrastructure/shutdown", version = "1.11.1-pre.1" } +], version = "1.11.3-pre.0" } +tari_script = { path = "../../infrastructure/tari_script", version = "1.11.3-pre.0" } +tari_service_framework = { path = "../service_framework", version = "1.11.3-pre.0" } +tari_shutdown = { path = "../../infrastructure/shutdown", version = "1.11.3-pre.0" } tari_utilities = { version = "0.8" } # Uncomment for tokio tracing via tokio-console (needs "tracing" features) @@ -74,7 +74,7 @@ zeroize = "1" tari_common = { path = "../../common", features = [ "build", "static-application-info", -], version = "1.11.1-pre.1" } +], version = "1.11.3-pre.0" } [dev-dependencies] tari_p2p = { path = "../p2p", features = ["test-mocks"] } diff --git a/base_layer/wallet/src/output_manager_service/mod.rs b/base_layer/wallet/src/output_manager_service/mod.rs index 24d16bb230..37db44ef45 100644 --- a/base_layer/wallet/src/output_manager_service/mod.rs +++ b/base_layer/wallet/src/output_manager_service/mod.rs @@ -62,6 +62,7 @@ use crate::{ service::OutputManagerService, storage::database::{OutputManagerBackend, OutputManagerDatabase}, }, + utxo_scanner_service::handle::UtxoScannerHandle, }; /// The maximum number of transaction inputs that can be created in a single transaction, slightly less than the maximum @@ -124,6 +125,7 @@ where let base_node_service_handle = handles.expect_handle::(); let connectivity = handles.expect_handle::(); let key_manager = handles.expect_handle::(); + let utxo_scanner_handle = handles.expect_handle::(); let service = OutputManagerService::new( config, @@ -137,6 +139,7 @@ where network, connectivity, key_manager, + utxo_scanner_handle, ) .await .expect("Could not initialize Output Manager Service") diff --git a/base_layer/wallet/src/output_manager_service/resources.rs b/base_layer/wallet/src/output_manager_service/resources.rs index bbac2b117a..9e5ed77445 100644 --- a/base_layer/wallet/src/output_manager_service/resources.rs +++ b/base_layer/wallet/src/output_manager_service/resources.rs @@ -24,10 +24,13 @@ use tari_common_types::tari_address::TariAddress; use tari_core::{consensus::ConsensusConstants, transactions::CryptoFactories}; use tari_shutdown::ShutdownSignal; -use crate::output_manager_service::{ - config::OutputManagerServiceConfig, - handle::OutputManagerEventSender, - storage::database::OutputManagerDatabase, +use crate::{ + output_manager_service::{ + config::OutputManagerServiceConfig, + handle::OutputManagerEventSender, + storage::database::OutputManagerDatabase, + }, + utxo_scanner_service::handle::UtxoScannerHandle, }; /// This struct is a collection of the common resources that a async task in the service requires. @@ -43,4 +46,5 @@ pub(crate) struct OutputManagerResources Result { let view_key = key_manager.get_view_key().await?; let spend_key = key_manager.get_spend_key().await?; @@ -182,6 +184,7 @@ where shutdown_signal, one_sided_tari_address, interactive_tari_address, + utxo_scanner_handle, }; Ok(Self { @@ -205,6 +208,8 @@ where let mut base_node_service_event_stream = self.base_node_service.get_event_stream(); + let mut utxo_scanner_events = self.resources.utxo_scanner_handle.get_event_receiver(); + debug!(target: LOG_TARGET, "Output Manager Service started"); // Outputs marked as shorttermencumbered are not yet stored as transactions in the TMS, so lets clear them self.resources.db.clear_short_term_encumberances()?; @@ -216,6 +221,12 @@ where Err(e) => debug!(target: LOG_TARGET, "Lagging read on base node event broadcast channel: {}", e), } }, + event = utxo_scanner_events.recv() => { + match event { + Ok(msg) => self.handle_utxo_scanner_service_event(msg), + Err(e) => debug!(target: LOG_TARGET, "Lagging read on utxo scanner event broadcast channel: {}", e), + } + }, Some(request_context) = request_stream.next() => { trace!(target: LOG_TARGET, "Handling Service API Request"); let (request, reply_tx) = request_context.split(); @@ -556,6 +567,23 @@ where .map(OutputManagerResponse::ClaimHtlcTransaction) } + fn handle_utxo_scanner_service_event(&mut self, event: UtxoScannerEvent) { + match event { + UtxoScannerEvent::ConnectingToBaseNode(_node_id) => {}, + UtxoScannerEvent::ConnectedToBaseNode(_node_id, _duration) => {}, + UtxoScannerEvent::ConnectionFailedToBaseNode { .. } => {}, + UtxoScannerEvent::ScanningRoundFailed { .. } => {}, + UtxoScannerEvent::Progress { .. } => {}, + UtxoScannerEvent::Completed { .. } => { + let _id = self.validate_outputs().map_err(|e| { + warn!(target: LOG_TARGET, "Error validating txos: {:?}", e); + e + }); + }, + UtxoScannerEvent::ScanningFailed => {}, + } + } + fn handle_base_node_service_event(&mut self, event: Arc) { match (*event).clone() { BaseNodeEvent::BaseNodeStateChanged(_state) => { @@ -566,10 +594,6 @@ where }, BaseNodeEvent::NewBlockDetected(_hash, height) => { self.last_seen_tip_height = Some(height); - let _id = self.validate_outputs().map_err(|e| { - warn!(target: LOG_TARGET, "Error validating txos: {:?}", e); - e - }); }, } } @@ -594,7 +618,7 @@ where let mut base_node_watch = self.resources.connectivity.get_current_base_node_watcher(); let event_publisher = self.resources.event_publisher.clone(); let validation_in_progress = self.validation_in_progress.clone(); - let mut base_node_service_event_stream = self.base_node_service.get_event_stream(); + let mut utxo_scanner_service_event_stream = self.resources.utxo_scanner_handle.get_event_receiver(); tokio::spawn(async move { // Note: We do not want the validation task to be queued let mut _lock = match validation_in_progress.try_lock() { @@ -657,12 +681,10 @@ where is shutting down", id); return; }, - event = base_node_service_event_stream.recv() => { - if let Ok(bn_event) = event { - if let BaseNodeEvent::NewBlockDetected(_hash, _height) = *bn_event { - debug!(target: LOG_TARGET, "TXO Validation Protocol (Id: {}) resetting because base node height changed", id); - continue 'outer; - } + event = utxo_scanner_service_event_stream.recv() => { + if let Ok(UtxoScannerEvent::Completed{..}) = event { + debug!(target: LOG_TARGET, "TXO Validation Protocol (Id: {}) resetting because base node height changed", id); + continue 'outer; } } _ = base_node_watch.changed() => { diff --git a/base_layer/wallet/src/transaction_service/mod.rs b/base_layer/wallet/src/transaction_service/mod.rs index f86c0bc3bb..c440db0833 100644 --- a/base_layer/wallet/src/transaction_service/mod.rs +++ b/base_layer/wallet/src/transaction_service/mod.rs @@ -63,6 +63,7 @@ use crate::{ service::TransactionService, storage::database::{TransactionBackend, TransactionDatabase}, }, + utxo_scanner_service::handle::UtxoScannerHandle, }; pub mod config; @@ -243,6 +244,7 @@ where let core_key_manager_service = handles.expect_handle::(); let connectivity = handles.expect_handle::(); let base_node_service_handle = handles.expect_handle::(); + let utxo_scanner_handle = handles.expect_handle::(); let result = TransactionService::new( config, @@ -266,6 +268,7 @@ where handles.get_shutdown_signal(), base_node_service_handle, wallet_type, + utxo_scanner_handle, ) .await .expect("Could not initialize Transaction Manager Service") diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs index 8c3357a8a9..c79d8054d7 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs @@ -58,6 +58,7 @@ use crate::{ const LOG_TARGET: &str = "wallet::transaction_service::protocols::validation_protocol"; +#[derive(Clone)] pub struct TransactionValidationProtocol { operation_id: OperationId, db: TransactionDatabase, diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 40226927c7..5c1bd6109f 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -145,7 +145,10 @@ use crate::{ utc::utc_duration_since, }, util::watch::Watch, - utxo_scanner_service::RECOVERY_KEY, + utxo_scanner_service::{ + handle::{UtxoScannerEvent, UtxoScannerHandle}, + RECOVERY_KEY, + }, OperationId, }; @@ -260,6 +263,7 @@ where shutdown_signal: ShutdownSignal, base_node_service: BaseNodeServiceHandle, wallet_type: Arc, + utxo_scanner_handle: UtxoScannerHandle, ) -> Result { // Collect the resources that all protocols will need so that they can be neatly cloned as the protocols are // spawned. @@ -294,6 +298,7 @@ where shutdown_signal, consensus_manager: consensus_manager.clone(), wallet_type, + utxo_scanner_handle, }; let power_mode = PowerMode::default(); let timeout = match power_mode { @@ -387,6 +392,7 @@ where let mut base_node_service_event_stream = self.base_node_service.get_event_stream(); let mut output_manager_event_stream = self.resources.output_manager_service.get_event_stream(); + let mut utxo_scanner_events = self.resources.utxo_scanner_handle.get_event_receiver(); debug!(target: LOG_TARGET, "Transaction Service started"); loop { @@ -400,10 +406,16 @@ where // Base Node Monitoring Service event event = base_node_service_event_stream.recv() => { match event { - Ok(msg) => self.handle_base_node_service_event(msg, &mut transaction_validation_protocol_handles).await, + Ok(msg) => self.handle_base_node_service_event(msg).await, Err(e) => debug!(target: LOG_TARGET, "Lagging read on base node event broadcast channel: {}", e), }; }, + event = utxo_scanner_events.recv() => { + match event { + Ok(msg) => self.handle_utxo_scanner_service_event(msg, &mut transaction_validation_protocol_handles).await, + Err(e) => debug!(target: LOG_TARGET, "Lagging read on utxo scanner event broadcast channel: {}", e), + } + }, //Incoming request Some(request_context) = request_stream.next() => { let start = Instant::now(); @@ -1039,18 +1051,31 @@ where }); } - async fn handle_base_node_service_event( - &mut self, - event: Arc, - transaction_validation_join_handles: &mut FuturesUnordered< - JoinHandle>>, - >, - ) { + async fn handle_base_node_service_event(&mut self, event: Arc) { match (*event).clone() { BaseNodeEvent::BaseNodeStateChanged(_state) => { trace!(target: LOG_TARGET, "Received BaseNodeStateChanged event, but igoring",); }, BaseNodeEvent::NewBlockDetected(_hash, height) => { + self.last_seen_tip_height = Some(height); + }, + } + } + + async fn handle_utxo_scanner_service_event( + &mut self, + event: UtxoScannerEvent, + transaction_validation_join_handles: &mut FuturesUnordered< + JoinHandle>>, + >, + ) { + match event { + UtxoScannerEvent::ConnectingToBaseNode(_node_id) => {}, + UtxoScannerEvent::ConnectedToBaseNode(_node_id, _duration) => {}, + UtxoScannerEvent::ConnectionFailedToBaseNode { .. } => {}, + UtxoScannerEvent::ScanningRoundFailed { .. } => {}, + UtxoScannerEvent::Progress { .. } => {}, + UtxoScannerEvent::Completed { .. } => { let _operation_id = self .start_transaction_validation_protocol(transaction_validation_join_handles) .await @@ -1058,9 +1083,8 @@ where warn!(target: LOG_TARGET, "Error validating txos: {:?}", e); e }); - - self.last_seen_tip_height = Some(height); }, + UtxoScannerEvent::ScanningFailed => {}, } } @@ -3382,6 +3406,9 @@ where let mut base_node_watch = self.connectivity().get_current_base_node_watcher(); let validation_in_progress = self.validation_in_progress.clone(); + + let mut utxo_scanner_service_event_stream = self.resources.utxo_scanner_handle.get_event_receiver(); + let join_handle = tokio::spawn(async move { let mut _lock = validation_in_progress.try_lock().map_err(|_| { debug!( @@ -3390,20 +3417,27 @@ where ); TransactionServiceProtocolError::new(id, TransactionServiceError::TransactionValidationInProgress) })?; - let exec_fut = protocol.execute(); - tokio::pin!(exec_fut); - loop { - tokio::select! { - result = &mut exec_fut => { - return result; - }, - _ = base_node_watch.changed() => { - if let Some(selected_peer) = base_node_watch.borrow().as_ref() { - if selected_peer.get_current_peer().node_id != current_base_node { - debug!(target: LOG_TARGET, "Base node changed, exiting transaction validation protocol"); - return Err(TransactionServiceProtocolError::new(id, TransactionServiceError::BaseNodeChanged { - task_name: "transaction validation_protocol", - })); + 'outer: loop { + let local_run = protocol.clone(); + let exec_fut = local_run.execute(); + tokio::pin!(exec_fut); + loop { + tokio::select! { + result = &mut exec_fut => { + return result; + }, + event = utxo_scanner_service_event_stream.recv() => { + if let Ok(UtxoScannerEvent::Completed{..}) = event { + debug!(target: LOG_TARGET, "TXO Validation Protocol (Id: {}) resetting because base node height changed", id); + continue 'outer; + } + } + _ = base_node_watch.changed() => { + if let Some(selected_peer) = base_node_watch.borrow().as_ref() { + if selected_peer.get_current_peer().node_id != current_base_node { + debug!(target: LOG_TARGET, "Base node changed, restarting transaction validation protocol"); + continue 'outer; + } } } } @@ -3842,6 +3876,7 @@ pub struct TransactionServiceResources, + pub utxo_scanner_handle: UtxoScannerHandle, } #[derive(Default, Clone, Copy)] diff --git a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs index 965c6cc4cc..737031f134 100644 --- a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs +++ b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs @@ -266,7 +266,7 @@ where // The node does not know of any of our cached headers so we will start the scan anew from the // wallet birthday self.resources.db.clear_scanned_blocks()?; - let birthday_height_hash = match self.resources.db.get_wallet_type()? { + let scanning_start_height_hash = match self.resources.db.get_wallet_type()? { Some(WalletType::ProvidedKeys(_)) => { let header_proto = client.get_header_by_height(0).await?; let header = BlockHeader::try_from(header_proto).map_err(UtxoScannerError::ConversionError)?; @@ -275,14 +275,14 @@ where header_hash: header.hash(), } }, - _ => self.get_birthday_header_height_hash(&mut client).await?, + _ => self.get_scanning_start_header_height_hash(&mut client).await?, }; ScannedBlock { - height: birthday_height_hash.height, + height: scanning_start_height_hash.height, num_outputs: None, amount: None, - header_hash: birthday_height_hash.header_hash, + header_hash: scanning_start_height_hash.header_hash, timestamp: Utc::now().naive_utc(), } }; @@ -746,37 +746,46 @@ where peer } - async fn get_birthday_header_height_hash( + async fn get_scanning_start_header_height_hash( &self, client: &mut BaseNodeWalletRpcClient, ) -> Result { let birthday = self.resources.db.get_wallet_birthday()?; + let epoch_time_birthday = get_birthday_from_unix_epoch_in_seconds(birthday, 0); + let block_height_birthday = client + .get_height_at_time(epoch_time_birthday) + .await + .unwrap_or_else(|e| { + warn!(target: LOG_TARGET, "Problem requesting `height_at_time` from Base Node: {}", e); + 0 + }); // Calculate the unix epoch time of two weeks (14 days), in seconds, before the // wallet birthday. The latter avoids any possible issues with reorgs. - let epoch_time = get_birthday_from_unix_epoch_in_seconds(birthday, 14u16); - - let block_height = match client.get_height_at_time(epoch_time).await { - Ok(b) => b, - Err(e) => { - warn!( - target: LOG_TARGET, - "Problem requesting `height_at_time` from Base Node: {}", e - ); + let epoch_time_scanning_start = get_birthday_from_unix_epoch_in_seconds(birthday, 14u16); + let block_height_scanning_start = client + .get_height_at_time(epoch_time_scanning_start) + .await + .unwrap_or_else(|e| { + warn!(target: LOG_TARGET, "Problem requesting `height_at_time` from Base Node: {}", e); 0 - }, - }; - let header = client.get_header_by_height(block_height).await?; + }); + let header = client.get_header_by_height(block_height_scanning_start).await?; let header = BlockHeader::try_from(header).map_err(UtxoScannerError::ConversionError)?; - let header_hash = header.hash(); + let header_hash_scanning_start = header.hash(); info!( target: LOG_TARGET, - "Fresh wallet recovery/scanning starting at Block {} (Header Hash: {})", - block_height, - header_hash.to_hex(), + "Fresh wallet recovery/scanning: Wallet birthday '{}' at epoch time '{}' with block height '{}', scanning \ + from epoch time '{}' at block height '{}' with header hash '{}'", + birthday, + epoch_time_birthday, + block_height_birthday, + epoch_time_scanning_start, + block_height_scanning_start, + header_hash_scanning_start.to_hex(), ); Ok(HeightHash { - height: block_height, - header_hash, + height: block_height_scanning_start, + header_hash: header_hash_scanning_start, }) } } diff --git a/base_layer/wallet/tests/output_manager_service_tests/service.rs b/base_layer/wallet/tests/output_manager_service_tests/service.rs index 4ac0e4eb92..c806aaeebe 100644 --- a/base_layer/wallet/tests/output_manager_service_tests/service.rs +++ b/base_layer/wallet/tests/output_manager_service_tests/service.rs @@ -40,6 +40,8 @@ use minotari_wallet::{ }, test_utils::create_consensus_constants, transaction_service::handle::TransactionServiceHandle, + util::watch::Watch, + utxo_scanner_service::handle::UtxoScannerHandle, }; use rand::{rngs::OsRng, RngCore}; use tari_common::configuration::Network; @@ -92,7 +94,6 @@ use crate::support::{ data::get_temp_sqlite_database_connection, utils::{make_input, make_input_with_features}, }; - fn default_features_and_scripts_size_byte_size() -> std::io::Result { Ok(TransactionWeight::latest().round_up_features_and_scripts_size( OutputFeatures::default().get_serialized_size()? + TariScript::default().get_serialized_size()?, @@ -107,7 +108,7 @@ struct TestOmsService { pub mock_rpc_service: MockRpcServer>, pub node_id: Arc, pub base_node_wallet_rpc_mock_state: BaseNodeWalletRpcMockState, - pub node_event: broadcast::Sender>, + pub _node_event: broadcast::Sender>, pub key_manager_handle: MemoryDbKeyManager, } @@ -162,6 +163,12 @@ async fn setup_output_manager_service( let key_manager = create_memory_db_key_manager().unwrap(); + let (event_sender, _) = broadcast::channel(200); + let recovery_message_watch = Watch::new("unset".to_string()); + let one_sided_message_watch = Watch::new("unset".to_string()); + + let scanner_handle = UtxoScannerHandle::new(event_sender.clone(), one_sided_message_watch, recovery_message_watch); + let output_manager_service = OutputManagerService::new( OutputManagerServiceConfig { ..Default::default() }, oms_request_receiver, @@ -174,6 +181,7 @@ async fn setup_output_manager_service( Network::LocalNet, wallet_connectivity_mock.clone(), key_manager.clone(), + scanner_handle, ) .await .unwrap(); @@ -189,7 +197,7 @@ async fn setup_output_manager_service( mock_rpc_service: mock_server, node_id: server_node_identity, base_node_wallet_rpc_mock_state: rpc_service_state, - node_event: event_publisher_bns, + _node_event: event_publisher_bns, key_manager_handle: key_manager, } } @@ -226,6 +234,10 @@ pub async fn setup_oms_with_bn_state( task::spawn(mock_base_node_service.run()); let connectivity = create_wallet_connectivity_mock(); let key_manager = create_memory_db_key_manager().unwrap(); + let (event_sender, _) = broadcast::channel(200); + let recovery_message_watch = Watch::new("unset".to_string()); + let one_sided_message_watch = Watch::new("unset".to_string()); + let scanner_handle = UtxoScannerHandle::new(event_sender.clone(), one_sided_message_watch, recovery_message_watch); let output_manager_service = OutputManagerService::new( OutputManagerServiceConfig { ..Default::default() }, oms_request_receiver, @@ -238,6 +250,7 @@ pub async fn setup_oms_with_bn_state( Network::LocalNet, connectivity, key_manager.clone(), + scanner_handle, ) .await .unwrap(); @@ -1822,31 +1835,7 @@ async fn test_txo_validation() { oms.base_node_wallet_rpc_mock_state .set_query_deleted_response(query_deleted_response.clone()); - // Trigger validation through a base_node_service event - oms.node_event - .send(Arc::new(BaseNodeEvent::NewBlockDetected( - (*block5_header_reorg.hash()).into(), - 5, - ))) - .unwrap(); - - let _result = oms - .base_node_wallet_rpc_mock_state - .wait_pop_get_header_by_height_calls(2, Duration::from_secs(60)) - .await - .unwrap(); - - let _utxo_query_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) - .await - .unwrap(); - - let _query_deleted_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_query_deleted(1, Duration::from_secs(60)) - .await - .unwrap(); + oms.output_manager_handle.validate_txos().await.unwrap(); // This is needed on a fast computer, otherwise the balance have not been updated correctly yet with the next // step diff --git a/base_layer/wallet/tests/transaction_service_tests/service.rs b/base_layer/wallet/tests/transaction_service_tests/service.rs index 720c49f107..1d022df42d 100644 --- a/base_layer/wallet/tests/transaction_service_tests/service.rs +++ b/base_layer/wallet/tests/transaction_service_tests/service.rs @@ -81,6 +81,8 @@ use minotari_wallet::{ }, TransactionServiceInitializer, }, + util::watch::Watch, + utxo_scanner_service::{handle::UtxoScannerHandle, initializer::UtxoScannerServiceInitializer}, }; use prost::Message; use rand::{rngs::OsRng, RngCore}; @@ -266,12 +268,20 @@ async fn setup_transaction_service>( node_identity.clone(), Network::LocalNet, consensus_manager, - factories, + factories.clone(), db.clone(), wallet_type, )) - .add_initializer(BaseNodeServiceInitializer::new(BaseNodeServiceConfig::default(), db)) + .add_initializer(BaseNodeServiceInitializer::new( + BaseNodeServiceConfig::default(), + db.clone(), + )) .add_initializer(WalletConnectivityInitializer::new(BaseNodeServiceConfig::default())) + .add_initializer(UtxoScannerServiceInitializer::<_, MemoryDbKeyManager>::new( + db, + factories.clone(), + Network::LocalNet, + )) .build() .await .unwrap(); @@ -389,6 +399,12 @@ async fn setup_transaction_service_no_comms( let ts_db = TransactionDatabase::new(ts_service_db.clone()); let key_manager = create_memory_db_key_manager().unwrap(); let oms_db = OutputManagerDatabase::new(OutputManagerSqliteDatabase::new(db_connection)); + let (event_sender, _) = broadcast::channel(200); + let recovery_message_watch = Watch::new("unset".to_string()); + let one_sided_message_watch = Watch::new("unset".to_string()); + + let scanner_handle = UtxoScannerHandle::new(event_sender.clone(), one_sided_message_watch, recovery_message_watch); + let output_manager_service = OutputManagerService::new( OutputManagerServiceConfig::default(), oms_request_receiver, @@ -401,6 +417,7 @@ async fn setup_transaction_service_no_comms( Network::LocalNet, wallet_connectivity_service_mock.clone(), key_manager.clone(), + scanner_handle, ) .await .unwrap(); @@ -421,6 +438,11 @@ async fn setup_transaction_service_no_comms( max_tx_query_batch_size: 2, ..Default::default() }); + let (event_sender, _) = broadcast::channel(200); + let recovery_message_watch = Watch::new("unset".to_string()); + let one_sided_message_watch = Watch::new("unset".to_string()); + + let scanner_handle = UtxoScannerHandle::new(event_sender.clone(), one_sided_message_watch, recovery_message_watch); let ts_service = TransactionService::new( test_config, ts_db.clone(), @@ -443,6 +465,7 @@ async fn setup_transaction_service_no_comms( shutdown.to_signal(), base_node_service_handle, key_manager.get_wallet_type().await, + scanner_handle, ) .await .unwrap(); diff --git a/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs b/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs index 58257c6c9d..eacb0200d2 100644 --- a/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs +++ b/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs @@ -48,6 +48,7 @@ use minotari_wallet::{ }, }, util::watch::Watch, + utxo_scanner_service::handle::UtxoScannerHandle, }; use rand::{rngs::OsRng, RngCore}; use tari_common::configuration::Network; @@ -178,6 +179,12 @@ pub async fn setup() -> ( let interactive_tari_address = TariAddress::new_dual_address(view_key.pub_key, spend_key.pub_key, network, interactive_features); let wallet_type = core_key_manager_service_handle.get_wallet_type().await; + let (event_sender, _) = broadcast::channel(200); + let recovery_message_watch = Watch::new("unset".to_string()); + let one_sided_message_watch = Watch::new("unset".to_string()); + + let utxo_scanner_handle = + UtxoScannerHandle::new(event_sender.clone(), one_sided_message_watch, recovery_message_watch); let resources = TransactionServiceResources { db, output_manager_service: output_manager_service_handle, @@ -197,6 +204,7 @@ pub async fn setup() -> ( }, shutdown_signal: shutdown.to_signal(), wallet_type, + utxo_scanner_handle, }; ( diff --git a/base_layer/wallet_ffi/Cargo.toml b/base_layer/wallet_ffi/Cargo.toml index 52b1ab0f44..0e5c5447b2 100644 --- a/base_layer/wallet_ffi/Cargo.toml +++ b/base_layer/wallet_ffi/Cargo.toml @@ -3,7 +3,7 @@ name = "minotari_wallet_ffi" authors = ["The Tari Development Community"] description = "Tari cryptocurrency wallet C FFI bindings" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" [dependencies] @@ -43,7 +43,7 @@ zeroize = "1" serde_json = "1.0" [target.'cfg(target_os="android")'.dependencies] -openssl = { version = "0.10.66", features = ["vendored"] } +openssl = { version = "0.10.70", features = ["vendored"] } [lib] crate-type = ["staticlib", "cdylib"] @@ -67,7 +67,7 @@ tari_common = { path = "../../common", features = [ "build", "static-application-info", ] } -tari_features = { path = "../../common/tari_features", version = "1.11.1-pre.1" } +tari_features = { path = "../../common/tari_features", version = "1.11.3-pre.0" } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = [ diff --git a/changelog-development.md b/changelog-development.md index 64b4c50f04..80df43c28f 100644 --- a/changelog-development.md +++ b/changelog-development.md @@ -2,6 +2,27 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [1.11.3-pre.0](https://github.com/tari-project/tari/compare/v1.11.2-pre.0...v1.11.3-pre.0) (2025-02-05) + + +### Features + +* Fix windows ([#6788](https://github.com/tari-project/tari/issues/6788)) ([e263caa](https://github.com/tari-project/tari/commit/e263caafd95d71c9c9277d51b8906c75d43ffda3)) + +### [1.11.2-pre.0](https://github.com/tari-project/tari/compare/v1.11.1-pre.1...v1.11.2-pre.0) (2025-02-05) + + +### Features + +* add libtor exiting logic ([#6782](https://github.com/tari-project/tari/issues/6782)) ([eeee441](https://github.com/tari-project/tari/commit/eeee4416eb496ca351a4608642bee9c22529bc04)) +* add monerod fallback strategy ([#6764](https://github.com/tari-project/tari/issues/6764)) ([f5365ca](https://github.com/tari-project/tari/commit/f5365caed6b0e03559158c1556d1cf7f0b5e794a)) +* upgrade crossterm ([#6783](https://github.com/tari-project/tari/issues/6783)) ([3ad35af](https://github.com/tari-project/tari/commit/3ad35afe0355569e593eb552f2f90ff312f4e866)) + + +### Bug Fixes + +* validation trigger ([#6784](https://github.com/tari-project/tari/issues/6784)) ([2698a58](https://github.com/tari-project/tari/commit/2698a58ab0cd66b63ded602d6b0f605db5981e8c)) + ### [1.11.1-pre.1](https://github.com/tari-project/tari/compare/v1.11.1-pre.0...v1.11.1-pre.1) (2025-01-31) ### Bug Fixes diff --git a/changelog-nextnet.md b/changelog-nextnet.md index 3df2771c39..414ca2706d 100644 --- a/changelog-nextnet.md +++ b/changelog-nextnet.md @@ -3,6 +3,27 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. # Changelog +### [1.11.3-rc.0](https://github.com/tari-project/tari/compare/v1.11.2-rc.0...v1.11.3-rc.0) (2025-02-05) + + +### Features + +* Fix windows ([#6788](https://github.com/tari-project/tari/issues/6788)) ([e263caa](https://github.com/tari-project/tari/commit/e263caafd95d71c9c9277d51b8906c75d43ffda3)) + +### [1.11.2-rc.0](https://github.com/tari-project/tari/compare/v1.11.1-rc.1...v1.11.2-rc.0) (2025-02-05) + + +### Features + +* add libtor exiting logic ([#6782](https://github.com/tari-project/tari/issues/6782)) ([eeee441](https://github.com/tari-project/tari/commit/eeee4416eb496ca351a4608642bee9c22529bc04)) +* add monerod fallback strategy ([#6764](https://github.com/tari-project/tari/issues/6764)) ([f5365ca](https://github.com/tari-project/tari/commit/f5365caed6b0e03559158c1556d1cf7f0b5e794a)) +* upgrade crossterm ([#6783](https://github.com/tari-project/tari/issues/6783)) ([3ad35af](https://github.com/tari-project/tari/commit/3ad35afe0355569e593eb552f2f90ff312f4e866)) + + +### Bug Fixes + +* validation trigger ([#6784](https://github.com/tari-project/tari/issues/6784)) ([2698a58](https://github.com/tari-project/tari/commit/2698a58ab0cd66b63ded602d6b0f605db5981e8c)) + ### [1.11.1-rc.1](https://github.com/tari-project/tari/compare/v1.11.1-rc.0...v1.11.1-rc.1) (2025-01-31) ### Bug Fixes diff --git a/common/Cargo.toml b/common/Cargo.toml index 978d4e9256..1042f36709 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -6,7 +6,7 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" [features] @@ -14,7 +14,7 @@ build = ["toml", "prost-build"] static-application-info = ["git2"] [dependencies] -tari_features = { path = "./tari_features", version = "1.11.1-pre.1" } +tari_features = { path = "./tari_features", version = "1.11.3-pre.0" } anyhow = "1.0.53" config = { version = "0.14.0", default-features = false, features = ["toml"] } @@ -43,7 +43,7 @@ tari_test_utils = { path = "../infrastructure/test_utils" } toml = "0.5.8" [build-dependencies] -tari_features = { path = "./tari_features", version = "1.11.1-pre.1" } +tari_features = { path = "./tari_features", version = "1.11.3-pre.0" } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = [ diff --git a/common/config/presets/f_merge_mining_proxy.toml b/common/config/presets/f_merge_mining_proxy.toml index 61c0b40a7b..c841ba0dd9 100644 --- a/common/config/presets/f_merge_mining_proxy.toml +++ b/common/config/presets/f_merge_mining_proxy.toml @@ -110,5 +110,15 @@ # The Tari wallet address (valid address in hex) where the mining funds will be sent to - must be assigned # e.g. "78e724f466d202abdee0f23c261289074e4a2fc9eb61e83e0179eead76ce2d3f17" #wallet_payment_address = "YOUR_WALLET_TARI_ADDRESS" -# Range proof type - revealed_value or bullet_proof_plus: (default = "revealed_value") +# Range proof type - "revealed_value" or "bullet_proof_plus": (default = "revealed_value") #range_proof_type = "revealed_value" + +# Use p2pool to submit and get block templates (default = false) +#p2pool_enabled = false, + +# Monero fallback strategy - ["monerod_only", "static_when_monerod_fails" or "static_only"] +# (default = "static_when_monerod_fails") +#monerod_fallback = "static_when_monerod_fails" + +# The timeout duration for connecting to monerod (default = 2s) +#monerod_connection_timeout = 2 diff --git a/common/tari_features/Cargo.toml b/common/tari_features/Cargo.toml index 5de2d00363..a4eddab4b1 100644 --- a/common/tari_features/Cargo.toml +++ b/common/tari_features/Cargo.toml @@ -6,7 +6,7 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/common_sqlite/Cargo.toml b/common_sqlite/Cargo.toml index 08557c7d85..661ca3e7f8 100644 --- a/common_sqlite/Cargo.toml +++ b/common_sqlite/Cargo.toml @@ -3,7 +3,7 @@ name = "tari_common_sqlite" authors = ["The Tari Development Community"] description = "Tari cryptocurrency wallet library" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/comms/core/Cargo.toml b/comms/core/Cargo.toml index 980efcacdd..e5c72b88ff 100644 --- a/comms/core/Cargo.toml +++ b/comms/core/Cargo.toml @@ -6,14 +6,14 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2021" [dependencies] tari_crypto = { version = "0.22.0" } -tari_metrics = { path = "../../infrastructure/metrics", optional = true, version = "1.11.1-pre.1" } -tari_storage = { path = "../../infrastructure/storage", version = "1.11.1-pre.1" } -tari_shutdown = { path = "../../infrastructure/shutdown", version = "1.11.1-pre.1" } +tari_metrics = { path = "../../infrastructure/metrics", optional = true, version = "1.11.3-pre.0" } +tari_storage = { path = "../../infrastructure/storage", version = "1.11.3-pre.0" } +tari_shutdown = { path = "../../infrastructure/shutdown", version = "1.11.3-pre.0" } tari_utilities = { version = "0.8" } anyhow = "1.0.53" @@ -71,7 +71,7 @@ tempfile = "3.1.0" [build-dependencies] tari_common = { path = "../../common", features = [ "build", -], version = "1.11.1-pre.1" } +], version = "1.11.3-pre.0" } [features] c_integration = [] diff --git a/comms/dht/Cargo.toml b/comms/dht/Cargo.toml index d566c2c98a..c447dd097b 100644 --- a/comms/dht/Cargo.toml +++ b/comms/dht/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tari_comms_dht" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" authors = ["The Tari Development Community"] description = "Tari comms DHT module" repository = "https://github.com/tari-project/tari" @@ -10,14 +10,14 @@ license = "BSD-3-Clause" edition = "2021" [dependencies] -tari_comms = { path = "../core", features = ["rpc"], version = "1.11.1-pre.1" } -tari_common = { path = "../../common", version = "1.11.1-pre.1" } -tari_comms_rpc_macros = { path = "../rpc_macros", version = "1.11.1-pre.1" } +tari_comms = { path = "../core", features = ["rpc"], version = "1.11.3-pre.0" } +tari_common = { path = "../../common", version = "1.11.3-pre.0" } +tari_comms_rpc_macros = { path = "../rpc_macros", version = "1.11.3-pre.0" } tari_crypto = { version = "0.22.0" } tari_utilities = { version = "0.8" } -tari_shutdown = { path = "../../infrastructure/shutdown", version = "1.11.1-pre.1" } -tari_storage = { path = "../../infrastructure/storage", version = "1.11.1-pre.1" } -tari_common_sqlite = { path = "../../common_sqlite", version = "1.11.1-pre.1" } +tari_shutdown = { path = "../../infrastructure/shutdown", version = "1.11.3-pre.0" } +tari_storage = { path = "../../infrastructure/storage", version = "1.11.3-pre.0" } +tari_common_sqlite = { path = "../../common_sqlite", version = "1.11.3-pre.0" } anyhow = "1.0.53" bitflags = { version = "2.4", features = ["serde"] } @@ -66,7 +66,7 @@ clap = "3.2" [build-dependencies] -tari_common = { path = "../../common", version = "1.11.1-pre.1" } +tari_common = { path = "../../common", version = "1.11.3-pre.0" } [features] test-mocks = [] diff --git a/comms/rpc_macros/Cargo.toml b/comms/rpc_macros/Cargo.toml index 1039b44d78..534d692d8d 100644 --- a/comms/rpc_macros/Cargo.toml +++ b/comms/rpc_macros/Cargo.toml @@ -6,7 +6,7 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" [lib] diff --git a/hashing/Cargo.toml b/hashing/Cargo.toml index 4a0ba08da4..7f15764e86 100644 --- a/hashing/Cargo.toml +++ b/hashing/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tari_hashing" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2021" description = "Tari hash domains" authors = ["The Tari Development Community"] diff --git a/infrastructure/derive/Cargo.toml b/infrastructure/derive/Cargo.toml index cdd30d5ac5..3187e07589 100644 --- a/infrastructure/derive/Cargo.toml +++ b/infrastructure/derive/Cargo.toml @@ -6,7 +6,7 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" [lib] diff --git a/infrastructure/libtor/Cargo.toml b/infrastructure/libtor/Cargo.toml index 8c9fe89781..c307a7b2f4 100644 --- a/infrastructure/libtor/Cargo.toml +++ b/infrastructure/libtor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tari_libtor" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2021" license = "BSD-3-Clause" @@ -11,7 +11,6 @@ tari_p2p = { path = "../../base_layer/p2p" } derivative = "2.2.0" log = "0.4.8" rand = "0.8" -tempfile = "3.1.0" tor-hash-passwd = "1.0.1" [target.'cfg(unix)'.dependencies] diff --git a/infrastructure/libtor/src/tor.rs b/infrastructure/libtor/src/tor.rs index 218e6f1928..f427cd33fe 100644 --- a/infrastructure/libtor/src/tor.rs +++ b/infrastructure/libtor/src/tor.rs @@ -20,7 +20,7 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::{fmt, io, net::TcpListener, path::PathBuf, thread}; +use std::{fmt, fs, io, net::TcpListener, path::PathBuf, thread}; use derivative::Derivative; use libtor::{LogDestination, LogLevel, TorFlag}; @@ -28,7 +28,6 @@ use log::*; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use tari_common::exit_codes::{ExitCode, ExitError}; use tari_p2p::{TorControlAuthentication, TransportConfig, TransportType}; -use tempfile::{tempdir, NamedTempFile, TempDir, TempPath}; use tor_hash_passwd::EncryptedKey; const LOG_TARGET: &str = "tari_libtor"; @@ -46,13 +45,11 @@ impl fmt::Debug for TorPassword { pub struct Tor { control_port: u16, data_dir: PathBuf, - log_destination: String, + log_destination: PathBuf, log_level: LogLevel, #[derivative(Debug = "ignore")] passphrase: TorPassword, socks_port: u16, - temp_dir: Option, - temp_file: Option, } impl Default for Tor { @@ -64,8 +61,6 @@ impl Default for Tor { log_level: LogLevel::Err, passphrase: TorPassword(None), socks_port: 0, - temp_dir: None, - temp_file: None, } } } @@ -76,7 +71,7 @@ impl Tor { /// Two TCP ports will be provided by the operating system. /// These ports are used for the control and socks ports, the onion address and port info are still loaded from the /// node identity file. - pub fn initialize() -> Result { + pub fn initialize(data_dir: PathBuf) -> Result { debug!(target: LOG_TARGET, "Initializing libtor"); let mut instance = Tor::default(); @@ -95,15 +90,33 @@ impl Tor { instance.passphrase = TorPassword(Some(passphrase)); // data dir - let temp = tempdir()?; - instance.data_dir = temp.path().to_path_buf(); - instance.temp_dir = Some(temp); + instance.data_dir = data_dir.join("data"); + + let exists = fs::exists(instance.data_dir.clone()).map_err(|e| { + ExitError::new( + ExitCode::InputError, + format!( + "Could not verify libtor data directory: {} ({})", + instance.data_dir.display(), + e + ), + ) + })?; + if !exists { + fs::create_dir_all(&instance.data_dir).map_err(|e| { + ExitError::new( + ExitCode::InputError, + format!( + "Could not create libtor data directory: {} ({})", + instance.data_dir.display(), + e + ), + ) + })?; + } // log destination - let temp = NamedTempFile::new()?.into_temp_path(); - let file = temp.to_string_lossy().to_string(); - instance.temp_file = Some(temp); - instance.log_destination = file; + instance.log_destination = data_dir.join("tor.log"); debug!(target: LOG_TARGET, "tor instance: {:?}", instance); Ok(instance) @@ -152,7 +165,7 @@ impl Tor { // Write the final control port to a file. This could be used to configure the node to use this port when auto is set. .flag(TorFlag::ControlPortWriteToFile(data_dir.join("control_port").to_string_lossy().to_string())) .flag(TorFlag::Hush()) - .flag(TorFlag::LogTo(log_level, LogDestination::File(log_destination))); + .flag(TorFlag::LogTo(log_level, LogDestination::File(log_destination.to_string_lossy().to_string()))); if socks_port == 0 { tor.flag(TorFlag::SocksPortAuto); diff --git a/infrastructure/max_size/Cargo.toml b/infrastructure/max_size/Cargo.toml index aff8aa0e37..a3a629887e 100644 --- a/infrastructure/max_size/Cargo.toml +++ b/infrastructure/max_size/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tari_max_size" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2021" description = "Tari max size library" authors = ["The Tari Development Community"] diff --git a/infrastructure/metrics/Cargo.toml b/infrastructure/metrics/Cargo.toml index 74235994cc..c2b8c4fe19 100644 --- a/infrastructure/metrics/Cargo.toml +++ b/infrastructure/metrics/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tari_metrics" description = "Tari metrics" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2021" authors = ["The Tari Development Community"] repository = "https://github.com/tari-project/tari" diff --git a/infrastructure/shutdown/Cargo.toml b/infrastructure/shutdown/Cargo.toml index 4b22d7c5fc..da556cefe6 100644 --- a/infrastructure/shutdown/Cargo.toml +++ b/infrastructure/shutdown/Cargo.toml @@ -6,7 +6,7 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/infrastructure/storage/Cargo.toml b/infrastructure/storage/Cargo.toml index 9d6f5d67e2..342394c04f 100644 --- a/infrastructure/storage/Cargo.toml +++ b/infrastructure/storage/Cargo.toml @@ -6,7 +6,7 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2018" [dependencies] diff --git a/infrastructure/tari_script/Cargo.toml b/infrastructure/tari_script/Cargo.toml index 65f8560f1b..9a0578fb68 100644 --- a/infrastructure/tari_script/Cargo.toml +++ b/infrastructure/tari_script/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tari_script" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" edition = "2021" description = "Tari script library" authors = ["The Tari Development Community"] diff --git a/infrastructure/test_utils/Cargo.toml b/infrastructure/test_utils/Cargo.toml index 2d7a7e629d..e390f7323f 100644 --- a/infrastructure/test_utils/Cargo.toml +++ b/infrastructure/test_utils/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tari_test_utils" description = "Utility functions used in Tari test functions" -version = "1.11.1-pre.1" +version = "1.11.3-pre.0" authors = ["The Tari Development Community"] edition = "2018" license = "BSD-3-Clause" @@ -9,8 +9,8 @@ license = "BSD-3-Clause" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tari_shutdown = { path = "../shutdown", version = "1.11.1-pre.1" } -tari_comms = { path = "../../comms/core", version = "1.11.1-pre.1" } +tari_shutdown = { path = "../shutdown", version = "1.11.3-pre.0" } +tari_comms = { path = "../../comms/core", version = "1.11.3-pre.0" } futures = { version = "^0.3.1" } rand = "0.8" diff --git a/integration_tests/src/wallet_process.rs b/integration_tests/src/wallet_process.rs index 2e3fcea656..464508a9f3 100644 --- a/integration_tests/src/wallet_process.rs +++ b/integration_tests/src/wallet_process.rs @@ -235,6 +235,7 @@ pub fn get_default_cli() -> Cli { profile_with_tokio_console: false, view_private_key: None, spend_key: None, + libtor_data_dir: None, } } diff --git a/package-lock.json b/package-lock.json index 5cbcd9cf45..a2b4225337 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "tari", - "version": "1.11.1-pre.1", + "version": "1.11.3-pre.0", "lockfileVersion": 2, "requires": true, "packages": {}