diff --git a/.ci/test_dev_on_prod_restart.sh b/.ci/test_dev_on_prod_restart.sh new file mode 100755 index 0000000000..326b41f268 --- /dev/null +++ b/.ci/test_dev_on_prod_restart.sh @@ -0,0 +1,237 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [ "${BASH_VERSINFO[0]}" -lt 5 ]; then + echo "Error: This script requires bash version 5.0 or higher." + exit 1 +fi + +# shellcheck source=SCRIPTDIR/utils.sh +. ./.ci/utils.sh + +network_id=0 +NETWORK_NAME=$(get_network_name "$network_id") +REST_PORT=3030 +NUM_DEV_NODES=4 +SETUP_ADVANCE_BLOCKS=5 +DEV_ADVANCE_BLOCKS=10 +SETUP_MAX_WAIT=40 +DEV_MAX_WAIT=100 + +BOOTSTRAP_PID="" +declare -a DEV_PIDS=() +SNARKOS_SETUP_BIN="snarkos" + +function get_height() { + local port="$1" + local result + result=$(curl -s --max-time 2 "http://${localhost}:${port}/v2/${NETWORK_NAME}/block/height/latest" || true) + if is_integer "$result"; then + echo "$result" + else + echo "" + fi +} + +function wait_for_node_ready() { + local port="$1" + local timeout="$2" + local start + start=$(now) + + while (( $(elapsed_since "$start") < timeout )); do + local height + height=$(get_height "$port") + if [ -n "$height" ]; then + log "Node on port ${port} is ready at height ${height}" + return 0 + fi + log "Sleeping for 2 seconds before retrying to get height" + sleep 2 + done + + log "Timed out waiting for node on port ${port} to become ready" + return 1 +} + +function wait_for_height_advance() { + local port="$1" + local advance_by="$2" + local timeout="$3" + local label="$4" + + wait_for_node_ready "$port" "$timeout" + + local start_height + start_height=$(get_height "$port") + if [ -z "$start_height" ]; then + log "${label}: failed to read initial block height from port ${port}" + return 1 + fi + + local target_height=$((start_height + advance_by)) + local start_time + start_time=$(now) + local last_log_time=0 + + log "${label}: waiting for height to advance by ${advance_by} blocks (${start_height} -> ${target_height})" + while (( $(elapsed_since "$start_time") < timeout )); do + local current_height + current_height=$(get_height "$port") + if [ -n "$current_height" ] && (( current_height >= target_height )); then + log "${label}: reached target height ${current_height} (>= ${target_height})" + return 0 + fi + + local elapsed + elapsed=$(elapsed_since "$start_time") + if (( elapsed - last_log_time >= 15 )); then + if [ -n "$current_height" ]; then + log "${label}: current height ${current_height}, target ${target_height}" + else + log "${label}: waiting for REST endpoint on port ${port}" + fi + last_log_time=$elapsed + fi + log "Sleeping for 2 seconds before retrying to get height" + sleep 2 + done + + local final_height + final_height=$(get_height "$port") + log "${label}: timed out waiting for height advance (final height: ${final_height:-unavailable}, target: ${target_height})" + return 1 +} + +function wait_for_pid_exit() { + local pid="$1" + local timeout="$2" + local start + start=$(now) + while (( $(elapsed_since "$start") < timeout )); do + if ! kill -0 "$pid" 2>/dev/null; then + return 0 + fi + sleep 1 + done + return 1 +} + +function graceful_stop_pid() { + local pid="$1" + local label="$2" + + if [ -z "$pid" ]; then + return 0 + fi + + if ! kill -0 "$pid" 2>/dev/null; then + return 0 + fi + + log "Stopping ${label} (pid=${pid}) with SIGINT" + kill -INT "$pid" 2>/dev/null || true + if wait_for_pid_exit "$pid" 60; then + return 0 + fi + + log "${label} did not exit after SIGINT; sending SIGTERM" + kill -TERM "$pid" 2>/dev/null || true + if wait_for_pid_exit "$pid" 20; then + return 0 + fi + + log "${label} did not exit after SIGTERM; sending SIGKILL" + kill -KILL "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true +} + +function graceful_stop_all_dev_nodes() { + for i in "${!DEV_PIDS[@]}"; do + graceful_stop_pid "${DEV_PIDS[$i]}" "dev-node-${i}" + done + DEV_PIDS=() +} + +function cleanup() { + graceful_stop_all_dev_nodes + graceful_stop_pid "$BOOTSTRAP_PID" "setup-node" +} + +function start_setup_node() { + mkdir -p dev_logs + log "Starting production setup node: ${SNARKOS_SETUP_BIN} start --client --nodisplay" + "$SNARKOS_SETUP_BIN" start --client --nodisplay > "dev_logs/setup-client.txt" 2>&1 & + BOOTSTRAP_PID=$! + log "Started setup node (pid=${BOOTSTRAP_PID})" +} + +function copy_setup_ledger() { + local source="${HOME}/.aleo/storage/ledger-0" + if [ ! -d "$source" ]; then + log "Missing source ledger at ${source}" + exit 1 + fi + + log "Copying setup ledger into local ledgers" + rm -rf ledger-0 ledger-1 ledger-2 ledger-3 + cp -r "$source" ledger-0 + cp -r "$source" ledger-1 + cp -r "$source" ledger-2 + cp -r "$source" ledger-3 +} + +function start_dev_nodes() { + mkdir -p dev_logs + DEV_PIDS=() + + for i in $(seq 0 $((NUM_DEV_NODES - 1))); do + log "Starting dev node ${i}" + DEV_COMMITTEE_NUM_VALIDATORS=${NUM_DEV_NODES} snarkos start --nodisplay --validator --ledger-storage "ledger-${i}" --node-data-storage "node-data-${i}" --dev "${i}" \ + --no-dev-txs --nocdn --dev-num-validators "${NUM_DEV_NODES}" --verbosity 2 \ + --allow-external-peers --logfile "dev_logs/val-${i}.txt" --dev-on-prod & + DEV_PIDS[$i]=$! + sleep 1 + done +} + +trap cleanup EXIT +trap 'log "Error at line $LINENO while running: $BASH_COMMAND"' ERR + +init_log_dir +require_cmd snarkos +require_cmd curl +require_cmd cargo +require_cmd tar + +SNARKOS_SETUP_BIN="snarkos" +log "Using setup binary: ${SNARKOS_SETUP_BIN}" + +log "Step 1: Start production node and wait for +${SETUP_ADVANCE_BLOCKS} blocks" +start_setup_node +wait_for_height_advance "$REST_PORT" "$SETUP_ADVANCE_BLOCKS" "$SETUP_MAX_WAIT" "setup-network" + +log "Step 2: Gracefully stop production node" +graceful_stop_pid "$BOOTSTRAP_PID" "setup-node" +BOOTSTRAP_PID="" + +log "Step 3: Copy production ledger and start 4 dev nodes" +copy_setup_ledger +start_dev_nodes + +log "Step 4: Wait until dev network advances by +${DEV_ADVANCE_BLOCKS} blocks" +wait_for_height_advance "$REST_PORT" "$DEV_ADVANCE_BLOCKS" "$DEV_MAX_WAIT" "dev-network-first-run" + +log "Step 5: Gracefully stop all dev nodes" +graceful_stop_all_dev_nodes + +# TODO: encountering the following warnings when stopping and starting all dev nodes: +# - WARN "Cannot propose a batch for round 32 - the latest proposal cache round is 34" +# - WARN "Failed to load stored certificate 1936933304208994.. from proposal cache — Previous certificates for a batch in round 32 did not reach quorum threshold (gc = 0)" +# - WARN "Failed to load stored certificate 8429685712854720.. from proposal cache — Failed to fetch missing transmissions and previous certificates for round 33 from '127.0.0.1:0 — Unable to fetch batch certificate 1936933304208994661214537875451736804168699382028485722463302790461889223166field (failed to send request)" +# log "Step 6: Restart all dev nodes and wait for +${DEV_ADVANCE_BLOCKS} blocks" +# start_dev_nodes +# wait_for_height_advance "$REST_PORT" "$DEV_ADVANCE_BLOCKS" "$DEV_MAX_WAIT" "dev-network-second-run" + +log "SUCCESS: Completed dev-on-prod restart flow" diff --git a/.circleci/config.yml b/.circleci/config.yml index 460481af4e..89acf80bf4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -587,6 +587,23 @@ jobs: command: | ./.ci/test_devnet.sh + dev-on-prod-restart-test: + executor: rust-docker + resource_class: << pipeline.parameters.xlarge >> + steps: + - checkout + - setup_environment: + cache_key: v4.2.0-rust-1.88.0-dev-on-prod-restart-test-cache + - install_snarkos + - run: + name: "Run dev-on-prod restart test" + timeout: 40m + no_output_timeout: 15m + command: | + ./.ci/test_dev_on_prod_restart.sh + - clear_environment: + cache_key: v4.2.0-rust-1.88.0-dev-on-prod-restart-test-cache + chaotic-minority-reset-test: executor: ubuntu-vm resource_class: << pipeline.parameters.twoxlarge >> @@ -791,8 +808,18 @@ workflows: jobs: - devnet-test - chaotic-devnet-workflow: + merge-devnet-workflow: jobs: + - dev-on-prod-restart-test: + filters: + branches: + only: + - test_fixed_dev_committee + - canary + - testnet + - mainnet + - staging + - chaotic-minority-reset-test: filters: branches: @@ -812,9 +839,6 @@ workflows: - testnet - mainnet - staging - - upgrade-workflow: - jobs: - upgrade-test: filters: branches: diff --git a/Cargo.lock b/Cargo.lock index 66d418219a..93d7326248 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4832,6 +4832,7 @@ dependencies = [ "locktick", "parking_lot", "rand 0.10.1", + "rand_chacha 0.10.0", "rayon", "snarkos-node-metrics", "snarkos-utilities", diff --git a/Cargo.toml b/Cargo.toml index 6069891aaa..71cb9aa5ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -305,7 +305,7 @@ serial = [ ] test_targets = [ "snarkos-cli/test_targets" ] test_consensus_heights = [ "snarkos-cli/test_consensus_heights" ] -test_network = [ "snarkos-cli/test_network" ] +test_network = [ "snarkos-cli/test_network", "snarkos-node/test_network" ] tokio_console = [ "snarkos-cli/tokio_console" ] [dependencies.clap] diff --git a/cli/src/commands/start.rs b/cli/src/commands/start.rs index 8e2e510b58..408effcbc9 100644 --- a/cli/src/commands/start.rs +++ b/cli/src/commands/start.rs @@ -287,13 +287,17 @@ pub struct Start { pub dev_num_clients: Option, /// If development mode is enabled, specify whether node 0 should generate traffic to drive the network. - #[clap(long, group = "dev_flag")] + #[clap(long, group = "dev_flags")] pub no_dev_txs: bool, /// If development mode is enabled, specify the custom bonded balances as a JSON object. #[clap(long, group = "dev_flags")] pub dev_bonded_balances: Option, + /// If development mode is enabled, specify whether to run the node on a production ledger. + #[clap(long, group = "dev_flags", default_value_t = false)] + pub dev_on_prod: bool, + /// If the flag is set, the node will attempt to automatically migrate the node data to the new format. #[clap(long)] pub auto_migrate_node_data: bool, @@ -525,7 +529,7 @@ impl Start { /// Returns an alternative genesis block if the node is in development mode. /// Otherwise, returns the actual genesis block. fn parse_genesis(&self) -> Result> { - if self.dev.is_some() { + if self.dev.is_some() && !self.dev_on_prod { // Determine the number of genesis committee members. let num_committee_members = self.dev_num_validators; ensure!( diff --git a/cli/src/helpers/dev.rs b/cli/src/helpers/dev.rs index 3557d69fb4..f5bf5c16ae 100644 --- a/cli/src/helpers/dev.rs +++ b/cli/src/helpers/dev.rs @@ -20,11 +20,9 @@ use snarkvm::{console::network::Network, prelude::PrivateKey}; use anyhow::Result; use rand::SeedableRng; use rand_chacha::ChaChaRng; +pub use snarkos_utilities::DEVELOPMENT_MODE_RNG_SEED; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; -/// The development mode RNG seed. -pub const DEVELOPMENT_MODE_RNG_SEED: u64 = 1234567890u64; - /// The development mode number of genesis committee members. pub const DEVELOPMENT_MODE_NUM_GENESIS_COMMITTEE_MEMBERS: u16 = 4; diff --git a/node/Cargo.toml b/node/Cargo.toml index e31f7075e1..e14fc595f4 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -56,6 +56,7 @@ serial = [ "snarkos-node-bft/serial" ] test = [] +test_network = [ "snarkos-node-bft/test_network", "snarkos-node-consensus/test_network" ] [dependencies.aleo-std] workspace = true diff --git a/node/bft/Cargo.toml b/node/bft/Cargo.toml index 4ebd222796..a19db2b02c 100644 --- a/node/bft/Cargo.toml +++ b/node/bft/Cargo.toml @@ -46,6 +46,9 @@ test = [ "snarkos-node-bft-ledger-service/test", "snarkos-node-bft-storage-service/test" ] +test_network = [ + "snarkos-node-bft-ledger-service/test_network", +] serial = [ "snarkos-node-metrics/serial", "snarkos-node-bft-ledger-service/serial" @@ -95,6 +98,9 @@ workspace = true [dependencies.rand] workspace = true +[dependencies.rand_chacha] +workspace = true + [dependencies.rayon] workspace = true diff --git a/node/bft/ledger-service/Cargo.toml b/node/bft/ledger-service/Cargo.toml index 175fc1b127..fa6fdebde5 100644 --- a/node/bft/ledger-service/Cargo.toml +++ b/node/bft/ledger-service/Cargo.toml @@ -18,7 +18,7 @@ edition = "2024" [features] default = [ ] -ledger = [ "parking_lot", "rand", "rayon", "tokio", "tracing" ] +ledger = [ "parking_lot", "rand", "rand_chacha", "rayon", "tokio", "tracing" ] ledger-write = [ ] locktick = [ "dep:locktick", @@ -33,6 +33,7 @@ serial = [ "snarkvm/serial" ] test = [ "mock", "translucent" ] +test_network = [ ] translucent = [ "ledger" ] [dependencies.anyhow] @@ -65,6 +66,10 @@ optional = true workspace = true optional = true +[dependencies.rand_chacha] +workspace = true +optional = true + [dependencies.rayon] workspace = true optional = true diff --git a/node/bft/ledger-service/src/ledger.rs b/node/bft/ledger-service/src/ledger.rs index 405cafabc1..c299a4e4a6 100644 --- a/node/bft/ledger-service/src/ledger.rs +++ b/node/bft/ledger-service/src/ledger.rs @@ -68,6 +68,8 @@ pub struct CoreLedgerService> { latest_leader: Arc)>>>, stoppable: Arc, update_lock: Arc>, + #[cfg(feature = "test_network")] + dev_committee: Option>, } /// A transactional update to the ledger. @@ -136,10 +138,56 @@ impl> CoreLedgerService { pub fn new(ledger: Ledger, stoppable: Arc) -> Self { // Initialize the block height metric. #[cfg(feature = "metrics")] - { - metrics::gauge(metrics::bft::HEIGHT, ledger.latest_block().height() as f64); + metrics::gauge(metrics::bft::HEIGHT, ledger.latest_block().height() as f64); + #[cfg(feature = "test_network")] + let dev_committee = Self::build_dev_committee(ledger.latest_round()) + .expect("Failed to build dev committee from DEV_COMMITTEE_NUM_VALIDATORS"); + Self { + ledger, + latest_leader: Default::default(), + stoppable, + update_lock: Default::default(), + #[cfg(feature = "test_network")] + dev_committee, } - Self { ledger, latest_leader: Default::default(), stoppable, update_lock: Default::default() } + } + + /// Builds the deterministic dev committee from `DEV_COMMITTEE_NUM_VALIDATORS`, if set. + /// + /// Returns `Ok(None)` when the env var is unset, otherwise returns a committee whose + /// starting round is `start_round`. + #[cfg(feature = "test_network")] + fn build_dev_committee(start_round: u64) -> Result>> { + let Some(dev_num_validators) = std::env::var("DEV_COMMITTEE_NUM_VALIDATORS").ok() else { + return Ok(None); + }; + let dev_num_validators = dev_num_validators.parse::()?; + + use rand::SeedableRng; + let mut rng = rand_chacha::ChaChaRng::seed_from_u64(snarkos_utilities::DEVELOPMENT_MODE_RNG_SEED); + let dev_keys = (0..dev_num_validators) + .map(|_| snarkvm::console::account::PrivateKey::::new(&mut rng)) + .collect::>>()?; + let members = dev_keys + .iter() + .map(Address::::try_from) + .collect::>>()? + .into_iter() + .map(|address| (address, (snarkvm::ledger::committee::MIN_VALIDATOR_STAKE, true, 0))) + .collect::>(); + Ok(Some(Committee::new(start_round, members)?)) + } + + /// Returns the deterministic dev committee for rounds at or after the hotswap start. + #[cfg(feature = "test_network")] + fn dev_committee_for_round(&self, round: u64) -> Result>> { + let Some(dev_committee) = self.dev_committee.as_ref() else { + return Ok(None); + }; + if round < dev_committee.starting_round() { + return Ok(None); + } + Ok(Some(dev_committee.clone())) } } @@ -234,6 +282,13 @@ impl> LedgerService for CoreLedgerService< /// Returns the current committee. fn current_committee(&self) -> Result> { + #[cfg(feature = "test_network")] + { + if let Some(dev_committee) = self.dev_committee.as_ref() { + return Ok(dev_committee.clone()); + } + } + self.ledger.latest_committee() } @@ -247,6 +302,13 @@ impl> LedgerService for CoreLedgerService< /// Returns the committee lookback for the given round. fn get_committee_lookback_for_round(&self, round: u64) -> Result> { + #[cfg(feature = "test_network")] + { + if let Some(dev_committee) = self.dev_committee_for_round(round)? { + return Ok(dev_committee); + } + } + // Get the round number for the previous committee. Note, we subtract 2 from odd rounds, // because committees are updated in even rounds. let previous_round = match round.is_multiple_of(2) { @@ -261,6 +323,12 @@ impl> LedgerService for CoreLedgerService< self.get_committee_for_round(committee_lookback_round) } + /// Returns the deterministic hotswapped dev committee for the given round, if active. + #[cfg(feature = "test_network")] + fn dev_committee_for_round(&self, round: u64) -> Result>> { + CoreLedgerService::dev_committee_for_round(self, round) + } + /// Returns `true` if the ledger contains the given certificate ID in block history. fn contains_certificate(&self, certificate_id: &Field) -> Result { self.ledger.contains_certificate(certificate_id) diff --git a/node/bft/ledger-service/src/traits.rs b/node/bft/ledger-service/src/traits.rs index 7ab4a238eb..de99af2434 100644 --- a/node/bft/ledger-service/src/traits.rs +++ b/node/bft/ledger-service/src/traits.rs @@ -118,6 +118,12 @@ pub trait LedgerService: std::fmt::Debug + Send + Sync { /// Returns the committee lookback for the given round. fn get_committee_lookback_for_round(&self, round: u64) -> Result>; + /// Returns the deterministic hotswapped dev committee for the given round, if active. + #[cfg(feature = "test_network")] + fn dev_committee_for_round(&self, _round: u64) -> Result>> { + Ok(None) + } + /// Returns `true` if the ledger contains the given certificate ID. fn contains_certificate(&self, certificate_id: &Field) -> Result; diff --git a/node/bft/src/helpers/storage.rs b/node/bft/src/helpers/storage.rs index 9265c1fdf7..957a02f87d 100644 --- a/node/bft/src/helpers/storage.rs +++ b/node/bft/src/helpers/storage.rs @@ -828,6 +828,7 @@ impl Storage { block: &Block, certificate: BatchCertificate, unconfirmed_transactions: &HashMap>, + trusted_ledger_certificate: bool, ) -> Result<()> { // Skip if the certificate round is below the GC round. let gc_round = self.gc_round(); @@ -912,8 +913,14 @@ impl Storage { certificate.transmission_ids().len() ); - self.insert_certificate(certificate, missing_transmissions, aborted_transmissions) - .with_context(|| format!("Failed to insert certificate '{certificate_id}' from block {}", block.height())) + if trusted_ledger_certificate { + self.insert_certificate_atomic(certificate, aborted_transmissions, missing_transmissions); + Ok(()) + } else { + self.insert_certificate(certificate, missing_transmissions, aborted_transmissions).with_context(|| { + format!("Failed to insert certificate '{certificate_id}' from block {}", block.height()) + }) + } } } diff --git a/node/bft/src/primary.rs b/node/bft/src/primary.rs index eaa428802f..5555bfa77f 100644 --- a/node/bft/src/primary.rs +++ b/node/bft/src/primary.rs @@ -551,6 +551,15 @@ impl Primary { if previous_committee_lookback.is_quorum_threshold_reached(&authors) { is_ready = true; } + #[cfg(feature = "test_network")] + { + // If we are using a hotswapped dev committee, use simplified checks to more easily advance. + if let Some(dev_committee) = self.ledger.dev_committee_for_round(previous_round)? { + if round <= dev_committee.starting_round() { + is_ready = true; + } + } + } } // If the batch is not ready to be proposed, return early. if !is_ready { diff --git a/node/bft/src/sync/mod.rs b/node/bft/src/sync/mod.rs index bf8bfab8cc..4f7979d503 100644 --- a/node/bft/src/sync/mod.rs +++ b/node/bft/src/sync/mod.rs @@ -420,7 +420,7 @@ impl Sync { for certificates in subdag.values().cloned() { cfg_into_iter!(certificates).try_for_each(|certificate| { self.storage - .sync_certificate_with_block(block, certificate, &unconfirmed_transactions) + .sync_certificate_with_block(block, certificate, &unconfirmed_transactions, true) .with_context(|| format!("Failed to sync certificate with block {}", block.height())) })?; } @@ -680,7 +680,7 @@ impl Sync { cfg_into_iter!(certificates.clone()).try_for_each(|certificate| -> Result<()> { // Sync the batch certificate with the block. self.storage - .sync_certificate_with_block(block, certificate.clone(), &unconfirmed_transactions) + .sync_certificate_with_block(block, certificate.clone(), &unconfirmed_transactions, false) .with_context(|| format!("Failed to sync certificate with block {}", block.height())) })?; } diff --git a/node/consensus/Cargo.toml b/node/consensus/Cargo.toml index 0704b83d31..902170703d 100644 --- a/node/consensus/Cargo.toml +++ b/node/consensus/Cargo.toml @@ -34,6 +34,7 @@ serial = [ "snarkos-node-metrics/serial", "snarkvm/serial" ] +test_network = [ ] [dependencies.aleo-std] workspace = true diff --git a/node/consensus/src/lib.rs b/node/consensus/src/lib.rs index 871b355b11..b3c66350c0 100644 --- a/node/consensus/src/lib.rs +++ b/node/consensus/src/lib.rs @@ -568,10 +568,16 @@ impl Consensus { // Create the candidate next block. let ledger_update = self.ledger.begin_ledger_update()?; - let block = match ledger_update - .prepare_advance_to_next_quorum_block(subdag, transmissions) - .and_then(|block| ledger_update.check_next_block(block)) - { + let block = match ledger_update.prepare_advance_to_next_quorum_block(subdag, transmissions).and_then(|block| { + #[cfg(feature = "test_network")] + { + // If we are using a hotswapped dev committee, skip checking the block. + if self.ledger.dev_committee_for_round(block.round())?.is_some() { + return Ok(block); + } + } + ledger_update.check_next_block(block) + }) { Ok(block) => block, Err(CheckBlockError::BlockAlreadyExists { .. }) => { debug!("The given block hash already exists in the ledger"); diff --git a/node/src/validator/mod.rs b/node/src/validator/mod.rs index fbbf78b13d..496f3b5e49 100644 --- a/node/src/validator/mod.rs +++ b/node/src/validator/mod.rs @@ -512,7 +512,7 @@ mod tests { let dev_txs = true; // Initialize an (insecure) fixed RNG. - let mut rng = ChaChaRng::seed_from_u64(1234567890u64); + let mut rng = ChaChaRng::seed_from_u64(snarkos_utilities::DEVELOPMENT_MODE_RNG_SEED); // Initialize the account. let account = Account::::new(&mut rng).unwrap(); // Initialize a new VM. diff --git a/utilities/src/lib.rs b/utilities/src/lib.rs index 4fa48563f5..8c64971457 100644 --- a/utilities/src/lib.rs +++ b/utilities/src/lib.rs @@ -23,3 +23,6 @@ pub use node_data::*; mod callback_handle; pub use callback_handle::CallbackHandle; + +/// Seed used for deterministic RNG in development mode. +pub const DEVELOPMENT_MODE_RNG_SEED: u64 = 1234567890u64;