@@ -68,6 +68,7 @@ tests =
6868 , testProperty " handleSubmittedTxs retains accepted and drops rejected" prop_handleSubmittedTxs_retainsAcceptedAndDropsRejected
6969 , testProperty " nextPeerAction prioritises submitting buffered owned txs" prop_nextPeerAction_prioritisesSubmit
7070 , testProperty " nextPeerAction claims claimable tx for best idle advertiser" prop_nextPeerAction_claimsClaimableTx
71+ , testProperty " nextPeerAction claims a released tx from another advertiser" prop_nextPeerAction_claimsRejectedTxFromOtherAdvertiser
7172 , testCaseSteps " nextPeerAction claims a tx once the score delay threshold has elapsed" unit_nextPeerAction_claimsAtScoreDelayThreshold
7273 , testProperty " nextPeerAction steals expired lease for best idle advertiser" prop_nextPeerAction_claimsExpiredLease
7374 , testProperty " nextPeerAction requests an oversized first tx within the soft budget" prop_nextPeerAction_requestsOversizedFirstTx
@@ -85,7 +86,7 @@ tests =
8586 , testProperty " nextPeerAction keeps retained txs before expiry" prop_nextPeerAction_keepsRetained
8687 , testProperty " PeerDoNothing waits for the earliest shared expiry" prop_nextPeerAction_earliestWakeDelay
8788 , testProperty " PeerDoNothing uses the current peer generation" prop_nextPeerAction_returnsPeerGeneration
88- , testProperty " handleSubmittedTxs bumps idle advertiser generations" prop_handleSubmittedTxs_bumpsIdleAdvertisers
89+ , testProperty " handleSubmittedTxs bumps advertiser generations" prop_handleSubmittedTxs_bumpsAdvertisers
8990 , testCaseSteps " advertisingPeersForTxExcept scans the authoritative peer key sets" unit_advertisingPeersForTxExcept_scansAuthoritativePeerSets
9091 , testCaseSteps " removeAdvertisingPeersForResolvedTx clears all advertising peers for a resolved key" unit_removeAdvertisingPeersForResolvedTx_clearsAllAdvertisingPeers
9192 , testCaseSteps " updatePeerPhase only wakes the peer becoming idle" unit_updatePeerPhase_wakesOnlyBecomingIdlePeer
@@ -746,19 +747,16 @@ prop_handleReceivedTxIds
746747 case otherPeerOpt of
747748 Just op | not (null mempoolResolveActiveGroup) ->
748749 let original = lookupPeerOrFail op sharedStateWithRetained
749- post = lookupPeerOrFail op sharedState'
750- bumpIfIdle g
751- | sharedPeerPhase original == PeerIdle = g + 1
752- | otherwise = g
753- in conjoin
750+ post = lookupPeerOrFail op sharedState' in
751+ conjoin
754752 [ counterexample " other peer's advertised keys not restored"
755753 (sharedPeerAdvertisedTxKeys post
756754 === sharedPeerAdvertisedTxKeys original)
757755 , counterexample " other peer's phase changed unexpectedly"
758756 (sharedPeerPhase post === sharedPeerPhase original)
759757 , counterexample " other peer's generation bump mismatch"
760758 (sharedPeerGeneration post
761- === bumpIfIdle ( sharedPeerGeneration original) )
759+ === sharedPeerGeneration original + 1 )
762760 ]
763761 _ -> property True
764762
@@ -1214,25 +1212,22 @@ prop_handleReceivedTxs
12141212 ]
12151213
12161214 -- otherPeer is added to wakePeers when any surviving omitted entry
1217- -- exists; its generation is bumped iff it was PeerIdle . LateMempool
1218- -- is set up single-advertiser so it does not wake otherPeer here.
1215+ -- exists; its generation is bumped unconditionally . LateMempool is
1216+ -- set up single-advertiser so it does not wake otherPeer here.
12191217 checkOtherPeerState =
12201218 case otherPeerOpt of
12211219 Just op | not (IntSet. null omittedSurvivingKeys) ->
12221220 let original = lookupPeerOrFail op sharedStateBase
1223- post = lookupPeerOrFail op sharedState'
1224- bumpIfIdle g
1225- | sharedPeerPhase original == PeerIdle = g + 1
1226- | otherwise = g
1227- in conjoin
1221+ post = lookupPeerOrFail op sharedState' in
1222+ conjoin
12281223 [ counterexample " other peer's advertised keys changed"
12291224 (sharedPeerAdvertisedTxKeys post
12301225 === sharedPeerAdvertisedTxKeys original)
12311226 , counterexample " other peer's phase changed"
12321227 (sharedPeerPhase post === sharedPeerPhase original)
12331228 , counterexample " other peer's generation bump mismatch"
12341229 (sharedPeerGeneration post
1235- === bumpIfIdle ( sharedPeerGeneration original) )
1230+ === sharedPeerGeneration original + 1 )
12361231 ]
12371232 _ -> property True
12381233
@@ -1520,6 +1515,80 @@ unit_nextPeerAction_claimsAtScoreDelayThreshold step = do
15201515 (peerAction, peerState', sharedState') =
15211516 nextPeerAction now defaultTxDecisionPolicy peeraddr peerState0 sharedState0
15221517
1518+ -- | A peer whose submission attempt has been cleared (e.g. after a
1519+ -- mempool rejection) must not prevent another advertiser from claiming
1520+ -- the same tx. After one peer's attempt is cleared and its lease
1521+ -- released back to 'TxClaimable', any other peer that still advertises
1522+ -- the tx should be able to claim it on its next 'nextPeerAction' pass.
1523+ --
1524+ -- Exercises the cross-peer retry invariant in the 'txSelectable' /
1525+ -- 'nextPeerAction' path: once no peer has an outstanding attempt on a
1526+ -- tx and its lease is claimable, a still-advertising peer is eligible
1527+ -- to re-claim.
1528+ prop_nextPeerAction_claimsRejectedTxFromOtherAdvertiser :: Property
1529+ prop_nextPeerAction_claimsRejectedTxFromOtherAdvertiser =
1530+ counterexample " peerB should be able to claim the released tx"
1531+ $ case action of
1532+ PeerRequestTxs keys ->
1533+ counterexample (" keys requested: " ++ show keys)
1534+ $ TxKey txKeyInt `elem` keys
1535+ _ ->
1536+ counterexample (" peerB action: " ++ show action) False
1537+ where
1538+ peerA , peerB :: PeerAddr
1539+ peerA = 1
1540+ peerB = 2
1541+
1542+ txid :: TxId
1543+ txid = 4
1544+
1545+ txKeyInt :: Int
1546+ txKeyInt = 0
1547+
1548+ txSize :: SizeInBytes
1549+ txSize = 100
1550+
1551+ txBody :: Tx TxId
1552+ txBody = mkTx txid txSize
1553+
1554+ sharedState0 :: SharedTxState PeerAddr TxId
1555+ sharedState0 =
1556+ ensurePeerAdvertisesTxKeys peerA [TxKey txKeyInt]
1557+ $ ensurePeerAdvertisesTxKeys peerB [TxKey txKeyInt]
1558+ $ emptySharedTxState
1559+ { sharedTxIdToKey = Map. singleton (getRawTxId txid) (TxKey txKeyInt)
1560+ , sharedKeyToTxId = IntMap. singleton txKeyInt txid
1561+ , sharedNextTxKey = txKeyInt + 1
1562+ , sharedTxTable = IntMap. singleton txKeyInt TxEntry
1563+ { txLease = TxLeased peerA (addTime 1 now)
1564+ , txAdvertiserCount = 2
1565+ , txAttempts = Map. singleton peerA TxBuffered
1566+ }
1567+ }
1568+
1569+ peerAState :: PeerTxLocalState (Tx TxId )
1570+ peerAState = emptyPeerTxLocalState
1571+ { peerUnacknowledgedTxIds = StrictSeq. singleton (TxKey txKeyInt)
1572+ , peerDownloadedTxs = IntMap. singleton txKeyInt txBody
1573+ }
1574+
1575+ peerBState :: PeerTxLocalState (Tx TxId )
1576+ peerBState = emptyPeerTxLocalState
1577+ { peerUnacknowledgedTxIds = StrictSeq. singleton (TxKey txKeyInt)
1578+ , peerAvailableTxIds = IntMap. singleton txKeyInt txSize
1579+ }
1580+
1581+ (_peerAStateAfter, sharedStateAfterRejection) =
1582+ handleSubmittedTxs now defaultTxDecisionPolicy peerA
1583+ []
1584+ [TxKey txKeyInt]
1585+ peerAState
1586+ sharedState0
1587+
1588+ (action, _peerBStateAfter, _sharedStateFinal) =
1589+ nextPeerAction now defaultTxDecisionPolicy peerB peerBState
1590+ sharedStateAfterRejection
1591+
15231592-- Verifies that nextPeerAction can steal an expired lease for the best idle
15241593-- advertiser and request that tx.
15251594prop_nextPeerAction_claimsExpiredLease
@@ -2324,20 +2393,21 @@ prop_nextPeerAction_returnsPeerGeneration (Positive peeraddr) =
23242393 peerState0 = emptyPeerTxLocalState { peerRequestedTxIds = maxNumTxIdsToRequest defaultTxDecisionPolicy }
23252394 (peerAction, _, _) = nextPeerAction now defaultTxDecisionPolicy peeraddr peerState0 sharedState0
23262395
2327- -- Verifies that handleSubmittedTxs bumps idle advertisers while leaving
2328- -- submitting and waiting advertisers unchanged.
2329- prop_handleSubmittedTxs_bumpsIdleAdvertisers
2396+ -- Verifies that handleSubmittedTxs bumps the generation of every other
2397+ -- advertiser of the resolved tx, regardless of phase, while leaving the
2398+ -- submitting peer's own generation unchanged.
2399+ prop_handleSubmittedTxs_bumpsAdvertisers
23302400 :: Positive Int
23312401 -> Positive Int
23322402 -> Positive Int
23332403 -> TxId
23342404 -> Positive Int
23352405 -> Property
2336- prop_handleSubmittedTxs_bumpsIdleAdvertisers (Positive owner0) (Positive peerA0) (Positive peerB0) txid0 txSize0 =
2406+ prop_handleSubmittedTxs_bumpsAdvertisers (Positive owner0) (Positive peerA0) (Positive peerB0) txid0 txSize0 =
23372407 owner /= peerA && owner /= peerB && peerA /= peerB ==>
23382408 conjoin
23392409 [ sharedPeerGeneration (lookupPeerOrFail peerA sharedState') === 1
2340- , sharedPeerGeneration (lookupPeerOrFail peerB sharedState') === 0
2410+ , sharedPeerGeneration (lookupPeerOrFail peerB sharedState') === 1
23412411 , sharedPeerGeneration (lookupPeerOrFail owner sharedState') === 0
23422412 ]
23432413 where
0 commit comments