From 3225f33f0f3724bb02a920291017227f899ecc28 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Thu, 14 Aug 2025 15:02:05 -0400 Subject: [PATCH 1/5] sim-rs: remove L_vote restriction in EB production --- sim-rs/CHANGELOG.md | 10 ++++++++++ sim-rs/sim-core/src/sim/linear_leios.rs | 6 +----- 2 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 sim-rs/CHANGELOG.md diff --git a/sim-rs/CHANGELOG.md b/sim-rs/CHANGELOG.md new file mode 100644 index 000000000..2100456cd --- /dev/null +++ b/sim-rs/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +# Unreleased + +## Linear Leios +- Allow RBs to include EB certificates produced at least `L_diff` slots ago, instead of `L_vote + L_diff` slots ago. When `L_diff` is 0, this removes any direct time factor from the decision to include an EB cert. + +# v0.1.0 + +This version was arbitrarily chosen as the point to start tracking major changes to the simulation. \ No newline at end of file diff --git a/sim-rs/sim-core/src/sim/linear_leios.rs b/sim-rs/sim-core/src/sim/linear_leios.rs index 72d82e291..df46ccbea 100644 --- a/sim-rs/sim-core/src/sim/linear_leios.rs +++ b/sim-rs/sim-core/src/sim/linear_leios.rs @@ -509,11 +509,7 @@ impl LinearLeiosNode { let parent = self.latest_rb().map(|rb| rb.header.id); let endorsement = parent.and_then(|rb_id| { - if rb_id.slot - + self.sim_config.linear_vote_stage_length - + self.sim_config.linear_diffuse_stage_length - > slot - { + if rb_id.slot + self.sim_config.linear_diffuse_stage_length > slot { // This RB was generated too quickly after another; hasn't been time to gather all the votes. // No endorsement. return None; From a63303977a0be285157bd631f2937a69af311f08 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Thu, 14 Aug 2025 16:33:20 -0400 Subject: [PATCH 2/5] sim-rs: fix mempool behavior with EBs propagated before TXs --- sim-rs/CHANGELOG.md | 1 + sim-rs/sim-core/src/sim/linear_leios.rs | 90 ++++++++++++++----------- 2 files changed, 53 insertions(+), 38 deletions(-) diff --git a/sim-rs/CHANGELOG.md b/sim-rs/CHANGELOG.md index 2100456cd..b8db90707 100644 --- a/sim-rs/CHANGELOG.md +++ b/sim-rs/CHANGELOG.md @@ -4,6 +4,7 @@ ## Linear Leios - Allow RBs to include EB certificates produced at least `L_diff` slots ago, instead of `L_vote + L_diff` slots ago. When `L_diff` is 0, this removes any direct time factor from the decision to include an EB cert. +- Add TXs to the mempool, even if they belong to an EB we've already seen. # v0.1.0 diff --git a/sim-rs/sim-core/src/sim/linear_leios.rs b/sim-rs/sim-core/src/sim/linear_leios.rs index df46ccbea..c6b54d8a7 100644 --- a/sim-rs/sim-core/src/sim/linear_leios.rs +++ b/sim-rs/sim-core/src/sim/linear_leios.rs @@ -456,32 +456,20 @@ impl LinearLeiosNode { { return; } + let referenced_by_eb = self.acknowledge_tx(&tx); - if !referenced_by_eb { - let rb_ref = self.latest_rb().map(|rb| rb.header.id); - let ledger_state = self.resolve_ledger_state(rb_ref); - if ledger_state.is_some_and(|ls| ls.spent_inputs.contains(&tx.input_id)) { - // Ignoring a TX which conflicts with something already onchain - return; - } - if self - .praos - .mempool - .values() - .any(|mempool_tx| mempool_tx.input_id == tx.input_id) - { - // Ignoring a TX which conflicts with the current mempool contents. - return; - } - self.praos.mempool.insert(tx.id, tx.clone()); - } + let added_to_mempool = self.try_add_tx_to_mempool(&tx); + // If we added the TX to our mempool, we want to propagate it so our peers can as well. + // If it was referenced by an EB, we want to propagate it so our peers have the full EB. // TODO: should send to producers instead (make configurable) - for peer in &self.consumers { - if *peer == from { - continue; + if referenced_by_eb || added_to_mempool { + for peer in &self.consumers { + if *peer == from { + continue; + } + self.queued.send_to(*peer, Message::AnnounceTx(id)); } - self.queued.send_to(*peer, Message::AnnounceTx(id)); } } @@ -1318,6 +1306,26 @@ impl LinearLeiosNode { // Ledger/mempool operations impl LinearLeiosNode { + fn try_add_tx_to_mempool(&mut self, tx: &Arc) -> bool { + let ledger_state = self.resolve_ledger_state(self.latest_rb().map(|rb| rb.header.id)); + if ledger_state.spent_inputs.contains(&tx.input_id) { + // This TX conflicts with something already on-chain + return false; + } + + if self + .praos + .mempool + .values() + .any(|t| t.input_id == tx.input_id) + { + // This TX conflicts with something already in the mempool + return false; + } + self.praos.mempool.insert(tx.id, tx.clone()); + true + } + fn sample_from_mempool( &mut self, txs: &mut Vec>, @@ -1373,12 +1381,12 @@ impl LinearLeiosNode { .retain(|_, tx| !inputs.contains(&tx.input_id)); } - fn resolve_ledger_state(&mut self, rb_ref: Option) -> Option> { + fn resolve_ledger_state(&mut self, rb_ref: Option) -> Arc { let Some(block_id) = rb_ref else { - return Some(Arc::new(LedgerState::default())); + return Arc::new(LedgerState::default()); }; if let Some(state) = self.ledger_states.get(&block_id) { - return Some(state.clone()); + return state.clone(); }; let mut state = self @@ -1388,6 +1396,7 @@ impl LinearLeiosNode { .unwrap_or_default(); let mut block_queue = vec![block_id]; + let mut complete = true; while let Some(block_id) = block_queue.pop() { if !state.seen_blocks.insert(block_id) { continue; @@ -1404,24 +1413,29 @@ impl LinearLeiosNode { } if let Some(endorsement) = &rb.endorsement { - let Some(EndorserBlockView::Received { - eb, - validated: true, - .. - }) = self.leios.ebs.get(&endorsement.eb) - else { - // We haven't validated the EB yet, so we don't know the ledger state - return None; - }; - for tx in &eb.txs { - state.spent_inputs.insert(tx.input_id); + match self.leios.ebs.get(&endorsement.eb) { + Some(EndorserBlockView::Received { eb, .. }) => { + for tx in &eb.txs { + if self.has_tx(tx.id) { + state.spent_inputs.insert(tx.input_id); + } else { + complete = false; + } + } + } + _ => { + // We haven't validated the EB yet, so we don't know the full ledger state + complete = false; + } } } } let state = Arc::new(state); - self.ledger_states.insert(block_id, state.clone()); - Some(state) + if complete { + self.ledger_states.insert(block_id, state.clone()); + } + state } } From 6adda5ec124e9d4db3dc6a9cf4334577df8fba9c Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Thu, 14 Aug 2025 16:45:50 -0400 Subject: [PATCH 3/5] sim-rs: add version number to changelog --- sim-rs/CHANGELOG.md | 3 +++ sim-rs/sim-cli/src/main.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sim-rs/CHANGELOG.md b/sim-rs/CHANGELOG.md index b8db90707..4e6beb927 100644 --- a/sim-rs/CHANGELOG.md +++ b/sim-rs/CHANGELOG.md @@ -6,6 +6,9 @@ - Allow RBs to include EB certificates produced at least `L_diff` slots ago, instead of `L_vote + L_diff` slots ago. When `L_diff` is 0, this removes any direct time factor from the decision to include an EB cert. - Add TXs to the mempool, even if they belong to an EB we've already seen. +## Other +- Add version number to the CLI tool's output. + # v0.1.0 This version was arbitrarily chosen as the point to start tracking major changes to the simulation. \ No newline at end of file diff --git a/sim-rs/sim-cli/src/main.rs b/sim-rs/sim-cli/src/main.rs index c77049495..2b0c199a8 100644 --- a/sim-rs/sim-cli/src/main.rs +++ b/sim-rs/sim-cli/src/main.rs @@ -32,7 +32,7 @@ const DEFAULT_TOPOLOGY_PATHS: &[&str] = &[ ]; #[derive(Parser)] -#[command(version = env!("VERGEN_GIT_SHA"))] +#[command(version = concat!(env!("CARGO_PKG_VERSION"), "-", env!("VERGEN_GIT_SHA")))] struct Args { #[clap(default_value = None)] topology: Option, From 78c37f742ba462029c4b6cc827d7d8c7d1b4852f Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Thu, 14 Aug 2025 16:59:45 -0400 Subject: [PATCH 4/5] sim-rs: support selecting attackers with stake-fraction --- data/simulation/config.d.ts | 5 +++- data/simulation/config.schema.json | 22 ++++++++++++-- sim-rs/CHANGELOG.md | 3 ++ sim-rs/implementations/LINEAR_LEIOS.md | 27 ++++++++++++++--- sim-rs/sim-core/src/config.rs | 40 +++++++++++++++++++------- 5 files changed, 80 insertions(+), 17 deletions(-) diff --git a/data/simulation/config.d.ts b/data/simulation/config.d.ts index f7c46f767..22f260571 100644 --- a/data/simulation/config.d.ts +++ b/data/simulation/config.d.ts @@ -367,7 +367,10 @@ export interface LateTXAttackConfig { "tx-generation-distribution": Distribution, } -export type NodeSelection = NodesNodeSelection; +export type NodeSelection = NodesNodeSelection | StakeFractionNodeSelection; export interface NodesNodeSelection { "nodes": string[]; +} +export interface StakeFractionNodeSelection { + "stake-fraction": number, } \ No newline at end of file diff --git a/data/simulation/config.schema.json b/data/simulation/config.schema.json index 1c330a918..569955133 100644 --- a/data/simulation/config.schema.json +++ b/data/simulation/config.schema.json @@ -80,7 +80,7 @@ "description": "Configuration for a \"late EB\" attack,\nwhere nodes deliberately withhold EBs until near the end of the voting phase.", "properties": { "attackers": { - "$ref": "#/definitions/NodesNodeSelection", + "$ref": "#/definitions/NodeSelection", "description": "The set of stake pools which are participating in the attack." }, "propagation-delay-ms": { @@ -98,7 +98,7 @@ "type": "number" }, "attackers": { - "$ref": "#/definitions/NodesNodeSelection", + "$ref": "#/definitions/NodeSelection", "description": "The set of stake pools which are participating in the attack." }, "tx-generation-distribution": { @@ -138,6 +138,16 @@ "enum": ["ordered-by-id", "random"], "type": "string" }, + "NodeSelection": { + "anyOf": [ + { + "$ref": "#/definitions/NodesNodeSelection" + }, + { + "$ref": "#/definitions/StakeFractionNodeSelection" + } + ] + }, "NodesNodeSelection": { "properties": { "nodes": { @@ -167,6 +177,14 @@ "RelayStrategy": { "enum": ["request-from-all", "request-from-first"], "type": "string" + }, + "StakeFractionNodeSelection": { + "properties": { + "stake-fraction": { + "type": "number" + } + }, + "type": "object" } }, "description": "A configuration for a Leios simulation.", diff --git a/sim-rs/CHANGELOG.md b/sim-rs/CHANGELOG.md index 4e6beb927..977e25bdc 100644 --- a/sim-rs/CHANGELOG.md +++ b/sim-rs/CHANGELOG.md @@ -3,10 +3,13 @@ # Unreleased ## Linear Leios + - Allow RBs to include EB certificates produced at least `L_diff` slots ago, instead of `L_vote + L_diff` slots ago. When `L_diff` is 0, this removes any direct time factor from the decision to include an EB cert. - Add TXs to the mempool, even if they belong to an EB we've already seen. +- Support choosing attackers by selecting a fraction of stake ## Other + - Add version number to the CLI tool's output. # v0.1.0 diff --git a/sim-rs/implementations/LINEAR_LEIOS.md b/sim-rs/implementations/LINEAR_LEIOS.md index 853db7814..976644d49 100644 --- a/sim-rs/implementations/LINEAR_LEIOS.md +++ b/sim-rs/implementations/LINEAR_LEIOS.md @@ -71,7 +71,7 @@ When a node receives an RB body, it immediately removes all referenced/conflicti A set of nodes can be configured to collude with each other, to distribute an EB close to the end of L_diff. -Example config: +Example config (with explicit list of attackers): ```yaml late-eb-attack: attackers: @@ -85,7 +85,15 @@ late-eb-attack: propagation-delay-ms: 4500.0 ``` -The `attackers` list controls which nodes are participating in the attack. (I will get around to letting you just choose a `stake` sometime soon). These nodes can communicate out of band, without taking latency or bandwidth into account. +Example config (with fraction of stake): +```yaml +late-eb-attack: + attackers: + stake-fraction: 0.51 + propagation-delay-ms: 4500.0 +``` + +The `attackers` list controls which nodes are participating in the attack. These nodes can communicate out of band, without taking latency or bandwidth into account. When one of the attackers generates an EB, it will instantly and instantaneously send that EB to all other attackers. The attackers will all wait for `propagation-delay-ms` to elapse, and _then_ announce the EB to all peers. @@ -93,7 +101,7 @@ When one of the attackers generates an EB, it will instantly and instantaneously A set of nodes can be configured to "withhold" some number of TXs until the moment they generate an EB. -Example config: +Example config (with explicit list of attackers): ```yaml late-tx-attack: attackers: @@ -110,7 +118,18 @@ late-tx-attack: value: 3 ``` -The `attackers` list controls which nodes are participating in the attack. (I will get around to letting you just choose a `stake` sometime soon). +Example config (with fraction of stake): +```yaml +late-tx-attack: + attackers: + stake-fraction: 0.51 + attack-probability: 1.0 + tx-generation-distribution: + distribution: constant + value: 3 +``` + +The `attackers` list controls which nodes are participating in the attack. When an attacker generates an EB, with probability `attack-probability` they will also generate `tx-generation-distribution` brand-new transactions. Both the EB and the transactions will be immediately announced to peers as normal. diff --git a/sim-rs/sim-core/src/config.rs b/sim-rs/sim-core/src/config.rs index f89a7d87f..4a1d05514 100644 --- a/sim-rs/sim-core/src/config.rs +++ b/sim-rs/sim-core/src/config.rs @@ -10,6 +10,7 @@ use rand::Rng; use rand_chacha::ChaCha20Rng; use rand_distr::Distribution; use serde::{Deserialize, Serialize}; +use tracing::info; use crate::{ clock::Timestamp, @@ -214,6 +215,7 @@ pub struct RawLateTXAttackConfig { #[serde(rename_all = "kebab-case")] pub enum NodeSelection { Nodes(HashSet), + StakeFraction(f64), } #[derive(Debug, Serialize, Deserialize)] @@ -305,10 +307,7 @@ impl Topology { Ok(()) } - pub fn select( - &mut self, - selection: &NodeSelection, - ) -> impl Iterator { + pub fn select(&mut self, selection: &NodeSelection) -> Vec<&mut NodeConfiguration> { let mut nodes = vec![]; match selection { NodeSelection::Nodes(names) => { @@ -318,8 +317,22 @@ impl Topology { .filter(|node| names.contains(&node.name)), ); } + NodeSelection::StakeFraction(fraction) => { + let mut all_nodes = self.nodes.iter_mut().collect::>(); + all_nodes.sort_by_key(|n| std::cmp::Reverse(n.stake)); + let total_stake = all_nodes.iter().map(|n| n.stake).sum::(); + let target_stake = ((total_stake as f64) * *fraction) as u64; + let mut stake_so_far = 0; + for node in all_nodes { + if stake_so_far >= target_stake { + break; + } + stake_so_far += node.stake; + nodes.push(node); + } + } } - nodes.into_iter() + nodes } } @@ -632,10 +645,12 @@ pub(crate) struct LateEBAttackConfig { impl LateEBAttackConfig { fn build(raw: &RawLateEBAttackConfig, topology: &mut Topology) -> Self { - let attackers = topology - .select(&raw.attackers) - .map(|node| node.id) - .collect(); + let all_attackers = topology.select(&raw.attackers); + info!( + "Late EB attackers: {:?}", + all_attackers.iter().map(|n| &n.name).collect::>() + ); + let attackers = all_attackers.into_iter().map(|node| node.id).collect(); Self { attackers, propagation_delay: duration_ms(raw.propagation_delay_ms), @@ -653,7 +668,12 @@ pub(crate) struct LateTXAttackConfig { impl LateTXAttackConfig { fn build(raw: &RawLateTXAttackConfig, topology: &mut Topology, params: &RawParameters) -> Self { - for attacker in topology.select(&raw.attackers) { + let all_attackers = topology.select(&raw.attackers); + info!( + "Late TX attackers: {:?}", + all_attackers.iter().map(|n| &n.name).collect::>() + ); + for attacker in all_attackers { attacker.behaviours.withhold_txs = true; } Self { From d965f3774e86d66209207f33ee9bc63d42eaf580 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Thu, 14 Aug 2025 17:05:20 -0400 Subject: [PATCH 5/5] sim-rs: bump version to 1.0.0 --- sim-rs/CHANGELOG.md | 8 ++++---- sim-rs/Cargo.lock | 4 ++-- sim-rs/sim-cli/Cargo.toml | 2 +- sim-rs/sim-core/Cargo.toml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sim-rs/CHANGELOG.md b/sim-rs/CHANGELOG.md index 977e25bdc..bd9fc6bd0 100644 --- a/sim-rs/CHANGELOG.md +++ b/sim-rs/CHANGELOG.md @@ -1,17 +1,17 @@ # Changelog -# Unreleased +## v1.0.0 -## Linear Leios +### Linear Leios - Allow RBs to include EB certificates produced at least `L_diff` slots ago, instead of `L_vote + L_diff` slots ago. When `L_diff` is 0, this removes any direct time factor from the decision to include an EB cert. - Add TXs to the mempool, even if they belong to an EB we've already seen. - Support choosing attackers by selecting a fraction of stake -## Other +### Other - Add version number to the CLI tool's output. -# v0.1.0 +## v0.1.0 This version was arbitrarily chosen as the point to start tracking major changes to the simulation. \ No newline at end of file diff --git a/sim-rs/Cargo.lock b/sim-rs/Cargo.lock index b440d253c..01eb2cbf3 100644 --- a/sim-rs/Cargo.lock +++ b/sim-rs/Cargo.lock @@ -1213,7 +1213,7 @@ dependencies = [ [[package]] name = "sim-cli" -version = "0.1.0" +version = "1.0.0" dependencies = [ "anyhow", "async-compression", @@ -1241,7 +1241,7 @@ dependencies = [ [[package]] name = "sim-core" -version = "0.1.0" +version = "1.0.0" dependencies = [ "anyhow", "async-stream", diff --git a/sim-rs/sim-cli/Cargo.toml b/sim-rs/sim-cli/Cargo.toml index 0fbb79ec4..89cd30863 100644 --- a/sim-rs/sim-cli/Cargo.toml +++ b/sim-rs/sim-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sim-cli" -version = "0.1.0" +version = "1.0.0" edition = "2024" default-run = "sim-cli" rust-version = "1.88" diff --git a/sim-rs/sim-core/Cargo.toml b/sim-rs/sim-core/Cargo.toml index 178a0ba73..d4ad22fc8 100644 --- a/sim-rs/sim-core/Cargo.toml +++ b/sim-rs/sim-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sim-core" -version = "0.1.0" +version = "1.0.0" edition = "2024" rust-version = "1.88"