test: make genesis time cluster-scoped and configurable (#3312)

This commit is contained in:
Hansie Odendaal
2026-08-15 04:55:06 +00:00
committed by GitHub
parent 26b9286dd4
commit 55f94492b6
20 changed files with 413 additions and 86 deletions
Generated
-1
View File
@@ -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",
-1
View File
@@ -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 }
+2 -1
View File
@@ -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
+156 -2
View File
@@ -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")
);
}
}
+17 -4
View File
@@ -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
}
})
}
+10 -2
View File
@@ -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 {
+7 -33
View File
@@ -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);
+3 -1
View File
@@ -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 {
+16 -19
View File
@@ -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
View File
@@ -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);