Skip to content

Commit 3cca6c9

Browse files
authored
Fail fast when adding a redundant transaction to the mempool (#4967)
* Fail fast when adding a redundant transaction to the mempool Closes #1294 Closes #4849
1 parent e7e3a3a commit 3cca6c9

File tree

7 files changed

+108
-37
lines changed

7 files changed

+108
-37
lines changed

eras/conway/impl/cardano-ledger-conway.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ library
113113
nothunks,
114114
plutus-ledger-api >=1.37,
115115
set-algebra,
116-
small-steps >=1.1,
116+
small-steps >=1.1.2,
117117
text,
118118
transformers,
119119
validation-selective,

eras/conway/impl/src/Cardano/Ledger/Conway/Rules/Mempool.hs

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ import Control.State.Transition (
4747
failOnNonEmpty,
4848
judgmentContext,
4949
transitionRules,
50+
whenFailureFreeDefault,
51+
(?!),
5052
)
5153
import Control.State.Transition.Extended (Embed (..), trans)
5254
import qualified Data.List.NonEmpty as NE
@@ -100,24 +102,40 @@ mempoolTransition ::
100102
mempoolTransition = do
101103
TRC trc@(_ledgerEnv, ledgerState, tx) <-
102104
judgmentContext
105+
103106
-- This rule only gets invoked on transactions within the mempool.
104107
-- Add checks here that sanitize undesired transactions.
108+
109+
-- Detect whether the transaction is probably a duplicate
105110
let
106-
authorizedElectedHotCreds = authorizedElectedHotCommitteeCredentials ledgerState
107-
collectUnelectedCommitteeVotes !unelectedHotCreds voter _ =
108-
case voter of
109-
CommitteeVoter hotCred
110-
| hotCred `Set.notMember` authorizedElectedHotCreds ->
111-
Set.insert hotCred unelectedHotCreds
112-
_ -> unelectedHotCreds
113-
unelectedCommitteeVoters =
114-
Map.foldlWithKey' collectUnelectedCommitteeVotes Set.empty $
115-
unVotingProcedures (tx ^. bodyTxL . votingProceduresTxBodyL)
116-
addPrefix =
117-
("Unelected committee members are not allowed to cast votes: " <>)
118-
failOnNonEmpty unelectedCommitteeVoters $
119-
ConwayMempoolFailure . addPrefix . T.pack . show . NE.toList
120-
trans @(EraRule "LEDGER" era) $ TRC trc
111+
inputs = tx ^. bodyTxL . inputsTxBodyL
112+
UTxO utxo = ledgerState ^. utxoG
113+
notAllSpent = any (`Map.member` utxo) inputs
114+
notAllSpent
115+
?! ConwayMempoolFailure
116+
"All inputs are spent. Transaction has probably already been included"
117+
118+
-- Skip all other checks if the transaction is probably a duplicate
119+
whenFailureFreeDefault ledgerState $ do
120+
-- Disallow votes by unelected committee members
121+
let
122+
authorizedElectedHotCreds = authorizedElectedHotCommitteeCredentials ledgerState
123+
collectUnelectedCommitteeVotes !unelectedHotCreds voter _ =
124+
case voter of
125+
CommitteeVoter hotCred
126+
| hotCred `Set.notMember` authorizedElectedHotCreds ->
127+
Set.insert hotCred unelectedHotCreds
128+
_ -> unelectedHotCreds
129+
unelectedCommitteeVoters =
130+
Map.foldlWithKey' collectUnelectedCommitteeVotes Set.empty $
131+
unVotingProcedures (tx ^. bodyTxL . votingProceduresTxBodyL)
132+
addPrefix =
133+
("Unelected committee members are not allowed to cast votes: " <>)
134+
failOnNonEmpty unelectedCommitteeVoters $
135+
ConwayMempoolFailure . addPrefix . T.pack . show . NE.toList
136+
137+
-- Continue with LEDGER rules
138+
trans @(EraRule "LEDGER" era) $ TRC trc
121139

122140
instance
123141
( AlonzoEraTx era

eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import Cardano.Ledger.Conway.Rules (
2828
ConwayHardForkEvent,
2929
ConwayLedgerPredFailure,
3030
ConwayNewEpochEvent,
31+
ConwayUtxoPredFailure,
3132
)
3233
import Cardano.Ledger.Conway.TxInfo (ConwayContextError)
3334
import Cardano.Ledger.Shelley.API.Mempool (ApplyTx (..))
@@ -78,6 +79,7 @@ spec ::
7879
, InjectRuleFailure "LEDGER" ConwayDelegPredFailure era
7980
, InjectRuleFailure "LEDGER" ConwayGovCertPredFailure era
8081
, InjectRuleFailure "LEDGER" ConwayLedgerPredFailure era
82+
, InjectRuleFailure "LEDGER" ConwayUtxoPredFailure era
8183
, InjectRuleFailure "BBODY" ConwayBbodyPredFailure era
8284
, InjectRuleEvent "TICK" ConwayEpochEvent era
8385
, Event (EraRule "EPOCH" era) ~ ConwayEpochEvent era
@@ -113,6 +115,7 @@ conwaySpec ::
113115
, InjectRuleFailure "LEDGER" ConwayDelegPredFailure era
114116
, InjectRuleFailure "LEDGER" ConwayGovCertPredFailure era
115117
, InjectRuleFailure "LEDGER" ConwayLedgerPredFailure era
118+
, InjectRuleFailure "LEDGER" ConwayUtxoPredFailure era
116119
, InjectRuleFailure "BBODY" ConwayBbodyPredFailure era
117120
, InjectRuleEvent "TICK" ConwayEpochEvent era
118121
, Event (EraRule "EPOCH" era) ~ ConwayEpochEvent era

eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/LedgerSpec.hs

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
{-# LANGUAGE ScopedTypeVariables #-}
77
{-# LANGUAGE TypeApplications #-}
88
{-# LANGUAGE TypeFamilies #-}
9-
{-# LANGUAGE TypeOperators #-}
109

1110
module Test.Cardano.Ledger.Conway.Imp.LedgerSpec (spec) where
1211

@@ -16,6 +15,7 @@ import Cardano.Ledger.Conway.Core
1615
import Cardano.Ledger.Conway.Governance
1716
import Cardano.Ledger.Conway.Rules (
1817
ConwayLedgerPredFailure (..),
18+
ConwayUtxoPredFailure (BadInputsUTxO),
1919
maxRefScriptSizePerTx,
2020
)
2121
import Cardano.Ledger.Credential (Credential (..))
@@ -24,10 +24,12 @@ import Cardano.Ledger.Plutus (SLanguage (..), hashPlutusScript)
2424
import Cardano.Ledger.Shelley.API.Mempool (ApplyTx (..), ApplyTxError (..), applyTx, mkMempoolEnv)
2525
import qualified Cardano.Ledger.Shelley.HardForks as HF (bootstrapPhase)
2626
import Cardano.Ledger.Shelley.LedgerState
27+
import Control.Monad.Reader (asks)
2728
import qualified Data.Map.Strict as Map
2829
import qualified Data.Set as Set
2930
import qualified Data.Text as T
30-
import Lens.Micro ((&), (.~), (^.))
31+
import GHC.Exts (fromList)
32+
import Lens.Micro ((&), (.~), (<>~), (^.))
3133
import Lens.Micro.Mtl (use)
3234
import Test.Cardano.Ledger.Conway.ImpTest
3335
import Test.Cardano.Ledger.Core.Rational (IsRatio (..))
@@ -41,6 +43,7 @@ spec ::
4143
forall era.
4244
( ConwayEraImp era
4345
, InjectRuleFailure "LEDGER" ConwayLedgerPredFailure era
46+
, InjectRuleFailure "LEDGER" ConwayUtxoPredFailure era
4447
, ApplyTx era
4548
) =>
4649
SpecWith (ImpInit (LedgerSpec era))
@@ -212,9 +215,54 @@ spec = do
212215
mkBasicTxBody & withdrawalsTxBodyL .~ Withdrawals [(ra, mempty)]
213216

214217
describe "Mempool" $ do
218+
let
219+
submitFailingMempoolTx cause tx expectedFailures = do
220+
globals <- use impGlobalsL
221+
nes <- use impNESL
222+
slotNo <- use impLastTickG
223+
let
224+
mempoolEnv = mkMempoolEnv nes slotNo
225+
ls = nes ^. nesEsL . esLStateL
226+
txFixed <- (tx &) =<< asks iteFixup
227+
logToExpr txFixed
228+
case applyTx globals mempoolEnv ls txFixed of
229+
Left err -> do
230+
err `shouldBe` ApplyTxError @era expectedFailures
231+
Right _ ->
232+
assertFailure $ "Expected failure due to " <> cause <> ": " <> show txFixed
233+
pure txFixed
234+
submitFailingMempoolTx_ c t = void . submitFailingMempoolTx c t
235+
236+
it "Duplicate transactions" $ do
237+
let
238+
newInput = do
239+
addr <- freshKeyAddr_
240+
amount <- Coin <$> choose (2_000_000, 8_000_000)
241+
sendCoinTo addr amount
242+
243+
inputsCommon <- replicateM 5 newInput
244+
inputs1 <- replicateM 2 newInput
245+
inputs2 <- replicateM 3 newInput
246+
247+
txFinal <-
248+
submitTx $
249+
mkBasicTx $
250+
mkBasicTxBody & inputsTxBodyL <>~ fromList (inputsCommon <> inputs1)
251+
252+
impAnn "Identical transaction" $ do
253+
withNoFixup $
254+
submitFailingMempoolTx_ "duplicate transaction" txFinal $
255+
pure . injectFailure . ConwayMempoolFailure $
256+
"All inputs are spent. Transaction has probably already been included"
257+
258+
impAnn "Overlapping transaction" $ do
259+
let txOverlap = mkBasicTx $ mkBasicTxBody & inputsTxBodyL <>~ fromList (inputsCommon <> inputs2)
260+
submitFailingMempoolTx_
261+
"overlapping transaction"
262+
txOverlap
263+
[injectFailure $ BadInputsUTxO $ fromList inputsCommon]
264+
215265
it "Unelected Committee voting" $ whenPostBootstrap $ do
216-
globals <- use impGlobalsL
217-
slotNo <- use impLastTickG
218266
_ <- registerInitialCommittee
219267
ccCold <- KeyHashObj <$> freshKeyHash
220268
curEpochNo <- getsNES nesELL
@@ -232,11 +280,8 @@ spec = do
232280
rewardAccount <- registerRewardAccount
233281
submitTreasuryWithdrawals [(rewardAccount, Coin 1)]
234282

235-
nes <- use impNESL
236-
let ls = nes ^. nesEsL . esLStateL
237-
mempoolEnv = mkMempoolEnv nes slotNo
238-
tx <-
239-
fixupTx $
283+
let
284+
tx =
240285
mkBasicTx $
241286
mkBasicTxBody
242287
& votingProceduresTxBodyL
@@ -246,11 +291,11 @@ spec = do
246291
(Map.singleton govActionId (VotingProcedure VoteYes SNothing))
247292
)
248293

249-
case applyTx globals mempoolEnv ls tx of
250-
Left err ->
251-
let expectedFailure =
252-
ConwayMempoolFailure $
253-
"Unelected committee members are not allowed to cast votes: " <> T.pack (show (pure @[] ccHot))
254-
in err `shouldBe` ApplyTxError @era (pure (injectFailure expectedFailure))
255-
Right _ -> assertFailure $ "Expected failure due to an unallowed vote: " <> show tx
256-
withNoFixup $ submitTx_ tx
294+
txFixed <-
295+
submitFailingMempoolTx "unallowed votes" tx $
296+
pure . injectFailure . ConwayMempoolFailure $
297+
"Unelected committee members are not allowed to cast votes: " <> T.pack (show (pure @[] ccHot))
298+
299+
-- The tx should pass all other rules
300+
withNoFixup $
301+
submitTx_ txFixed

libs/small-steps/CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Version history for `small-steps`
22

3-
## 1.1.1.1
3+
## 1.1.2.0
44

5-
*
5+
* Add `whenFailureFreeDefault`
66

77
## 1.1.1.0
88

libs/small-steps/small-steps.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cabal-version: 3.0
22
name: small-steps
3-
version: 1.1.1.0
3+
version: 1.1.2.0
44
license: Apache-2.0
55
maintainer: operations@iohk.io
66
author: IOHK

libs/small-steps/src/Control/State/Transition/Extended.hs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ module Control.State.Transition.Extended (
5151
labeledPredE,
5252
ifFailureFree,
5353
whenFailureFree,
54+
whenFailureFreeDefault,
5455
failBecause,
5556
failOnJust,
5657
failOnNonEmpty,
@@ -449,7 +450,11 @@ ifFailureFree :: Rule sts rtype a -> Rule sts rtype a -> Rule sts rtype a
449450
ifFailureFree x y = liftF (IfFailureFree x y)
450451

451452
whenFailureFree :: Rule sts rtype () -> Rule sts rtype ()
452-
whenFailureFree action = ifFailureFree action (pure ())
453+
whenFailureFree = whenFailureFreeDefault ()
454+
455+
whenFailureFreeDefault :: a -> Rule sts rtype a -> Rule sts rtype a
456+
whenFailureFreeDefault defValOnFailure actionOnNoFailure =
457+
ifFailureFree actionOnNoFailure (pure defValOnFailure)
453458

454459
liftSTS ::
455460
STS sts =>

0 commit comments

Comments
 (0)