cuprate_test_utils/
monerod.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//! Monerod Module
//!
//! This module contains a function [`monerod`] to start `monerod` - the core Monero node. Cuprate can then use
//! this to test compatibility with monerod.
//!
use std::{
    env::current_dir,
    ffi::OsStr,
    fs::read_dir,
    io::Read,
    net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener},
    path::PathBuf,
    process::{Child, Command, Stdio},
    str::from_utf8,
    thread::panicking,
    time::Duration,
};

use tokio::{task::yield_now, time::timeout};

/// IPv4 local host.
const LOCALHOST: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);

/// The log line `monerod` emits indicated it has successfully started up.
const MONEROD_STARTUP_TEXT: &str =
    "The daemon will start synchronizing with the network. This may take a long time to complete.";

/// The log line `monerod` emits indicated it has stopped.
const MONEROD_SHUTDOWN_TEXT: &str = "Stopping cryptonote protocol";

/// Spawns monerod and returns [`SpawnedMoneroD`].
///
/// This function will set `regtest` and the P2P/ RPC ports so these can't be included in the flags.
pub async fn monerod<T: AsRef<OsStr>>(flags: impl IntoIterator<Item = T>) -> SpawnedMoneroD {
    let path_to_monerod = find_root().join("monerod");

    let rpc_port = get_available_port(&[]);
    let p2p_port = get_available_port(&[rpc_port]);
    let zmq_port = get_available_port(&[rpc_port, p2p_port]);

    let data_dir = tempfile::tempdir().unwrap();

    let mut monerod = Command::new(path_to_monerod)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .args(flags)
        .arg("--regtest")
        .arg("--log-level=2")
        .arg(format!("--p2p-bind-port={p2p_port}"))
        .arg(format!("--rpc-bind-port={rpc_port}"))
        .arg(format!("--zmq-rpc-bind-port={zmq_port}"))
        .arg(format!("--data-dir={}", data_dir.path().display()))
        .arg("--non-interactive")
        .spawn()
        .expect(
            "Failed to start monerod, you need to have the monerod binary in the root of the repo",
        );

    let mut logs = String::new();

    timeout(Duration::from_secs(30), async {
        loop {
            let mut next_str = [0];
            let _ = monerod
                .stdout
                .as_mut()
                .unwrap()
                .read(&mut next_str)
                .unwrap();

            logs.push_str(from_utf8(&next_str).unwrap());

            if logs.contains(MONEROD_SHUTDOWN_TEXT) {
                panic!("Failed to start monerod, logs: \n {logs}");
            } else if logs.contains(MONEROD_STARTUP_TEXT) {
                break;
            }
            // this is blocking code but as this is for tests performance isn't a priority. However we should still yield so
            // the timeout works.
            yield_now().await;
        }
    })
    .await
    .unwrap_or_else(|_| panic!("Failed to start monerod in time, logs: {logs}"));

    SpawnedMoneroD {
        process: monerod,
        rpc_port,
        p2p_port,
        _data_dir: data_dir,
        start_up_logs: logs,
    }
}

/// Finds the root of the repo by finding the `target` directory, this will work up from the current
/// directory until it finds a `target` directory, then returns the directory that the target is contained
/// in.
fn find_root() -> PathBuf {
    let mut current_dir = current_dir().unwrap();
    loop {
        if read_dir(current_dir.join("target")).is_ok() {
            return current_dir;
        } else if !current_dir.pop() {
            panic!("Could not find ./target");
        }
    }
}

/// Fetch an available TCP port on the machine for `monerod` to bind to.
fn get_available_port(already_taken: &[u16]) -> u16 {
    loop {
        // Using `0` makes the OS return a random available port.
        let port = TcpListener::bind("127.0.0.1:0")
            .unwrap()
            .local_addr()
            .unwrap()
            .port();

        if !already_taken.contains(&port) {
            return port;
        }
    }
}

/// A struct representing a spawned monerod.
pub struct SpawnedMoneroD {
    /// A handle to the monerod process, monerod will be stopped when this is dropped.
    process: Child,
    /// The RPC port of the monerod instance.
    rpc_port: u16,
    /// The P2P port of the monerod instance.
    p2p_port: u16,
    /// The data dir for monerod - when this is dropped the dir will be deleted.
    _data_dir: tempfile::TempDir,
    /// The logs upto [`MONEROD_STARTUP_TEXT`].
    start_up_logs: String,
}

impl SpawnedMoneroD {
    /// Returns the p2p port of the spawned monerod
    pub const fn p2p_addr(&self) -> SocketAddr {
        SocketAddr::new(LOCALHOST, self.p2p_port)
    }

    /// Returns the RPC port of the spawned monerod
    pub const fn rpc_port(&self) -> SocketAddr {
        SocketAddr::new(LOCALHOST, self.rpc_port)
    }
}

impl Drop for SpawnedMoneroD {
    fn drop(&mut self) {
        let mut error = false;

        if self.process.kill().is_err() {
            error = true;
            println!("Failed to kill monerod, process id: {}", self.process.id());
        }

        if panicking() {
            // If we are panicking then a test failed so print monerod's logs.

            let mut out = String::new();

            if self
                .process
                .stdout
                .as_mut()
                .unwrap()
                .read_to_string(&mut out)
                .is_err()
            {
                println!("Failed to get monerod's logs.");
            }

            println!("-----START-MONEROD-LOGS-----");
            println!("{}{out}", self.start_up_logs);
            println!("------END-MONEROD-LOGS------");
        }

        #[expect(clippy::manual_assert, reason = "`if` is more clear")]
        if error && !panicking() {
            // `println` only outputs in a test when panicking so if there is an error while
            // dropping monerod but not an error in the test then we need to panic to make sure
            // the println!s are output.
            panic!("Error while dropping monerod");
        }
    }
}