@@ -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