Skip to content

Commit 6878acf

Browse files
committed
sim-rs: simulate a late EB attack
1 parent c11be4b commit 6878acf

File tree

12 files changed

+437
-21
lines changed

12 files changed

+437
-21
lines changed

data/simulation/config.d.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,13 @@ export interface Config {
241241
"cert-validation-cpu-time-ms-per-node": number;
242242
"cert-size-bytes-constant": bigint;
243243
"cert-size-bytes-per-node": bigint;
244+
245+
// Attacks
246+
/**
247+
* Configuration for a "late EB" attack,
248+
* where nodes deliberately withhold EBs until near the end of the voting phase.
249+
*/
250+
"late-eb-attack"?: LateEBAttackConfig | null;
244251
}
245252

246253
export type CleanupPolicies = "all" | CleanupPolicy[];
@@ -313,3 +320,14 @@ export enum MempoolSamplingStrategy {
313320
/** Include transactions in random order. */
314321
Random = "random",
315322
}
323+
324+
/**
325+
* Configuration for a "late EB" attack,
326+
* where nodes deliberately withhold EBs until near the end of the voting phase.
327+
*/
328+
export interface LateEBAttackConfig {
329+
/** The set of stake pools which are participating in the attack. */
330+
"attacker-nodes": string[],
331+
/** How long the attackers will wait before diffusing their EBs */
332+
"propagation-delay-ms": number,
333+
}

data/simulation/config.schema.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,23 @@
7272
},
7373
"type": "object"
7474
},
75+
"LateEBAttackConfig": {
76+
"description": "Configuration for a \"late EB\" attack,\nwhere nodes deliberately withhold EBs until near the end of the voting phase.",
77+
"properties": {
78+
"attacker-nodes": {
79+
"description": "The set of stake pools which are participating in the attack.",
80+
"items": {
81+
"type": "string"
82+
},
83+
"type": "array"
84+
},
85+
"propagation-delay-ms": {
86+
"description": "How long the attackers will wait before diffusing their EBs",
87+
"type": "number"
88+
}
89+
},
90+
"type": "object"
91+
},
7592
"LeiosVariant": {
7693
"enum": [
7794
"short",
@@ -290,6 +307,10 @@
290307
"description": "The total number of shards available for IBs.\nMust be divisible by ib_shard_group_count.\n\nOnly supported by Rust simulation.",
291308
"type": "number"
292309
},
310+
"late-eb-attack": {
311+
"$ref": "#/definitions/LateEBAttackConfig",
312+
"description": "Configuration for a \"late EB\" attack,\nwhere nodes deliberately withhold EBs until near the end of the voting phase."
313+
},
293314
"leios-header-diffusion-time-ms": {
294315
"description": "The expected time it takes a header to fully diffuse across the network.\nThis is Δhdr from the Leios paper.",
295316
"type": "number"

sim-rs/parameters/late-eb-attack.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
late-eb-attack:
2+
attacker-nodes:
3+
- node-95
4+
- node-94
5+
propagation-delay-ms: 4500.0

sim-rs/sim-core/src/clock.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ pub struct Clock {
5252
timestamp_resolution: Duration,
5353
time: Arc<AtomicTimestamp>,
5454
waiters: Arc<AtomicUsize>,
55-
tasks: Arc<AtomicUsize>,
55+
tasks: TaskInitiator,
5656
tx: mpsc::UnboundedSender<ClockEvent>,
5757
}
5858

@@ -61,7 +61,7 @@ impl Clock {
6161
timestamp_resolution: Duration,
6262
time: Arc<AtomicTimestamp>,
6363
waiters: Arc<AtomicUsize>,
64-
tasks: Arc<AtomicUsize>,
64+
tasks: TaskInitiator,
6565
tx: mpsc::UnboundedSender<ClockEvent>,
6666
) -> Self {
6767
Self {
@@ -95,7 +95,7 @@ pub struct ClockBarrier {
9595
id: usize,
9696
timestamp_resolution: Duration,
9797
time: Arc<AtomicTimestamp>,
98-
tasks: Arc<AtomicUsize>,
98+
tasks: TaskInitiator,
9999
tx: mpsc::UnboundedSender<ClockEvent>,
100100
}
101101

@@ -105,13 +105,17 @@ impl ClockBarrier {
105105
}
106106

107107
pub fn start_task(&self) {
108-
self.tasks.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
108+
self.tasks.start_task();
109109
}
110110

111111
pub fn finish_task(&self) {
112112
let _ = self.tx.send(ClockEvent::FinishTask);
113113
}
114114

115+
pub fn task_initiator(&self) -> TaskInitiator {
116+
self.tasks.clone()
117+
}
118+
115119
pub fn wait_until(&mut self, timestamp: Timestamp) -> Waiter {
116120
self.wait(Some(timestamp.with_resolution(self.timestamp_resolution)))
117121
}
@@ -141,6 +145,20 @@ impl ClockBarrier {
141145
}
142146
}
143147

148+
#[derive(Clone)]
149+
pub struct TaskInitiator {
150+
tasks: Arc<AtomicUsize>,
151+
}
152+
153+
impl TaskInitiator {
154+
pub fn new(tasks: Arc<AtomicUsize>) -> Self {
155+
Self { tasks }
156+
}
157+
pub fn start_task(&self) {
158+
self.tasks.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
159+
}
160+
}
161+
144162
pub struct Waiter<'a> {
145163
id: usize,
146164
tx: &'a mpsc::UnboundedSender<ClockEvent>,

sim-rs/sim-core/src/clock/coordinator.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use std::{
99

1010
use tokio::sync::{mpsc, oneshot};
1111

12+
use crate::clock::TaskInitiator;
13+
1214
use super::{Clock, Timestamp, timestamp::AtomicTimestamp};
1315

1416
pub struct ClockCoordinator {
@@ -41,7 +43,7 @@ impl ClockCoordinator {
4143
self.timestamp_resolution,
4244
self.time.clone(),
4345
self.waiter_count.clone(),
44-
self.tasks.clone(),
46+
TaskInitiator::new(self.tasks.clone()),
4547
self.tx.clone(),
4648
)
4749
}
@@ -61,7 +63,7 @@ impl ClockCoordinator {
6163
if waiters[actor].replace(Waiter { until, done }).is_some() {
6264
panic!("An actor has somehow managed to wait twice");
6365
}
64-
running -= 1;
66+
running = running.checked_sub(1).unwrap();
6567
if let Some(timestamp) = until {
6668
queue.entry(timestamp).or_default().push(actor);
6769
}
@@ -89,7 +91,8 @@ impl ClockCoordinator {
8991
}
9092
}
9193
ClockEvent::FinishTask => {
92-
self.tasks.fetch_sub(1, Ordering::AcqRel);
94+
let prev_tasks = self.tasks.fetch_sub(1, Ordering::AcqRel);
95+
assert!(prev_tasks != 0, "Finished a task that was never started");
9396
assert!(
9497
running != 0,
9598
"All tasks were completed while there were no actors to complete them"

sim-rs/sim-core/src/config.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ pub struct RawParameters {
142142
pub cert_validation_cpu_time_ms_per_node: f64,
143143
pub cert_size_bytes_constant: u64,
144144
pub cert_size_bytes_per_node: u64,
145+
146+
// attacks,
147+
pub late_eb_attack: Option<RawLateEBAttackConfig>,
145148
}
146149

147150
#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Eq)]
@@ -177,6 +180,13 @@ pub enum MempoolSamplingStrategy {
177180
Random,
178181
}
179182

183+
#[derive(Deserialize)]
184+
#[serde(rename_all = "kebab-case")]
185+
pub struct RawLateEBAttackConfig {
186+
pub attacker_nodes: Vec<String>,
187+
pub propagation_delay_ms: f64,
188+
}
189+
180190
#[derive(Debug, Serialize, Deserialize)]
181191
#[serde(rename_all = "kebab-case")]
182192
pub struct RawTopology {
@@ -512,6 +522,48 @@ impl MockTransactionConfig {
512522
}
513523
}
514524

525+
#[derive(Debug, Clone)]
526+
pub(crate) struct AttackConfig {
527+
pub(crate) late_eb: Option<LateEBAttackConfig>,
528+
}
529+
impl AttackConfig {
530+
fn build(params: &RawParameters, topology: &Topology) -> Self {
531+
Self {
532+
late_eb: params
533+
.late_eb_attack
534+
.as_ref()
535+
.map(|raw| LateEBAttackConfig::build(raw, topology)),
536+
}
537+
}
538+
}
539+
540+
#[derive(Debug, Clone)]
541+
pub(crate) struct LateEBAttackConfig {
542+
pub(crate) attackers: HashSet<NodeId>,
543+
pub(crate) propagation_delay: Duration,
544+
}
545+
546+
impl LateEBAttackConfig {
547+
fn build(raw: &RawLateEBAttackConfig, topology: &Topology) -> Self {
548+
let attacker_names = raw.attacker_nodes.iter().collect::<HashSet<_>>();
549+
let attackers = topology
550+
.nodes
551+
.iter()
552+
.filter_map(|node| {
553+
if attacker_names.contains(&node.name) {
554+
Some(node.id)
555+
} else {
556+
None
557+
}
558+
})
559+
.collect();
560+
Self {
561+
attackers,
562+
propagation_delay: duration_ms(raw.propagation_delay_ms),
563+
}
564+
}
565+
}
566+
515567
#[derive(Debug, Clone)]
516568
pub struct SimConfiguration {
517569
pub seed: u64,
@@ -554,6 +606,7 @@ pub struct SimConfiguration {
554606
pub(crate) cpu_times: CpuTimeConfig,
555607
pub(crate) sizes: BlockSizeConfig,
556608
pub(crate) transactions: TransactionConfig,
609+
pub(crate) attacks: AttackConfig,
557610
}
558611

559612
impl SimConfiguration {
@@ -577,6 +630,7 @@ impl SimConfiguration {
577630
);
578631
}
579632
let total_stake = topology.nodes.iter().map(|n| n.stake).sum();
633+
let attacks = AttackConfig::build(&params, &topology);
580634
Ok(Self {
581635
seed: 0,
582636
timestamp_resolution: duration_ms(params.timestamp_resolution_ms),
@@ -618,6 +672,7 @@ impl SimConfiguration {
618672
cpu_times: CpuTimeConfig::new(&params),
619673
sizes: BlockSizeConfig::new(&params),
620674
transactions: TransactionConfig::new(&params),
675+
attacks,
621676
})
622677
}
623678
}

0 commit comments

Comments
 (0)