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 new file mode 100644 index 000000000..bd9fc6bd0 --- /dev/null +++ b/sim-rs/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +## v1.0.0 + +### 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 + +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/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-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-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, 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" 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 { diff --git a/sim-rs/sim-core/src/sim/linear_leios.rs b/sim-rs/sim-core/src/sim/linear_leios.rs index 72d82e291..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)); } } @@ -509,11 +497,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; @@ -1322,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>, @@ -1377,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 @@ -1392,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; @@ -1408,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 } }