Skip to content

Commit 39d0226

Browse files
authored
Merge pull request #1309 from input-output-hk/validate-txs-in-model
Validate txs in model
2 parents 8c6e4bf + b0a91e2 commit 39d0226

File tree

6 files changed

+158
-95
lines changed

6 files changed

+158
-95
lines changed

hydra-node/hydra-node.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ test-suite tests
368368
, regex-tdfa
369369
, req
370370
, silently
371+
, temporary
371372
, text
372373
, time
373374
, typed-protocols-examples >=0.1.0.0

hydra-node/src/Hydra/Ledger.hs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,13 @@ data Ledger tx = Ledger
7474
UTxOType tx ->
7575
[tx] ->
7676
Either (tx, ValidationError) (UTxOType tx)
77-
-- ^ Apply a set of transaction to a given UTXO set. Returns the new UTXO or
77+
-- ^ Apply a set of transaction to a given UTxO set. Returns the new UTxO or
7878
-- validation failures returned from the ledger.
7979
-- TODO: 'ValidationError' should also include the UTxO, which is not
8080
-- necessarily the same as the given UTxO after some transactions
8181
, initUTxO :: UTxOType tx
82-
-- ^ Generates an initial UTXO set. This is only temporary as it does not
83-
-- allow to initialize the UTXO.
82+
-- ^ Generates an initial UTxO set. This is only temporary as it does not
83+
-- allow to initialize the UTxO.
8484
--
8585
-- TODO: This seems redundant with the `Monoid (UTxOType tx)` constraints
8686
-- coming with `IsTx`. We probably want to dry this out.
@@ -90,6 +90,17 @@ canApply :: Ledger tx -> ChainSlot -> UTxOType tx -> tx -> ValidationResult
9090
canApply ledger slot utxo tx =
9191
either (Invalid . snd) (const Valid) $ applyTransactions ledger slot utxo (pure tx)
9292

93+
-- | Collect applicable transactions and resulting UTxO. In contrast to
94+
-- 'applyTransactions', this functions continues on validation errors.
95+
collectTransactions :: Ledger tx -> ChainSlot -> UTxOType tx -> [tx] -> ([tx], UTxOType tx)
96+
collectTransactions Ledger{applyTransactions} slot utxo =
97+
foldr go ([], utxo)
98+
where
99+
go tx (applicableTxs, u) =
100+
case applyTransactions slot u [tx] of
101+
Left _ -> (applicableTxs, u)
102+
Right u' -> (applicableTxs <> [tx], u')
103+
93104
-- | Either valid or an error which we get from the ledger-specs tx validation.
94105
data ValidationResult
95106
= Valid

hydra-node/test/Hydra/Ledger/CardanoSpec.hs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
module Hydra.Ledger.CardanoSpec where
44

5+
import Cardano.Api.UTxO (fromApi, toApi)
56
import Hydra.Cardano.Api
67
import Hydra.Prelude
78
import Test.Hydra.Prelude
@@ -56,6 +57,8 @@ spec =
5657
\ \"value\":{\"lovelace\":14}}}"
5758
shouldParseJSONAs @UTxO bs
5859

60+
prop "Roundtrip to and from Api" roundtripFromAndToApi
61+
5962
describe "ProtocolParameters" $
6063
prop "Roundtrip JSON encoding" roundtripProtocolParameters
6164

@@ -107,6 +110,10 @@ shouldParseJSONAs bs =
107110
Left err -> failure err
108111
Right (_ :: a) -> pure ()
109112

113+
roundtripFromAndToApi :: UTxO -> Property
114+
roundtripFromAndToApi utxo =
115+
fromApi (toApi utxo) === utxo
116+
110117
-- | Test that the 'ProtocolParameters' To/FromJSON instances to roundtrip. Note
111118
-- that we use the ledger 'PParams' type to generate values, but the cardano-api
112119
-- type 'ProtocolParameters' is used for the serialization.

hydra-node/test/Hydra/Model.hs

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,12 @@ instance StateModel WorldState where
223223

224224
precondition WorldState{hydraState = Start} Seed{} =
225225
True
226-
precondition WorldState{hydraState = Idle{}} Init{} =
227-
True
228-
precondition WorldState{hydraState = hydraState@Initial{}} (Commit party _) =
229-
isPendingCommitFrom party hydraState
230-
precondition WorldState{hydraState = Initial{}} Abort{} =
231-
True
226+
precondition WorldState{hydraState = Idle{idleParties}} (Init p) =
227+
p `elem` idleParties
228+
precondition WorldState{hydraState = Initial{pendingCommits}} (Commit party _) =
229+
party `Map.member` pendingCommits
230+
precondition WorldState{hydraState = Initial{commits, pendingCommits}} (Abort party) =
231+
party `Set.member` (Map.keysSet pendingCommits <> Map.keysSet commits)
232232
precondition WorldState{hydraState = Open{}} (Close _) =
233233
True
234234
precondition WorldState{hydraState = Open{offChainState}} (NewTx _ tx) =
@@ -347,6 +347,14 @@ instance StateModel WorldState where
347347
ObserveHeadIsOpen -> s
348348
StopTheWorld -> s
349349

350+
shrinkAction _ctx _st = \case
351+
seed@Seed{seedKeys, toCommit} ->
352+
[ Some seed{seedKeys = seedKeys', toCommit = toCommit'}
353+
| seedKeys' <- shrink seedKeys
354+
, let toCommit' = Map.filterWithKey (\p _ -> p `elem` (deriveParty . fst <$> seedKeys')) toCommit
355+
]
356+
_other -> []
357+
350358
instance HasVariables WorldState where
351359
getAllVariables _ = mempty
352360

@@ -383,16 +391,6 @@ genInit hydraParties = do
383391
let party = deriveParty key
384392
pure $ Init party
385393

386-
genCommit' ::
387-
[(SigningKey HydraKey, CardanoSigningKey)] ->
388-
(SigningKey HydraKey, CardanoSigningKey) ->
389-
Gen (Action WorldState [(CardanoSigningKey, Value)])
390-
genCommit' hydraParties hydraParty = do
391-
let (_, sk) = fromJust $ find (== hydraParty) hydraParties
392-
value <- genAdaValue
393-
let utxo = [(sk, value)]
394-
pure $ Commit (deriveParty . fst $ hydraParty) utxo
395-
396394
genPayment :: WorldState -> Gen (Party, Payment)
397395
genPayment WorldState{hydraParties, hydraState} =
398396
case hydraState of
@@ -609,17 +607,18 @@ performCommit parties party paymentUTxO = do
609607
SimulatedChainNetwork{simulateCommit} <- gets chain
610608
case Map.lookup party nodes of
611609
Nothing -> throwIO $ UnexpectedParty party
612-
Just actorNode -> do
610+
Just{} -> do
613611
let realUTxO = toRealUTxO paymentUTxO
614612
lift $ simulateCommit (party, realUTxO)
615613
observedUTxO <-
616614
lift $
617-
waitMatch actorNode $ \case
618-
Committed{party = cp, utxo = committedUTxO}
619-
| cp == party -> Just committedUTxO
620-
err@CommandFailed{} -> error $ show err
621-
_ -> Nothing
622-
pure $ fromUtxo observedUTxO
615+
forM nodes $ \n ->
616+
waitMatch n $ \case
617+
Committed{party = cp, utxo = committedUTxO}
618+
| cp == party, committedUTxO == realUTxO -> Just committedUTxO
619+
err@CommandFailed{} -> error $ show err
620+
_ -> Nothing
621+
pure $ fromUtxo $ List.head $ toList observedUTxO
623622
where
624623
fromUtxo :: UTxO -> [(CardanoSigningKey, Value)]
625624
fromUtxo utxo = findSigningKey . (txOutAddress &&& txOutValue) . snd <$> pairs utxo

hydra-node/test/Hydra/Model/MockChain.hs

Lines changed: 62 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ module Hydra.Model.MockChain where
55
import Hydra.Cardano.Api
66
import Hydra.Prelude hiding (Any, label)
77

8-
import Cardano.Api.UTxO (fromPairs, pairs)
8+
import Cardano.Api.UTxO (fromPairs)
99
import Control.Concurrent.Class.MonadSTM (
1010
MonadLabelledSTM,
1111
MonadSTM (newTVarIO, writeTVar),
@@ -15,6 +15,7 @@ import Control.Concurrent.Class.MonadSTM (
1515
newTQueueIO,
1616
newTVarIO,
1717
readTVarIO,
18+
throwSTM,
1819
tryReadTQueue,
1920
writeTQueue,
2021
writeTVar,
@@ -25,9 +26,11 @@ import Data.Sequence (Seq (Empty, (:|>)))
2526
import Data.Sequence qualified as Seq
2627
import Data.Time (secondsToNominalDiffTime)
2728
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
29+
import GHC.IO.Exception (userError)
2830
import Hydra.BehaviorSpec (
2931
SimulatedChainNetwork (..),
3032
)
33+
import Hydra.Cardano.Api.Pretty (renderTxWithUTxO)
3134
import Hydra.Chain (Chain (..), initHistory)
3235
import Hydra.Chain.Direct.Fixture (testNetworkId)
3336
import Hydra.Chain.Direct.Handlers (
@@ -52,8 +55,14 @@ import Hydra.HeadLogic (
5255
Event (..),
5356
defaultTTL,
5457
)
55-
import Hydra.HeadLogic.State (ClosedState (..), HeadState (..), IdleState (..), InitialState (..), OpenState (..))
56-
import Hydra.Ledger (ChainSlot (..), Ledger (..), txId)
58+
import Hydra.HeadLogic.State (
59+
ClosedState (..),
60+
HeadState (..),
61+
IdleState (..),
62+
InitialState (..),
63+
OpenState (..),
64+
)
65+
import Hydra.Ledger (ChainSlot (..), Ledger (..), ValidationError (..), collectTransactions)
5766
import Hydra.Ledger.Cardano (adjustUTxO, fromChainSlot, genTxOutAdaOnly)
5867
import Hydra.Ledger.Cardano.Evaluate (eraHistoryWithoutHorizon, evaluateTx)
5968
import Hydra.Logging (Tracer)
@@ -92,7 +101,7 @@ mockChainAndNetwork tr seedKeys commits = do
92101
link tickThread
93102
pure
94103
SimulatedChainNetwork
95-
{ connectNode = connectNode nodes queue
104+
{ connectNode = connectNode nodes chain queue
96105
, tickThread
97106
, rollbackAndForward = rollbackAndForward nodes chain
98107
, simulateCommit = simulateCommit nodes
@@ -117,7 +126,7 @@ mockChainAndNetwork tr seedKeys commits = do
117126
let vks = getVerificationKey . signingKey . snd <$> seedKeys
118127
env{participants = verificationKeyToOnChainId <$> vks}
119128

120-
connectNode nodes queue node = do
129+
connectNode nodes chain queue node = do
121130
localChainState <- newLocalChainState (initHistory initialChainState)
122131
let Environment{party = ownParty} = env node
123132
let vkey = fst $ findOwnCardanoKey ownParty seedKeys
@@ -130,12 +139,25 @@ mockChainAndNetwork tr seedKeys commits = do
130139
}
131140
let getTimeHandle = pure $ fixedTimeHandleIndefiniteHorizon `generateWith` 42
132141
let HydraNode{eq = EventQueue{putEvent}} = node
133-
let
134-
-- NOTE: this very simple function put the transaction in a queue for
135-
-- inclusion into the chain. We could want to simulate the local
136-
-- submission of a transaction and the possible failures it introduces,
137-
-- perhaps caused by the node lagging behind
138-
submitTx = atomically . writeTQueue queue
142+
-- Validate transactions on submission and queue them for inclusion if valid.
143+
let submitTx tx =
144+
atomically $ do
145+
-- NOTE: Determine the current "view" on the chain (important while
146+
-- rolled back, before new roll forwards were issued)
147+
(slot, position, blocks, globalUTxO) <- readTVar chain
148+
let utxo = case Seq.lookup (fromIntegral position) blocks of
149+
Nothing -> globalUTxO
150+
Just (_, _, blockUTxO) -> blockUTxO
151+
case applyTransactions slot utxo [tx] of
152+
Left (_tx, err) ->
153+
throwSTM . userError . toString $
154+
unlines
155+
[ "MockChain: Invalid tx submitted"
156+
, "Tx: " <> toText (renderTxWithUTxO utxo tx)
157+
, "Error: " <> show err
158+
]
159+
Right _utxo' ->
160+
writeTQueue queue tx
139161
let chainHandle =
140162
createMockChain
141163
tr
@@ -202,12 +224,20 @@ mockChainAndNetwork tr seedKeys commits = do
202224
(slotNum, position, blocks, _) <- readTVarIO chain
203225
case Seq.lookup (fromIntegral position) blocks of
204226
Just (header, txs, utxo) -> do
227+
let position' = position + 1
205228
allHandlers <- fmap chainHandler <$> readTVarIO nodes
229+
-- NOTE: Need to reset the mocked chain ledger to this utxo before
230+
-- calling the node handlers (as they might submit transactions
231+
-- directly).
232+
atomically $ writeTVar chain (slotNum, position', blocks, utxo)
206233
forM_ allHandlers (\h -> onRollForward h header txs)
207-
atomically $ writeTVar chain (slotNum, position + 1, blocks, utxo)
208234
Nothing ->
209235
pure ()
210236

237+
-- XXX: This should actually work more like a chain fork / switch to longer
238+
-- chain. That is, the ledger switches to the longer chain state right away
239+
-- and we issue rollback and forwards to synchronize clients. However,
240+
-- submission will already validate against the new ledger state.
211241
rollbackAndForward nodes chain numberOfBlocks = do
212242
doRollBackward nodes chain numberOfBlocks
213243
replicateM_ (fromIntegral numberOfBlocks) $
@@ -217,29 +247,25 @@ mockChainAndNetwork tr seedKeys commits = do
217247
(slotNum, position, blocks, _) <- readTVarIO chain
218248
case Seq.lookup (fromIntegral $ position - nbBlocks) blocks of
219249
Just (header, _, utxo) -> do
250+
let position' = position - nbBlocks + 1
220251
allHandlers <- fmap chainHandler <$> readTVarIO nodes
221252
let point = getChainPoint header
253+
atomically $ writeTVar chain (slotNum, position', blocks, utxo)
222254
forM_ allHandlers (`onRollBackward` point)
223-
atomically $ writeTVar chain (slotNum, position - nbBlocks + 1, blocks, utxo)
224255
Nothing ->
225256
pure ()
226257

227258
addNewBlockToChain chain transactions =
228-
modifyTVar chain $ \(slotNum, position, blocks, utxo) ->
259+
modifyTVar chain $ \(slotNum, position, blocks, utxo) -> do
229260
-- NOTE: Assumes 1 slot = 1 second
230261
let newSlot = slotNum + ChainSlot (truncate blockTime)
231262
header = genBlockHeaderAt (fromChainSlot newSlot) `generateWith` 42
232-
in case applyTransactions newSlot utxo transactions of
233-
Left err ->
234-
error $
235-
toText $
236-
"On-chain transactions are not supposed to fail: "
237-
<> show err
238-
<> "\nTx:\n"
239-
<> (show @String $ txId <$> transactions)
240-
<> "\nUTxO:\n"
241-
<> show (fst <$> pairs utxo)
242-
Right utxo' -> (newSlot, position, blocks :|> (header, transactions, utxo), utxo')
263+
-- NOTE: Transactions that do not apply to the current state (eg.
264+
-- UTxO) are silently dropped which emulates the chain behaviour that
265+
-- only the client is potentially witnessing the failure, and no
266+
-- invalid transaction will ever be included in the chain.
267+
(txs', utxo') = collectTransactions ledger newSlot utxo transactions
268+
in (newSlot, position, blocks :|> (header, txs', utxo'), utxo')
243269

244270
-- | Construct fixed 'TimeHandle' that starts from 0 and has the era horizon far in the future.
245271
-- This is used in our 'Model' tests and we want to make sure the tests finish before
@@ -264,19 +290,20 @@ scriptLedger seedInput =
264290
where
265291
initUTxO = fromPairs [(seedInput, (arbitrary >>= genTxOutAdaOnly) `generateWith` 42)]
266292

267-
applyTransactions slot utxo = \case
293+
-- XXX: We could easily add 'slot' validation here and this would already
294+
-- emulate the dropping of outdated transactions from the cardano-node
295+
-- mempool.
296+
applyTransactions !slot utxo = \case
268297
[] -> Right utxo
269298
(tx : txs) ->
270299
case evaluateTx tx utxo of
271-
Left _ ->
272-
-- Transactions that do not apply to the current state (eg. UTxO) are
273-
-- silently dropped which emulates the chain behaviour that only the
274-
-- client is potentially witnessing the failure, and no invalid
275-
-- transaction will ever be included in the chain
276-
applyTransactions slot utxo txs
277-
Right _ ->
278-
let utxo' = adjustUTxO tx utxo
279-
in applyTransactions slot utxo' txs
300+
Left err ->
301+
Left (tx, ValidationError{reason = show err})
302+
Right report
303+
| any isLeft report ->
304+
Left (tx, ValidationError{reason = show . lefts $ toList report})
305+
| otherwise ->
306+
applyTransactions slot (adjustUTxO tx utxo) txs
280307

281308
-- | Find Cardano vkey corresponding to our Hydra vkey using signing keys lookup.
282309
-- This is a bit cumbersome and a tribute to the fact the `HydraNode` itself has no

0 commit comments

Comments
 (0)