Skip to content

Commit 576cb4d

Browse files
committed
sim-rs: correctly handle late RBs, EBs, TXs
1 parent 900a670 commit 576cb4d

File tree

3 files changed

+152
-47
lines changed

3 files changed

+152
-47
lines changed

sim-rs/implementations/LINEAR_LEIOS.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ When voting, a node runs a VRF lottery to decide how many times it can vote for
2020
## Mempool behavior
2121

2222
When a node creates an RB, it will follow these steps in order:
23-
1. Try to produce a cert for the parent RB's EB.
24-
1. If this succeeds, remove all of this EB's transactions from its mempool.
25-
2. Fill the RB body with transactions from its mempool
26-
3. Create a new EB, filled with transactions from its mempool WITHOUT removing those transactions from the mempool.
27-
28-
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.
23+
1. Try to produce a cert for the parent RB's EB.
24+
1. If this succeeds, remove all of this EB's transactions from its mempool.
25+
2. Create an empty RB and empty EB.
26+
3. If we have received and fully validated the RB, along with all referenced transactions,
27+
1. Fill the RB body with transactions from our mempool
28+
2. Fill the EB with transactions from our mempool WITHOUT removing those transactions from the mempool.
29+
30+
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.
2931

3032
## New parameters
3133

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
late-eb-attack:
22
attackers:
33
nodes:
4+
- node-99
5+
- node-98
6+
- node-97
7+
- node-96
48
- node-95
59
- node-94
610
propagation-delay-ms: 4500.0
711
late-tx-attack:
812
attackers:
913
nodes:
14+
- node-99
15+
- node-98
16+
- node-97
17+
- node-96
1018
- node-95
1119
- node-94
1220
attack-probability: 1.0

sim-rs/sim-core/src/sim/linear_leios.rs

Lines changed: 136 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ struct NodeLeiosState {
259259
votes: HashMap<VoteBundleId, VoteBundleView>,
260260
votes_by_eb: HashMap<EndorserBlockId, BTreeMap<NodeId, usize>>,
261261
certified_ebs: HashSet<EndorserBlockId>,
262+
incomplete_onchain_ebs: HashSet<EndorserBlockId>,
263+
missing_onchain_txs: HashMap<TransactionId, Vec<EndorserBlockId>>,
262264
}
263265

264266
#[derive(Clone, Default)]
@@ -447,22 +449,25 @@ impl LinearLeiosNode {
447449
{
448450
return;
449451
}
450-
let rb_ref = self.latest_rb().map(|rb| rb.header.id);
451-
let ledger_state = self.resolve_ledger_state(rb_ref);
452-
if ledger_state.spent_inputs.contains(&tx.input_id) {
453-
// Ignoring a TX which conflicts with something already onchain
454-
return;
455-
}
456-
if self
457-
.praos
458-
.mempool
459-
.values()
460-
.any(|mempool_tx| mempool_tx.input_id == tx.input_id)
461-
{
462-
// Ignoring a TX which conflicts with the current mempool contents.
463-
return;
452+
let was_already_endorsed = self.acknowledge_endorsed_tx(&tx);
453+
if !was_already_endorsed {
454+
let rb_ref = self.latest_rb().map(|rb| rb.header.id);
455+
let ledger_state = self.resolve_ledger_state(rb_ref);
456+
if ledger_state.is_some_and(|ls| ls.spent_inputs.contains(&tx.input_id)) {
457+
// Ignoring a TX which conflicts with something already onchain
458+
return;
459+
}
460+
if self
461+
.praos
462+
.mempool
463+
.values()
464+
.any(|mempool_tx| mempool_tx.input_id == tx.input_id)
465+
{
466+
// Ignoring a TX which conflicts with the current mempool contents.
467+
return;
468+
}
469+
self.praos.mempool.insert(tx.id, tx.clone());
464470
}
465-
self.praos.mempool.insert(tx.id, tx.clone());
466471

467472
// TODO: should send to producers instead (make configurable)
468473
for peer in &self.consumers {
@@ -516,8 +521,12 @@ impl LinearLeiosNode {
516521
Some(endorsement)
517522
});
518523

524+
// If we are missing any EBs from the current chain, we have no way to tell whether
525+
// including a TX would introduce conflicts. So, don't include ANY TXs, just to be safe.
526+
let produce_empty_block = !self.leios.incomplete_onchain_ebs.is_empty();
527+
519528
let mut rb_transactions = vec![];
520-
if self.sim_config.praos_fallback {
529+
if !produce_empty_block && self.sim_config.praos_fallback {
521530
if let TransactionConfig::Mock(config) = &self.sim_config.transactions {
522531
// Add one transaction, the right size for the extra RB payload
523532
let tx = config.mock_tx(config.rb_size);
@@ -539,22 +548,25 @@ impl LinearLeiosNode {
539548
};
540549

541550
let mut eb_transactions = vec![];
551+
let mut withheld_txs = vec![];
552+
if !produce_empty_block {
553+
// If we are performing a "withheld TX" attack, we will include a bunch of brand-new TXs in this EB.
554+
// They will get disseminated through the network at the same time as the EB.
555+
withheld_txs = self.generate_withheld_txs();
556+
eb_transactions.extend(withheld_txs.iter().cloned());
542557

543-
// If we are performing a "withheld TX" attack, we will include a bunch of brand-new TXs in this EB.
544-
// They will get disseminated through the network at the same time as the EB.
545-
let withheld_txs = self.generate_withheld_txs();
546-
eb_transactions.extend(withheld_txs.iter().cloned());
547-
548-
if let TransactionConfig::Mock(config) = &self.sim_config.transactions {
549-
// Add one transaction, the right size for the extra RB payload
550-
let extra_size = config.eb_size - withheld_txs.iter().map(|tx| tx.bytes).sum::<u64>();
551-
if extra_size > 0 {
552-
let tx = config.mock_tx(extra_size);
553-
self.tracker.track_transaction_generated(&tx, self.id);
554-
eb_transactions.push(Arc::new(tx));
558+
if let TransactionConfig::Mock(config) = &self.sim_config.transactions {
559+
// Add one transaction, the right size for the extra RB payload
560+
let extra_size =
561+
config.eb_size - withheld_txs.iter().map(|tx| tx.bytes).sum::<u64>();
562+
if extra_size > 0 {
563+
let tx = config.mock_tx(extra_size);
564+
self.tracker.track_transaction_generated(&tx, self.id);
565+
eb_transactions.push(Arc::new(tx));
566+
}
567+
} else {
568+
self.sample_from_mempool(&mut eb_transactions, self.sim_config.max_eb_size, false);
555569
}
556-
} else {
557-
self.sample_from_mempool(&mut eb_transactions, self.sim_config.max_eb_size, false);
558570
}
559571

560572
let rb = RankingBlock {
@@ -686,7 +698,15 @@ impl LinearLeiosNode {
686698
// We like our block better than this new one.
687699
return;
688700
}
689-
self.praos.blocks.remove(old_block_id);
701+
702+
// Forget we ever saw that other block
703+
if let Some(RankingBlockView::Received { rb, .. }) =
704+
self.praos.blocks.remove(old_block_id)
705+
{
706+
if let Some(endorsement) = &rb.endorsement {
707+
self.leios.incomplete_onchain_ebs.remove(&endorsement.eb);
708+
}
709+
}
690710
}
691711
}
692712
self.praos
@@ -800,6 +820,9 @@ impl LinearLeiosNode {
800820
header_seen,
801821
},
802822
);
823+
if let Some(endorsement) = &rb.endorsement {
824+
self.expect_eb_from_endorsement(endorsement.eb);
825+
}
803826

804827
self.publish_rb(rb, true);
805828
}
@@ -883,6 +906,8 @@ impl LinearLeiosNode {
883906
return;
884907
}
885908

909+
self.acknowledge_endorsed_eb(&eb);
910+
886911
// TODO: sleep
887912
for peer in &self.consumers {
888913
if *peer == from {
@@ -898,6 +923,75 @@ impl LinearLeiosNode {
898923
fn finish_validating_eb(&mut self, eb: Arc<EndorserBlock>, seen: Timestamp) {
899924
self.vote_for_endorser_block(&eb, seen);
900925
}
926+
927+
// Check if we have seen this EB, and all of its TXs.
928+
// If we haven't, we will need to produce empty blocks until we see it.
929+
fn expect_eb_from_endorsement(&mut self, eb_id: EndorserBlockId) {
930+
let eb = self.leios.ebs.get(&eb_id);
931+
let Some(EndorserBlockView::Received { eb }) = eb else {
932+
self.leios.incomplete_onchain_ebs.insert(eb_id);
933+
return;
934+
};
935+
936+
let some_tx_is_missing = self.expect_txs_from_endorsement(&eb.clone());
937+
if some_tx_is_missing {
938+
self.leios.incomplete_onchain_ebs.insert(eb_id);
939+
}
940+
}
941+
942+
// If this EB has been endorsed, track that it has been received.
943+
fn acknowledge_endorsed_eb(&mut self, eb: &EndorserBlock) {
944+
let eb_id = eb.id();
945+
if !self.leios.incomplete_onchain_ebs.contains(&eb_id) {
946+
return;
947+
}
948+
let some_tx_is_missing = self.expect_txs_from_endorsement(eb);
949+
if !some_tx_is_missing {
950+
self.leios.incomplete_onchain_ebs.remove(&eb_id);
951+
self.remove_eb_txs_from_mempool(eb);
952+
}
953+
}
954+
955+
fn expect_txs_from_endorsement(&mut self, eb: &EndorserBlock) -> bool {
956+
if matches!(self.sim_config.variant, LeiosVariant::Linear) {
957+
// EBs contain TX bodies, so there's no concept of a "missing" TX
958+
return false;
959+
}
960+
let mut some_tx_is_missing = false;
961+
for tx in &eb.txs {
962+
let tx_id = tx.id;
963+
if !matches!(self.txs.get(&tx_id), Some(TransactionView::Received(_))) {
964+
self.leios
965+
.missing_onchain_txs
966+
.entry(tx_id)
967+
.or_default()
968+
.push(eb.id());
969+
some_tx_is_missing = true;
970+
}
971+
}
972+
some_tx_is_missing
973+
}
974+
975+
// If any endorsed EBs referenced this transaction, track that it has been received.
976+
fn acknowledge_endorsed_tx(&mut self, tx: &Transaction) -> bool {
977+
let Some(eb_ids) = self.leios.missing_onchain_txs.remove(&tx.id) else {
978+
return false;
979+
};
980+
for eb_id in eb_ids {
981+
let Some(EndorserBlockView::Received { eb }) = self.leios.ebs.get(&eb_id) else {
982+
unreachable!("how did we know this EB needed a TX if we never saw the EB?");
983+
};
984+
if !eb
985+
.txs
986+
.iter()
987+
.any(|tx| matches!(self.txs.get(&tx.id), Some(TransactionView::Received(_))))
988+
{
989+
// we have received all missing TXs for this EB, so now we have a complete view of it!
990+
self.leios.incomplete_onchain_ebs.remove(&eb_id);
991+
}
992+
}
993+
true
994+
}
901995
}
902996

903997
// EB withholding:
@@ -1206,12 +1300,12 @@ impl LinearLeiosNode {
12061300
.retain(|_, tx| !inputs.contains(&tx.input_id));
12071301
}
12081302

1209-
fn resolve_ledger_state(&mut self, rb_ref: Option<BlockId>) -> Arc<LedgerState> {
1303+
fn resolve_ledger_state(&mut self, rb_ref: Option<BlockId>) -> Option<Arc<LedgerState>> {
12101304
let Some(block_id) = rb_ref else {
1211-
return Arc::new(LedgerState::default());
1305+
return Some(Arc::new(LedgerState::default()));
12121306
};
12131307
if let Some(state) = self.ledger_states.get(&block_id) {
1214-
return state.clone();
1308+
return Some(state.clone());
12151309
};
12161310

12171311
let mut state = self
@@ -1237,19 +1331,20 @@ impl LinearLeiosNode {
12371331
}
12381332

12391333
if let Some(endorsement) = &rb.endorsement {
1240-
if let Some(EndorserBlockView::Received { eb }) =
1241-
self.leios.ebs.get(&endorsement.eb)
1242-
{
1243-
for tx in &eb.txs {
1244-
state.spent_inputs.insert(tx.input_id);
1245-
}
1334+
let Some(EndorserBlockView::Received { eb }) = self.leios.ebs.get(&endorsement.eb)
1335+
else {
1336+
// We don't have the EB yet, so we don't know the current ledger state.
1337+
return None;
1338+
};
1339+
for tx in &eb.txs {
1340+
state.spent_inputs.insert(tx.input_id);
12461341
}
12471342
}
12481343
}
12491344

12501345
let state = Arc::new(state);
12511346
self.ledger_states.insert(block_id, state.clone());
1252-
state
1347+
Some(state)
12531348
}
12541349
}
12551350

0 commit comments

Comments
 (0)