From fd048fe18fe1b374b6c9cafb9278508151b96f49 Mon Sep 17 00:00:00 2001 From: Noon van der Silk Date: Wed, 15 Jan 2025 09:45:23 +0000 Subject: [PATCH] Example of the simple dropout example on-top of the raft branch --- hydra-cluster/src/Hydra/Cluster/Scenarios.hs | 78 +++++++++++++++++++- hydra-cluster/test/Test/EndToEndSpec.hs | 7 ++ hydra-node/src/Hydra/Ledger/Cardano.hs | 19 +++++ 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/hydra-cluster/src/Hydra/Cluster/Scenarios.hs b/hydra-cluster/src/Hydra/Cluster/Scenarios.hs index c78119ac2af..24e313a1278 100644 --- a/hydra-cluster/src/Hydra/Cluster/Scenarios.hs +++ b/hydra-cluster/src/Hydra/Cluster/Scenarios.hs @@ -57,11 +57,11 @@ import Hydra.Cardano.Api ( ) import Hydra.Cluster.Faucet (FaucetLog, seedFromFaucet, seedFromFaucet_) import Hydra.Cluster.Faucet qualified as Faucet -import Hydra.Cluster.Fixture (Actor (..), actorName, alice, aliceSk, aliceVk, bob, bobSk, bobVk, carol, carolSk) +import Hydra.Cluster.Fixture (Actor (..), actorName, alice, aliceSk, aliceVk, bob, bobSk, bobVk, carol, carolSk, carolVk) import Hydra.Cluster.Mithril (MithrilLog) import Hydra.Cluster.Options (Options) import Hydra.Cluster.Util (chainConfigFor, keysFor, modifyConfig, setNetworkId) -import Hydra.Ledger.Cardano (mkSimpleTx) +import Hydra.Ledger.Cardano (mkTransferTx, mkSimpleTx) import Hydra.Logging (Tracer, traceWith) import Hydra.Options (DirectChainConfig (..), networkId, startChainFrom) import Hydra.Tx (HeadId, IsTx (balance), Party, txId) @@ -117,6 +117,80 @@ data EndToEndLog deriving stock (Eq, Show, Generic) deriving anyclass (ToJSON, FromJSON) +oneOfNNodesCanDropForAWhile :: Tracer IO EndToEndLog -> FilePath -> RunningNode -> TxId -> IO () +oneOfNNodesCanDropForAWhile tracer workDir cardanoNode hydraScriptsTxId = do + let clients = [Alice, Bob, Carol] + [(aliceCardanoVk, aliceCardanoSk), (bobCardanoVk, _), (carolCardanoVk, _)] <- forM clients keysFor + seedFromFaucet_ cardanoNode aliceCardanoVk 100_000_000 (contramap FromFaucet tracer) + seedFromFaucet_ cardanoNode bobCardanoVk 100_000_000 (contramap FromFaucet tracer) + seedFromFaucet_ cardanoNode carolCardanoVk 100_000_000 (contramap FromFaucet tracer) + + let contestationPeriod = UnsafeContestationPeriod 1 + aliceChainConfig <- + chainConfigFor Alice workDir nodeSocket hydraScriptsTxId [Bob, Carol] contestationPeriod + <&> setNetworkId networkId + + bobChainConfig <- + chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice, Carol] contestationPeriod + <&> setNetworkId networkId + + carolChainConfig <- + chainConfigFor Carol workDir nodeSocket hydraScriptsTxId [Alice, Bob] contestationPeriod + <&> setNetworkId networkId + + withHydraNode hydraTracer aliceChainConfig workDir 1 aliceSk [bobVk, carolVk] [1, 2, 3] $ \n1 -> do + aliceUTxO <- seedFromFaucet cardanoNode aliceCardanoVk 1_000_000 (contramap FromFaucet tracer) + withHydraNode hydraTracer bobChainConfig workDir 2 bobSk [aliceVk, carolVk] [1, 2, 3] $ \n2 -> do + withHydraNode hydraTracer carolChainConfig workDir 3 carolSk [aliceVk, bobVk] [1, 2, 3] $ \n3 -> do + -- Init + send n1 $ input "Init" [] + headId <- waitForAllMatch (10 * blockTime) [n1, n2, n3] $ headIsInitializingWith (Set.fromList [alice, bob, carol]) + + -- Alice commits something + requestCommitTx n1 aliceUTxO >>= submitTx cardanoNode + + -- Everyone else commits nothing + mapConcurrently_ (\n -> requestCommitTx n mempty >>= submitTx cardanoNode) [n2, n3] + + -- Observe open with the relevant UTxOs + waitFor hydraTracer (20 * blockTime) [n1, n2, n3] $ + output "HeadIsOpen" ["utxo" .= toJSON aliceUTxO, "headId" .= headId] + + -- Perform a simple transaction from alice to herself + utxo <- getSnapshotUTxO n1 + tx <- mkTransferTx networkId utxo aliceCardanoSk aliceCardanoVk + send n1 $ input "NewTx" ["transaction" .= tx] + + -- Everyone confirms it + waitForAllMatch (200 * blockTime) [n1, n2, n3] $ \v -> do + guard $ v ^? key "tag" == Just "SnapshotConfirmed" + guard $ v ^? key "snapshot" . key "snapshotNumber" == Just (toJSON (1 :: Integer)) + + -- Carol disconnects and the others observe it + -- waitForAllMatch (100 * blockTime) [n1, n2] $ \v -> do + -- guard $ v ^? key "tag" == Just "PeerDisconnected" + + -- Alice never-the-less submits a transaction + utxo <- getSnapshotUTxO n1 + tx <- mkTransferTx networkId utxo aliceCardanoSk aliceCardanoVk + send n1 $ input "NewTx" ["transaction" .= tx] + + -- Carol reconnects, and then the snapshot can be confirmed + withHydraNode hydraTracer carolChainConfig workDir 3 carolSk [aliceVk, bobVk] [1, 2, 3] $ \n3 -> do + -- Note: We can't use `waitForAlMatch` here as it expects them to + -- emit the exact same datatype; but Carol will be behind in sequence + -- numbers as she was offline. + flip mapConcurrently_ [n1, n2, n3] $ \n -> + waitMatch (200 * blockTime) n $ \v -> do + guard $ v ^? key "tag" == Just "SnapshotConfirmed" + guard $ v ^? key "snapshot" . key "snapshotNumber" == Just (toJSON (2 :: Integer)) + -- Just check that everyone signed it. + let sigs = v ^.. key "signatures" . key "multiSignature" . values + guard $ length sigs == 3 + where + RunningNode{nodeSocket, networkId, blockTime} = cardanoNode + hydraTracer = contramap FromHydraNode tracer + restartedNodeCanObserveCommitTx :: Tracer IO EndToEndLog -> FilePath -> RunningNode -> TxId -> IO () restartedNodeCanObserveCommitTx tracer workDir cardanoNode hydraScriptsTxId = do let clients = [Alice, Bob] diff --git a/hydra-cluster/test/Test/EndToEndSpec.hs b/hydra-cluster/test/Test/EndToEndSpec.hs index 51cce7f798a..2f97604a393 100644 --- a/hydra-cluster/test/Test/EndToEndSpec.hs +++ b/hydra-cluster/test/Test/EndToEndSpec.hs @@ -69,6 +69,7 @@ import Hydra.Cluster.Scenarios ( singlePartyHeadFullLifeCycle, testPreventResumeReconfiguredPeer, threeNodesNoErrorsOnOpen, + oneOfNNodesCanDropForAWhile, ) import Hydra.Cluster.Util (chainConfigFor, keysFor, modifyConfig) import Hydra.Ledger.Cardano (mkRangedTx, mkSimpleTx) @@ -195,6 +196,12 @@ spec = around (showLogsOnFailure "EndToEndSpec") $ do >>= canDecommit tracer tmpDir node describe "three hydra nodes scenario" $ do + it "can survive a bit of downtime of 1 in 3 nodes" $ \tracer -> do + withClusterTempDir $ \tmpDir -> do + withCardanoNodeDevnet (contramap FromCardanoNode tracer) tmpDir $ \node -> + publishHydraScriptsAs node Faucet + >>= oneOfNNodesCanDropForAWhile tracer tmpDir node + it "does not error when all nodes open the head concurrently" $ \tracer -> failAfter 60 $ withClusterTempDir $ \tmpDir -> do diff --git a/hydra-node/src/Hydra/Ledger/Cardano.hs b/hydra-node/src/Hydra/Ledger/Cardano.hs index ad0e83ea5bf..b33235cd9e0 100644 --- a/hydra-node/src/Hydra/Ledger/Cardano.hs +++ b/hydra-node/src/Hydra/Ledger/Cardano.hs @@ -40,6 +40,25 @@ import Test.QuickCheck ( vectorOf, ) +-- | Build a zero-fee transaction which spends the first output owned by given +-- signing key and transfers it in full to given verification key. +mkTransferTx :: + MonadFail m => + NetworkId -> + UTxO -> + SigningKey PaymentKey -> + VerificationKey PaymentKey -> + m Tx +mkTransferTx networkId utxo sender recipient = + case UTxO.find (isVkTxOut $ getVerificationKey sender) utxo of + Nothing -> fail "no utxo left to spend" + Just (txIn, txOut) -> + case mkSimpleTx (txIn, txOut) (mkVkAddress networkId recipient, txOutValue txOut) sender of + Left err -> + fail $ "mkSimpleTx failed: " <> show err + Right tx -> + pure tx + -- * Ledger -- | Use the cardano-ledger as an in-hydra 'Ledger'.