Skip to content

Commit a71cb88

Browse files
committed
Generate compatible state change events when testing hydrate
This is unfortunately very "white-boxy" and the testing code needs to know that the generated events must be using parameters consistent with the environment. The alternative would be to move checkHeadState outside of hydrate, but then 'WetHydraNode's can exist without this check happening.
1 parent 46d83d4 commit a71cb88

File tree

6 files changed

+73
-36
lines changed

6 files changed

+73
-36
lines changed

hydra-node/src/Hydra/Chain.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import Hydra.Cardano.Api (
2727
Witness,
2828
)
2929
import Hydra.ContestationPeriod (ContestationPeriod)
30+
import Hydra.Environment (Environment (..))
3031
import Hydra.HeadId (HeadId, HeadSeed)
3132
import Hydra.Ledger (ChainSlot, IsTx, UTxOType)
3233
import Hydra.OnChainId (OnChainId)
@@ -60,6 +61,11 @@ instance Arbitrary HeadParameters where
6061
dedupParties HeadParameters{contestationPeriod, parties} =
6162
HeadParameters{contestationPeriod, parties = nub parties}
6263

64+
-- | Make 'HeadParameters' that are consistent with the given 'Environment'.
65+
mkHeadParameters :: Environment -> HeadParameters
66+
mkHeadParameters Environment{party, otherParties, contestationPeriod} =
67+
HeadParameters{contestationPeriod, parties = party : otherParties}
68+
6369
-- | Data type used to post transactions on chain. It holds everything to
6470
-- construct corresponding Head protocol transactions.
6571
data PostChainTx tx

hydra-node/src/Hydra/Environment.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ data Environment = Environment
1818
participants :: [OnChainId]
1919
, contestationPeriod :: ContestationPeriod
2020
}
21-
deriving stock (Show)
21+
deriving stock (Show, Eq)
2222

2323
instance Arbitrary Environment where
2424
arbitrary = do

hydra-node/src/Hydra/HeadLogic.hs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import Hydra.Chain (
3737
OnChainTx (..),
3838
PostChainTx (..),
3939
initHistory,
40+
mkHeadParameters,
4041
pushNewState,
4142
rollbackHistory,
4243
)
@@ -109,13 +110,9 @@ onIdleClientInit ::
109110
onIdleClientInit env =
110111
cause OnChainEffect{postChainTx = InitTx{participants, headParameters}}
111112
where
112-
headParameters =
113-
HeadParameters
114-
{ contestationPeriod
115-
, parties = party : otherParties
116-
}
113+
headParameters = mkHeadParameters env
117114

118-
Environment{party, otherParties, contestationPeriod, participants} = env
115+
Environment{participants} = env
119116

120117
-- | Observe an init transaction, initialize parameters in an 'InitialState' and
121118
-- notify clients that they can now commit.

hydra-node/src/Hydra/HeadLogic/Outcome.hs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module Hydra.HeadLogic.Outcome where
66
import Hydra.Prelude
77

88
import Hydra.API.ServerOutput (ServerOutput)
9-
import Hydra.Chain (ChainStateType, HeadParameters, IsChainState, PostChainTx)
9+
import Hydra.Chain (ChainStateType, HeadParameters, IsChainState, PostChainTx, mkHeadParameters)
1010
import Hydra.Crypto (MultiSignature, Signature)
1111
import Hydra.Environment (Environment (..))
1212
import Hydra.Events (HasEventId (..))
@@ -17,6 +17,7 @@ import Hydra.Ledger (ChainSlot, IsTx, TxIdType, UTxOType, ValidationError)
1717
import Hydra.Network.Message (Message)
1818
import Hydra.Party (Party)
1919
import Hydra.Snapshot (Snapshot, SnapshotNumber)
20+
import Test.QuickCheck (oneof)
2021

2122
-- | Analogous to inputs, the pure head logic "core" can have effects emited to
2223
-- the "shell" layers and we distinguish the same: effects onto the client, the
@@ -105,14 +106,37 @@ instance HasEventId (StateChanged tx) where
105106
-- FIXME(Elaine): these stateChangeID fields were added in an attempt to make every StateChanged keep track of its ID
106107
-- it's not clear how to handle the state for this. but for now the field is kept so that the type of putEvent can be kept simple, and shouldn't do harm
107108

108-
instance (IsTx tx, Arbitrary (HeadState tx), Arbitrary (ChainStateType tx)) => Arbitrary (StateChanged tx) where
109-
arbitrary = genericArbitrary
110-
111109
deriving stock instance (IsTx tx, Eq (HeadState tx), Eq (ChainStateType tx)) => Eq (StateChanged tx)
112110
deriving stock instance (IsTx tx, Show (HeadState tx), Show (ChainStateType tx)) => Show (StateChanged tx)
113111
deriving anyclass instance (IsTx tx, ToJSON (ChainStateType tx)) => ToJSON (StateChanged tx)
114112
deriving anyclass instance (IsTx tx, FromJSON (HeadState tx), FromJSON (ChainStateType tx)) => FromJSON (StateChanged tx)
115113

114+
instance IsChainState tx => Arbitrary (StateChanged tx) where
115+
arbitrary = arbitrary >>= genStateChanged
116+
117+
genStateChanged :: IsChainState tx => Environment -> Gen (StateChanged tx)
118+
genStateChanged env =
119+
oneof
120+
[ HeadInitialized (mkHeadParameters env) <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
121+
, CommittedUTxO party <$> arbitrary <*> arbitrary <*> arbitrary
122+
, HeadAborted <$> arbitrary <*> arbitrary
123+
, HeadOpened <$> arbitrary <*> arbitrary <*> arbitrary
124+
, TransactionAppliedToLocalUTxO <$> arbitrary <*> arbitrary <*> arbitrary
125+
, SnapshotRequestDecided <$> arbitrary <*> arbitrary
126+
, SnapshotRequested <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
127+
, TransactionReceived <$> arbitrary <*> arbitrary
128+
, PartySignedSnapshot <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
129+
, SnapshotConfirmed <$> arbitrary <*> arbitrary <*> arbitrary
130+
, HeadClosed <$> arbitrary <*> arbitrary <*> arbitrary
131+
, HeadContested <$> arbitrary <*> arbitrary <*> arbitrary
132+
, HeadIsReadyToFanout <$> arbitrary
133+
, HeadFannedOut <$> arbitrary <*> arbitrary
134+
, ChainRolledBack <$> arbitrary <*> arbitrary
135+
, TickObserved <$> arbitrary <*> arbitrary
136+
]
137+
where
138+
Environment{party} = env
139+
116140
data Outcome tx
117141
= -- | Continue with the given state updates and side effects.
118142
Continue {events :: [StateChanged tx], effects :: [Effect tx]}

hydra-node/src/Hydra/Node.hs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,19 +177,17 @@ hydrate ::
177177
EventSource (StateChanged tx) m ->
178178
[EventSink (StateChanged tx) m] ->
179179
DryHydraNode tx m ->
180-
m
181-
( WetHydraNode tx m
182-
)
180+
m (WetHydraNode tx m)
183181
hydrate eventSource eventSinks dryNode = do
184182
events <- getEvents eventSource
185183
traceWith tracer LoadedState{numberOfEvents = fromIntegral $ length events}
186184
let headState = recoverState initialState events
187185
chainStateHistory = recoverChainStateHistory initialChainState events
186+
-- Check whether the loaded state matches our configuration (env)
187+
checkHeadState tracer env headState
188188
-- deliver to sinks per spec, deduplication is handled by the sinks
189189
-- FIXME(Elaine): persistence currently not handling duplication, so this relies on not providing the eventSource's sink as an arg here
190190
putEventsToSinks eventSinks events
191-
-- FIXME: move this outside (how access headstate?)
192-
-- checkHeadState tracer env hs
193191
nodeState <- createNodeState headState
194192
inputQueue <- createInputQueue
195193
pure

hydra-node/test/Hydra/NodeSpec.hs

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ import Hydra.API.ClientInput (ClientInput (..))
1010
import Hydra.API.Server (Server (..))
1111
import Hydra.API.ServerOutput (ServerOutput (..))
1212
import Hydra.Cardano.Api (SigningKey)
13-
import Hydra.Chain (Chain (..), ChainEvent (..), HeadParameters (..), IsChainState, OnChainTx (..), PostTxError (NoSeedInput))
13+
import Hydra.Chain (Chain (..), ChainEvent (..), HeadParameters (..), IsChainState, OnChainTx (..), PostTxError (NoSeedInput), mkHeadParameters)
1414
import Hydra.ContestationPeriod (ContestationPeriod (..))
1515
import Hydra.Crypto (HydraKey, sign)
1616
import Hydra.Environment (Environment (..))
1717
import Hydra.Environment qualified as Environment
1818
import Hydra.Events (EventSink (..), EventSource (..))
1919
import Hydra.HeadLogic (Input (..), defaultTTL)
20+
import Hydra.HeadLogic.Outcome (StateChanged (HeadInitialized), genStateChanged)
2021
import Hydra.HeadLogicSpec (inInitialState, testSnapshot)
2122
import Hydra.Ledger (ChainSlot (ChainSlot))
2223
import Hydra.Ledger.Simple (SimpleChainState (..), SimpleTx (..), simpleLedger, utxoRef, utxoRefs)
@@ -41,8 +42,7 @@ import Hydra.Options (defaultContestationPeriod)
4142
import Hydra.Party (Party, deriveParty)
4243
import Hydra.Persistence (PersistenceIncremental (..), eventPairFromPersistenceIncremental)
4344
import Test.Hydra.Fixture (alice, aliceSk, bob, bobSk, carol, carolSk, cperiod, deriveOnChainId, testEnvironment, testHeadId, testHeadSeed)
44-
import Test.QuickCheck (NonEmptyList (..), listOf, oneof, property)
45-
import Test.QuickCheck.Property (forAllBlind)
45+
import Test.QuickCheck (elements, forAllBlind, forAllShrink, listOf, listOf1, (==>))
4646

4747
spec :: Spec
4848
spec = parallel $ do
@@ -52,23 +52,35 @@ spec = parallel $ do
5252

5353
describe "hydrate" $ do
5454
around setupDryNode $ do
55-
it "loads events from source into all sinks" $ \node -> do
56-
property $ \someEvents -> do
57-
(mockSink1, getMockSinkEvents1) <- createRecordingSink
58-
(mockSink2, getMockSinkEvents2) <- createRecordingSink
59-
60-
void $ hydrate (mockSource someEvents) [mockSink1, mockSink2] node
61-
62-
getMockSinkEvents1 `shouldReturn` someEvents
63-
getMockSinkEvents2 `shouldReturn` someEvents
64-
65-
it "fails if one sink fails" $ \node -> do
66-
property $ \(NonEmpty someEvents) -> do
67-
let genSinks = oneof [pure mockSink, pure failingSink]
68-
failingSink = EventSink{putEvent = \_ -> failure "failing sink called"}
69-
forAllBlind (listOf genSinks) $ \sinks -> do
70-
hydrate (mockSource someEvents) (sinks <> [failingSink]) node
71-
`shouldThrow` \(_ :: HUnitFailure) -> True
55+
it "loads events from source into all sinks" $ \node ->
56+
forAllShrink (listOf $ genStateChanged testEnvironment) shrink $
57+
\someEvents -> do
58+
(mockSink1, getMockSinkEvents1) <- createRecordingSink
59+
(mockSink2, getMockSinkEvents2) <- createRecordingSink
60+
61+
void $ hydrate (mockSource someEvents) [mockSink1, mockSink2] node
62+
63+
getMockSinkEvents1 `shouldReturn` someEvents
64+
getMockSinkEvents2 `shouldReturn` someEvents
65+
66+
it "fails if one sink fails" $ \node ->
67+
forAllShrink (listOf1 $ genStateChanged testEnvironment) shrink $
68+
\someEvents -> do
69+
let genSinks = elements [mockSink, failingSink]
70+
failingSink = EventSink{putEvent = \_ -> failure "failing sink called"}
71+
forAllBlind (listOf genSinks) $ \sinks ->
72+
hydrate (mockSource someEvents) (sinks <> [failingSink]) node
73+
`shouldThrow` \(_ :: HUnitFailure) -> True
74+
75+
it "checks head state" $ \node ->
76+
forAllShrink arbitrary shrink $ \env ->
77+
env /= testEnvironment ==> do
78+
-- XXX: This is very tied to the fact that 'HeadInitialized' results in
79+
-- a head state that gets checked by 'checkHeadState'
80+
let genEvent = HeadInitialized (mkHeadParameters env) <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
81+
forAllShrink genEvent shrink $ \incompatibleEvent ->
82+
hydrate (mockSource [incompatibleEvent]) [] node
83+
`shouldThrow` \(_ :: ParameterMismatch) -> True
7284

7385
describe "stepHydraNode" $ do
7486
around setupDryNode $ do

0 commit comments

Comments
 (0)