Skip to content

Commit cc5f100

Browse files
Merge pull request #1143 from input-output-hk/ensemble/529/observe-other-head-initialised
Client receives notification when an irrelevant InitTx is observed
2 parents 7a83a7d + b83f5fa commit cc5f100

File tree

23 files changed

+448
-179
lines changed

23 files changed

+448
-179
lines changed

CHANGELOG.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,22 @@ changes.
1313
- Improved `gen-hydra-keys` command to not overwrite keys if they are present
1414
already.
1515

16+
- Clients are notified when head initialization is ignored via a new
17+
`IgnoredHeadInitializing` API server output. This helps detecting
18+
misconfigurations of credentials and head parameters (which need to match).
19+
[#529](https://github.com/input-output-hk/hydra/issues/529)
20+
1621
- Hydra node API `submit-transaction` endpoint now accepts three types of
1722
encoding: Base16 encoded CBOR string, TextEnvelope type and JSON.
1823

1924
- **BREAKING** Introduce messages resending logic in the `Network`
2025
layer to improve reliability in the face of transient connection
21-
issues
26+
issues.
2227

2328
- Persist network messages on disk in order to gracefully handle crashes
2429

25-
- **BREAKING** Changes to Hydra scripts due to upgrading our toolchain to
26-
GHC 9.6.2.
30+
- **BREAKING** Changes to Hydra script hashes due to upgrading our toolchain to
31+
GHC 9.6
2732

2833
## [0.13.0] - 2023-10-03
2934

fourmolu.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ diff-friendly-import-export: true # 'false' uses Ormolu-style lists
66
respectful: true # don't be too opinionated about newlines etc.
77
haddock-style: single-line # '--' vs. '{-'
88
newlines-between-decls: 1 # number of newlines between top-level declarations
9+
single-constraint-parens: never # whether or not to put braces around single constraints: https://fourmolu.github.io/config/single-constraint-parens/

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

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import CardanoClient (
1919
import CardanoNode (RunningNode (..))
2020
import Control.Lens ((^?))
2121
import Data.Aeson (Value, object, (.=))
22+
import qualified Data.Aeson as Aeson
2223
import Data.Aeson.Lens (key, _JSON)
2324
import Data.Aeson.Types (parseMaybe)
2425
import Data.ByteString (isInfixOf)
@@ -54,6 +55,7 @@ import Hydra.Cardano.Api (
5455
)
5556
import Hydra.Cardano.Api.Prelude (ReferenceScript (..), TxOut (..), TxOutDatum (..))
5657
import Hydra.Chain (HeadId)
58+
import Hydra.Chain.Direct.Tx (assetNameFromVerificationKey)
5759
import Hydra.Cluster.Faucet (createOutputAtAddress, seedFromFaucet, seedFromFaucet_)
5860
import qualified Hydra.Cluster.Faucet as Faucet
5961
import Hydra.Cluster.Fixture (Actor (..), actorName, alice, aliceSk, aliceVk, bob, bobSk, bobVk)
@@ -62,9 +64,20 @@ import Hydra.ContestationPeriod (ContestationPeriod (UnsafeContestationPeriod))
6264
import Hydra.Ledger (IsTx (balance))
6365
import Hydra.Ledger.Cardano (genKeyPair)
6466
import Hydra.Logging (Tracer, traceWith)
65-
import Hydra.Options (ChainConfig, networkId, startChainFrom)
67+
import Hydra.Options (ChainConfig (..), networkId, startChainFrom)
6668
import Hydra.Party (Party)
67-
import HydraNode (EndToEndLog (..), HydraClient, input, output, requestCommitTx, send, waitFor, waitForAllMatch, waitMatch, withHydraNode)
69+
import HydraNode (
70+
EndToEndLog (..),
71+
HydraClient,
72+
input,
73+
output,
74+
requestCommitTx,
75+
send,
76+
waitFor,
77+
waitForAllMatch,
78+
waitMatch,
79+
withHydraNode,
80+
)
6881
import qualified Network.HTTP.Client as L
6982
import Network.HTTP.Req (
7083
HttpException (VanillaHttpException),
@@ -419,7 +432,7 @@ canSubmitTransactionThroughAPI ::
419432
TxId ->
420433
IO ()
421434
canSubmitTransactionThroughAPI tracer workDir node hydraScriptsTxId =
422-
(`finally` returnFundsToFaucet tracer node Alice) $ do
435+
(`finally` returnFundsToFaucet tracer node Alice) $ do
423436
refuelIfNeeded tracer node Alice 25_000_000
424437
aliceChainConfig <- chainConfigFor Alice workDir nodeSocket [] $ UnsafeContestationPeriod 100
425438
let hydraNodeId = 1
@@ -451,7 +464,6 @@ canSubmitTransactionThroughAPI tracer workDir node hydraScriptsTxId =
451464
(sendRequest hydraNodeId signedRequest <&> responseBody)
452465
`shouldReturn` TransactionSubmitted
453466
where
454-
455467
RunningNode{networkId, nodeSocket} = node
456468
sendRequest hydraNodeId tx =
457469
runReq defaultHttpConfig $
@@ -462,6 +474,39 @@ canSubmitTransactionThroughAPI tracer workDir node hydraScriptsTxId =
462474
(Proxy :: Proxy (JsonResponse TransactionSubmitted))
463475
(port $ 4000 + hydraNodeId)
464476

477+
-- | Two hydra node setup where Alice is wrongly configured to use Carol's
478+
-- cardano keys instead of Bob's which will prevent him to be notified the
479+
-- `HeadIsInitializing` but he should still receive some notification.
480+
initWithWrongKeys :: FilePath -> Tracer IO EndToEndLog -> RunningNode -> TxId -> IO ()
481+
initWithWrongKeys workDir tracer node@RunningNode{nodeSocket} hydraScriptsTxId = do
482+
(aliceCardanoVk, _) <- keysFor Alice
483+
(carolCardanoVk, _) <- keysFor Carol
484+
485+
aliceChainConfig <- chainConfigFor Alice workDir nodeSocket [Carol] (UnsafeContestationPeriod 2)
486+
bobChainConfig <- chainConfigFor Bob workDir nodeSocket [Alice] (UnsafeContestationPeriod 2)
487+
488+
withHydraNode tracer aliceChainConfig workDir 3 aliceSk [bobVk] [3, 4] hydraScriptsTxId $ \n1 -> do
489+
withHydraNode tracer bobChainConfig workDir 4 bobSk [aliceVk] [3, 4] hydraScriptsTxId $ \n2 -> do
490+
seedFromFaucet_ node aliceCardanoVk 100_000_000 (contramap FromFaucet tracer)
491+
492+
send n1 $ input "Init" []
493+
headId <-
494+
waitForAllMatch 10 [n1] $
495+
headIsInitializingWith (Set.fromList [alice, bob])
496+
497+
let expectedHashes =
498+
assetNameFromVerificationKey
499+
<$> [aliceCardanoVk, carolCardanoVk]
500+
501+
-- We want the client to observe headId being opened without bob (node 2)
502+
-- being part of it
503+
pubKeyHashes <- waitMatch 10 n2 $ \v -> do
504+
guard $ v ^? key "tag" == Just (Aeson.String "IgnoredHeadInitializing")
505+
guard $ v ^? key "headId" == Just (toJSON headId)
506+
v ^? key "participants" . _JSON
507+
508+
Set.fromList pubKeyHashes `shouldBe` Set.fromList expectedHashes
509+
465510
-- | Refuel given 'Actor' with given 'Lovelace' if current marked UTxO is below that amount.
466511
refuelIfNeeded ::
467512
Tracer IO EndToEndLog ->

hydra-cluster/src/HydraNode.hs

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ send HydraClient{tracer, hydraNodeId, connection} v = do
6060
sendTextData connection (Aeson.encode v)
6161
traceWith tracer $ SentMessage hydraNodeId v
6262

63-
waitNext :: HasCallStack => HydraClient -> IO Aeson.Value
63+
waitNext :: (HasCallStack) => HydraClient -> IO Aeson.Value
6464
waitNext HydraClient{connection} = do
6565
bytes <- receiveData connection
6666
case Aeson.eitherDecode' bytes of
@@ -74,11 +74,11 @@ output tag pairs = object $ ("tag" .= tag) : pairs
7474
-- | Wait some time for a single API server output from each of given nodes.
7575
-- This function waits for @delay@ seconds for message @expected@ to be seen by all
7676
-- given @nodes@.
77-
waitFor :: HasCallStack => Tracer IO EndToEndLog -> DiffTime -> [HydraClient] -> Aeson.Value -> IO ()
77+
waitFor :: (HasCallStack) => Tracer IO EndToEndLog -> DiffTime -> [HydraClient] -> Aeson.Value -> IO ()
7878
waitFor tracer delay nodes v = waitForAll tracer delay nodes [v]
7979

8080
-- | Wait up to some time for an API server output to match the given predicate.
81-
waitMatch :: HasCallStack => DiffTime -> HydraClient -> (Aeson.Value -> Maybe a) -> IO a
81+
waitMatch :: (HasCallStack) => DiffTime -> HydraClient -> (Aeson.Value -> Maybe a) -> IO a
8282
waitMatch delay client@HydraClient{tracer, hydraNodeId} match = do
8383
seenMsgs <- newTVarIO []
8484
timeout delay (go seenMsgs) >>= \case
@@ -122,7 +122,7 @@ waitForAllMatch delay nodes match = do
122122
-- | Wait some time for a list of outputs from each of given nodes.
123123
-- This function is the generalised version of 'waitFor', allowing several messages
124124
-- to be waited for and received in /any order/.
125-
waitForAll :: HasCallStack => Tracer IO EndToEndLog -> DiffTime -> [HydraClient] -> [Aeson.Value] -> IO ()
125+
waitForAll :: (HasCallStack) => Tracer IO EndToEndLog -> DiffTime -> [HydraClient] -> [Aeson.Value] -> IO ()
126126
waitForAll tracer delay nodes expected = do
127127
traceWith tracer (StartWaiting (map hydraNodeId nodes) expected)
128128
forConcurrently_ nodes $ \client@HydraClient{hydraNodeId} -> do
@@ -180,7 +180,7 @@ requestCommitTx :: HydraClient -> UTxO -> IO Tx
180180
requestCommitTx client =
181181
requestCommitTx' client . fmap (`TxOutWithWitness` Nothing)
182182

183-
getMetrics :: HasCallStack => HydraClient -> IO ByteString
183+
getMetrics :: (HasCallStack) => HydraClient -> IO ByteString
184184
getMetrics HydraClient{hydraNodeId} = do
185185
failAfter 3 $
186186
try (runReq defaultHttpConfig request) >>= \case
@@ -208,13 +208,13 @@ data EndToEndLog
208208
| RemainingFunds {actor :: String, utxo :: UTxO}
209209
| PublishedHydraScriptsAt {hydraScriptsTxId :: TxId}
210210
| UsingHydraScriptsAt {hydraScriptsTxId :: TxId}
211-
| CreatedKey { keyPath :: FilePath }
211+
| CreatedKey {keyPath :: FilePath}
212212
deriving (Eq, Show, Generic, ToJSON, FromJSON, ToObject)
213213

214214
-- XXX: The two lists need to be of same length. Also the verification keys can
215215
-- be derived from the signing keys.
216216
withHydraCluster ::
217-
HasCallStack =>
217+
(HasCallStack) =>
218218
Tracer IO EndToEndLog ->
219219
FilePath ->
220220
SocketPath ->
@@ -229,7 +229,28 @@ withHydraCluster ::
229229
ContestationPeriod ->
230230
(NonEmpty HydraClient -> IO a) ->
231231
IO a
232-
withHydraCluster tracer workDir nodeSocket firstNodeId allKeys hydraKeys hydraScriptsTxId contestationPeriod action = do
232+
withHydraCluster tracer workDir nodeSocket firstNodeId allKeys hydraKeys hydraScriptsTxId contestationPeriod action =
233+
withConfiguredHydraCluster tracer workDir nodeSocket firstNodeId allKeys hydraKeys hydraScriptsTxId (const $ id) contestationPeriod action
234+
235+
withConfiguredHydraCluster ::
236+
(HasCallStack) =>
237+
Tracer IO EndToEndLog ->
238+
FilePath ->
239+
SocketPath ->
240+
-- | First node id
241+
-- This sets the starting point for assigning ports
242+
Int ->
243+
-- | NOTE: This decides on the size of the cluster!
244+
[(VerificationKey PaymentKey, SigningKey PaymentKey)] ->
245+
[SigningKey HydraKey] ->
246+
-- | Transaction id at which Hydra scripts should have been published.
247+
TxId ->
248+
-- | Modifies the `ChainConfig` passed to a node upon startup
249+
(Int -> ChainConfig -> ChainConfig) ->
250+
ContestationPeriod ->
251+
(NonEmpty HydraClient -> IO a) ->
252+
IO a
253+
withConfiguredHydraCluster tracer workDir nodeSocket firstNodeId allKeys hydraKeys hydraScriptsTxId chainConfigDecorator contestationPeriod action = do
233254
when (clusterSize == 0) $
234255
failure "Cannot run a cluster with 0 number of nodes"
235256
when (length allKeys /= length hydraKeys) $
@@ -248,24 +269,25 @@ withHydraCluster tracer workDir nodeSocket firstNodeId allKeys hydraKeys hydraSc
248269
startNodes clients = \case
249270
[] -> action (fromList $ reverse clients)
250271
(nodeId : rest) -> do
251-
let hydraSKey = hydraKeys Prelude.!! (nodeId - firstNodeId)
252-
hydraVKeys = map getVerificationKey $ filter (/= hydraSKey) hydraKeys
253-
cardanoVerificationKeys = [workDir </> show i <.> "vk" | i <- allNodeIds, i /= nodeId]
272+
let hydraSigningKey = hydraKeys Prelude.!! (nodeId - firstNodeId)
273+
hydraVerificationKeys = map getVerificationKey $ filter (/= hydraSigningKey) hydraKeys
254274
cardanoSigningKey = workDir </> show nodeId <.> "sk"
275+
cardanoVerificationKeys = [workDir </> show i <.> "vk" | i <- allNodeIds, i /= nodeId]
255276
chainConfig =
256-
defaultChainConfig
257-
{ nodeSocket
258-
, cardanoSigningKey
259-
, cardanoVerificationKeys
260-
, contestationPeriod
261-
}
277+
chainConfigDecorator nodeId $
278+
defaultChainConfig
279+
{ nodeSocket
280+
, cardanoSigningKey
281+
, cardanoVerificationKeys
282+
, contestationPeriod
283+
}
262284
withHydraNode
263285
tracer
264286
chainConfig
265287
workDir
266288
nodeId
267-
hydraSKey
268-
hydraVKeys
289+
hydraSigningKey
290+
hydraVerificationKeys
269291
allNodeIds
270292
hydraScriptsTxId
271293
(\c -> startNodes (c : clients) rest)
@@ -384,7 +406,7 @@ withConnectionToNode tracer hydraNodeId action = do
384406
hydraNodeProcess :: RunOptions -> CreateProcess
385407
hydraNodeProcess = proc "hydra-node" . toArgs
386408

387-
waitForNodesConnected :: HasCallStack => Tracer IO EndToEndLog -> [HydraClient] -> IO ()
409+
waitForNodesConnected :: (HasCallStack) => Tracer IO EndToEndLog -> [HydraClient] -> IO ()
388410
waitForNodesConnected tracer clients =
389411
mapM_ waitForNodeConnected clients
390412
where

hydra-cluster/test/Test/EndToEndSpec.hs

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,7 @@ import Hydra.Cluster.Fixture (
5555
carolSk,
5656
carolVk,
5757
)
58-
import Hydra.Cluster.Scenarios (
59-
canCloseWithLongContestationPeriod,
60-
canSubmitTransactionThroughAPI,
61-
headIsInitializingWith,
62-
refuelIfNeeded,
63-
restartedNodeCanAbort,
64-
restartedNodeCanObserveCommitTx,
65-
singlePartyCannotCommitExternallyWalletUtxo,
66-
singlePartyCommitsExternalScriptWithInlineDatum,
67-
singlePartyCommitsFromExternalScript,
68-
singlePartyHeadFullLifeCycle,
69-
)
58+
import Hydra.Cluster.Scenarios (canCloseWithLongContestationPeriod, canSubmitTransactionThroughAPI, headIsInitializingWith, initWithWrongKeys, refuelIfNeeded, restartedNodeCanAbort, restartedNodeCanObserveCommitTx, singlePartyCannotCommitExternallyWalletUtxo, singlePartyCommitsExternalScriptWithInlineDatum, singlePartyCommitsFromExternalScript, singlePartyHeadFullLifeCycle)
7059
import Hydra.Cluster.Util (chainConfigFor, keysFor)
7160
import Hydra.ContestationPeriod (ContestationPeriod (UnsafeContestationPeriod))
7261
import Hydra.Crypto (generateSigningKey)
@@ -351,6 +340,13 @@ spec = around showLogsOnFailure $
351340
(initAndClose tmpDir tracer 0 hydraScriptsTxId node)
352341
(initAndClose tmpDir tracer 1 hydraScriptsTxId node)
353342

343+
it "alice inits a Head with incorrect keys preventing bob from observing InitTx" $ \tracer ->
344+
failAfter 60 $
345+
withClusterTempDir "incorrect-cardano-keys" $ \tmpDir -> do
346+
withCardanoNodeDevnet (contramap FromCardanoNode tracer) tmpDir $ \node -> do
347+
publishHydraScriptsAs node Faucet
348+
>>= initWithWrongKeys tmpDir tracer node
349+
354350
it "bob cannot abort alice's head" $ \tracer -> do
355351
failAfter 60 $
356352
withClusterTempDir "two-heads-cant-abort" $ \tmpDir -> do
@@ -533,14 +529,6 @@ initAndClose tmpDir tracer clusterIx hydraScriptsTxId node@RunningNode{nodeSocke
533529
bobKeys@(bobCardanoVk, _) <- generate genKeyPair
534530
carolKeys@(carolCardanoVk, _) <- generate genKeyPair
535531

536-
let aliceSk = generateSigningKey ("alice-" <> show clusterIx)
537-
let bobSk = generateSigningKey ("bob-" <> show clusterIx)
538-
let carolSk = generateSigningKey ("carol-" <> show clusterIx)
539-
540-
let alice = deriveParty aliceSk
541-
let bob = deriveParty bobSk
542-
let carol = deriveParty carolSk
543-
544532
let cardanoKeys = [aliceKeys, bobKeys, carolKeys]
545533
hydraKeys = [aliceSk, bobSk, carolSk]
546534

hydra-node/exe/hydra-node/Main.hs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ main = do
101101
RunOptions{apiHost, apiPort} = opts
102102
apiPersistence <- createPersistenceIncremental $ persistenceDir <> "/server-output"
103103
withAPIServer apiHost apiPort party apiPersistence (contramap APIServer tracer) chain pparams (putEvent . ClientEvent) $ \server -> do
104-
105104
-- Network
106105
withNetwork tracer persistenceDir (connectionMessages server) signingKey otherParties host port peers nodeId putNetworkEvent $ \hn -> do
107106
-- Main loop

hydra-node/hydra-node.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ library
9999
Hydra.HeadLogic
100100
Hydra.HeadLogic.Error
101101
Hydra.HeadLogic.Event
102+
Hydra.HeadLogic.Heads
102103
Hydra.HeadLogic.Outcome
103104
Hydra.HeadLogic.SnapshotOutcome
104105
Hydra.HeadLogic.State
@@ -147,7 +148,6 @@ library
147148
, cardano-ledger-core
148149
, cardano-ledger-mary
149150
, cardano-ledger-shelley
150-
, cardano-prelude
151151
, cardano-slotting
152152
, cardano-strict-containers
153153
, cborg

hydra-node/json-schemas/api.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,39 @@ components:
852852
additionalProperties: false
853853
$ref: "https://raw.githubusercontent.com/CardanoSolutions/cardanonical/main/cardano.json#/definitions/ProtocolParameters"
854854

855+
IgnoredHeadInitializing:
856+
title: IgnoredHeadInitializing
857+
description: |
858+
A `Init` transaction has been observed on-chain, with the given HeadId
859+
and the given participant identifiers (commonly public key hashes), but
860+
we are not part of it. It could denote a misconfiguration, or simply
861+
some other group of parties opening a head.
862+
payload:
863+
type: object
864+
required:
865+
- tag
866+
- headId
867+
- participants
868+
- seq
869+
- timestamp
870+
properties:
871+
tag:
872+
type: string
873+
enum: ["IgnoredHeadInitializing"]
874+
headId:
875+
$ref: "api.yaml#/components/schemas/HeadId"
876+
participants:
877+
type: array
878+
items:
879+
type: string
880+
contentEncoding: base16
881+
description: |
882+
A hex-encoded string identifying a participant on-chain (usually a hash of a key)
883+
seq:
884+
$ref: "api.yaml#/components/schemas/SequenceNumber"
885+
timestamp:
886+
$ref: "api.yaml#/components/schemas/UTCTime"
887+
855888
########
856889
#
857890
# Schemas

0 commit comments

Comments
 (0)