@@ -97,6 +97,8 @@ import Ouroboros.Network.TxSubmission.Outbound (TxSubmissionProtocolError (..))
9797
9898import Simulation.Network.Snocket (BearerInfo (.. ), noAttenuation )
9999
100+ import Test.Cardano.Network.Diffusion.Testnet.ChainedTxs (ChainedPeerTxs (.. ))
101+ import Test.Cardano.Network.Diffusion.Testnet.ChainedTxs qualified as ChainedTxs
100102import Test.Cardano.Network.Diffusion.Testnet.Simulation
101103
102104import Test.Ouroboros.Network.ConnectionManager.Timeouts
@@ -179,6 +181,8 @@ tests =
179181 , testGroup " Tx Submission"
180182 [ nightlyTest $ testProperty " no protocol errors"
181183 prop_no_txSubmission_error_iosimpor
184+ , nightlyTest $ testProperty " tx chain integrity"
185+ prop_txSubmission_chainIntegrity_iosimpor
182186 ]
183187 , testGroup " Churn"
184188 [ nightlyTest $ testProperty " no timeouts"
@@ -265,12 +269,15 @@ tests =
265269 prop_no_peershare_unwilling_iosim
266270 ]
267271 , testGroup " Tx Submission"
268- [ testProperty " no protocol errors"
272+ [ ChainedTxs. tests
273+ , testProperty " no protocol errors"
269274 prop_no_txSubmission_error_iosim
270275 , testProperty " all transactions"
271276 prop_txSubmission_allTransactions
272277 , testProperty " inflight coverage"
273278 prop_check_inflight_ratio
279+ , testProperty " tx chain integrity"
280+ prop_txSubmission_chainIntegrity_iosim
274281 ]
275282 , testGroup " Churn"
276283 [ testProperty " no timeouts" prop_churn_notimeouts_iosim
@@ -1080,6 +1087,215 @@ prop_txSubmission_allTransactions (ArbTxDecisionPolicy decisionPolicy)
10801087 | otherwise -> counterexample " Didn't found any entry in the map!" False
10811088
10821089
1090+ -- | Three-node diffusion topology used by the Tx Chain Integrity
1091+ -- property: one node (0.0.0.0) downloading txs from two downstream
1092+ -- peers (0.0.0.1 and 0.0.0.2). The node has no outbound txs of its own;
1093+ -- each downstream peer advertises the tx list it was assigned by
1094+ -- 'ChainedPeerTxs'.
1095+ txChainIntegrityDiffScript :: ArbTxDecisionPolicy
1096+ -> ChainedPeerTxs
1097+ -> DiffusionScript
1098+ txChainIntegrityDiffScript (ArbTxDecisionPolicy decisionPolicy)
1099+ (ChainedPeerTxs chainedTxsB chainedTxsC) =
1100+ let localRootConfig = LocalRootConfig
1101+ DoNotAdvertisePeer
1102+ InitiatorAndResponderDiffusionMode
1103+ Outbound
1104+ IsNotTrustable
1105+
1106+ noPeerTargets = PeerSelectionTargets {
1107+ targetNumberOfRootPeers = 0 ,
1108+ targetNumberOfKnownPeers = 0 ,
1109+ targetNumberOfEstablishedPeers = 0 ,
1110+ targetNumberOfActivePeers = 0 ,
1111+ targetNumberOfKnownBigLedgerPeers = 0 ,
1112+ targetNumberOfEstablishedBigLedgerPeers = 0 ,
1113+ targetNumberOfActiveBigLedgerPeers = 0
1114+ }
1115+
1116+ upstreamTargets = PeerSelectionTargets {
1117+ targetNumberOfRootPeers = 1 ,
1118+ targetNumberOfKnownPeers = 1 ,
1119+ targetNumberOfEstablishedPeers = 1 ,
1120+ targetNumberOfActivePeers = 1 ,
1121+ targetNumberOfKnownBigLedgerPeers = 0 ,
1122+ targetNumberOfEstablishedBigLedgerPeers = 0 ,
1123+ targetNumberOfActiveBigLedgerPeers = 0
1124+ }
1125+
1126+ in DiffusionScript
1127+ (SimArgs 1 10 decisionPolicy)
1128+ (singletonTimedScript Map. empty)
1129+ [ ( NodeArgs
1130+ (- 1 )
1131+ InitiatorAndResponderDiffusionMode
1132+ Map. empty
1133+ PraosMode
1134+ (Script (DontUseBootstrapPeers :| [] ))
1135+ (TestAddress (IPAddr (read " 0.0.0.0" ) 0 ))
1136+ PeerSharingDisabled
1137+ []
1138+ (Script (LedgerPools [] :| [] ))
1139+ (noPeerTargets, noPeerTargets)
1140+ (Script (DNSTimeout {getDNSTimeout = 10 } :| [] ))
1141+ (Script (DNSLookupDelay {getDNSLookupDelay = 0 } :| [] ))
1142+ Nothing
1143+ False
1144+ (Script (PraosFetchMode FetchModeDeadline :| [] ))
1145+ []
1146+ , [JoinNetwork 0 ] )
1147+ , ( NodeArgs
1148+ (- 2 )
1149+ InitiatorAndResponderDiffusionMode
1150+ Map. empty
1151+ PraosMode
1152+ (Script (DontUseBootstrapPeers :| [] ))
1153+ (TestAddress (IPAddr (read " 0.0.0.1" ) 0 ))
1154+ PeerSharingDisabled
1155+ [(1 , 1 , Map. fromList
1156+ [(RelayAccessAddress " 0.0.0.0" 0 , localRootConfig)])]
1157+ (Script (LedgerPools [] :| [] ))
1158+ (upstreamTargets, upstreamTargets)
1159+ (Script (DNSTimeout {getDNSTimeout = 10 } :| [] ))
1160+ (Script (DNSLookupDelay {getDNSLookupDelay = 0 } :| [] ))
1161+ Nothing
1162+ False
1163+ (Script (PraosFetchMode FetchModeDeadline :| [] ))
1164+ chainedTxsB
1165+ , [JoinNetwork 0 ] )
1166+ , ( NodeArgs
1167+ (- 3 )
1168+ InitiatorAndResponderDiffusionMode
1169+ Map. empty
1170+ PraosMode
1171+ (Script (DontUseBootstrapPeers :| [] ))
1172+ (TestAddress (IPAddr (read " 0.0.0.2" ) 0 ))
1173+ PeerSharingDisabled
1174+ [(1 , 1 , Map. fromList
1175+ [(RelayAccessAddress " 0.0.0.0" 0 , localRootConfig)])]
1176+ (Script (LedgerPools [] :| [] ))
1177+ (upstreamTargets, upstreamTargets)
1178+ (Script (DNSTimeout {getDNSTimeout = 10 } :| [] ))
1179+ (Script (DNSLookupDelay {getDNSLookupDelay = 0 } :| [] ))
1180+ Nothing
1181+ False
1182+ (Script (PraosFetchMode FetchModeDeadline :| [] ))
1183+ chainedTxsC
1184+ , [JoinNetwork 0 ] )
1185+ ]
1186+
1187+ -- | Txs guaranteed to reach the node's mempool: at least one downstream
1188+ -- peer carries the tx together with all of its ancestors, and every
1189+ -- member of that chain is valid. Txs whose chain isn't fully represented
1190+ -- on either downstream peer are legitimately unreachable and correctly
1191+ -- excluded from this lower bound. V2 may deliver additional txs when a
1192+ -- split chain is assembled across downstream peers via favourable
1193+ -- ordering, but those are not guaranteed.
1194+ txChainIntegrityExpected :: ChainedPeerTxs -> Set TxId
1195+ txChainIntegrityExpected (ChainedPeerTxs chainedTxsB chainedTxsC) =
1196+ Set. union
1197+ (perPeerDeliverable chainedTxsB)
1198+ (perPeerDeliverable chainedTxsC)
1199+ where
1200+ perPeerDeliverable :: [Tx TxId ] -> Set TxId
1201+ perPeerDeliverable peerTxs =
1202+ let txMap = Map. fromList [(getTxId t, t) | t <- peerTxs]
1203+ complete tid = case Map. lookup tid txMap of
1204+ Nothing -> False
1205+ Just t -> getTxValid t
1206+ && maybe True complete (getTxParent t) in
1207+ Set. fromList [ getTxId t | t <- peerTxs, complete (getTxId t) ]
1208+
1209+ checkTxChainIntegrity :: forall r .
1210+ ChainedPeerTxs
1211+ -> Set TxId
1212+ -> SimTrace r
1213+ -> Int
1214+ -> Property
1215+ checkTxChainIntegrity (ChainedPeerTxs chainedTxsB chainedTxsC)
1216+ expectedAtReceiver
1217+ ioSimTrace
1218+ traceNumber =
1219+ let trace = Trace. take traceNumber ioSimTrace
1220+
1221+ events = fmap (\ (WithTime t (WithName name b)) ->
1222+ WithName name (WithTime t b))
1223+ . withTimeNameTraceEvents
1224+ @ DiffusionTestTrace
1225+ @ NtNAddr
1226+ $ trace
1227+
1228+ sortedAcceptedTxidsMap :: Map NtNAddr [TxId ]
1229+ sortedAcceptedTxidsMap =
1230+ foldr (\ l r ->
1231+ List. foldl' (\ rr (WithName n (WithTime _ x)) ->
1232+ case x of
1233+ DiffusionTxSubmissionInbound (TraceTxInboundAddedToMempool txids _) ->
1234+ Map. alter (maybe (Just txids) (Just . sort . (txids ++ ))) n rr
1235+ _ -> rr) r l
1236+ ) Map. empty
1237+ . Trace. toList
1238+ . splitWithNameTrace
1239+ $ events
1240+
1241+ receiverAddr = TestAddress (IPAddr (read " 0.0.0.0" ) 0 )
1242+ accepted = Map. lookup receiverAddr sortedAcceptedTxidsMap
1243+ actualSet = maybe Set. empty Set. fromList accepted
1244+ missing = expectedAtReceiver `Set.difference` actualSet in
1245+
1246+ counterexample (" accepted txids map: " ++ show sortedAcceptedTxidsMap)
1247+ $ counterexample (" downstream peer B outbound: " ++ show chainedTxsB)
1248+ $ counterexample (" downstream peer C outbound: " ++ show chainedTxsC)
1249+ $ counterexample (" expected (reliably deliverable): "
1250+ ++ show (Set. toList expectedAtReceiver))
1251+ $ counterexample (" missing from node: " ++ show (Set. toList missing))
1252+ $ label (" expected count: "
1253+ ++ renderRanges 5 (Set. size expectedAtReceiver))
1254+ $ label (" accepted count: "
1255+ ++ renderRanges 5 (Set. size actualSet))
1256+ $ counterexample " node (0.0.0.0)"
1257+ $ property (Set. null missing)
1258+
1259+ -- | Tx Chain Integrity property: the node's mempool accumulates every
1260+ -- transaction reachable via a complete, valid ancestor chain on at least
1261+ -- one of its downstream peers.
1262+ --
1263+ -- This exercises V2's cross-peer retry path: if an adversarial downstream
1264+ -- peer delivers a child out-of-order and the mempool rejects with
1265+ -- 'MissingParent', the tx must still reach the node via the well-behaved
1266+ -- downstream peer's re-advertisement.
1267+ prop_txSubmission_chainIntegrity :: ArbTxDecisionPolicy
1268+ -> ChainedPeerTxs
1269+ -> Property
1270+ prop_txSubmission_chainIntegrity argPolicy chainedTxs =
1271+ let diffScript = txChainIntegrityDiffScript argPolicy chainedTxs
1272+ expected = txChainIntegrityExpected chainedTxs in
1273+ checkTxChainIntegrity
1274+ chainedTxs
1275+ expected
1276+ (runSimTrace (diffusionSimulation noAttenuation diffScript))
1277+ long_trace
1278+
1279+ prop_txSubmission_chainIntegrity_iosimpor :: ArbTxDecisionPolicy
1280+ -> ChainedPeerTxs
1281+ -> Property
1282+ prop_txSubmission_chainIntegrity_iosimpor argPolicy chainedTxs =
1283+ let diffScript = txChainIntegrityDiffScript argPolicy chainedTxs
1284+ expected = txChainIntegrityExpected chainedTxs
1285+ sim :: forall s . IOSim s DiffSimResult
1286+ sim = do
1287+ exploreRaces
1288+ diffusionSimulation noAttenuation diffScript
1289+ in labelDiffusionScript diffScript
1290+ $ exploreSimTrace (\ a -> a { explorationScheduleBound = 10 }) sim $ \ _ trace ->
1291+ checkTxChainIntegrity chainedTxs expected trace long_trace
1292+
1293+ prop_txSubmission_chainIntegrity_iosim :: ArbTxDecisionPolicy
1294+ -> ChainedPeerTxs
1295+ -> Property
1296+ prop_txSubmission_chainIntegrity_iosim = prop_txSubmission_chainIntegrity
1297+
1298+
10831299-- | This test checks the ratio of the inflight txs against the allowed by the
10841300-- TxDecisionPolicy.
10851301--
0 commit comments