diff --git a/sim-rs/CHANGELOG.md b/sim-rs/CHANGELOG.md index 231051ae5..bdae751e5 100644 --- a/sim-rs/CHANGELOG.md +++ b/sim-rs/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## v1.3.0 + +### Linear Leios + +- Respect timings according to the latest CIP draft: + - RB headers must be received within `Delta_header` + - Voting must wait until `3 * Delta_header` + - Voting must finish by `3 * Delta_header + L_vote` + - EB cannot be referenced until after `3 * Delta_header + L_vote + L_diff` +- Don't include transactions directly in an RB if it also includes an endorsement +- Don't produce empty EBs + +### Other + +- Fix linter warnings from newer rust version + ## v1.2.0 ### All Leios variants diff --git a/sim-rs/Cargo.lock b/sim-rs/Cargo.lock index 5103c01c5..fd0f17fb4 100644 --- a/sim-rs/Cargo.lock +++ b/sim-rs/Cargo.lock @@ -1213,7 +1213,7 @@ dependencies = [ [[package]] name = "sim-cli" -version = "1.2.0" +version = "1.3.0" dependencies = [ "anyhow", "async-compression", @@ -1241,7 +1241,7 @@ dependencies = [ [[package]] name = "sim-core" -version = "1.2.0" +version = "1.3.0" dependencies = [ "anyhow", "async-stream", diff --git a/sim-rs/implementations/LINEAR_LEIOS.md b/sim-rs/implementations/LINEAR_LEIOS.md index e37e4b3e0..79448ad3c 100644 --- a/sim-rs/implementations/LINEAR_LEIOS.md +++ b/sim-rs/implementations/LINEAR_LEIOS.md @@ -7,7 +7,7 @@ The log file schema is currently identical to every other variant (though `pipel ## Description -Whenever a node creates an RB, it also creates an EB. The RB header contains a reference to this new EB. If the RB producer has a certificate for the parent RB’s EB, it will include that certificate in the RB body. +Whenever a node creates an RB, it also has an opportunity to create an EB (though it will not produce empty EBs). The RB header contains a reference to this new EB. If the RB producer has a certificate for the parent RB’s EB, and at least `3 * Δhdr + L_vote + L_diff` has passed since that RB was created, it will include that certificate in the RB body. RB headers are diffused separately from bodies. When a node receives an RB header, it checks whether that RB should be the new head of its chain. If so, it will request the RB body and the referenced EB (from the first peer which announces them). @@ -36,17 +36,17 @@ A node will wait at least `3 * Δhdr` after an EB was created before voting for For a node to vote for an EB, all of the following must be true. - The RB which announced that EB is currently the head of that node's chain. - The node received the relevant RB header at most `Δhdr` after it was created. -- The node received the EB body itself at most `L_vote` after it was created. +- The node finished validating the EB body itself at most `3 * Δhdr` + `L_vote` after it was created. ## Mempool behavior When a node creates an RB, it will follow these steps in order: 1. Try to produce a cert for the parent RB's EB. 1. If this succeeds, remove all of this EB's transactions from its mempool. -2. Create an empty RB and empty EB. +2. Create an empty RB. 3. If we have received and fully validated the RB, along with all referenced transactions, 1. Fill the RB body with transactions from our mempool - 2. Fill the EB with transactions from our mempool WITHOUT removing those transactions from the mempool. + 2. Build an EB with transactions from our mempool WITHOUT removing those transactions from the mempool. When a node receives an RB body, it immediately removes all referenced/conflicting transactions from its mempool. If the RB has an EB certificate, it also removes that EB’s transactions from its mempool. If the certified EB arrives after the RB body, we remove its TXs from the mempool once it arrives. diff --git a/sim-rs/sim-cli/Cargo.toml b/sim-rs/sim-cli/Cargo.toml index 0a51c3b89..f01e4a3ad 100644 --- a/sim-rs/sim-cli/Cargo.toml +++ b/sim-rs/sim-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sim-cli" -version = "1.2.0" +version = "1.3.0" edition = "2024" default-run = "sim-cli" rust-version = "1.88" diff --git a/sim-rs/sim-cli/src/events.rs b/sim-rs/sim-cli/src/events.rs index 434915ba3..6717d980d 100644 --- a/sim-rs/sim-cli/src/events.rs +++ b/sim-rs/sim-cli/src/events.rs @@ -114,10 +114,10 @@ impl EventMonitor { remove_zero_decimal: Some(true), }); - if let Some(path) = &self.output_path { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).await?; - } + if let Some(path) = &self.output_path + && let Some(parent) = path.parent() + { + fs::create_dir_all(parent).await?; } let mut output = match self.output_path.as_mut() { diff --git a/sim-rs/sim-core/Cargo.toml b/sim-rs/sim-core/Cargo.toml index 99f55a4d2..6ca59b835 100644 --- a/sim-rs/sim-core/Cargo.toml +++ b/sim-rs/sim-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sim-core" -version = "1.2.0" +version = "1.3.0" edition = "2024" rust-version = "1.88" diff --git a/sim-rs/sim-core/src/clock.rs b/sim-rs/sim-core/src/clock.rs index ffd5917f9..0c4485fe0 100644 --- a/sim-rs/sim-core/src/clock.rs +++ b/sim-rs/sim-core/src/clock.rs @@ -116,15 +116,15 @@ impl ClockBarrier { self.tasks.clone() } - pub fn wait_until(&mut self, timestamp: Timestamp) -> Waiter { + pub fn wait_until(&mut self, timestamp: Timestamp) -> Waiter<'_> { self.wait(Some(timestamp.with_resolution(self.timestamp_resolution))) } - pub fn wait_forever(&mut self) -> Waiter { + pub fn wait_forever(&mut self) -> Waiter<'_> { self.wait(None) } - fn wait(&mut self, until: Option) -> Waiter { + fn wait(&mut self, until: Option) -> Waiter<'_> { let (tx, rx) = oneshot::channel(); let done = until.is_some_and(|ts| ts == self.now()) || self diff --git a/sim-rs/sim-core/src/events.rs b/sim-rs/sim-core/src/events.rs index 3aeed8328..833c28c48 100644 --- a/sim-rs/sim-core/src/events.rs +++ b/sim-rs/sim-core/src/events.rs @@ -8,9 +8,8 @@ use crate::{ clock::{Clock, Timestamp}, config::{NodeConfiguration, NodeId}, model::{ - Block, BlockId, CpuTaskId, EndorserBlockId, InputBlockId, LinearEndorserBlock, - LinearRankingBlock, NoVoteReason, Transaction, TransactionId, TransactionLostReason, - VoteBundle, VoteBundleId, + Block, BlockId, CpuTaskId, EndorserBlockId, InputBlockId, LinearRankingBlock, NoVoteReason, + Transaction, TransactionId, TransactionLostReason, VoteBundle, VoteBundleId, }, }; @@ -44,7 +43,7 @@ impl Eq for Node {} impl PartialOrd for Node { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.id.cmp(&other.id)) + Some(self.cmp(other)) } } @@ -407,7 +406,7 @@ impl EventTracker { }); } - pub fn track_linear_rb_generated(&self, rb: &LinearRankingBlock, eb: &LinearEndorserBlock) { + pub fn track_linear_rb_generated(&self, rb: &LinearRankingBlock) { self.send(Event::RBGenerated { id: self.to_block(rb.header.id), slot: rb.header.id.slot, @@ -431,17 +430,6 @@ impl EventTracker { }), transactions: rb.transactions.iter().map(|tx| tx.id).collect(), }); - self.send(Event::EBGenerated { - id: self.to_endorser_block(eb.id()), - slot: eb.slot, - pipeline: 0, - producer: self.to_node(eb.producer), - shard: 0, - size_bytes: eb.bytes, - transactions: eb.txs.iter().map(|tx| BlockRef { id: tx.id }).collect(), - input_blocks: vec![], - endorser_blocks: vec![], - }); } pub fn track_praos_block_sent(&self, block: &Block, sender: NodeId, recipient: NodeId) { @@ -649,6 +637,20 @@ impl EventTracker { }); } + pub fn track_linear_eb_generated(&self, block: &crate::model::LinearEndorserBlock) { + self.send(Event::EBGenerated { + id: self.to_endorser_block(block.id()), + slot: block.slot, + pipeline: 0, + producer: self.to_node(block.producer), + shard: 0, + size_bytes: block.bytes, + transactions: block.txs.iter().map(|tx| BlockRef { id: tx.id }).collect(), + input_blocks: vec![], + endorser_blocks: vec![], + }); + } + pub fn track_no_eb_generated(&self, node: NodeId, slot: u64) { self.send(Event::NoEBGenerated { node: self.to_node(node), diff --git a/sim-rs/sim-core/src/model.rs b/sim-rs/sim-core/src/model.rs index 9940c4141..d4ddc2040 100644 --- a/sim-rs/sim-core/src/model.rs +++ b/sim-rs/sim-core/src/model.rs @@ -90,7 +90,7 @@ pub struct LinearRankingBlockHeader { pub vrf: u64, pub parent: Option, pub bytes: u64, - pub eb_announcement: EndorserBlockId, + pub eb_announcement: Option, } #[derive(Clone, Debug)] diff --git a/sim-rs/sim-core/src/sim/leios.rs b/sim-rs/sim-core/src/sim/leios.rs index 34edd9c2a..4c111c6d5 100644 --- a/sim-rs/sim-core/src/sim/leios.rs +++ b/sim-rs/sim-core/src/sim/leios.rs @@ -1114,12 +1114,12 @@ impl LeiosNode { } fn receive_request_ib_header(&mut self, from: NodeId, id: InputBlockId) { - if let Some(ib) = self.leios.ibs.get(&id) { - if let Some(header) = ib.header() { - let have_body = matches!(ib, InputBlockState::Received { .. }); - self.queued - .send_to(from, SimulationMessage::IBHeader(header.clone(), have_body)); - } + if let Some(ib) = self.leios.ibs.get(&id) + && let Some(header) = ib.header() + { + let have_body = matches!(ib, InputBlockState::Received { .. }); + self.queued + .send_to(from, SimulationMessage::IBHeader(header.clone(), have_body)); } } diff --git a/sim-rs/sim-core/src/sim/linear_leios.rs b/sim-rs/sim-core/src/sim/linear_leios.rs index ea907e96c..22b591325 100644 --- a/sim-rs/sim-core/src/sim/linear_leios.rs +++ b/sim-rs/sim-core/src/sim/linear_leios.rs @@ -118,7 +118,7 @@ pub enum CpuTask { /// A transaction has been received and validated, and is ready to propagate TransactionValidated(NodeId, Arc), /// A ranking block has been generated and is ready to propagate - RBBlockGenerated(RankingBlock, EndorserBlock, Vec>), + RBBlockGenerated(RankingBlock, Option<(EndorserBlock, Vec>)>), /// An RB header has been received and validated, and ready to propagate RBHeaderValidated(NodeId, RankingBlockHeader, bool, bool), /// A ranking block has been received and validated, and is ready to propagate @@ -137,7 +137,7 @@ impl SimCpuTask for CpuTask { fn name(&self) -> String { match self { Self::TransactionValidated(_, _) => "ValTX", - Self::RBBlockGenerated(_, _, _) => "GenRB", + Self::RBBlockGenerated(_, _) => "GenRB", Self::RBHeaderValidated(_, _, _, _) => "ValRH", Self::RBBlockValidated(_) => "ValRB", Self::EBHeaderValidated(_, _) => "ValEH", @@ -151,7 +151,7 @@ impl SimCpuTask for CpuTask { fn extra(&self) -> String { match self { Self::TransactionValidated(_, _) => "".to_string(), - Self::RBBlockGenerated(_, _, _) => "".to_string(), + Self::RBBlockGenerated(_, _) => "".to_string(), Self::RBHeaderValidated(_, _, _, _) => "".to_string(), Self::RBBlockValidated(_) => "".to_string(), Self::EBHeaderValidated(_, _) => "".to_string(), @@ -166,7 +166,7 @@ impl SimCpuTask for CpuTask { Self::TransactionValidated(_, tx) => vec![ config.tx_validation_constant + config.tx_validation_per_byte * tx.bytes as u32, ], - Self::RBBlockGenerated(rb, eb, _) => { + Self::RBBlockGenerated(rb, eb) => { let mut rb_time = config.rb_generation + config.rb_body_validation_constant; let rb_bytes: u64 = rb.transactions.iter().map(|tx| tx.bytes).sum(); rb_time += config.rb_validation_per_byte * (rb_bytes as u32); @@ -175,12 +175,15 @@ impl SimCpuTask for CpuTask { rb_time += config.cert_generation_constant + (config.cert_generation_per_node * nodes as u32); } + let mut times = vec![rb_time]; - let mut eb_time = config.eb_generation + config.eb_body_validation_constant; - let eb_bytes: u64 = eb.txs.iter().map(|tx| tx.bytes).sum(); - eb_time += config.eb_body_validation_per_byte * (eb_bytes as u32); - - vec![rb_time, eb_time] + if let Some((eb, _)) = eb { + let mut eb_time = config.eb_generation + config.eb_body_validation_constant; + let eb_bytes: u64 = eb.txs.iter().map(|tx| tx.bytes).sum(); + eb_time += config.eb_body_validation_per_byte * (eb_bytes as u32); + times.push(eb_time); + } + times } Self::RBHeaderValidated(_, _, _, _) => vec![config.rb_head_validation], Self::RBBlockValidated(rb) => { @@ -399,9 +402,7 @@ impl NodeImpl for LinearLeiosNode { fn handle_cpu_task(&mut self, task: Self::Task) -> EventResult { match task { CpuTask::TransactionValidated(from, tx) => self.propagate_tx(from, tx), - CpuTask::RBBlockGenerated(rb, eb, withheld_txs) => { - self.finish_generating_rb(rb, eb, withheld_txs) - } + CpuTask::RBBlockGenerated(rb, eb) => self.finish_generating_rb(rb, eb), CpuTask::RBHeaderValidated(from, header, has_body, has_eb) => { self.finish_validating_rb_header(from, header, has_body, has_eb) } @@ -517,7 +518,12 @@ impl LinearLeiosNode { let parent = self.latest_rb_id(); let endorsement = parent.and_then(|rb_id| { - if rb_id.slot + self.sim_config.linear_diffuse_stage_length > slot { + let earliest_endorse_time = Timestamp::from_secs(rb_id.slot) + + (self.sim_config.header_diffusion_time * 3) + + Duration::from_secs(self.sim_config.linear_vote_stage_length) + + Duration::from_secs(self.sim_config.linear_diffuse_stage_length); + + if earliest_endorse_time > Timestamp::from_secs(slot) { // This RB was generated too quickly after another; hasn't been time to gather all the votes. // No endorsement. return None; @@ -549,7 +555,7 @@ impl LinearLeiosNode { let produce_empty_block = !self.leios.incomplete_onchain_ebs.is_empty(); let mut rb_transactions = vec![]; - if !produce_empty_block && self.sim_config.praos_fallback { + if !produce_empty_block && self.sim_config.praos_fallback && endorsement.is_none() { if let TransactionConfig::Mock(config) = &self.sim_config.transactions { // Add one transaction, the right size for the extra RB payload let tx = config.mock_tx(config.rb_size); @@ -564,12 +570,6 @@ impl LinearLeiosNode { } } - let eb_id = EndorserBlockId { - slot, - pipeline: 0, - producer: self.id, - }; - let mut eb_transactions = vec![]; let mut withheld_txs = vec![]; if !produce_empty_block { @@ -591,6 +591,22 @@ impl LinearLeiosNode { self.sample_from_mempool(&mut eb_transactions, self.sim_config.max_eb_size, false); } } + let (eb_announcement, eb) = if eb_transactions.is_empty() { + (None, None) + } else { + let eb_id = EndorserBlockId { + slot, + pipeline: 0, + producer: self.id, + }; + let eb = EndorserBlock { + slot, + producer: self.id, + bytes: self.sim_config.sizes.linear_eb(&eb_transactions), + txs: eb_transactions, + }; + (Some(eb_id), Some((eb, withheld_txs))) + }; let rb = RankingBlock { header: RankingBlockHeader { @@ -601,32 +617,28 @@ impl LinearLeiosNode { vrf, parent, bytes: self.sim_config.sizes.block_header, - eb_announcement: eb_id, + eb_announcement, }, transactions: rb_transactions, endorsement, }; - let eb = EndorserBlock { - slot, - producer: self.id, - bytes: self.sim_config.sizes.linear_eb(&eb_transactions), - txs: eb_transactions, - }; self.tracker.track_praos_block_lottery_won(rb.header.id); self.queued - .schedule_cpu_task(CpuTask::RBBlockGenerated(rb, eb, withheld_txs)); + .schedule_cpu_task(CpuTask::RBBlockGenerated(rb, eb)); } fn finish_generating_rb( &mut self, rb: RankingBlock, - eb: EndorserBlock, - withheld_txs: Vec>, + eb: Option<(EndorserBlock, Vec>)>, ) { - self.tracker.track_linear_rb_generated(&rb, &eb); + self.tracker.track_linear_rb_generated(&rb); self.publish_rb(Arc::new(rb), false); - self.finish_generating_eb(eb, withheld_txs); + if let Some((eb, withheld_txs)) = eb { + self.tracker.track_linear_eb_generated(&eb); + self.finish_generating_eb(eb, withheld_txs); + } } fn publish_rb(&mut self, rb: Arc, already_sent_header: bool) { @@ -653,9 +665,9 @@ impl LinearLeiosNode { .get(&rb.header.id) .and_then(|rb| rb.header_seen()) .unwrap_or(self.clock.now()); - self.leios - .ebs_by_rb - .insert(rb.header.id, rb.header.eb_announcement); + if let Some(eb_id) = rb.header.eb_announcement { + self.leios.ebs_by_rb.insert(rb.header.id, eb_id); + } self.praos .blocks .insert(rb.header.id, RankingBlockView::Received { rb, header_seen }); @@ -678,22 +690,24 @@ impl LinearLeiosNode { } fn receive_request_rb_header(&mut self, from: NodeId, id: BlockId) { - if let Some(rb) = self.praos.blocks.get(&id) { - if let Some(header) = rb.header() { - // If we already have this RB's body, - // let the requester know that it's ready to fetch. - let have_body = matches!(rb, RankingBlockView::Received { .. }); - // If we already have the EB announced by this RB, - // let the requester know that they can fetch it. - // But if we are maliciously withholding the EB, do not let them know. - let have_eb = matches!( - self.leios.ebs.get(&header.eb_announcement), + if let Some(rb) = self.praos.blocks.get(&id) + && let Some(header) = rb.header() + { + // If we already have this RB's body, + // let the requester know that it's ready to fetch. + let have_body = matches!(rb, RankingBlockView::Received { .. }); + // If we already have the EB announced by this RB, + // let the requester know that they can fetch it. + // But if we are maliciously withholding the EB, do not let them know. + let have_eb = header.eb_announcement.is_some_and(|eb_id| { + matches!( + self.leios.ebs.get(&eb_id), Some(EndorserBlockView::Received { .. }) - ) && !self.should_withhold_ebs(); - self.queued - .send_to(from, Message::RBHeader(header.clone(), have_body, have_eb)); - }; - }; + ) + }) && !self.should_withhold_ebs(); + self.queued + .send_to(from, Message::RBHeader(header.clone(), have_body, have_eb)); + } } fn receive_rb_header( @@ -725,10 +739,9 @@ impl LinearLeiosNode { // Forget we ever saw that other block if let Some(RankingBlockView::Received { rb, .. }) = self.praos.blocks.remove(old_block_id) + && let Some(endorsement) = &rb.endorsement { - if let Some(endorsement) = &rb.endorsement { - self.leios.incomplete_onchain_ebs.remove(&endorsement.eb); - } + self.leios.incomplete_onchain_ebs.remove(&endorsement.eb); } } } @@ -759,7 +772,9 @@ impl LinearLeiosNode { } // Get ready to fetch the announced EB (if we don't have it already) - let eb_id = header.eb_announcement; + let Some(eb_id) = header.eb_announcement else { + return; + }; if matches!( self.leios.ebs.get(&eb_id), Some(EndorserBlockView::Received { .. }) @@ -850,10 +865,10 @@ impl LinearLeiosNode { header_seen, }, ); - if let Some(endorsement) = &rb.endorsement { - if !self.is_eb_validated(endorsement.eb) { - self.leios.incomplete_onchain_ebs.insert(endorsement.eb); - } + if let Some(endorsement) = &rb.endorsement + && !self.is_eb_validated(endorsement.eb) + { + self.leios.incomplete_onchain_ebs.insert(endorsement.eb); } self.publish_rb(rb, true); @@ -1222,8 +1237,9 @@ impl LinearLeiosNode { } fn should_vote_for(&self, eb: &EndorserBlock, seen: Timestamp) -> Result<(), NoVoteReason> { - let eb_must_be_received_by = - Timestamp::from_secs(eb.slot + self.sim_config.linear_vote_stage_length); + let eb_must_be_received_by = Timestamp::from_secs(eb.slot) + + (self.sim_config.header_diffusion_time * 3) + + Duration::from_secs(self.sim_config.linear_vote_stage_length); if seen > eb_must_be_received_by { // An EB must be received within L_vote slots of its creation. return Err(NoVoteReason::LateEB); @@ -1232,7 +1248,7 @@ impl LinearLeiosNode { // We only vote for whichever EB we was referenced by the head of the current chain. return Err(NoVoteReason::WrongEB); }; - if rb.header.eb_announcement != eb.id() { + if rb.header.eb_announcement != Some(eb.id()) { // We only vote for whichever EB we was referenced by the head of the current chain. return Err(NoVoteReason::WrongEB); } @@ -1391,12 +1407,11 @@ impl LinearLeiosNode { fn remove_rb_txs_from_mempool(&mut self, rb: &RankingBlock) { let mut txs = rb.transactions.clone(); - if let Some(endorsement) = &rb.endorsement { - if let Some(EndorserBlockView::Received { eb, .. }) = + if let Some(endorsement) = &rb.endorsement + && let Some(EndorserBlockView::Received { eb, .. }) = self.leios.ebs.get(&endorsement.eb) - { - txs.extend(eb.txs.iter().cloned()); - } + { + txs.extend(eb.txs.iter().cloned()); } self.remove_txs_from_mempool(&txs); } diff --git a/sim-rs/sim-core/src/sim/stracciatella.rs b/sim-rs/sim-core/src/sim/stracciatella.rs index bac0e3645..e0c896e9f 100644 --- a/sim-rs/sim-core/src/sim/stracciatella.rs +++ b/sim-rs/sim-core/src/sim/stracciatella.rs @@ -583,8 +583,8 @@ impl StracciatellaLeiosNode { fn select_txs_for_eb(&mut self, shard: u64, pipeline: u64) -> Vec> { let mut txs = vec![]; - if self.sim_config.eb_include_txs_from_previous_stage { - if let Some(eb_from_last_pipeline) = + if self.sim_config.eb_include_txs_from_previous_stage + && let Some(eb_from_last_pipeline) = self.leios.ebs_by_pipeline.get(&pipeline).and_then(|ebs| { ebs.iter() .find_map(|eb_id| match self.leios.ebs.get(eb_id) { @@ -592,12 +592,11 @@ impl StracciatellaLeiosNode { _ => None, }) }) - { - // include TXs from the first EB in the last pipeline - for tx in &eb_from_last_pipeline.txs { - if matches!(self.txs.get(&tx.id), Some(TransactionView::Received(_))) { - txs.push(tx.clone()); - } + { + // include TXs from the first EB in the last pipeline + for tx in &eb_from_last_pipeline.txs { + if matches!(self.txs.get(&tx.id), Some(TransactionView::Received(_))) { + txs.push(tx.clone()); } } } @@ -1088,12 +1087,11 @@ impl StracciatellaLeiosNode { fn remove_rb_txs_from_mempools(&mut self, rb: &RankingBlock) { let mut txs = rb.transactions.clone(); - if let Some(endorsement) = &rb.endorsement { - if let Some(EndorserBlockView::Received { eb, .. }) = + if let Some(endorsement) = &rb.endorsement + && let Some(EndorserBlockView::Received { eb, .. }) = self.leios.ebs.get(&endorsement.eb) - { - txs.extend(eb.txs.iter().cloned()); - } + { + txs.extend(eb.txs.iter().cloned()); } self.remove_txs_from_mempools(&txs); }