1use std::{
22 ffi::{OsStr, OsString},
23 fs,
24 path::PathBuf,
25 process::{Command, Stdio},
26 str::FromStr,
27};
28
29use node::{
30 account::AccountSecretKey,
31 core::log::{info, system_time, warn},
32 p2p::connection::outgoing::P2pConnectionOutgoingInitOpts,
33};
34use serde::{Deserialize, Serialize};
35
36#[derive(Serialize, Deserialize, Debug, Clone)]
42pub struct OcamlNodeTestingConfig {
43 pub initial_peers: Vec<P2pConnectionOutgoingInitOpts>,
45 pub daemon_json: DaemonJson,
47 pub block_producer: Option<AccountSecretKey>,
49}
50
51impl Default for OcamlNodeTestingConfig {
52 fn default() -> Self {
53 Self {
54 initial_peers: vec![],
55 daemon_json: DaemonJson::Custom("/var/lib/coda/config_6929a7ec.json".to_owned()),
56 block_producer: None,
57 }
58 }
59}
60
61#[derive(Serialize, Deserialize, Debug, Clone)]
62pub enum DaemonJson {
63 Custom(String),
65 InMem(serde_json::Value),
66}
67
68#[derive(Serialize, Deserialize, Debug, Clone)]
69pub enum DaemonJsonGenConfig {
70 Counts { whales: usize, fish: usize },
71 DelegateTable(Vec<(u64, Vec<u64>)>),
72}
73
74#[derive(Debug, Clone)]
75pub struct OcamlNodeConfig {
76 pub executable: OcamlNodeExecutable,
78 pub dir: temp_dir::TempDir,
79 pub libp2p_keypair_i: usize,
80 pub libp2p_port: u16,
81 pub graphql_port: u16,
82 pub client_port: u16,
83 pub initial_peers: Vec<P2pConnectionOutgoingInitOpts>,
84 pub daemon_json: DaemonJson,
85 pub block_producer: Option<AccountSecretKey>,
86}
87
88#[derive(Serialize, Deserialize, Debug, Clone)]
94pub enum OcamlNodeExecutable {
95 Installed(String),
105
106 Docker(String),
116
117 DockerDefault,
123}
124
125#[derive(Serialize, Deserialize, Debug, Clone)]
126#[serde(rename_all = "camelCase")]
127pub struct OcamlVrfOutput {
128 pub vrf_output: String,
129 pub vrf_output_fractional: f64,
130 pub threshold_met: bool,
131 pub public_key: String,
132}
133
134impl OcamlNodeConfig {
135 pub fn cmd<I, K, V>(&self, envs: I) -> Command
139 where
140 I: IntoIterator<Item = (K, V)>,
141 K: AsRef<OsStr>,
142 V: AsRef<OsStr>,
143 {
144 match &self.executable {
145 OcamlNodeExecutable::Installed(program) => {
146 info!(system_time(); "Using local Mina binary: {}", program);
147 let mut cmd = Command::new(program);
148 cmd.envs(envs);
149 cmd
150 }
151 OcamlNodeExecutable::Docker(tag) => {
152 info!(system_time(); "Using custom Docker image: {}", tag);
153 self.docker_run_cmd(tag, envs)
154 }
155 OcamlNodeExecutable::DockerDefault => {
156 info!(
157 system_time();
158 "Using default Docker image: {}",
159 OcamlNodeExecutable::DEFAULT_DOCKER_IMAGE
160 );
161 self.docker_run_cmd(OcamlNodeExecutable::DEFAULT_DOCKER_IMAGE, envs)
162 }
163 }
164 }
165
166 fn docker_run_cmd<I, K, V>(&self, tag: &str, envs: I) -> Command
181 where
182 I: IntoIterator<Item = (K, V)>,
183 K: AsRef<OsStr>,
184 V: AsRef<OsStr>,
185 {
186 let mut cmd = Command::new("docker");
187 let dir_path = self.dir.path().display();
188
189 let uid = std::env::var("$UID").unwrap_or_else(|_| "1000".to_owned());
190 let container_name = OcamlNodeExecutable::docker_container_name(&self.dir);
191
192 info!(
193 system_time();
194 "Configuring Docker container: name={}, image={}, uid={}, mount={}",
195 container_name,
196 tag,
197 uid,
198 dir_path
199 );
200
201 cmd.arg("run")
203 .args(["--name".to_owned(), container_name.clone()])
204 .args(["--network", "host"])
205 .args(["--user".to_owned(), format!("{uid}:{uid}")])
206 .args(["-v".to_owned(), format!("{dir_path}:{dir_path}")])
207 .args(["-w", "/tmp"]);
211
212 let mut env_count = 0;
214 for (key, value) in envs {
215 let arg: OsString = [key.as_ref(), value.as_ref()].join(OsStr::new("="));
216 cmd.args(["-e".as_ref(), arg.as_os_str()]);
217 env_count += 1;
218 }
219
220 info!(system_time(); "Added {} environment variables to Docker container", env_count);
221
222 cmd.arg(tag);
224
225 info!(system_time(); "Docker command configured for container: {}", container_name);
226 cmd
227 }
228}
229
230impl OcamlNodeExecutable {
231 pub const DEFAULT_DOCKER_IMAGE: &'static str =
232 "gcr.io/o1labs-192920/mina-daemon:3.3.0-alpha1-6929a7e-noble-devnet";
233 pub const DEFAULT_MINA_EXECUTABLE: &'static str = "mina";
234
235 fn docker_container_name(tmp_dir: &temp_dir::TempDir) -> String {
236 let path = tmp_dir.path().file_name().unwrap().to_str().unwrap();
237 format!("mina_testing_ocaml_{}", &path[1..])
238 }
239
240 pub fn kill(&self, tmp_dir: &temp_dir::TempDir) {
249 match self {
250 OcamlNodeExecutable::Installed(program) => {
251 info!(system_time(); "No additional cleanup needed for local binary: {}", program);
252 }
253 OcamlNodeExecutable::Docker(_) | OcamlNodeExecutable::DockerDefault => {
254 let name = Self::docker_container_name(tmp_dir);
255 let image_info = match self {
256 OcamlNodeExecutable::Docker(img) => img.clone(),
257 OcamlNodeExecutable::DockerDefault => Self::DEFAULT_DOCKER_IMAGE.to_string(),
258 _ => unreachable!(),
259 };
260
261 info!(
262 system_time();
263 "Cleaning up Docker container: {} (image: {})",
264 name,
265 image_info
266 );
267
268 info!(system_time(); "Stopping Docker container: {}", name);
270 let mut cmd = Command::new("docker");
271 cmd.args(["stop".to_owned(), name.clone()]);
272 match cmd.status() {
273 Ok(status) if status.success() => {
274 info!(system_time(); "Successfully stopped Docker container: {}", name);
275 }
276 Ok(status) => {
277 warn!(
278 system_time();
279 "Docker stop command failed for container {}: exit code {:?}",
280 name,
281 status.code()
282 );
283 }
284 Err(e) => {
285 warn!(system_time(); "Failed to stop Docker container {}: {}", name, e);
286 }
287 }
288
289 info!(system_time(); "Removing Docker container: {}", name);
291 let mut cmd = Command::new("docker");
292 cmd.args(["rm".to_owned(), name.clone()]);
293 match cmd.status() {
294 Ok(status) if status.success() => {
295 info!(system_time(); "Successfully removed Docker container: {}", name);
296 }
297 Ok(status) => {
298 warn!(
299 system_time();
300 "Docker rm command failed for container {}: exit code {:?}",
301 name,
302 status.code()
303 );
304 }
305 Err(e) => {
306 warn!(system_time(); "Failed to remove Docker container {}: {}", name, e);
307 }
308 }
309 }
310 }
311 }
312
313 pub fn find_working() -> anyhow::Result<Self> {
328 let program_name = Self::DEFAULT_MINA_EXECUTABLE;
329 info!(system_time(); "Attempting to find local Mina binary: {}", program_name);
330
331 match Command::new(program_name)
332 .stdout(Stdio::null())
333 .stderr(Stdio::null())
334 .spawn()
335 {
336 Ok(_) => {
337 info!(system_time(); "Found working local Mina binary: {}", program_name);
338 return Ok(Self::Installed(program_name.to_owned()));
339 }
340 Err(err) => match err.kind() {
341 std::io::ErrorKind::NotFound => {
342 info!(system_time(); "Local Mina binary not found, falling back to Docker");
343 }
344 _ => anyhow::bail!("'{program_name}' returned an error: {err}"),
345 },
346 };
347
348 info!(
349 system_time();
350 "Pulling default Docker image: {}",
351 Self::DEFAULT_DOCKER_IMAGE
352 );
353 let mut cmd = Command::new("docker");
354
355 let status = cmd
356 .stdout(Stdio::inherit())
357 .stderr(Stdio::inherit())
358 .args(["pull", Self::DEFAULT_DOCKER_IMAGE])
359 .status()
360 .map_err(|err| anyhow::anyhow!("error pulling ocaml docker: {err}"))?;
361 if !status.success() {
362 anyhow::bail!("error status pulling ocaml node: {status:?}");
363 }
364
365 info!(system_time(); "Successfully pulled Docker image, using DockerDefault");
366 Ok(Self::DockerDefault)
367 }
368}
369
370impl DaemonJson {
371 pub fn load(
372 mut add_account_sec_key: impl FnMut(AccountSecretKey),
373 path: PathBuf,
374 set_timestamp: Option<&str>,
375 ) -> Self {
376 let mut deamon_json: serde_json::Value =
377 serde_json::from_str(&fs::read_to_string(path).unwrap()).unwrap();
378
379 if let Some(time_str) = set_timestamp {
380 deamon_json["genesis"]["genesis_state_timestamp"] = time_str.into();
381 }
382
383 deamon_json
384 .get("ledger")
385 .unwrap()
386 .get("accounts")
387 .unwrap()
388 .as_array()
389 .unwrap()
390 .iter()
391 .for_each(|val| {
392 let sec_key_str = val.get("sk").unwrap().as_str().unwrap();
393 add_account_sec_key(AccountSecretKey::from_str(sec_key_str).unwrap());
394 });
395
396 Self::InMem(deamon_json)
397 }
398
399 pub fn gen(
400 add_account_sec_key: impl FnMut(AccountSecretKey),
401 genesis_timestamp: &str,
402 config: DaemonJsonGenConfig,
403 ) -> Self {
404 match config {
405 DaemonJsonGenConfig::Counts { whales, fish } => {
406 Self::gen_with_counts(add_account_sec_key, genesis_timestamp, whales, fish)
407 }
408 DaemonJsonGenConfig::DelegateTable(delegate_table) => Self::gen_with_delegate_table(
409 add_account_sec_key,
410 genesis_timestamp,
411 delegate_table,
412 ),
413 }
414 }
415
416 pub fn gen_with_counts(
417 add_account_sec_key: impl FnMut(AccountSecretKey),
418 genesis_timestamp: &str,
419 whales_n: usize,
420 fish_n: usize,
421 ) -> Self {
422 let delegator_balance = |balance: u64| move |i| balance / i as u64;
423 let whales = (0..whales_n).map(|i| {
424 let balance = 8333_u64;
425 let delegators = (1..=(i + 1) * 2).map(delegator_balance(50_000_000));
426 (balance, delegators)
427 });
428 let fish = (0..fish_n).map(|i| {
429 let balance = 6333_u64;
430 let delegators = (1..=(i + 1) * 2).map(delegator_balance(5_000_000));
431 (balance, delegators)
432 });
433 let delegate_table = whales.chain(fish);
434 Self::gen_with_delegate_table(add_account_sec_key, genesis_timestamp, delegate_table)
435 }
436
437 pub fn gen_with_delegate_table(
438 mut add_account_sec_key: impl FnMut(AccountSecretKey),
439 genesis_timestamp: &str,
440 delegate_table: impl IntoIterator<Item = (u64, impl IntoIterator<Item = u64>)>,
441 ) -> Self {
442 let gen_bp = |balance: u64| {
443 let sec_key = AccountSecretKey::rand();
444 let pub_key = sec_key.public_key();
445 let account = serde_json::json!({
446 "sk": sec_key.to_string(),
447 "pk": pub_key.to_string(),
448 "balance": format!("{balance}.000000000"),
449 "delegate": pub_key.to_string(),
450 });
451 (sec_key, account)
452 };
453 let gen_account = |balance: u64, delegate: &str| {
454 let (sec_key, mut account) = gen_bp(balance);
455 account["delegate"] = delegate.into();
456 (sec_key, account)
457 };
458
459 let all_accounts = delegate_table
460 .into_iter()
461 .flat_map(|(bp_balance, delegate_balances)| {
462 let bp = gen_bp(bp_balance);
463 let bp_pub_key = bp.0.public_key().to_string();
464 let delegates = delegate_balances
465 .into_iter()
466 .map(move |balance| gen_account(balance, &bp_pub_key));
467 std::iter::once(bp).chain(delegates)
468 })
469 .map(|(sec_key, account)| {
470 add_account_sec_key(sec_key);
471 account
472 })
473 .collect::<Vec<_>>();
474
475 DaemonJson::InMem(serde_json::json!({
476 "genesis": {
477 "genesis_state_timestamp": genesis_timestamp,
478 },
479 "ledger": {
480 "name": "custom",
481 "accounts": all_accounts,
482 },
483 }))
484 }
485}
486
487impl DaemonJsonGenConfig {
488 pub fn block_producer_count(&self) -> usize {
489 match self {
490 Self::Counts { whales, fish } => whales + fish,
491 Self::DelegateTable(bps) => bps.len(),
492 }
493 }
494}