Skip to content
This repository was archived by the owner on Dec 2, 2024. It is now read-only.

Commit cbbd984

Browse files
authored
Plt 745 fix tx processing in emulator with regards to slot ranges (#679)
* Emulator : Don't filter txs that must be send to validation according to their validity range * Fix the vesting contract * Fix the crowdfunding contract tests * Fix plutus playground for the new txPool management * Document the slot vs POSIX time conversion
1 parent 2162ebe commit cbbd984

File tree

7 files changed

+86
-100
lines changed

7 files changed

+86
-100
lines changed

plutus-contract/src/Wallet/Emulator/Chain.hs

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ import Control.Applicative ((<|>))
2121
import Control.Lens hiding (index)
2222
import Control.Monad.Freer
2323
import Control.Monad.Freer.Extras.Log (LogMsg, logDebug, logInfo, logWarn)
24-
import Control.Monad.Freer.State
24+
import Control.Monad.Freer.State (State, gets, modify)
2525
import Control.Monad.State qualified as S
2626
import Data.Aeson (FromJSON, ToJSON)
2727
import Data.Either (fromRight)
2828
import Data.Foldable (traverse_)
29-
import Data.List (partition, (\\))
29+
import Data.List ((\\))
3030
import Data.Maybe (mapMaybe)
3131
import Data.Monoid (Ap (Ap))
3232
import Data.Traversable (for)
@@ -103,21 +103,20 @@ type ChainEffs = '[State ChainState, LogMsg ChainEvent]
103103
handleControlChain :: Members ChainEffs effs => Params -> ChainControlEffect ~> Eff effs
104104
handleControlChain params = \case
105105
ProcessBlock -> do
106-
st <- get
107-
let pool = st ^. txPool
108-
slot = st ^. currentSlot
109-
idx = st ^. index
110-
ValidatedBlock block events rest idx' =
111-
validateBlock params slot idx pool
112-
113-
let st' = st & txPool .~ rest
114-
& index .~ idx'
115-
& addBlock block
116-
117-
put st'
118-
traverse_ logEvent events
119106

107+
pool <- gets $ view txPool
108+
slot <- gets $ view currentSlot
109+
idx <- gets $ view index
110+
111+
let ValidatedBlock block events idx' = validateBlock params slot idx pool
112+
113+
modify $ txPool .~ []
114+
modify $ index .~ idx'
115+
modify $ addBlock block
116+
117+
traverse_ logEvent events
120118
pure block
119+
121120
ModifySlot f -> modify @ChainState (over currentSlot f) >> gets (view currentSlot)
122121

123122
logEvent :: Member (LogMsg ChainEvent) effs => ChainEvent -> Eff effs ()
@@ -138,26 +137,18 @@ data ValidatedBlock = ValidatedBlock
138137
-- ^ The transactions that have been validated in this block.
139138
, vlbEvents :: [ChainEvent]
140139
-- ^ Transaction validation events for the transactions in this block.
141-
, vlbRest :: TxPool
142-
-- ^ The transactions that haven't been validated because the current slot is
143-
-- not in their validation interval.
144140
, vlbIndex :: Index.UtxoIndex
145141
-- ^ The updated UTxO index after processing the block
146142
}
147143

148144
-- | Validate a block given the current slot and UTxO index, returning the valid
149-
-- transactions, success/failure events, remaining transactions and the
150-
-- updated UTxO set.
145+
-- transactions, success/failure events and the updated UTxO set.
151146
validateBlock :: Params -> Slot -> Index.UtxoIndex -> TxPool -> ValidatedBlock
152147
validateBlock params slot@(Slot s) idx txns =
153148
let
154-
-- Select those transactions that can be validated in the
155-
-- current slot
156-
(eligibleTxns, rest) = partition (canValidateNow slot) txns
157-
158-
-- Validate eligible transactions, updating the UTXO index each time
149+
-- Validate transactions, updating the UTXO index each time
159150
(processed, Index.ValidationCtx idx' _) =
160-
flip S.runState (Index.ValidationCtx idx params) $ for eligibleTxns $ \tx -> do
151+
flip S.runState (Index.ValidationCtx idx params) $ for txns $ \tx -> do
161152
(err, events_) <- validateEm slot cUtxoIndex tx
162153
pure (tx, err, events_)
163154

@@ -176,7 +167,7 @@ validateBlock params slot@(Slot s) idx txns =
176167

177168
cUtxoIndex = either (error . show) id $ Validation.fromPlutusIndex params idx
178169

179-
in ValidatedBlock block events rest idx'
170+
in ValidatedBlock block events idx'
180171

181172
getCollateral :: Index.UtxoIndex -> CardanoTx -> Value
182173
getCollateral idx tx = fromRight (getCardanoTxFee tx) $

plutus-contract/test/Spec/Emulator.hs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ tests = testGroup "all tests" [
5757
testGroup "UTXO model" [
5858
testProperty "compute UTxO of trivial blockchain" utxo,
5959
testProperty "validate transaction" txnValid,
60-
testProperty "validate transaction when it can be validated" txnValidFrom,
6160
testProperty "update UTXO set after each transaction" txnUpdateUtxo
6261
],
6362
testGroup "traces" [
@@ -127,22 +126,6 @@ txnValid = property $ do
127126
(m, txn) <- forAll genChainTxn
128127
Gen.assertValid txn m
129128

130-
txnValidFrom :: Property
131-
txnValidFrom =
132-
let five = Ada.adaValueOf 5
133-
-- Set the validation interval to (5, 5] for the
134-
-- transaction generated by payToPaymentPublicKeyHash_
135-
-- so that the transaction can be validated only during slot 5
136-
range = W.singleton 5
137-
138-
in checkPredicateGen Gen.generatorModel
139-
(walletFundsChange wallet1 (P.negate five)
140-
.&&. walletFundsChange wallet2 five
141-
)
142-
$ do
143-
Trace.liftWallet wallet1 $ payToPaymentPublicKeyHash_ def range five pubKey2
144-
void $ Trace.waitUntilSlot 6
145-
146129
selectCoinProp :: Property
147130
selectCoinProp = property $ do
148131
inputs <- forAll $ zip [(1 :: Integer) ..] <$> Gen.list (Range.linear 1 100) Gen.genValueNonNegative

plutus-contract/test/Spec/TxConstraints/TimeValidity.hs

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE LambdaCase #-}
23
{-# LANGUAGE OverloadedStrings #-}
34
{-# LANGUAGE ScopedTypeVariables #-}
45
{-# LANGUAGE TemplateHaskell #-}
@@ -13,6 +14,8 @@ import Data.Map qualified as Map
1314
import Data.Void (Void)
1415
import Test.Tasty (TestTree, testGroup)
1516

17+
import Data.List (isSubsequenceOf)
18+
import Ledger (POSIXTimeRange)
1619
import Ledger qualified
1720
import Ledger.Ada qualified as Ada
1821
import Ledger.Constraints qualified as Constraints
@@ -48,6 +51,7 @@ tests = testGroup "time validitity constraint"
4851
-- , protocolV5Cardano
4952
, defaultProtocolParamsValidCardano
5053
, defaultProtocolParamsPastTxCardano
54+
, defaultProtocolParamsFutureTxCardano
5155
]
5256
]
5357

@@ -68,6 +72,7 @@ contract = do
6872
foldMap (\oref -> Constraints.mustSpendScriptOutput oref unitRedeemer) orefs
6973
<> Constraints.mustIncludeDatum unitDatum
7074
<> Constraints.mustValidateIn (from $ now + 1000)
75+
void $ waitNSlots 2
7176
ledgerTx2 <- submitTxConstraintsWith @Void lookups2 tx2
7277
awaitTxConfirmed $ Tx.getCardanoTxId ledgerTx2
7378
cSlot <- Con.currentPABSlot
@@ -104,8 +109,8 @@ traceCardano c = do
104109
void $ Trace.activateContractWallet w1 c
105110
void $ Trace.waitNSlots 4
106111

107-
validContractCardano :: Ledger.Params -> Contract () Empty ContractError ()
108-
validContractCardano p = do
112+
contractCardano :: (POSIXTime -> POSIXTimeRange) -> Ledger.Params -> Contract () Empty ContractError ()
113+
contractCardano f p = do
109114
let mkTx lookups constraints = either (error . show) id $ Tx.Constraints.mkTx @UnitTest p lookups constraints
110115
pkh <- Con.ownFirstPaymentPubKeyHash
111116
utxos <- Con.ownUtxos
@@ -115,7 +120,8 @@ validContractCardano p = do
115120
lookups = Tx.Constraints.unspentOutputs utxos
116121
tx = Tx.Constraints.mustPayToPubKey pkh (Ada.toValue Ledger.minAdaTxOut)
117122
<> Tx.Constraints.mustSpendPubKeyOutput utxoRef
118-
<> Tx.Constraints.mustValidateIn (from $ 1000 + now)
123+
<> Tx.Constraints.mustValidateIn (f now)
124+
void $ waitNSlots 2
119125
ledgerTx <- submitUnbalancedTx $ mkTx lookups tx
120126
awaitTxConfirmed $ Tx.getCardanoTxId ledgerTx
121127

@@ -126,34 +132,14 @@ validContractCardano p = do
126132

127133
P.unless (cSlot `I.member` txRange) $ P.traceError "InvalidRange"
128134

135+
validContractCardano :: Ledger.Params -> Contract () Empty ContractError ()
136+
validContractCardano = contractCardano $ from . (+ 1000)
129137

130138
pastTxContractCardano :: Ledger.Params -> Contract () Empty ContractError ()
131-
pastTxContractCardano p = do
132-
let mkTx lookups constraints = either (error . show) id $ Tx.Constraints.mkTx @UnitTest p lookups constraints
133-
pkh <- Con.ownFirstPaymentPubKeyHash
134-
utxos <- Con.ownUtxos
135-
now <- Con.currentTime
136-
logInfo @String $ "now: " ++ show now
137-
let (utxoRef1, utxoRef2) = get2 $ map fst $ Map.toList utxos
138-
lookups = Tx.Constraints.unspentOutputs utxos
139-
tx1 = Tx.Constraints.mustPayToPubKey pkh (Ada.toValue Ledger.minAdaTxOut)
140-
<> Tx.Constraints.mustSpendPubKeyOutput utxoRef1
141-
tx2 = Tx.Constraints.mustPayToPubKey pkh (Ada.toValue Ledger.minAdaTxOut)
142-
<> Tx.Constraints.mustSpendPubKeyOutput utxoRef2
143-
<> Tx.Constraints.mustValidateIn (I.to now)
144-
-- submit a first transaction to occupy the first slot
145-
ledgerTx1 <- submitUnbalancedTx $ mkTx lookups tx1
146-
awaitTxConfirmed $ Tx.getCardanoTxId ledgerTx1
147-
-- submit a tx that should be validated at the latest in slot 1
148-
ledgerTx2 <- submitUnbalancedTx $ mkTx lookups tx2
149-
awaitTxConfirmed $ Tx.getCardanoTxId ledgerTx2
139+
pastTxContractCardano = contractCardano I.to
150140

151-
cSlot <- Con.currentPABSlot
152-
logInfo @String $ "Current slot: " ++ show cSlot
153-
let txRange = Tx.getCardanoTxValidityRange ledgerTx1
154-
logInfo @String $ show txRange
155-
156-
P.unless (cSlot `I.member` txRange) $ P.traceError "InvalidRange"
141+
futureTxContractCardano :: Ledger.Params -> Contract () Empty ContractError ()
142+
futureTxContractCardano = contractCardano $ from . (+ 4000)
157143

158144
protocolV6Cardano :: TestTree
159145
protocolV6Cardano =
@@ -171,19 +157,29 @@ defaultProtocolParamsValidCardano = checkPredicateOptions
171157
(assertValidatedTransactionCount 1)
172158
(void $ traceCardano $ validContractCardano $ view (emulatorConfig . params) defaultCheckOptions)
173159

174-
-- We only test here if the contract rejects transactions with a time range uppper bound set before the
175-
-- current slot.
176-
-- We submit a first successful transaction to ensure that the validityRange of the second one is in the
177-
-- past.
178-
-- As the range of the second transaction is unreachable, the transaction should fail.
179-
-- (it's unfortunately not the case at the moment, the tx is just postponed indefinitely).
160+
161+
outsideValidityIntervalError :: Ledger.ValidationError -> Bool
162+
outsideValidityIntervalError = \case
163+
Ledger.CardanoLedgerValidationError msg ->
164+
"OutsideValidityIntervalUTxO" `isSubsequenceOf` msg
165+
_ -> False
166+
167+
-- | Past range are rejected
180168
defaultProtocolParamsPastTxCardano :: TestTree
181169
defaultProtocolParamsPastTxCardano = checkPredicateOptions
182170
defaultCheckOptions
183171
"tx valid time interval in the past make transactions fail"
184-
(assertValidatedTransactionCount 1)
172+
(assertFailedTransaction $ \_ err _ -> outsideValidityIntervalError err)
185173
(void $ traceCardano $ pastTxContractCardano $ view (emulatorConfig . params) defaultCheckOptions)
186174

175+
-- | Future range are rejected
176+
defaultProtocolParamsFutureTxCardano :: TestTree
177+
defaultProtocolParamsFutureTxCardano = checkPredicateOptions
178+
defaultCheckOptions
179+
"tx valid time interval in the past make transactions fail"
180+
(assertFailedTransaction $ \_ err _ -> outsideValidityIntervalError err)
181+
(void $ traceCardano $ futureTxContractCardano $ view (emulatorConfig . params) defaultCheckOptions)
182+
187183
deadline :: POSIXTime
188184
deadline = 1596059092000 -- (milliseconds) transaction's valid range must be after this
189185

plutus-pab/src/Cardano/Chain.hs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import Control.Concurrent.STM
1717
import Control.Lens hiding (index)
1818
import Control.Monad.Freer
1919
import Control.Monad.Freer.Extras.Log (LogMsg, logDebug, logInfo, logWarn)
20-
import Control.Monad.Freer.State (State, get, gets, modify, put)
20+
import Control.Monad.Freer.State (State, gets, modify)
2121
import Control.Monad.IO.Class (MonadIO, liftIO)
2222
import Data.Foldable (traverse_)
2323
import Data.Functor (void)
@@ -85,21 +85,20 @@ handleControlChain ::
8585
=> Params -> EC.ChainControlEffect ~> Eff effs
8686
handleControlChain params = \case
8787
EC.ProcessBlock -> do
88-
st <- get
89-
let pool = st ^. txPool
90-
slot = st ^. currentSlot
91-
idx = st ^. index
92-
EC.ValidatedBlock block events rest idx' =
93-
EC.validateBlock params slot idx pool
94-
95-
let st' = st & txPool .~ rest
96-
& tip ?~ block
97-
& index .~ idx'
98-
99-
put st'
88+
pool <- gets $ view txPool
89+
slot <- gets $ view currentSlot
90+
idx <- gets $ view index
91+
chan <- gets $ view channel
92+
93+
let EC.ValidatedBlock block events idx' = EC.validateBlock params slot idx pool
94+
95+
modify $ txPool .~ []
96+
modify $ tip ?~ block
97+
modify $ index .~ idx'
98+
10099
traverse_ logEvent events
101100

102-
liftIO $ atomically $ writeTChan (st ^. channel) block
101+
liftIO $ atomically $ writeTChan chan block
103102
pure block
104103
EC.ModifySlot f -> modify @MockNodeServerChainState (over currentSlot f) >> gets (view currentSlot)
105104

plutus-playground-server/usecases/Vesting.hs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,12 +176,12 @@ retrieveFundsC
176176
retrieveFundsC vesting payment = do
177177
let inst = typedValidator vesting
178178
addr = Scripts.validatorAddress inst
179-
nextTime <- awaitTime 0
179+
now <- currentTime
180180
unspentOutputs <- utxosAt addr
181181
let
182182
currentlyLocked = foldMap (view Tx.ciTxOutValue) (Map.elems unspentOutputs)
183183
remainingValue = currentlyLocked - payment
184-
mustRemainLocked = totalAmount vesting - availableAt vesting nextTime
184+
mustRemainLocked = totalAmount vesting - availableAt vesting now
185185
maxPayment = currentlyLocked - mustRemainLocked
186186

187187
when (remainingValue `Value.lt` mustRemainLocked)
@@ -202,11 +202,12 @@ retrieveFundsC vesting payment = do
202202
Dead -> mempty
203203
txn = Constraints.collectFromTheScript unspentOutputs ()
204204
<> remainingOutputs
205-
<> mustValidateIn (Interval.from nextTime)
205+
<> mustValidateIn (Interval.from now)
206206
<> mustBeSignedBy (vestingOwner vesting)
207207
-- we don't need to add a pubkey output for 'vestingOwner' here
208208
-- because this will be done by the wallet when it balances the
209209
-- transaction.
210+
void $ waitNSlots 1 -- wait until we reach a slot in the validity range
210211
mkTxConstraints (Constraints.plutusV1TypedValidatorLookups inst
211212
<> Constraints.unspentOutputs unspentOutputs) txn
212213
>>= adjustUnbalancedTx >>= void . submitUnbalancedTx

plutus-use-cases/src/Plutus/Contracts/Vesting.hs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,15 @@ vestFundsC vesting = mapError (review _VestingError) $ do
192192

193193
data Liveness = Alive | Dead
194194

195+
{- Note [slots and POSIX time]
196+
- A slot has a given duration. As a consequence, 'currentTime' does not return exactly the current time but,
197+
- by convention, the last POSIX time of the current slot.
198+
- A consequence to this design choice is that when we use this time to build the 'mustValidateIn constraints',
199+
- we get a range that start at the slot after the current one.
200+
- To be sure that the validity range is valid when the transaction will be validated by the pool, we must therefore
201+
- wait the next slot before sumitting it (which is done using 'waitNSlots 1').
202+
-
203+
-}
195204
retrieveFundsC
196205
:: ( AsVestingError e
197206
)
@@ -201,12 +210,12 @@ retrieveFundsC
201210
retrieveFundsC vesting payment = mapError (review _VestingError) $ do
202211
let inst = typedValidator vesting
203212
addr = Scripts.validatorAddress inst
204-
nextTime <- awaitTime 0
213+
now <- currentTime
205214
unspentOutputs <- utxosAt addr
206215
let
207216
currentlyLocked = foldMap (view Tx.ciTxOutValue) (Map.elems unspentOutputs)
208217
remainingValue = currentlyLocked - payment
209-
mustRemainLocked = totalAmount vesting - availableAt vesting nextTime
218+
mustRemainLocked = totalAmount vesting - availableAt vesting now
210219
maxPayment = currentlyLocked - mustRemainLocked
211220

212221
when (remainingValue `Value.lt` mustRemainLocked)
@@ -219,11 +228,12 @@ retrieveFundsC vesting payment = mapError (review _VestingError) $ do
219228
Dead -> mempty
220229
tx = Constraints.collectFromTheScript unspentOutputs ()
221230
<> remainingOutputs
222-
<> mustValidateIn (Interval.from nextTime)
231+
<> mustValidateIn (Interval.from now)
223232
<> mustBeSignedBy (vestingOwner vesting)
224233
-- we don't need to add a pubkey output for 'vestingOwner' here
225234
-- because this will be done by the wallet when it balances the
226235
-- transaction.
236+
void $ waitNSlots 1 -- see [slots and POSIX time]
227237
mkTxConstraints (Constraints.plutusV1TypedValidatorLookups inst
228238
<> Constraints.unspentOutputs unspentOutputs) tx
229239
>>= adjustUnbalancedTx >>= void . submitUnbalancedTx

plutus-use-cases/test/Spec/Crowdfunding.hs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import Test.Tasty.Golden (goldenVsString)
3535
import Test.Tasty.HUnit qualified as HUnit
3636
import Test.Tasty.QuickCheck hiding ((.&&.))
3737

38+
import Data.List (isSubsequenceOf)
3839
import Ledger (Value)
3940
import Ledger qualified
4041
import Ledger.Ada qualified as Ada
@@ -89,7 +90,12 @@ tests = testGroup "crowdfunding"
8990

9091
, checkPredicate "cannot collect money too late"
9192
(walletFundsChange w1 PlutusTx.zero
92-
.&&. assertNoFailedTransactions)
93+
.&&. assertFailedTransaction (\_ err _ ->
94+
case err of
95+
Ledger.CardanoLedgerValidationError msg ->
96+
"OutsideValidityIntervalUTxO" `isSubsequenceOf` msg
97+
_ -> False
98+
))
9399
$ do
94100
ContractHandle{chInstanceId} <- startCampaign
95101
makeContribution w2 (Ada.adaValueOf 10)

0 commit comments

Comments
 (0)