mirror of
https://github.com/logos-co/nomos-node.git
synced 2026-08-27 09:31:10 +00:00
test: make genesis time cluster-scoped and configurable (#3312)
This commit is contained in:
Generated
-1
@@ -5051,7 +5051,6 @@ dependencies = [
|
||||
"libp2p",
|
||||
"logos-blockchain-api-service",
|
||||
"logos-blockchain-chain-service",
|
||||
"logos-blockchain-codec",
|
||||
"logos-blockchain-common-http-client",
|
||||
"logos-blockchain-config",
|
||||
"logos-blockchain-core",
|
||||
|
||||
@@ -31,7 +31,6 @@ futures-util = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
lb-api-service = { workspace = true }
|
||||
lb-chain-service = { workspace = true }
|
||||
lb-codec = { workspace = true }
|
||||
lb-common-http-client = { workspace = true }
|
||||
lb-config = { workspace = true }
|
||||
lb-core = { workspace = true }
|
||||
|
||||
@@ -29,7 +29,7 @@ use cucumber::{
|
||||
use lb_testing_framework::{
|
||||
hash_str, is_truthy_env, reap_all_stale_port_blocks, record_system_monitor_event,
|
||||
register_system_monitor_output_file, release_reserved_port_block,
|
||||
unregister_system_monitor_output_file,
|
||||
resolve_automatic_genesis_time, unregister_system_monitor_output_file,
|
||||
};
|
||||
use logos_blockchain_tests::cucumber::{
|
||||
defaults::{
|
||||
@@ -228,6 +228,7 @@ fn prepare_world_for_scenario(
|
||||
scenario_name: &str,
|
||||
) {
|
||||
world.set_deployer(deployer);
|
||||
world.set_genesis_time(resolve_automatic_genesis_time());
|
||||
|
||||
if let Err(err) = world.preflight(deployer) {
|
||||
println!("Preflight failed for scenario '{scenario_name}': {err}");
|
||||
|
||||
@@ -16,17 +16,16 @@ Feature: Cryptarchia
|
||||
Then I stop all nodes
|
||||
|
||||
@cryptarchia_ci
|
||||
Scenario: Nodes with delayed genesis start joins network
|
||||
Scenario: Nodes started before and after delayed genesis converge
|
||||
Given I have a cluster with capacity of 3 nodes
|
||||
And I have deployment config override "time.chain_start_time" as "now_plus_seconds(60)"
|
||||
And the chain starts 60 seconds from now
|
||||
And I have user config override "cryptarchia.service.bootstrap.prolonged_bootstrap_period" as "seconds(0)"
|
||||
And I immediate start node "NODE_1"
|
||||
And I immediate start peer node "NODE_2" connected to node "NODE_1"
|
||||
And I immediate start peer node "NODE_3" connected to node "NODE_2"
|
||||
# TODO: Activate these steps when states before blockchain start are implemented and we can check that nodes are in
|
||||
# TODO: waiting for genesis state
|
||||
# When I wait for all nodes to be responsive in 45 seconds
|
||||
# When all nodes have at least 3 blocks and converged to within 1 blocks in 300 seconds
|
||||
And I start peer node "NODE_2" connected to node "NODE_1"
|
||||
Then the configured genesis time has not passed for 40 seconds
|
||||
When node "NODE_1" is at height 1 in 300 seconds
|
||||
And I start peer node "NODE_3" connected to node "NODE_1"
|
||||
Then all nodes have at least 2 blocks and converged to within 0 blocks in 300 seconds
|
||||
Then I stop all nodes
|
||||
|
||||
@cryptarchia_ci
|
||||
|
||||
@@ -5,6 +5,7 @@ use lb_testing_framework::{
|
||||
DeploymentBuilder, LbcEnv, LbcLocalDeployer, NodeHttpClient, TopologyConfig,
|
||||
configs::{deployment::NodeBinaryProfile, wallet::WalletAccount},
|
||||
internal::DeploymentPlan,
|
||||
resolve_automatic_genesis_time,
|
||||
};
|
||||
use testing_framework_core::scenario::{StartNodeOptions, StartedNode};
|
||||
use tokio::time::{Instant, sleep};
|
||||
@@ -43,6 +44,11 @@ pub fn build_manual_cluster_deployment(
|
||||
world: &mut CucumberWorld,
|
||||
nodes_count: usize,
|
||||
) -> Result<DeploymentPlan, StepError> {
|
||||
let genesis_time = world.genesis_time.unwrap_or_else(|| {
|
||||
let genesis_time = resolve_automatic_genesis_time();
|
||||
world.set_genesis_time(genesis_time);
|
||||
genesis_time
|
||||
});
|
||||
let config = TopologyConfig::with_node_numbers(nodes_count)
|
||||
.with_allow_multiple_genesis_tokens(true)
|
||||
.with_allow_zero_value_genesis_tokens(true)
|
||||
@@ -53,6 +59,7 @@ pub fn build_manual_cluster_deployment(
|
||||
NodeBinaryProfile::Normal
|
||||
});
|
||||
let mut config = apply_blend_core_nodes(world, config, nodes_count)?;
|
||||
config = config.with_genesis_time(genesis_time);
|
||||
|
||||
for genesis_token in &world.genesis_tokens {
|
||||
let wallet_account = WalletAccount::deterministic(
|
||||
@@ -129,6 +136,11 @@ fn build_devnet_manual_cluster_deployment(
|
||||
world: &mut CucumberWorld,
|
||||
nodes_count: usize,
|
||||
) -> Result<DeploymentPlan, StepError> {
|
||||
let genesis_time = world.genesis_time.unwrap_or_else(|| {
|
||||
let genesis_time = resolve_automatic_genesis_time();
|
||||
world.set_genesis_time(genesis_time);
|
||||
genesis_time
|
||||
});
|
||||
// For devnet runs we do not allocate genesis tokens/accounts here.
|
||||
// Wallet keys are derived later, and node startup may switch deployment
|
||||
// settings, so locally generated genesis outputs are not meaningful for
|
||||
@@ -149,6 +161,8 @@ fn build_devnet_manual_cluster_deployment(
|
||||
});
|
||||
let config = apply_blend_core_nodes(world, config, nodes_count)?;
|
||||
|
||||
let config = config.with_genesis_time(genesis_time);
|
||||
|
||||
DeploymentBuilder::new(config)
|
||||
.with_deployment_seed(world.manual_cluster_deployment_seed())
|
||||
.build()
|
||||
@@ -410,3 +424,34 @@ pub async fn insert_started_node_info<S: BuildHasher>(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lb_core::mantle::GenesisTime;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pending_cucumber_deployment_rebuild_reuses_genesis_time() {
|
||||
let mut world = CucumberWorld::default();
|
||||
world.set_test_context("pending-genesis-rebuild".to_owned());
|
||||
world.set_genesis_time(GenesisTime::new(1_000));
|
||||
world.manual_cluster_spec = Some(ManualClusterSpec {
|
||||
kind: ManualClusterKind::Generated,
|
||||
capacity: 0,
|
||||
});
|
||||
|
||||
let first = build_manual_cluster_deployment(&mut world, 0)
|
||||
.expect("initial pending deployment should build");
|
||||
let first_genesis_time = first.config().genesis_time();
|
||||
|
||||
rebuild_pending_local_manual_cluster(&mut world)
|
||||
.expect("pending deployment should rebuild");
|
||||
let rebuilt = build_manual_cluster_deployment(&mut world, 0)
|
||||
.expect("rebuilt pending deployment should build");
|
||||
|
||||
assert_eq!(first_genesis_time, GenesisTime::new(1_000));
|
||||
assert_eq!(rebuilt.config().genesis_time(), first_genesis_time);
|
||||
assert_eq!(world.genesis_time, Some(first_genesis_time));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,13 +620,16 @@ mod tests {
|
||||
create_general_configs, deployment::e2e_deployment_settings_with_genesis_block,
|
||||
node::create_node_user_config,
|
||||
};
|
||||
use lb_core::mantle::GenesisTime;
|
||||
use lb_libp2p::Multiaddr;
|
||||
|
||||
use super::*;
|
||||
use crate::add_strings;
|
||||
|
||||
fn test_run_config(test_context: &str) -> RunConfig {
|
||||
let (configs, genesis_block) = create_general_configs(1, Some(test_context));
|
||||
let genesis_time = GenesisTime::try_from(OffsetDateTime::now_utc())
|
||||
.expect("current time should fit in GenesisTime");
|
||||
let (configs, genesis_block) = create_general_configs(1, Some(test_context), genesis_time);
|
||||
let deployment = e2e_deployment_settings_with_genesis_block(&genesis_block);
|
||||
let user = create_node_user_config(
|
||||
configs
|
||||
|
||||
@@ -6,10 +6,13 @@ use std::{
|
||||
|
||||
use cucumber::{gherkin::Step, given, then, when};
|
||||
use lb_common_http_client::CommonHttpClient;
|
||||
use lb_core::codec::DeserializeOp as _;
|
||||
use lb_core::{codec::DeserializeOp as _, mantle::GenesisTime};
|
||||
use lb_key_management_system_service::keys::ZkPublicKey;
|
||||
use lb_libp2p::{Multiaddr, PeerId};
|
||||
use lb_testing_framework::USER_CONFIG_FILE;
|
||||
use lb_testing_framework::{
|
||||
USER_CONFIG_FILE, configs::deployment::NodeBinaryProfile, ensure_node_binary_built,
|
||||
};
|
||||
use time::{Duration as TimeDuration, OffsetDateTime};
|
||||
use tokio::time::{Instant, sleep};
|
||||
use tracing::{info, warn};
|
||||
|
||||
@@ -70,6 +73,117 @@ const PUBLIC_CRYPTARCHIA_ENDPOINT: &str = "public_cryptarchia_endpoint";
|
||||
const PUBLIC_CRYPTARCHIA_ENDPOINT_USERNAME: &str = "username";
|
||||
const PUBLIC_CRYPTARCHIA_ENDPOINT_PASSWORD: &str = "password";
|
||||
|
||||
fn resolve_step_genesis_time(
|
||||
step_value: &str,
|
||||
now: OffsetDateTime,
|
||||
seconds: i64,
|
||||
) -> Result<GenesisTime, StepError> {
|
||||
if seconds < 0 {
|
||||
return Err(StepError::InvalidArgument {
|
||||
message: format!("step `{step_value}` requires a non-negative offset"),
|
||||
});
|
||||
}
|
||||
|
||||
let genesis_datetime = now
|
||||
.checked_add(TimeDuration::seconds(seconds))
|
||||
.ok_or_else(|| StepError::InvalidArgument {
|
||||
message: format!(
|
||||
"step `{step_value}` has an invalid genesis time: offset is out of range"
|
||||
),
|
||||
})?;
|
||||
|
||||
GenesisTime::try_from(genesis_datetime).map_err(|error| StepError::InvalidArgument {
|
||||
message: format!("step `{step_value}` has an invalid genesis time: {error}"),
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_genesis_time_change(
|
||||
existing_genesis_time: Option<GenesisTime>,
|
||||
nodes_started: bool,
|
||||
requested_genesis_time: GenesisTime,
|
||||
) -> StepResult {
|
||||
if nodes_started && existing_genesis_time != Some(requested_genesis_time) {
|
||||
return Err(StepError::LogicalError {
|
||||
message: "cannot change genesis time after nodes have started".to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(expr = "the chain starts {int} seconds from now")]
|
||||
#[when(expr = "the chain starts {int} seconds from now")]
|
||||
async fn step_chain_starts_from_now(
|
||||
world: &mut CucumberWorld,
|
||||
step: &Step,
|
||||
seconds: i64,
|
||||
) -> StepResult {
|
||||
let node_binary_profile = if world.tokio_console_profile_enabled() {
|
||||
NodeBinaryProfile::TokioConsole
|
||||
} else {
|
||||
NodeBinaryProfile::default()
|
||||
};
|
||||
ensure_node_binary_built(&node_binary_profile)
|
||||
.await
|
||||
.map_err(|error| StepError::Preflight {
|
||||
message: format!("failed to resolve/build node binary: {error}"),
|
||||
})?;
|
||||
|
||||
let genesis_time = resolve_step_genesis_time(&step.value, OffsetDateTime::now_utc(), seconds)?;
|
||||
validate_genesis_time_change(
|
||||
world.genesis_time,
|
||||
!world.nodes_info.is_empty(),
|
||||
genesis_time,
|
||||
)?;
|
||||
|
||||
world.set_genesis_time(genesis_time);
|
||||
if world.nodes_info.is_empty() && world.manual_cluster_spec.is_some() {
|
||||
rebuild_pending_local_manual_cluster(world)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(expr = "the configured genesis time has not passed for {int} seconds")]
|
||||
#[expect(
|
||||
clippy::needless_pass_by_ref_mut,
|
||||
reason = "Cucumber step functions require the world as the first `&mut` argument"
|
||||
)]
|
||||
async fn step_genesis_time_has_not_passed(
|
||||
world: &mut CucumberWorld,
|
||||
step: &Step,
|
||||
seconds: u64,
|
||||
) -> StepResult {
|
||||
let genesis_time = world.genesis_time.ok_or_else(|| StepError::LogicalError {
|
||||
message: "the scenario has no configured genesis time".to_owned(),
|
||||
})?;
|
||||
let genesis_datetime = OffsetDateTime::from(genesis_time);
|
||||
let timeout = Duration::from_secs(seconds);
|
||||
let started_waiting = Instant::now();
|
||||
|
||||
loop {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
if now >= genesis_datetime {
|
||||
return Err(StepError::StepFail {
|
||||
message: format!(
|
||||
"Step `{}` failed: configured genesis time {genesis_datetime} had passed at {now}",
|
||||
step.value
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if started_waiting.elapsed() >= timeout {
|
||||
info!(
|
||||
target: TARGET,
|
||||
"Configured genesis time {genesis_datetime} remained in the future for {seconds} seconds"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(250)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[given(expr = "I have a cluster with capacity of {int} nodes")]
|
||||
#[when(expr = "I have a cluster with capacity of {int} nodes")]
|
||||
fn step_manual_cluster(world: &mut CucumberWorld, step: &Step, nodes_count: usize) -> StepResult {
|
||||
@@ -1334,3 +1448,43 @@ async fn step_verify_blend_sdp_declaration_included(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn step_relative_genesis_time_uses_now_plus_offset() {
|
||||
let now = OffsetDateTime::from_unix_timestamp(1_000).expect("valid timestamp");
|
||||
|
||||
let genesis_time =
|
||||
resolve_step_genesis_time("the chain starts 60 seconds from now", now, 60)
|
||||
.expect("offset should produce a valid genesis time");
|
||||
|
||||
assert_eq!(genesis_time, GenesisTime::new(1_060));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflowing_step_relative_genesis_time_is_invalid_argument() {
|
||||
let now = time::Date::MAX.midnight().assume_utc();
|
||||
|
||||
let error = resolve_step_genesis_time("the chain starts 1 seconds from now", now, 1)
|
||||
.expect_err("overflowing offset should fail");
|
||||
|
||||
assert!(matches!(error, StepError::InvalidArgument { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn genesis_time_change_after_nodes_started_is_rejected() {
|
||||
let error = validate_genesis_time_change(
|
||||
Some(GenesisTime::new(1_000)),
|
||||
true,
|
||||
GenesisTime::new(1_001),
|
||||
)
|
||||
.expect_err("genesis time should not change after nodes start");
|
||||
|
||||
assert!(
|
||||
matches!(error, StepError::LogicalError { message } if message == "cannot change genesis time after nodes have started")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ use std::{
|
||||
};
|
||||
|
||||
use hex::ToHex as _;
|
||||
use lb_core::{codec::SerializeOp as _, mantle::TxHash, sdp::Locator};
|
||||
use lb_core::{
|
||||
codec::SerializeOp as _,
|
||||
mantle::{GenesisTime, TxHash},
|
||||
sdp::Locator,
|
||||
};
|
||||
use lb_key_management_system_service::keys::ZkPublicKey;
|
||||
use lb_libp2p::{PeerId, identity, identity::ed25519};
|
||||
use lb_node::UserConfig;
|
||||
@@ -23,13 +27,22 @@ use crate::cucumber::{
|
||||
type ScenarioBuilderWith = ScenarioBuilder;
|
||||
|
||||
#[must_use]
|
||||
pub fn make_builder(topology: &TopologySpec) -> ScenarioBuilderWith {
|
||||
pub fn make_builder(
|
||||
topology: &TopologySpec,
|
||||
requested_genesis_time: Option<GenesisTime>,
|
||||
) -> ScenarioBuilderWith {
|
||||
ScenarioBuilder::deployment_with(|t| {
|
||||
let base = match topology.network {
|
||||
NetworkKind::Star => t,
|
||||
};
|
||||
base.nodes(topology.nodes.get())
|
||||
.scenario_base_dir(topology.scenario_base_dir.clone())
|
||||
let base = base
|
||||
.nodes(topology.nodes.get())
|
||||
.scenario_base_dir(topology.scenario_base_dir.clone());
|
||||
if let Some(genesis_time) = requested_genesis_time {
|
||||
base.with_genesis_time(genesis_time)
|
||||
} else {
|
||||
base
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use lb_core::{
|
||||
codec::DeserializeOp as _,
|
||||
header::HeaderId,
|
||||
mantle::{
|
||||
SignedMantleTx, Utxo, Value,
|
||||
GenesisTime, SignedMantleTx, Utxo, Value,
|
||||
ops::channel::{
|
||||
ChannelId, deposit::DepositOp, inscribe::Inscription, withdraw::ChannelWithdrawOp,
|
||||
},
|
||||
@@ -841,6 +841,9 @@ pub struct CucumberWorld {
|
||||
pub deployer: Option<DeployerKind>,
|
||||
/// A unique per-scenario context string used to isolate runtime resources.
|
||||
pub test_context: Option<String>,
|
||||
/// Resolved genesis time for this Cucumber scenario attempt. It is set in
|
||||
/// the scenario hook and reused by every deployment build or rebuild.
|
||||
pub genesis_time: Option<GenesisTime>,
|
||||
/// Base directory for scenario artifacts like logs and generated configs.
|
||||
pub scenario_base_dir: PathBuf,
|
||||
/// Automated: Scenario specification
|
||||
@@ -1049,6 +1052,7 @@ impl Debug for CucumberWorld {
|
||||
f.debug_struct("CucumberWorld")
|
||||
.field("deployer", &format!("{:?}", self.deployer))
|
||||
.field("test_context", &format!("{:?}", self.test_context))
|
||||
.field("genesis_time", &self.genesis_time)
|
||||
.field("scenario_base_dir", &self.scenario_base_dir)
|
||||
.field("spec", &format!("{:?}", self.spec))
|
||||
.field("run", &format!("{:?}", self.run))
|
||||
@@ -1415,6 +1419,10 @@ impl CucumberWorld {
|
||||
self.test_context = Some(test_context);
|
||||
}
|
||||
|
||||
pub const fn set_genesis_time(&mut self, genesis_time: GenesisTime) {
|
||||
self.genesis_time = Some(genesis_time);
|
||||
}
|
||||
|
||||
/// Remove all scenario artifacts from the scenario base directory. This is
|
||||
/// useful for ensuring a clean state before starting a new scenario.
|
||||
pub fn clear_scenario_artifacts(&self) -> StepResult {
|
||||
@@ -1691,7 +1699,7 @@ impl CucumberWorld {
|
||||
.ok_or(StepError::MissingRunDuration)?
|
||||
.get();
|
||||
|
||||
let mut builder: ScenarioBuilderWith = make_builder(&topology);
|
||||
let mut builder: ScenarioBuilderWith = make_builder(&topology, self.genesis_time);
|
||||
|
||||
builder = builder.with_run_duration(Duration::from_secs(duration_secs));
|
||||
if let Some(wallets) = self.spec.wallets {
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
use std::{num::NonZero, path::PathBuf, time::Duration};
|
||||
|
||||
use lb_chain_service::PhaseTag;
|
||||
use lb_codec::BinaryEncode as _;
|
||||
use lb_core::{
|
||||
block::genesis::GenesisBlockBuilder,
|
||||
mantle::{
|
||||
GenesisTime,
|
||||
ops::channel::inscribe::{Inscription, InscriptionOp},
|
||||
traits::GenesisTx as _,
|
||||
},
|
||||
};
|
||||
use lb_node::config::{RunConfig, cryptarchia::deployment::EpochConfig};
|
||||
use lb_testing_framework::{
|
||||
DeploymentBuilder, NodeHttpClient, TopologyConfig as TfTopologyConfig,
|
||||
@@ -43,15 +34,15 @@ async fn delayed_chain_start() {
|
||||
DeploymentBuilder::new(
|
||||
TfTopologyConfig::with_node_numbers(NODE_COUNT)
|
||||
.with_test_context(Some("delayed_chain_start".to_owned())),
|
||||
)
|
||||
.with_genesis_time(
|
||||
genesis_time
|
||||
.try_into()
|
||||
.expect("genesis time should fit in GenesisTime"),
|
||||
),
|
||||
NODE_COUNT,
|
||||
ManualNodeLayout::SelectNodeSeed(0),
|
||||
move |config| {
|
||||
Ok(test_config(
|
||||
config,
|
||||
genesis_time.try_into().expect("should fit in GenesisTime"),
|
||||
))
|
||||
},
|
||||
|config| Ok(test_config(config)),
|
||||
Some(PathBuf::from(E2E_ARTIFACTS_DIR)),
|
||||
)
|
||||
.await;
|
||||
@@ -104,24 +95,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn test_config(mut config: RunConfig, genesis_time: GenesisTime) -> RunConfig {
|
||||
let genesis_tx = config.deployment.cryptarchia.genesis_block.genesis_tx();
|
||||
|
||||
let mut cryptarchia_parameter = genesis_tx.cryptarchia_parameter();
|
||||
cryptarchia_parameter.genesis_time = genesis_time;
|
||||
|
||||
let inscription = InscriptionOp {
|
||||
inscription: Inscription::new_unchecked(cryptarchia_parameter.encode_to_vec()),
|
||||
..genesis_tx.genesis_inscription().clone()
|
||||
};
|
||||
|
||||
config.deployment.cryptarchia.genesis_block = GenesisBlockBuilder::new()
|
||||
.try_add_notes(genesis_tx.genesis_transfer().outputs.iter().copied())
|
||||
.unwrap()
|
||||
.set_inscription(inscription)
|
||||
.build()
|
||||
.expect("Failed to build genesis block");
|
||||
|
||||
fn test_config(mut config: RunConfig) -> RunConfig {
|
||||
config.deployment.time.slot_duration = Duration::from_secs(1);
|
||||
config.deployment.cryptarchia.epoch_config = EpochConfig {
|
||||
epoch_stake_distribution_stabilization: 1.try_into().unwrap(),
|
||||
|
||||
@@ -98,6 +98,7 @@ fn deployment_settings(
|
||||
transfer_op,
|
||||
providers,
|
||||
topology.config.test_context.as_deref(),
|
||||
topology.config().genesis_time(),
|
||||
);
|
||||
|
||||
Ok(deployment_settings_for_topology(
|
||||
|
||||
@@ -636,6 +636,7 @@ fn plan_local_node_config(
|
||||
base_consensus,
|
||||
base_time,
|
||||
descriptors.config.test_context.as_deref(),
|
||||
descriptors.config.genesis_time(),
|
||||
)
|
||||
.map_err(|source| -> DynError { source.into() })?;
|
||||
|
||||
|
||||
@@ -178,6 +178,7 @@ pub fn apply_wallet_config_to_deployment(deployment: &mut DeploymentPlan, wallet
|
||||
&wallet_accounts,
|
||||
key_id_for_preload_backend,
|
||||
deployment.config.test_context.as_deref(),
|
||||
deployment.config.genesis_time(),
|
||||
);
|
||||
deployment.config.genesis_block = Some(genesis_block);
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ pub use framework::{
|
||||
block_feed_source_provider, block_feed_sources, named_block_feed_sources,
|
||||
};
|
||||
// Required by reused node-test config modules importing from crate root.
|
||||
pub use node::configs::deployment::{DeploymentBuilder, TopologyConfig};
|
||||
pub use node::configs::deployment::{
|
||||
DeploymentBuilder, TopologyConfig, resolve_automatic_genesis_time,
|
||||
};
|
||||
pub use node::{NodeHttpClient, configs};
|
||||
pub use testing_framework_runner_compose::ComposeRunnerError;
|
||||
pub use testing_framework_runner_k8s::{
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use std::{
|
||||
collections::HashMap, error::Error, num::NonZeroU32, path::PathBuf, sync::Arc, time::Duration,
|
||||
collections::{HashMap, HashSet},
|
||||
error::Error,
|
||||
num::NonZeroU32,
|
||||
path::PathBuf,
|
||||
sync::{Arc, LazyLock, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use lb_config::kms::key_id_for_preload_backend;
|
||||
use lb_core::block::genesis::GenesisBlock;
|
||||
use lb_core::{block::genesis::GenesisBlock, mantle::GenesisTime};
|
||||
use lb_node::config::{RunConfig, deployment::DeploymentSettings};
|
||||
use lb_utils::math::NonNegativeRatio;
|
||||
use rand::{Rng, SeedableRng as _};
|
||||
@@ -29,6 +34,31 @@ const DEFAULT_ACTIVE_SLOT_COEFF: NonNegativeRatio =
|
||||
NonNegativeRatio::new(1, NonZeroU32::new(10).unwrap());
|
||||
const DEFAULT_SECURITY_PARAM: NonZeroU32 = NonZeroU32::new(20).unwrap();
|
||||
|
||||
static RESERVED_AUTOMATIC_GENESIS_TIMES: LazyLock<Mutex<HashSet<GenesisTime>>> =
|
||||
LazyLock::new(|| Mutex::new(HashSet::new()));
|
||||
|
||||
/// Reserves a near-current genesis second for a new automatically configured
|
||||
/// deployment in this process.
|
||||
#[must_use]
|
||||
pub fn resolve_automatic_genesis_time() -> GenesisTime {
|
||||
let requested = time::OffsetDateTime::now_utc()
|
||||
.try_into()
|
||||
.expect("current time should fit in GenesisTime");
|
||||
let mut reserved = RESERVED_AUTOMATIC_GENESIS_TIMES
|
||||
.lock()
|
||||
.expect("automatic genesis time reservation lock should not be poisoned");
|
||||
|
||||
let mut candidate = requested;
|
||||
while !reserved.insert(candidate) {
|
||||
let next = time::OffsetDateTime::from(candidate) + time::Duration::seconds(1);
|
||||
candidate = next
|
||||
.try_into()
|
||||
.expect("automatic genesis time should fit in GenesisTime");
|
||||
}
|
||||
|
||||
candidate
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TopologyBuildError {
|
||||
#[error("internal config vector mismatch for {label} (expected {expected}, got {actual})")]
|
||||
@@ -85,6 +115,8 @@ pub struct TopologyConfig {
|
||||
pub wallet_config: WalletConfig,
|
||||
pub scenario_base_dir: PathBuf,
|
||||
pub genesis_block: Option<GenesisBlock>,
|
||||
requested_genesis_time: Option<GenesisTime>,
|
||||
genesis_time: Option<GenesisTime>,
|
||||
pub slot_duration: Option<Duration>,
|
||||
pub active_slot_coeff: NonNegativeRatio,
|
||||
pub security_param: NonZeroU32,
|
||||
@@ -122,6 +154,19 @@ impl TopologyConfig {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn with_genesis_time(mut self, genesis_time: GenesisTime) -> Self {
|
||||
self.requested_genesis_time = Some(genesis_time);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the genesis time resolved while building this deployment.
|
||||
#[must_use]
|
||||
pub const fn genesis_time(&self) -> GenesisTime {
|
||||
self.genesis_time
|
||||
.expect("genesis time is available only on a built deployment")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn with_node_binary_profile(
|
||||
mut self,
|
||||
@@ -167,6 +212,8 @@ impl Default for TopologyConfig {
|
||||
wallet_config: WalletConfig::default(),
|
||||
scenario_base_dir: std::env::temp_dir(),
|
||||
genesis_block: None,
|
||||
requested_genesis_time: None,
|
||||
genesis_time: None,
|
||||
slot_duration: Some(Duration::from_secs(DEFAULT_SLOT_TIME_IN_SECS)),
|
||||
active_slot_coeff: DEFAULT_ACTIVE_SLOT_COEFF,
|
||||
security_param: DEFAULT_SECURITY_PARAM,
|
||||
@@ -253,7 +300,19 @@ impl DeploymentBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn with_genesis_time(mut self, genesis_time: GenesisTime) -> Self {
|
||||
self.config.requested_genesis_time = Some(genesis_time);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(mut self) -> Result<DeploymentPlan, TopologyBuildError> {
|
||||
let genesis_time = self
|
||||
.config
|
||||
.requested_genesis_time
|
||||
.unwrap_or_else(resolve_automatic_genesis_time);
|
||||
self.config.genesis_time = Some(genesis_time);
|
||||
|
||||
self.config.wallet_config.validate(
|
||||
self.config.allow_multiple_genesis_tokens,
|
||||
self.config.allow_zero_value_genesis_tokens,
|
||||
@@ -279,6 +338,7 @@ impl DeploymentBuilder {
|
||||
self.config.blend_core_nodes,
|
||||
self.config.network_params.as_ref(),
|
||||
self.config.test_context.as_deref(),
|
||||
genesis_time,
|
||||
);
|
||||
|
||||
let wallet_accounts = self
|
||||
@@ -296,6 +356,7 @@ impl DeploymentBuilder {
|
||||
&wallet_accounts,
|
||||
key_id_for_preload_backend,
|
||||
self.config.test_context.as_deref(),
|
||||
genesis_time,
|
||||
);
|
||||
|
||||
let nodes = build_node_plans(node_count, &ids, &node_configs)?;
|
||||
@@ -396,3 +457,39 @@ impl DeploymentProvider<DeploymentPlan> for DeploymentBuilder {
|
||||
.map_err(|error| Box::new(error) as DynTopologyError)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn automatic_genesis_times_are_unique_within_a_process() {
|
||||
let first = DeploymentBuilder::new(TopologyConfig::empty())
|
||||
.build()
|
||||
.expect("first deployment should build");
|
||||
let second = DeploymentBuilder::new(TopologyConfig::empty())
|
||||
.build()
|
||||
.expect("second deployment should build");
|
||||
|
||||
assert_ne!(
|
||||
first.config().genesis_time(),
|
||||
second.config().genesis_time()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_genesis_times_can_be_shared() {
|
||||
let genesis_time = GenesisTime::new(1_000);
|
||||
let first = DeploymentBuilder::new(TopologyConfig::empty())
|
||||
.with_genesis_time(genesis_time)
|
||||
.build()
|
||||
.expect("first deployment should build");
|
||||
let second = DeploymentBuilder::new(TopologyConfig::empty())
|
||||
.with_genesis_time(genesis_time)
|
||||
.build()
|
||||
.expect("second deployment should build");
|
||||
|
||||
assert_eq!(first.config().genesis_time(), genesis_time);
|
||||
assert_eq!(second.config().genesis_time(), genesis_time);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use lb_config::kms::key_id_for_preload_backend;
|
||||
use lb_core::mantle::GenesisTime;
|
||||
use lb_key_management_system_service::keys::Key;
|
||||
use lb_libp2p::Multiaddr;
|
||||
use lb_node::config::KmsConfig;
|
||||
@@ -27,6 +28,7 @@ pub enum DynamicConfigBuildError {
|
||||
Tracing,
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments, reason = "need all args")]
|
||||
pub fn create_node_config_for_node(
|
||||
id: [u8; 32],
|
||||
network_port: u16,
|
||||
@@ -35,8 +37,10 @@ pub fn create_node_config_for_node(
|
||||
base_consensus: &GeneralConsensusConfig,
|
||||
time_config: &GeneralTimeConfig,
|
||||
test_context: Option<&str>,
|
||||
genesis_time: GenesisTime,
|
||||
) -> Result<Config, DynamicConfigBuildError> {
|
||||
let consensus_config = build_consensus_config_for_node(id, base_consensus, test_context)?;
|
||||
let consensus_config =
|
||||
build_consensus_config_for_node(id, base_consensus, test_context, genesis_time)?;
|
||||
|
||||
let blend_config = node_configs::blend::create_blend_configs(&[id], &[blend_port])
|
||||
.into_iter()
|
||||
@@ -81,11 +85,13 @@ fn build_consensus_config_for_node(
|
||||
id: [u8; 32],
|
||||
base: &GeneralConsensusConfig,
|
||||
test_context: Option<&str>,
|
||||
genesis_time: GenesisTime,
|
||||
) -> Result<GeneralConsensusConfig, DynamicConfigBuildError> {
|
||||
let (mut configs, _) = node_configs::consensus::create_consensus_configs(
|
||||
&[id],
|
||||
SHORT_PROLONGED_BOOTSTRAP_PERIOD,
|
||||
test_context,
|
||||
genesis_time,
|
||||
);
|
||||
let mut config = configs.pop().ok_or(DynamicConfigBuildError::Consensus)?;
|
||||
config.blend_note.clone_from(&base.blend_note);
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::time::Duration;
|
||||
|
||||
pub use lb_config::GeneralConfig;
|
||||
pub(crate) use lb_config::{api, blend, consensus, network, sdp, time, tracing};
|
||||
use lb_core::block::genesis::GenesisBlock;
|
||||
use lb_core::{block::genesis::GenesisBlock, mantle::GenesisTime};
|
||||
use network::NetworkParams;
|
||||
|
||||
const PROLONGED_BOOTSTRAP_PERIOD: Duration = Duration::from_secs(5);
|
||||
@@ -19,6 +19,7 @@ pub fn create_general_configs_from_ids(
|
||||
n_blend_core_nodes: usize,
|
||||
network_params: &NetworkParams,
|
||||
test_context: Option<&str>,
|
||||
genesis_time: GenesisTime,
|
||||
) -> (Vec<GeneralConfig>, GenesisBlock) {
|
||||
lb_config::create_general_configs_from_ids(
|
||||
ids,
|
||||
@@ -27,5 +28,6 @@ pub fn create_general_configs_from_ids(
|
||||
network_params,
|
||||
PROLONGED_BOOTSTRAP_PERIOD,
|
||||
test_context,
|
||||
genesis_time,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::HashSet;
|
||||
|
||||
use lb_core::{
|
||||
block::genesis::GenesisBlock,
|
||||
mantle::{Note, traits::GenesisTx as _},
|
||||
mantle::{GenesisTime, Note, traits::GenesisTx as _},
|
||||
sdp::{Locator, ServiceType},
|
||||
};
|
||||
use lb_key_management_system_service::keys::{Key, ZkKey};
|
||||
@@ -37,6 +37,7 @@ pub fn apply_wallet_genesis_overrides(
|
||||
wallet_accounts: &[(ZkKey, u64)],
|
||||
key_id_for_preload_backend: impl Fn(&Key) -> String,
|
||||
test_context: Option<&str>,
|
||||
genesis_time: GenesisTime,
|
||||
) -> GenesisBlock {
|
||||
if wallet_accounts.is_empty() {
|
||||
return genesis_block.clone();
|
||||
@@ -92,7 +93,7 @@ pub fn apply_wallet_genesis_overrides(
|
||||
}
|
||||
|
||||
let genesis_block =
|
||||
create_genesis_block_with_declarations(transfer_op, providers, test_context);
|
||||
create_genesis_block_with_declarations(transfer_op, providers, test_context, genesis_time);
|
||||
|
||||
for general in general_configs {
|
||||
for (secret_key, _) in wallet_accounts {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use core::time::Duration;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use lb_codec::BinaryEncode as _;
|
||||
use lb_core::{
|
||||
@@ -24,7 +23,6 @@ use lb_key_management_system_service::keys::{
|
||||
};
|
||||
use lb_node::{Hashable as _, SignedMantleTx};
|
||||
use num_bigint::BigUint;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::unique::unique_test_context;
|
||||
|
||||
@@ -87,16 +85,6 @@ pub struct ServiceNote {
|
||||
pub output_index: usize,
|
||||
}
|
||||
|
||||
static GENESIS_TIME: OnceLock<GenesisTime> = OnceLock::new();
|
||||
|
||||
fn get_or_init_genesis_time() -> GenesisTime {
|
||||
*GENESIS_TIME.get_or_init(|| {
|
||||
OffsetDateTime::now_utc()
|
||||
.try_into()
|
||||
.expect("should fit in GenesisTime")
|
||||
})
|
||||
}
|
||||
|
||||
pub struct BaseConsensusMaterial {
|
||||
pub regular_note_keys: Vec<ZkKey>,
|
||||
pub blend_notes: Vec<ServiceNote>,
|
||||
@@ -104,15 +92,18 @@ pub struct BaseConsensusMaterial {
|
||||
pub utxos: Vec<Utxo>,
|
||||
}
|
||||
|
||||
fn inscription_for_current_test(test_context: Option<&str>) -> InscriptionOp {
|
||||
fn inscription_for_current_test(
|
||||
test_context: Option<&str>,
|
||||
genesis_time: GenesisTime,
|
||||
) -> InscriptionOp {
|
||||
let chain_id = unique_test_context(test_context);
|
||||
println!("Genesis inscription: {chain_id}");
|
||||
println!("Genesis inscription: {chain_id}, genesis_time: {genesis_time:?}");
|
||||
InscriptionOp {
|
||||
channel_id: ChannelId::from(EMPTY_CHANNEL_ID),
|
||||
inscription: Inscription::new_unchecked(
|
||||
CryptarchiaParameter {
|
||||
chain_id,
|
||||
genesis_time: get_or_init_genesis_time(),
|
||||
genesis_time,
|
||||
epoch_nonce: Fr::ZERO,
|
||||
}
|
||||
.encode_to_vec(),
|
||||
@@ -123,7 +114,11 @@ fn inscription_for_current_test(test_context: Option<&str>) -> InscriptionOp {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn create_genesis_block(utxos: &[Utxo], test_context: Option<&str>) -> GenesisBlock {
|
||||
pub fn create_genesis_block(
|
||||
utxos: &[Utxo],
|
||||
test_context: Option<&str>,
|
||||
genesis_time: GenesisTime,
|
||||
) -> GenesisBlock {
|
||||
// Create transfer op with the utxos as outputs
|
||||
let mut outputs = utxos.iter().map(|u| u.note);
|
||||
#[expect(
|
||||
@@ -142,7 +137,7 @@ pub fn create_genesis_block(utxos: &[Utxo], test_context: Option<&str>) -> Genes
|
||||
panic!("No outputs provided for genesis block")
|
||||
};
|
||||
|
||||
let inscription = inscription_for_current_test(test_context);
|
||||
let inscription = inscription_for_current_test(test_context, genesis_time);
|
||||
|
||||
genesis_builder
|
||||
.set_inscription(inscription)
|
||||
@@ -155,9 +150,10 @@ pub fn create_consensus_configs(
|
||||
ids: &[[u8; 32]],
|
||||
prolonged_bootstrap_period: Duration,
|
||||
test_context: Option<&str>,
|
||||
genesis_time: GenesisTime,
|
||||
) -> (Vec<GeneralConsensusConfig>, GenesisBlock) {
|
||||
let material = create_base_consensus_material(ids);
|
||||
let genesis_block = create_genesis_block(&material.utxos, test_context);
|
||||
let genesis_block = create_genesis_block(&material.utxos, test_context, genesis_time);
|
||||
|
||||
(
|
||||
material
|
||||
@@ -282,8 +278,9 @@ pub fn create_genesis_block_with_declarations(
|
||||
transfer_op: TransferOp,
|
||||
providers: Vec<ProviderInfo>,
|
||||
test_context: Option<&str>,
|
||||
genesis_time: GenesisTime,
|
||||
) -> GenesisBlock {
|
||||
let inscription = inscription_for_current_test(test_context);
|
||||
let inscription = inscription_for_current_test(test_context, genesis_time);
|
||||
let transfer_id = transfer_op.op_id();
|
||||
|
||||
let mut ops = vec![Op::Transfer(transfer_op), Op::ChannelInscribe(inscription)];
|
||||
|
||||
+31
-7
@@ -17,7 +17,7 @@ use std::sync::LazyLock;
|
||||
use blend::GeneralBlendConfig;
|
||||
use lb_core::{
|
||||
block::genesis::GenesisBlock,
|
||||
mantle::traits::GenesisTx as _,
|
||||
mantle::{GenesisTime, traits::GenesisTx as _},
|
||||
sdp::{Locator, ServiceType},
|
||||
};
|
||||
use lb_node::config::KmsConfig;
|
||||
@@ -59,8 +59,14 @@ pub struct GeneralConfig {
|
||||
pub fn create_general_configs(
|
||||
n_nodes: usize,
|
||||
test_context: Option<&str>,
|
||||
genesis_time: GenesisTime,
|
||||
) -> (Vec<GeneralConfig>, GenesisBlock) {
|
||||
create_general_configs_with_network(n_nodes, &NetworkParams::default(), test_context)
|
||||
create_general_configs_with_network(
|
||||
n_nodes,
|
||||
&NetworkParams::default(),
|
||||
test_context,
|
||||
genesis_time,
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -68,8 +74,15 @@ pub fn create_general_configs_with_network(
|
||||
n_nodes: usize,
|
||||
network_params: &NetworkParams,
|
||||
test_context: Option<&str>,
|
||||
genesis_time: GenesisTime,
|
||||
) -> (Vec<GeneralConfig>, GenesisBlock) {
|
||||
create_general_configs_with_blend_core_subset(n_nodes, n_nodes, network_params, test_context)
|
||||
create_general_configs_with_blend_core_subset(
|
||||
n_nodes,
|
||||
n_nodes,
|
||||
network_params,
|
||||
test_context,
|
||||
genesis_time,
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -78,6 +91,7 @@ pub fn create_general_configs_with_blend_core_subset(
|
||||
n_blend_core_nodes: usize,
|
||||
network_params: &NetworkParams,
|
||||
test_context: Option<&str>,
|
||||
genesis_time: GenesisTime,
|
||||
) -> (Vec<GeneralConfig>, GenesisBlock) {
|
||||
assert!(
|
||||
n_blend_core_nodes <= n_nodes,
|
||||
@@ -99,6 +113,7 @@ pub fn create_general_configs_with_blend_core_subset(
|
||||
network_params,
|
||||
SHORT_PROLONGED_BOOTSTRAP_PERIOD,
|
||||
test_context,
|
||||
genesis_time,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -110,6 +125,7 @@ pub fn create_general_configs_from_ids(
|
||||
network_params: &NetworkParams,
|
||||
prolonged_bootstrap_period: Duration,
|
||||
test_context: Option<&str>,
|
||||
genesis_time: GenesisTime,
|
||||
) -> (Vec<GeneralConfig>, GenesisBlock) {
|
||||
let n_nodes = ids.len();
|
||||
|
||||
@@ -126,8 +142,12 @@ pub fn create_general_configs_from_ids(
|
||||
ids.len()
|
||||
);
|
||||
|
||||
let (consensus_configs, genesis_block) =
|
||||
consensus::create_consensus_configs(ids, prolonged_bootstrap_period, test_context);
|
||||
let (consensus_configs, genesis_block) = consensus::create_consensus_configs(
|
||||
ids,
|
||||
prolonged_bootstrap_period,
|
||||
test_context,
|
||||
genesis_time,
|
||||
);
|
||||
let network_configs = network::create_network_configs(ids, network_params);
|
||||
let api_configs = api::create_api_configs(ids);
|
||||
let blend_configs = blend::create_blend_configs(ids, blend_ports);
|
||||
@@ -150,8 +170,12 @@ pub fn create_general_configs_from_ids(
|
||||
.collect();
|
||||
let binding = genesis_block.genesis_tx();
|
||||
let transfer_op = binding.genesis_transfer();
|
||||
let genesis_block_with_declarations =
|
||||
create_genesis_block_with_declarations(transfer_op.clone(), providers, test_context);
|
||||
let genesis_block_with_declarations = create_genesis_block_with_declarations(
|
||||
transfer_op.clone(),
|
||||
providers,
|
||||
test_context,
|
||||
genesis_time,
|
||||
);
|
||||
let sdp_configs = create_sdp_configs(&genesis_block_with_declarations.genesis_tx(), n_nodes);
|
||||
let kms_configs = create_kms_configs(&blend_configs, &consensus_configs, None);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user