Skip to content

Commit 5c79937

Browse files
committed
draft e2e scenario for head getting stuck
1 parent 9a709f6 commit 5c79937

File tree

3 files changed

+214
-2
lines changed

3 files changed

+214
-2
lines changed

hydra-cluster/src/Hydra/Cluster/Scenarios.hs

+82-2
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ import CardanoClient (
2626
)
2727
import CardanoNode (NodeLog)
2828
import Control.Concurrent.Async (mapConcurrently_)
29-
import Control.Lens ((.~), (^.), (^..), (^?))
29+
import Control.Lens ((.~), (?~), (^.), (^..), (^?))
3030
import Data.Aeson (Value, object, (.=))
3131
import Data.Aeson qualified as Aeson
32-
import Data.Aeson.Lens (key, values, _JSON, _String)
32+
import Data.Aeson.Lens (atKey, key, values, _JSON, _String)
3333
import Data.Aeson.Types (parseMaybe)
3434
import Data.ByteString (isInfixOf)
3535
import Data.ByteString qualified as B
@@ -108,6 +108,7 @@ import HydraNode (
108108
input,
109109
output,
110110
postDecommit,
111+
prepareHydraNode,
111112
requestCommitTx,
112113
send,
113114
waitFor,
@@ -116,6 +117,7 @@ import HydraNode (
116117
waitMatch,
117118
withHydraCluster,
118119
withHydraNode,
120+
withPreparedHydraNode,
119121
)
120122
import Network.HTTP.Conduit (parseUrlThrow)
121123
import Network.HTTP.Conduit qualified as L
@@ -1316,6 +1318,84 @@ canDecommit tracer workDir node hydraScriptsTxId =
13161318

13171319
RunningNode{networkId, nodeSocket, blockTime} = node
13181320

1321+
-- | Can side load snapshot and resume agreement after a peer comes back online with healthy configuration
1322+
canSideLoadSnapshot :: Tracer IO EndToEndLog -> FilePath -> RunningNode -> [TxId] -> IO ()
1323+
canSideLoadSnapshot tracer workDir cardanoNode hydraScriptsTxId = do
1324+
let clients = [Alice, Bob, Carol]
1325+
[(aliceCardanoVk, aliceCardanoSk), (bobCardanoVk, _), (carolCardanoVk, _)] <- forM clients keysFor
1326+
seedFromFaucet_ cardanoNode aliceCardanoVk 100_000_000 (contramap FromFaucet tracer)
1327+
seedFromFaucet_ cardanoNode bobCardanoVk 100_000_000 (contramap FromFaucet tracer)
1328+
seedFromFaucet_ cardanoNode carolCardanoVk 100_000_000 (contramap FromFaucet tracer)
1329+
1330+
let contestationPeriod = UnsafeContestationPeriod 1
1331+
let depositDeadline = UnsafeDepositDeadline 200
1332+
aliceChainConfig <-
1333+
chainConfigFor Alice workDir nodeSocket hydraScriptsTxId [Bob, Carol] contestationPeriod depositDeadline
1334+
<&> setNetworkId networkId
1335+
bobChainConfig <-
1336+
chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice, Carol] contestationPeriod depositDeadline
1337+
<&> setNetworkId networkId
1338+
carolChainConfig <-
1339+
chainConfigFor Carol workDir nodeSocket hydraScriptsTxId [Alice, Bob] contestationPeriod depositDeadline
1340+
<&> setNetworkId networkId
1341+
1342+
withHydraNode hydraTracer aliceChainConfig workDir 1 aliceSk [bobVk, carolVk] [1, 2, 3] $ \n1 -> do
1343+
aliceUTxO <- seedFromFaucet cardanoNode aliceCardanoVk 1_000_000 (contramap FromFaucet tracer)
1344+
withHydraNode hydraTracer bobChainConfig workDir 2 bobSk [aliceVk, carolVk] [1, 2, 3] $ \n2 -> do
1345+
-- Carol starts its node missconfigured
1346+
let pparamsDecorator = atKey "maxTxSize" ?~ toJSON (Aeson.Number 0)
1347+
wrongOptions <- prepareHydraNode carolChainConfig workDir 3 carolSk [aliceVk, bobVk] [1, 2, 3] pparamsDecorator
1348+
withPreparedHydraNode hydraTracer workDir 3 wrongOptions $ \n3 -> do
1349+
-- Init
1350+
send n1 $ input "Init" []
1351+
headId <- waitForAllMatch (10 * blockTime) [n1, n2, n3] $ headIsInitializingWith (Set.fromList [alice, bob, carol])
1352+
1353+
-- Alice commits something
1354+
requestCommitTx n1 aliceUTxO >>= submitTx cardanoNode
1355+
1356+
-- Everyone else commits nothing
1357+
mapConcurrently_ (\n -> requestCommitTx n mempty >>= submitTx cardanoNode) [n2, n3]
1358+
1359+
-- Observe open with the relevant UTxOs
1360+
waitFor hydraTracer (20 * blockTime) [n1, n2, n3] $
1361+
output "HeadIsOpen" ["utxo" .= toJSON aliceUTxO, "headId" .= headId]
1362+
1363+
-- Alice submits a new transaction
1364+
utxo <- getSnapshotUTxO n1
1365+
tx <- mkTransferTx testNetworkId utxo aliceCardanoSk aliceCardanoVk
1366+
send n1 $ input "NewTx" ["transaction" .= tx]
1367+
1368+
-- Alice and Bob accept it
1369+
waitForAllMatch (200 * blockTime) [n1, n2] $ \v -> do
1370+
guard $ v ^? key "tag" == Just "TxValid"
1371+
guard $ v ^? key "transactionId" == Just (toJSON $ txId tx)
1372+
1373+
-- Carol does not because of its node being missconfigured
1374+
waitMatch 3 n3 $ \v -> do
1375+
guard $ v ^? key "tag" == Just "TxInvalid"
1376+
guard $ v ^? key "transaction" . key "txId" == Just (toJSON $ txId tx)
1377+
1378+
-- Carol disconnects and the others observe it
1379+
waitForAllMatch (100 * blockTime) [n1, n2] $ \v -> do
1380+
guard $ v ^? key "tag" == Just "PeerDisconnected"
1381+
1382+
-- Carol reconnects with reconfigured node
1383+
withHydraNode hydraTracer carolChainConfig workDir 3 carolSk [aliceVk, bobVk] [1, 2, 3] $ \n3 -> do
1384+
-- Everyone confirms it
1385+
-- Note: We can't use `waitForAlMatch` here as it expects them to
1386+
-- emit the exact same datatype; but Carol will be behind in sequence
1387+
-- numbers as she was offline.
1388+
flip mapConcurrently_ [n1, n2, n3] $ \n ->
1389+
waitMatch (200 * blockTime) n $ \v -> do
1390+
guard $ v ^? key "tag" == Just "SnapshotConfirmed"
1391+
guard $ v ^? key "snapshot" . key "number" == Just (toJSON (2 :: Integer))
1392+
-- Just check that everyone signed it.
1393+
let sigs = v ^.. key "signatures" . key "multiSignature" . values
1394+
guard $ length sigs == 3
1395+
where
1396+
RunningNode{nodeSocket, networkId, blockTime} = cardanoNode
1397+
hydraTracer = contramap FromHydraNode tracer
1398+
13191399
-- * L2 scenarios
13201400

13211401
-- | Finds UTxO owned by given key in the head and creates transactions

hydra-cluster/src/HydraNode.hs

+125
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,131 @@ withHydraCluster tracer workDir nodeSocket firstNodeId allKeys hydraKeys hydraSc
302302

303303
-- * Start / connect to a hydra-node
304304

305+
-- | Prepare protocol-parameters to run a hydra-node with given 'ChainConfig' and using the config from
306+
-- config/.
307+
preparePParams ::
308+
ChainConfig ->
309+
FilePath ->
310+
(Aeson.Value -> Aeson.Value) ->
311+
IO FilePath
312+
preparePParams chainConfig stateDir paramsDecorator = do
313+
let cardanoLedgerProtocolParametersFile = stateDir </> "protocol-parameters.json"
314+
case chainConfig of
315+
Offline _ ->
316+
readConfigFile "protocol-parameters.json"
317+
>>= writeFileBS cardanoLedgerProtocolParametersFile
318+
Direct DirectChainConfig{nodeSocket, networkId} -> do
319+
-- NOTE: This implicitly tests of cardano-cli with hydra-node
320+
protocolParameters <- cliQueryProtocolParameters nodeSocket networkId
321+
Aeson.encodeFile cardanoLedgerProtocolParametersFile $
322+
paramsDecorator protocolParameters
323+
& atKey "txFeeFixed" ?~ toJSON (Number 0)
324+
& atKey "txFeePerByte" ?~ toJSON (Number 0)
325+
& key "executionUnitPrices" . atKey "priceMemory" ?~ toJSON (Number 0)
326+
& key "executionUnitPrices" . atKey "priceSteps" ?~ toJSON (Number 0)
327+
& atKey "utxoCostPerByte" ?~ toJSON (Number 0)
328+
& atKey "treasuryCut" ?~ toJSON (Number 0)
329+
& atKey "minFeeRefScriptCostPerByte" ?~ toJSON (Number 0)
330+
pure cardanoLedgerProtocolParametersFile
331+
332+
-- | Prepare 'RunOptions' to run a hydra-node with given 'ChainConfig' and using the config from
333+
-- config/.
334+
prepareHydraNode ::
335+
HasCallStack =>
336+
ChainConfig ->
337+
FilePath ->
338+
Int ->
339+
SigningKey HydraKey ->
340+
[VerificationKey HydraKey] ->
341+
[Int] ->
342+
(Aeson.Value -> Aeson.Value) ->
343+
IO RunOptions
344+
prepareHydraNode chainConfig workDir hydraNodeId hydraSKey hydraVKeys allNodeIds paramsDecorator = do
345+
-- NOTE: AirPlay on MacOS uses 5000 and we must avoid it.
346+
when (os == "darwin") $ port `shouldNotBe` (5_000 :: Network.PortNumber)
347+
let stateDir = workDir </> "state-" <> show hydraNodeId
348+
createDirectoryIfMissing True stateDir
349+
cardanoLedgerProtocolParametersFile <- preparePParams chainConfig stateDir paramsDecorator
350+
let hydraSigningKey = stateDir </> "me.sk"
351+
void $ writeFileTextEnvelope (File hydraSigningKey) Nothing hydraSKey
352+
hydraVerificationKeys <- forM (zip [1 ..] hydraVKeys) $ \(i :: Int, vKey) -> do
353+
let filepath = stateDir </> ("other-" <> show i <> ".vk")
354+
filepath <$ writeFileTextEnvelope (File filepath) Nothing vKey
355+
pure $
356+
RunOptions
357+
{ verbosity = Verbose "HydraNode"
358+
, nodeId = NodeId $ show hydraNodeId
359+
, listen = Host "0.0.0.0" (fromIntegral $ 5_000 + hydraNodeId)
360+
, advertise = Nothing
361+
, peers
362+
, apiHost = "0.0.0.0"
363+
, apiPort = fromIntegral $ 4_000 + hydraNodeId
364+
, tlsCertPath = Nothing
365+
, tlsKeyPath = Nothing
366+
, monitoringPort = Just $ fromIntegral $ 6_000 + hydraNodeId
367+
, hydraSigningKey
368+
, hydraVerificationKeys
369+
, persistenceDir = stateDir
370+
, chainConfig
371+
, ledgerConfig =
372+
CardanoLedgerConfig
373+
{ cardanoLedgerProtocolParametersFile
374+
}
375+
}
376+
where
377+
port = fromIntegral $ 5_000 + hydraNodeId
378+
-- NOTE: See comment above about 0.0.0.0 vs 127.0.0.1
379+
peers =
380+
[ Host
381+
{ Network.hostname = "0.0.0.0"
382+
, Network.port = fromIntegral $ 5_000 + i
383+
}
384+
| i <- allNodeIds
385+
, i /= hydraNodeId
386+
]
387+
388+
-- | Run a hydra-node with given 'RunOptions'.
389+
withPreparedHydraNode ::
390+
HasCallStack =>
391+
Tracer IO HydraNodeLog ->
392+
FilePath ->
393+
Int ->
394+
RunOptions ->
395+
(HydraClient -> IO a) ->
396+
IO a
397+
withPreparedHydraNode tracer workDir hydraNodeId runOptions action =
398+
withLogFile logFilePath $ \logFileHandle -> do
399+
-- XXX: using a dedicated pipe as 'createPipe' from typed-process closes too early
400+
(readErr, writeErr) <- P.createPipe
401+
let cmd =
402+
(proc "hydra-node" . toArgs $ runOptions)
403+
& setStdout (useHandleOpen logFileHandle)
404+
& setStderr (useHandleOpen writeErr)
405+
406+
traceWith tracer $ HydraNodeCommandSpec $ show cmd
407+
408+
withProcessTerm cmd $ \p -> do
409+
hClose writeErr
410+
-- NOTE: exit code thread gets cancelled if 'action' terminates first
411+
race
412+
(collectAndCheckExitCode p readErr)
413+
(withConnectionToNode tracer hydraNodeId action)
414+
<&> either absurd id
415+
where
416+
collectAndCheckExitCode p h =
417+
(`finally` hClose h) $
418+
waitExitCode p >>= \case
419+
ExitSuccess -> failure "hydra-node stopped early"
420+
ExitFailure ec -> do
421+
err <- hGetContents h
422+
failure . toString $
423+
unlines
424+
[ "hydra-node (nodeId = " <> show hydraNodeId <> ") exited with failure code: " <> show ec
425+
, decodeUtf8 err
426+
]
427+
428+
logFilePath = workDir </> "logs" </> "hydra-node-" <> show hydraNodeId <.> "log"
429+
305430
-- | Run a hydra-node with given 'ChainConfig' and using the config from
306431
-- config/.
307432
withHydraNode ::

hydra-cluster/test/Test/EndToEndSpec.hs

+7
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import Hydra.Cluster.Scenarios (
5555
canDecommit,
5656
canRecoverDeposit,
5757
canSeePendingDeposits,
58+
canSideLoadSnapshot,
5859
canSubmitTransactionThroughAPI,
5960
headIsInitializingWith,
6061
initWithWrongKeys,
@@ -466,6 +467,12 @@ spec = around (showLogsOnFailure "EndToEndSpec") $ do
466467
guard $ v ^? key "tag" == Just "HeadIsContested"
467468
guard $ v ^? key "headId" == Just (toJSON headId)
468469

470+
fit "can side load snapshot" $ \tracer -> do
471+
withClusterTempDir $ \tmpDir -> do
472+
withCardanoNodeDevnet (contramap FromCardanoNode tracer) tmpDir $ \node ->
473+
publishHydraScriptsAs node Faucet
474+
>>= canSideLoadSnapshot tracer tmpDir node
475+
469476
describe "two hydra heads scenario" $ do
470477
it "two heads on the same network do not conflict" $ \tracer ->
471478
failAfter 60 $

0 commit comments

Comments
 (0)