Skip to content

Commit 1fccdb0

Browse files
committed
PR review
Signed-off-by: Sasha Bogicevic <[email protected]>
1 parent f30833c commit 1fccdb0

24 files changed

+705
-672
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ changes.
3030
- Change to the `ReqSn` message in the Hydra network protocol
3131
- Added `DepositExpired` for when a deposit was deemed expired.
3232

33+
- Enable blockfrost integration for hydra-node.
34+
3335
## [0.21.0] - 2025-04-28
3436

3537
- New metric for counting the number of active peers: `hydra_head_peers_connected`
@@ -49,8 +51,6 @@ changes.
4951
ETCD_AUTO_COMPACTION_RETENTION=168h
5052
```
5153
52-
- Enable blockfrost integration for hydra-node.
53-
5454
- Changed default contestation period to 600 seconds and deposit deadline to 3600 seconds.
5555
5656
- Introduce an option to publish hydra scripts using blockfrost.

hydra-cluster/hydra-cluster.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ test-suite tests
144144
other-modules:
145145
Paths_hydra_cluster
146146
Spec
147+
Test.BlockfrostChainSpec
147148
Test.CardanoClientSpec
148149
Test.CardanoNodeSpec
149150
Test.ChainObserverSpec

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
{-# LANGUAGE DuplicateRecordFields #-}
2+
13
module Hydra.Cluster.Faucet where
24

35
import Hydra.Cardano.Api
@@ -26,12 +28,14 @@ import Data.Set qualified as Set
2628
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
2729
import GHC.IO.Exception (IOErrorType (ResourceExhausted), IOException (ioe_type))
2830
import Hydra.Chain.Blockfrost.Client qualified as Blockfrost
31+
import Hydra.Chain.Direct (DirectBackend (..))
2932
import Hydra.Chain.ScriptRegistry (
3033
publishHydraScripts,
3134
)
3235
import Hydra.Cluster.Fixture (Actor (Faucet))
3336
import Hydra.Cluster.Util (keysFor)
3437
import Hydra.Ledger.Cardano ()
38+
import Hydra.Options (DirectOptions (..))
3539
import Hydra.Tx (balance, txId)
3640

3741
data FaucetException
@@ -255,6 +259,6 @@ retryOnExceptions tracer action =
255259
publishHydraScriptsAs :: RunningNode -> Actor -> IO [TxId]
256260
publishHydraScriptsAs RunningNode{networkId, nodeSocket} actor = do
257261
(_, sk) <- keysFor actor
258-
txIds <- publishHydraScripts networkId nodeSocket sk
262+
txIds <- publishHydraScripts (DirectBackend $ DirectOptions{networkId, nodeSocket}) sk
259263
mapM_ (awaitTransactionId networkId nodeSocket) txIds
260264
pure txIds

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ restartedNodeCanAbort tracer workDir cardanoNode hydraScriptsTxId = do
287287
chainConfigFor Alice workDir nodeSocket hydraScriptsTxId [] 2
288288
-- we delibelately do not start from a chain point here to highlight the
289289
-- need for persistence
290-
<&> modifyConfig (\config -> config{startChainFrom = Nothing, chainBackend = Direct defaultDirectBackend{networkId}})
290+
<&> modifyConfig (\config -> config{startChainFrom = Nothing, chainBackendOptions = Direct defaultDirectOptions{networkId}})
291291

292292
let hydraTracer = contramap FromHydraNode tracer
293293
headId1 <- withHydraNode hydraTracer aliceChainConfig workDir 1 aliceSk [] [1] $ \n1 -> do
@@ -314,11 +314,11 @@ nodeReObservesOnChainTxs tracer workDir cardanoNode hydraScriptsTxId = do
314314
let contestationPeriod = 2
315315
aliceChainConfig <-
316316
chainConfigFor Alice workDir nodeSocket hydraScriptsTxId [Bob] contestationPeriod depositDeadline
317-
<&> modifyConfig (\config -> config{startChainFrom = Nothing, chainBackend = Direct defaultDirectBackend{networkId}})
317+
<&> modifyConfig (\config -> config{startChainFrom = Nothing, chainBackendOptions = Direct defaultDirectOptions{networkId}})
318318

319319
bobChainConfig <-
320320
chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice] contestationPeriod depositDeadline
321-
<&> modifyConfig (\config -> config{startChainFrom = Nothing, chainBackend = Direct defaultDirectBackend{networkId}})
321+
<&> modifyConfig (\config -> config{startChainFrom = Nothing, chainBackendOptions = Direct defaultDirectOptions{networkId}})
322322

323323
(aliceCardanoVk, aliceCardanoSk) <- keysFor Alice
324324
commitUTxO <- seedFromFaucet cardanoNode aliceCardanoVk 5_000_000 (contramap FromFaucet tracer)
@@ -391,7 +391,7 @@ nodeReObservesOnChainTxs tracer workDir cardanoNode hydraScriptsTxId = do
391391

392392
bobChainConfigFromTip <-
393393
chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice] contestationPeriod depositDeadline
394-
<&> modifyConfig (\config -> config{startChainFrom = Just tip, chainBackend = Direct defaultDirectBackend{networkId}})
394+
<&> modifyConfig (\config -> config{startChainFrom = Just tip, chainBackendOptions = Direct defaultDirectOptions{networkId}})
395395

396396
withTempDir "blank-state" $ \tmpDir -> do
397397
void $ readCreateProcessWithExitCode (proc "cp" ["-r", workDir </> "state-2", tmpDir]) ""
@@ -450,7 +450,7 @@ singlePartyHeadFullLifeCycle tracer workDir node hydraScriptsTxId =
450450
contestationPeriod <- CP.fromNominalDiffTime $ 10 * blockTime
451451
aliceChainConfig <-
452452
chainConfigFor Alice workDir nodeSocket hydraScriptsTxId [] contestationPeriod depositDeadline
453-
<&> modifyConfig (\config -> config{startChainFrom = Just tip, chainBackend = Direct defaultDirectBackend{networkId}})
453+
<&> modifyConfig (\config -> config{startChainFrom = Just tip, chainBackendOptions = Direct defaultDirectOptions{networkId}})
454454
withHydraNode hydraTracer aliceChainConfig workDir 1 aliceSk [] [1] $ \n1 -> do
455455
-- Initialize & open head
456456
send n1 $ input "Init" []
@@ -506,7 +506,7 @@ singlePartyOpenAHead tracer workDir node hydraScriptsTxId callback =
506506
let contestationPeriod = 100
507507
aliceChainConfig <-
508508
chainConfigFor Alice workDir nodeSocket hydraScriptsTxId [] contestationPeriod depositDeadline
509-
<&> modifyConfig (\config -> config{startChainFrom = Just tip, chainBackend = Direct defaultDirectBackend{networkId}})
509+
<&> modifyConfig (\config -> config{startChainFrom = Just tip, chainBackendOptions = Direct defaultDirectOptions{networkId}})
510510

511511
(walletVk, walletSk) <- generate genKeyPair
512512
let keyPath = workDir <> "/wallet.sk"
@@ -961,7 +961,7 @@ canCloseWithLongContestationPeriod tracer workDir node hydraScriptsTxId = do
961961
let oneWeek = 60 * 60 * 24 * 7
962962
aliceChainConfig <-
963963
chainConfigFor Alice workDir nodeSocket hydraScriptsTxId [] oneWeek depositDeadline
964-
<&> modifyConfig (\config -> config{startChainFrom = Just tip, chainBackend = Direct defaultDirectBackend{networkId}})
964+
<&> modifyConfig (\config -> config{startChainFrom = Just tip, chainBackendOptions = Direct defaultDirectOptions{networkId}})
965965
let hydraTracer = contramap FromHydraNode tracer
966966
withHydraNode hydraTracer aliceChainConfig workDir 1 aliceSk [] [1] $ \n1 -> do
967967
-- Initialize & open head

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import Hydra.Cardano.Api (
1717
textEnvelopeToJSON,
1818
)
1919
import Hydra.Cluster.Fixture (Actor, actorName, fundsOf)
20-
import Hydra.Options (BlockfrostBackend (..), CardanoChainConfig (..), ChainBackend (..), ChainConfig (..), DirectBackend (..), defaultCardanoChainConfig, defaultDirectBackend)
20+
import Hydra.Options (BlockfrostOptions (..), CardanoChainConfig (..), ChainBackendOptions (..), ChainConfig (..), DirectOptions (..), defaultCardanoChainConfig, defaultDirectOptions)
2121
import Hydra.Tx.ContestationPeriod (ContestationPeriod)
2222
import Paths_hydra_cluster qualified as Pkg
2323
import System.FilePath ((<.>), (</>))
@@ -99,10 +99,10 @@ chainConfigFor' me targetDir socketOrProjectPath hydraScriptsTxId them contestat
9999
, cardanoVerificationKeys = [actorFilePath himOrHer "vk" | himOrHer <- them]
100100
, contestationPeriod
101101
, depositDeadline
102-
, chainBackend =
102+
, chainBackendOptions =
103103
case socketOrProjectPath of
104-
Left projectPath -> Blockfrost BlockfrostBackend{projectPath}
105-
Right nodeSocket -> Direct defaultDirectBackend{nodeSocket = nodeSocket}
104+
Left projectPath -> Blockfrost BlockfrostOptions{projectPath}
105+
Right nodeSocket -> Direct defaultDirectOptions{nodeSocket = nodeSocket}
106106
}
107107
where
108108
actorFilePath actor fileType = targetDir </> actorFileName actor fileType
@@ -120,8 +120,8 @@ modifyConfig fn = \case
120120

121121
setNetworkId :: NetworkId -> ChainConfig -> ChainConfig
122122
setNetworkId networkId = \case
123-
Cardano config@CardanoChainConfig{chainBackend} ->
124-
case chainBackend of
125-
Direct direct@DirectBackend{} -> Cardano config{chainBackend = Direct direct{networkId = networkId}}
123+
Cardano config@CardanoChainConfig{chainBackendOptions} ->
124+
case chainBackendOptions of
125+
Direct direct@DirectOptions{} -> Cardano config{chainBackendOptions = Direct direct{networkId = networkId}}
126126
_ -> Cardano config
127127
x -> x

hydra-cluster/src/HydraNode.hs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import Hydra.HeadLogic.State (SeenSnapshot)
2626
import Hydra.Logging (Tracer, Verbosity (..), traceWith)
2727
import Hydra.Network (Host (Host), NodeId (NodeId), WhichEtcd (EmbeddedEtcd))
2828
import Hydra.Network qualified as Network
29-
import Hydra.Options (BlockfrostBackend (..), CardanoChainConfig (..), ChainBackend (..), ChainConfig (..), DirectBackend (..), LedgerConfig (..), RunOptions (..), defaultCardanoChainConfig, defaultDirectBackend, nodeSocket, toArgs)
29+
import Hydra.Options (BlockfrostOptions (..), CardanoChainConfig (..), ChainBackendOptions (..), ChainConfig (..), DirectOptions (..), LedgerConfig (..), RunOptions (..), defaultCardanoChainConfig, defaultDirectOptions, nodeSocket, toArgs)
3030
import Hydra.Tx (ConfirmedSnapshot)
3131
import Hydra.Tx.ContestationPeriod (ContestationPeriod)
3232
import Hydra.Tx.Crypto (HydraKey)
@@ -318,9 +318,9 @@ withHydraCluster tracer workDir nodeSocket firstNodeId allKeys hydraKeys hydraSc
318318
, cardanoVerificationKeys
319319
, contestationPeriod
320320
, depositDeadline
321-
, chainBackend =
321+
, chainBackendOptions =
322322
Direct
323-
defaultDirectBackend
323+
defaultDirectOptions
324324
{ nodeSocket = nodeSocket
325325
}
326326
}
@@ -349,12 +349,12 @@ preparePParams chainConfig stateDir paramsDecorator = do
349349
Offline _ ->
350350
readConfigFile "protocol-parameters.json"
351351
>>= writeFileBS cardanoLedgerProtocolParametersFile
352-
Cardano CardanoChainConfig{chainBackend} -> do
353-
protocolParameters <- case chainBackend of
354-
Direct DirectBackend{networkId, nodeSocket} ->
352+
Cardano CardanoChainConfig{chainBackendOptions} -> do
353+
protocolParameters <- case chainBackendOptions of
354+
Direct DirectOptions{networkId, nodeSocket} ->
355355
-- NOTE: This implicitly tests of cardano-cli with hydra-node
356356
cliQueryProtocolParameters nodeSocket networkId
357-
Blockfrost BlockfrostBackend{projectPath} -> do
357+
Blockfrost BlockfrostOptions{projectPath} -> do
358358
prj <- Blockfrost.projectFromFile projectPath
359359
toJSON <$> Blockfrost.runBlockfrostM prj Blockfrost.queryProtocolParameters
360360
Aeson.encodeFile cardanoLedgerProtocolParametersFile $
@@ -488,12 +488,12 @@ withHydraNode tracer chainConfig workDir hydraNodeId hydraSKey hydraVKeys allNod
488488
Offline _ ->
489489
readConfigFile "protocol-parameters.json"
490490
>>= writeFileBS cardanoLedgerProtocolParametersFile
491-
Cardano CardanoChainConfig{chainBackend} -> do
492-
protocolParameters <- case chainBackend of
493-
Direct DirectBackend{networkId, nodeSocket} ->
491+
Cardano CardanoChainConfig{chainBackendOptions} -> do
492+
protocolParameters <- case chainBackendOptions of
493+
Direct DirectOptions{networkId, nodeSocket} ->
494494
-- NOTE: This implicitly tests of cardano-cli with hydra-node
495495
cliQueryProtocolParameters nodeSocket networkId
496-
Blockfrost BlockfrostBackend{projectPath} -> do
496+
Blockfrost BlockfrostOptions{projectPath} -> do
497497
prj <- Blockfrost.projectFromFile projectPath
498498
toJSON <$> Blockfrost.runBlockfrostM prj Blockfrost.queryProtocolParameters
499499

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
{-# LANGUAGE DuplicateRecordFields #-}
2+
3+
module Test.BlockfrostChainSpec where
4+
5+
import Hydra.Prelude
6+
import Test.Hydra.Prelude
7+
8+
import Cardano.Api.UTxO qualified as UTxO
9+
import CardanoClient (
10+
buildAddress,
11+
)
12+
import Control.Concurrent.STM (newEmptyTMVarIO, takeTMVar)
13+
import Control.Concurrent.STM.TMVar (putTMVar)
14+
import Data.List qualified as List
15+
import Hydra.Chain (
16+
Chain (Chain, draftCommitTx, postTx),
17+
ChainEvent (..),
18+
OnChainTx (..),
19+
PostChainTx (..),
20+
initHistory,
21+
)
22+
import Hydra.Chain.Blockfrost (BlockfrostBackend (..), withBlockfrostChain)
23+
import Hydra.Chain.Blockfrost.Client qualified as Blockfrost
24+
import Hydra.Chain.Direct (loadChainContext, mkTinyWallet)
25+
import Hydra.Chain.Direct.Handlers (DirectChainLog)
26+
import Hydra.Chain.Direct.State (initialChainState)
27+
import Hydra.Chain.ScriptRegistry (publishHydraScripts)
28+
import Hydra.Cluster.Faucet (
29+
seedFromFaucetBlockfrost,
30+
)
31+
import Hydra.Cluster.Fixture (
32+
Actor (Alice, Faucet),
33+
alice,
34+
aliceSk,
35+
blockfrostcperiod,
36+
ddeadline,
37+
)
38+
import Hydra.Cluster.Util (chainConfigFor', keysFor)
39+
import Hydra.Ledger.Cardano (Tx)
40+
import Hydra.Logging (Tracer, showLogsOnFailure)
41+
import Hydra.Options (
42+
BlockfrostOptions (..),
43+
CardanoChainConfig (..),
44+
ChainBackendOptions (..),
45+
ChainConfig (..),
46+
)
47+
import Hydra.Tx.BlueprintTx (CommitBlueprintTx (..))
48+
import Hydra.Tx.Crypto (aggregate, sign)
49+
import Hydra.Tx.HeadParameters (HeadParameters (..))
50+
import Hydra.Tx.IsTx (IsTx (..))
51+
import Hydra.Tx.Party (Party)
52+
import Hydra.Tx.Snapshot (ConfirmedSnapshot (..), Snapshot (..))
53+
import Hydra.Tx.Snapshot qualified as Snapshot
54+
import Test.DirectChainSpec (
55+
CardanoChainTest (..),
56+
DirectChainTestLog (..),
57+
externalCommit',
58+
hasInitTxWith,
59+
loadParticipants,
60+
observesInTime',
61+
observesInTimeSatisfying',
62+
waitMatch,
63+
)
64+
import Test.Hydra.Tx.Gen (genKeyPair)
65+
import Test.QuickCheck (generate)
66+
67+
spec :: Spec
68+
spec = around (showLogsOnFailure "BlockfrostChainSpec") $ do
69+
it "can open, close & fanout a Head using Blockfrost" $ \tracer -> do
70+
withTempDir "hydra-cluster" $ \tmp -> do
71+
(vk, sk) <- keysFor Faucet
72+
let projectPath = "./../blockfrost-project.txt"
73+
prj <- Blockfrost.projectFromFile projectPath
74+
(aliceCardanoVk, _) <- keysFor Alice
75+
(aliceExternalVk, aliceExternalSk) <- generate genKeyPair
76+
hydraScriptsTxId <- publishHydraScripts (BlockfrostBackend $ BlockfrostOptions{projectPath}) sk
77+
78+
Blockfrost.Genesis
79+
{ _genesisNetworkMagic
80+
, _genesisSystemStart
81+
} <-
82+
Blockfrost.runBlockfrostM prj Blockfrost.queryGenesisParameters
83+
84+
let networkId = Blockfrost.toCardanoNetworkId _genesisNetworkMagic
85+
let faucetAddress = buildAddress vk networkId
86+
-- wait to see the last txid propagated on the blockfrost network
87+
void $ Blockfrost.runBlockfrostM prj $ Blockfrost.awaitUTxO networkId [faucetAddress] (List.last hydraScriptsTxId) 100
88+
89+
-- Alice setup
90+
aliceChainConfig <- chainConfigFor' Alice tmp (Left projectPath) hydraScriptsTxId [] blockfrostcperiod ddeadline
91+
92+
withBlockfrostChainTest (contramap (FromBlockfrostChain "alice") tracer) aliceChainConfig alice $
93+
\aliceChain@CardanoChainTest{postTx} -> do
94+
_ <- Blockfrost.runBlockfrostM prj $ seedFromFaucetBlockfrost aliceCardanoVk 100_000_000
95+
someUTxO <- Blockfrost.runBlockfrostM prj $ seedFromFaucetBlockfrost aliceExternalVk 7_000_000
96+
-- Scenario
97+
participants <- loadParticipants [Alice]
98+
let headParameters = HeadParameters blockfrostcperiod [alice]
99+
postTx $ InitTx{participants, headParameters}
100+
(headId, headSeed) <- observesInTimeSatisfying' aliceChain 500 $ hasInitTxWith headParameters participants
101+
102+
let blueprintTx = txSpendingUTxO someUTxO
103+
externalCommit' (Left projectPath) aliceChain [aliceExternalSk] headId someUTxO blueprintTx
104+
aliceChain `observesInTime'` OnCommitTx headId alice someUTxO
105+
106+
postTx $ CollectComTx someUTxO headId headParameters
107+
aliceChain `observesInTime'` OnCollectComTx{headId}
108+
109+
let snapshotVersion = 0
110+
let snapshot =
111+
Snapshot
112+
{ headId
113+
, number = 1
114+
, utxo = someUTxO
115+
, confirmed = []
116+
, utxoToCommit = Nothing
117+
, utxoToDecommit = Nothing
118+
, version = snapshotVersion
119+
}
120+
121+
postTx $ CloseTx headId headParameters snapshotVersion (ConfirmedSnapshot{snapshot, signatures = aggregate [sign aliceSk snapshot]})
122+
123+
deadline <-
124+
waitMatch aliceChain $ \case
125+
Observation{observedTx = OnCloseTx{snapshotNumber, contestationDeadline}}
126+
| snapshotNumber == 1 -> Just contestationDeadline
127+
_ -> Nothing
128+
129+
waitMatch aliceChain $ \case
130+
Tick t _ | t > deadline -> Just ()
131+
_ -> Nothing
132+
postTx $
133+
FanoutTx
134+
{ utxo = Snapshot.utxo snapshot
135+
, utxoToCommit = Nothing
136+
, utxoToDecommit = Nothing
137+
, headSeed
138+
, contestationDeadline = deadline
139+
}
140+
let expectedUTxO =
141+
(Snapshot.utxo snapshot <> fromMaybe mempty (Snapshot.utxoToCommit snapshot))
142+
`withoutUTxO` fromMaybe mempty (Snapshot.utxoToDecommit snapshot)
143+
observesInTimeSatisfying' aliceChain 500 $ \case
144+
OnFanoutTx _ finalUTxO ->
145+
if UTxO.containsOutputs finalUTxO expectedUTxO
146+
then pure ()
147+
else failure "OnFanoutTx does not contain expected UTxO"
148+
_ -> failure "expected OnFanoutTx"
149+
150+
-- | Wrapper around 'withBlockfrostChain' that threads a 'ChainStateType tx' through
151+
-- 'postTx' and 'waitCallback' calls.
152+
withBlockfrostChainTest ::
153+
Tracer IO DirectChainLog ->
154+
ChainConfig ->
155+
Party ->
156+
(CardanoChainTest Tx IO -> IO a) ->
157+
IO a
158+
withBlockfrostChainTest tracer config party action = do
159+
(configuration, backend) <-
160+
case config of
161+
Cardano cfg@CardanoChainConfig{chainBackendOptions} ->
162+
case chainBackendOptions of
163+
Blockfrost blockfrostOptions -> pure (cfg, BlockfrostBackend blockfrostOptions)
164+
_ -> failure $ "unexpected chainBackendOptions: " <> show chainBackendOptions
165+
otherConfig -> failure $ "unexpected chainConfig: " <> show otherConfig
166+
ctx <- loadChainContext backend configuration party
167+
eventMVar <- newEmptyTMVarIO
168+
169+
let callback event = atomically $ putTMVar eventMVar event
170+
171+
wallet <- mkTinyWallet backend tracer configuration
172+
withBlockfrostChain backend tracer configuration ctx wallet (initHistory initialChainState) callback $ \Chain{postTx, draftCommitTx} -> do
173+
action
174+
CardanoChainTest
175+
{ postTx
176+
, waitCallback = atomically $ takeTMVar eventMVar
177+
, draftCommitTx = \headId utxo blueprintTx -> do
178+
eTx <- draftCommitTx headId $ CommitBlueprintTx{lookupUTxO = utxo, blueprintTx}
179+
case eTx of
180+
Left e -> throwIO e
181+
Right tx -> pure tx
182+
}

0 commit comments

Comments
 (0)