Skip to content

Commit

Permalink
Merge pull request #1118 from SundaeSwap-finance/offline-mode-nicer
Browse files Browse the repository at this point in the history
Offline mode implementation
  • Loading branch information
ch1bo authored Dec 19, 2023
2 parents d14ff60 + bea0b76 commit c92753a
Show file tree
Hide file tree
Showing 22 changed files with 1,681 additions and 645 deletions.
Binary file removed .DS_Store
Binary file not shown.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ changes.
- Renamed `HasInlineDatums` type class to `IsBabbageEraOnwards`. Use
`babbageEraOnwards` to produce witnesses for features from babbage onwards.

- New top-level offline mode command, `offline`
- Initializes ledger via `--initial-utxo` parameter, and does not connect to a
cardano-node.
- Hydra.Options split into Hydra.Options.Common, Hydra.Options.Offline,
Hydra.Options.Online, re-exported from Hydra.Options.


## [0.14.0] - 2023-12-04

Expand Down
80 changes: 76 additions & 4 deletions hydra-cluster/src/Hydra/Cluster/Util.hs
Original file line number Diff line number Diff line change
@@ -1,25 +1,42 @@
{-# LANGUAGE AllowAmbiguousTypes #-}

-- | Utilities used across hydra-cluster
module Hydra.Cluster.Util where

import Hydra.Prelude

import Data.Aeson qualified as Aeson
import Data.ByteString qualified as BS
import Data.Map qualified as Map
import Hydra.Cardano.Api (
Address,
AsType (AsPaymentKey, AsSigningKey),
HasTypeProxy (AsType),
Key (VerificationKey, getVerificationKey),
IsMaryEraOnwards,
IsShelleyBasedEra,
Key (VerificationKey, getVerificationKey, verificationKeyHash),
NetworkId,
PaymentKey,
ShelleyAddr,
SigningKey,
SocketPath,
StakeAddressReference (NoStakeAddress),
TextEnvelopeError (TextEnvelopeAesonDecodeError),
Tx,
UTxO' (UTxO),
VerificationKey (GenesisUTxOVerificationKey, PaymentVerificationKey),
deserialiseFromTextEnvelope,
genesisUTxOPseudoTxIn,
mkTxOutValue,
mkVkAddress,
textEnvelopeToJSON,
)
import Hydra.Cardano.Api.Prelude (PaymentCredential (PaymentCredentialByKey), ReferenceScript (ReferenceScriptNone), TxOut (TxOut), TxOutDatum (TxOutDatumNone), Value, makeShelleyAddress)
import Hydra.Cluster.Fixture (Actor, actorName)
import Hydra.ContestationPeriod (ContestationPeriod)
import Hydra.Ledger (IsTx (UTxOType))
import Hydra.Ledger.Cardano (genSigningKey)
import Hydra.Options (ChainConfig (..), defaultChainConfig)
import Hydra.Options (ChainConfig (..), OfflineConfig (OfflineConfig, initialUTxOFile, ledgerGenesisFile), defaultChainConfig)
import Paths_hydra_cluster qualified as Pkg
import System.FilePath ((<.>), (</>))
import Test.Hydra.Prelude (failure)
Expand Down Expand Up @@ -59,8 +76,63 @@ createAndSaveSigningKey path = do
writeFileLBS path $ textEnvelopeToJSON (Just "Key used to commit funds into a Head") sk
pure sk

offlineConfigFor :: [(Actor, Value)] -> FilePath -> NetworkId -> IO OfflineConfig
offlineConfigFor actorToVal targetDir networkId = do
initialUtxoForActors actorToVal networkId >>= offlineConfigForUTxO @Tx targetDir

offlineConfigForUTxO :: forall tx. IsTx tx => FilePath -> UTxOType tx -> IO OfflineConfig
offlineConfigForUTxO targetDir utxo = do
utxoPath <- seedInitialUTxOFromOffline @tx targetDir utxo
pure $
OfflineConfig
{ initialUTxOFile = utxoPath
, ledgerGenesisFile = Nothing
}

seedInitialUTxOFromOffline :: IsTx tx => FilePath -> UTxOType tx -> IO FilePath
seedInitialUTxOFromOffline targetDir utxo = do
let destinationPath = targetDir </> "utxo.json"
writeFileBS destinationPath . toStrict . Aeson.encode $ utxo

pure destinationPath

buildAddress :: VerificationKey PaymentKey -> NetworkId -> Address ShelleyAddr
buildAddress vKey networkId =
makeShelleyAddress networkId (PaymentCredentialByKey $ verificationKeyHash vKey) NoStakeAddress

initialUtxoWithFunds ::
forall era ctx.
(IsShelleyBasedEra era, IsMaryEraOnwards era) =>
NetworkId ->
[(VerificationKey PaymentKey, Value)] ->
IO (UTxO' (TxOut ctx era))
initialUtxoWithFunds networkId valueMap =
pure
. UTxO
. Map.fromList
. map (\(vKey, val) -> (txin vKey, txout vKey val))
$ valueMap
where
txout vKey val =
TxOut
(mkVkAddress networkId vKey)
(mkTxOutValue val)
TxOutDatumNone
ReferenceScriptNone
txin vKey = genesisUTxOPseudoTxIn networkId (verificationKeyHash . castKey $ vKey)
castKey (PaymentVerificationKey vkey) = GenesisUTxOVerificationKey vkey

initialUtxoForActors :: [(Actor, Value)] -> NetworkId -> IO (UTxOType Tx)
initialUtxoForActors actorToVal networkId = do
initialUtxoWithFunds networkId =<< vkToVal
where
vkForActor actor = fmap fst (keysFor actor)
vkToVal =
forM actorToVal $ \(actor, val) ->
vkForActor actor <&> (,val)

chainConfigFor :: HasCallStack => Actor -> FilePath -> SocketPath -> [Actor] -> ContestationPeriod -> IO ChainConfig
chainConfigFor me targetDir nodeSocket them cp = do
chainConfigFor me targetDir nodeSocket them contestationPeriod = do
when (me `elem` them) $
failure $
show me <> " must not be in " <> show them
Expand All @@ -73,7 +145,7 @@ chainConfigFor me targetDir nodeSocket them cp = do
{ nodeSocket
, cardanoSigningKey = skTarget me
, cardanoVerificationKeys = [vkTarget himOrHer | himOrHer <- them]
, contestationPeriod = cp
, contestationPeriod
}
where
skTarget x = targetDir </> skName x
Expand Down
78 changes: 75 additions & 3 deletions hydra-cluster/src/HydraNode.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import Hydra.Ledger.Cardano ()
import Hydra.Logging (Tracer, Verbosity (..), traceWith)
import Hydra.Network (Host (Host), NodeId (NodeId))
import Hydra.Network qualified as Network
import Hydra.Options (ChainConfig (..), LedgerConfig (..), RunOptions (..), defaultChainConfig, toArgs)
import Hydra.Options (ChainConfig (..), LedgerConfig (..), OfflineConfig, RunOfflineOptions (..), RunOptions (..))
import Hydra.Options.Offline qualified as OfflineOptions
import Hydra.Options.Online qualified as OnlineOptions
import Network.HTTP.Req (GET (..), HttpException, JsonResponse, NoReqBody (..), POST (..), ReqBodyJson (..), defaultHttpConfig, responseBody, runReq, (/:))
import Network.HTTP.Req qualified as Req
import Network.WebSockets (Connection, receiveData, runClient, sendClose, sendTextData)
Expand Down Expand Up @@ -264,7 +266,7 @@ withConfiguredHydraCluster tracer workDir nodeSocket firstNodeId allKeys hydraKe
cardanoVerificationKeys = [workDir </> show i <.> "vk" | i <- allNodeIds, i /= nodeId]
chainConfig =
chainConfigDecorator nodeId $
defaultChainConfig
OnlineOptions.defaultChainConfig
{ nodeSocket
, cardanoSigningKey
, cardanoVerificationKeys
Expand All @@ -281,6 +283,73 @@ withConfiguredHydraCluster tracer workDir nodeSocket firstNodeId allKeys hydraKe
hydraScriptsTxId
(\c -> startNodes (c : clients) rest)

withOfflineHydraNode ::
Tracer IO HydraNodeLog ->
OfflineConfig ->
FilePath ->
Int ->
SigningKey HydraKey ->
(HydraClient -> IO a) ->
IO a
withOfflineHydraNode tracer offlineConfig workDir hydraNodeId hydraSKey action =
withLogFile logFilePath $ \logFileHandle -> do
withOfflineHydraNode' offlineConfig workDir hydraNodeId hydraSKey (Just logFileHandle) $ do
\_stdoutHandle _stderrHandle processHandle -> do
result <-
race
(checkProcessHasNotDied ("hydra-node (" <> show hydraNodeId <> ")") processHandle)
(withConnectionToNode tracer hydraNodeId action)
case result of
Left e -> absurd e
Right a -> pure a
where
logFilePath = workDir </> "logs" </> "hydra-node-" <> show hydraNodeId <.> "log"

withOfflineHydraNode' ::
OfflineConfig ->
FilePath ->
Int ->
SigningKey HydraKey ->
-- | If given use this as std out.
Maybe Handle ->
(Handle -> Handle -> ProcessHandle -> IO a) ->
IO a
withOfflineHydraNode' offlineConfig workDir hydraNodeId hydraSKey mGivenStdOut action =
withSystemTempDirectory "hydra-node-e2e" $ \dir -> do
let cardanoLedgerProtocolParametersFile = dir </> "protocol-parameters.json"
readConfigFile "protocol-parameters.json" >>= writeFileBS cardanoLedgerProtocolParametersFile
let hydraSigningKey = dir </> (show hydraNodeId <> ".sk")
void $ writeFileTextEnvelope (File hydraSigningKey) Nothing hydraSKey
let ledgerConfig =
CardanoLedgerConfig
{ cardanoLedgerProtocolParametersFile
}
let p =
( hydraNodeOfflineProcess $
RunOfflineOptions
{ verbosity = Verbose "HydraNode"
, host = "127.0.0.1"
, port = fromIntegral $ 5_000 + hydraNodeId
, apiHost = "127.0.0.1"
, apiPort = fromIntegral $ 4_000 + hydraNodeId
, monitoringPort = Just $ fromIntegral $ 6_000 + hydraNodeId
, hydraSigningKey
, hydraVerificationKeys = []
, persistenceDir = workDir </> "state-" <> show hydraNodeId
, ledgerConfig
, offlineConfig
}
)
{ std_out = maybe CreatePipe UseHandle mGivenStdOut
, std_err = CreatePipe
}
withCreateProcess p $ \_stdin mCreatedHandle mErr processHandle ->
case (mCreatedHandle, mGivenStdOut, mErr) of
(Just out, _, Just err) -> action out err processHandle
(Nothing, Just out, Just err) -> action out err processHandle
(_, _, _) -> error "Should not happen™"
where

-- | Run a hydra-node with given 'ChainConfig' and using the config from
-- config/.
withHydraNode ::
Expand Down Expand Up @@ -392,7 +461,10 @@ withConnectionToNode tracer hydraNodeId action = do
pure res

hydraNodeProcess :: RunOptions -> CreateProcess
hydraNodeProcess = proc "hydra-node" . toArgs
hydraNodeProcess = proc "hydra-node" . OnlineOptions.toArgs

hydraNodeOfflineProcess :: RunOfflineOptions -> CreateProcess
hydraNodeOfflineProcess = proc "hydra-node" . OfflineOptions.toArgs

waitForNodesConnected :: HasCallStack => Tracer IO HydraNodeLog -> DiffTime -> [HydraClient] -> IO ()
waitForNodesConnected tracer timeOut clients =
Expand Down
36 changes: 34 additions & 2 deletions hydra-cluster/test/Test/EndToEndSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import Hydra.Cardano.Api (
mkVkAddress,
serialiseAddress,
signTx,
pattern TxOut,
pattern TxValidityLowerBound,
)
import Hydra.Chain.Direct.State ()
Expand Down Expand Up @@ -74,7 +75,7 @@ import Hydra.Cluster.Util (chainConfigFor, keysFor)
import Hydra.ContestationPeriod (ContestationPeriod (UnsafeContestationPeriod))
import Hydra.Crypto (generateSigningKey)
import Hydra.Ledger (txId)
import Hydra.Ledger.Cardano (genKeyPair, mkRangedTx, mkSimpleTx)
import Hydra.Ledger.Cardano (genKeyPair, genUTxOFor, mkRangedTx, mkSimpleTx)
import Hydra.Logging (Tracer, showLogsOnFailure)
import Hydra.Options
import Hydra.Party (deriveParty)
Expand All @@ -92,6 +93,7 @@ import HydraNode (
withHydraCluster,
withHydraNode,
withHydraNode',
withOfflineHydraNode,
)
import System.Directory (removeDirectoryRecursive)
import System.FilePath ((</>))
Expand All @@ -112,7 +114,37 @@ withClusterTempDir name =
withTempDir ("hydra-cluster-e2e-" <> name)

spec :: Spec
spec = around (showLogsOnFailure "EndToEndSpec") $
spec = around (showLogsOnFailure "EndToEndSpec") $ do
it "End-to-end offline mode" $ \tracer -> do
withTempDir ("offline-mode-e2e") $ \tmpDir -> do
let networkId = Testnet (NetworkMagic 42) -- from defaultChainConfig
(aliceCardanoVk, aliceCardanoSk) <- keysFor Alice
(bobCardanoVk, _) <- keysFor Bob
initialUtxo <- generate $ do
a <- genUTxOFor aliceCardanoVk
b <- genUTxOFor bobCardanoVk
pure $ a <> b
Aeson.encodeFile (tmpDir </> "utxo.json") initialUtxo
let offlineConfig =
OfflineConfig
{ initialUTxOFile = tmpDir </> "utxo.json"
, ledgerGenesisFile = Nothing
}

let Just (aliceSeedTxIn, aliceSeedTxOut) = UTxO.find (\(TxOut addr _ _ _) -> addr == mkVkAddress networkId aliceCardanoVk) initialUtxo

withOfflineHydraNode (contramap FromHydraNode tracer) offlineConfig tmpDir 0 aliceSk $ \node -> do
let Right tx =
mkSimpleTx
(aliceSeedTxIn, aliceSeedTxOut)
(mkVkAddress networkId bobCardanoVk, lovelaceToValue paymentFromAliceToBob)
aliceCardanoSk

send node $ input "NewTx" ["transaction" .= tx]

waitMatch 10 node $ \v -> do
guard $ v ^? key "tag" == Just "SnapshotConfirmed"

describe "End-to-end on Cardano devnet" $ do
describe "single party hydra head" $ do
it "full head life-cycle" $ \tracer -> do
Expand Down
9 changes: 6 additions & 3 deletions hydra-node/exe/hydra-node/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import Hydra.Cardano.Api (
import Hydra.Chain.Direct.ScriptRegistry (publishHydraScripts)
import Hydra.Chain.Direct.Util (readKeyPair)
import Hydra.Logging (Verbosity (..))
import Hydra.Node.Run (explain, run)
import Hydra.Node.Run (explain, run, runOffline)
import Hydra.Options (
Command (GenHydraKey, Publish, Run),
Command (GenHydraKey, Publish, Run, RunOffline),
PublishOptions (..),
RunOptions (..),
parseHydraCommand,
)
import Hydra.Options.Online qualified as OnlineOptions
import Hydra.Utils (genHydraKeys)

main :: IO ()
Expand All @@ -25,6 +26,8 @@ main = do
case command of
Run options ->
run (identifyNode options) `catch` (die . explain)
RunOffline options ->
runOffline options `catch` (die . explain)
Publish options ->
publish options
GenHydraKey outputFile ->
Expand All @@ -37,5 +40,5 @@ main = do
putStr (decodeUtf8 (serialiseToRawBytesHex txId))

identifyNode :: RunOptions -> RunOptions
identifyNode opt@RunOptions{verbosity = Verbose "HydraNode", nodeId} = opt{verbosity = Verbose $ "HydraNode-" <> show nodeId}
identifyNode opt@RunOptions{verbosity = Verbose "HydraNode", nodeId} = opt{OnlineOptions.verbosity = Verbose $ "HydraNode-" <> show nodeId}
identifyNode opt = opt
5 changes: 5 additions & 0 deletions hydra-node/golden/RunOptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
},
"monitoringPort": 10,
"nodeId": "ibqamqhmfggaqsj",
"offlineConfig": null,
"peers": [
{
"hostname": "0.0.0.2",
Expand Down Expand Up @@ -89,6 +90,7 @@
},
"monitoringPort": null,
"nodeId": "spdagobgcblquqlviwbymhcdr",
"offlineConfig": null,
"peers": [
{
"hostname": "0.0.0.7",
Expand Down Expand Up @@ -145,6 +147,7 @@
},
"monitoringPort": 19746,
"nodeId": "icqna",
"offlineConfig": null,
"peers": [
{
"hostname": "0.0.0.4",
Expand Down Expand Up @@ -216,6 +219,7 @@
},
"monitoringPort": 6616,
"nodeId": "ip",
"offlineConfig": null,
"peers": [],
"persistenceDir": "b/b/c",
"port": 12529,
Expand Down Expand Up @@ -264,6 +268,7 @@
},
"monitoringPort": 9885,
"nodeId": "vgnfmvtyrtutqozcjppmpaab",
"offlineConfig": null,
"peers": [
{
"hostname": "0.0.0.0",
Expand Down
6 changes: 6 additions & 0 deletions hydra-node/hydra-node.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ library
Hydra.Chain.Direct.Tx
Hydra.Chain.Direct.Util
Hydra.Chain.Direct.Wallet
Hydra.Chain.Offline
Hydra.Chain.Offline.Handlers
Hydra.Chain.Offline.Persistence
Hydra.ContestationPeriod
Hydra.Crypto
Hydra.HeadId
Expand Down Expand Up @@ -99,6 +102,9 @@ library
Hydra.Node.Run
Hydra.OnChainId
Hydra.Options
Hydra.Options.Common
Hydra.Options.Offline
Hydra.Options.Online
Hydra.Party
Hydra.Persistence
Hydra.Snapshot
Expand Down
Loading

0 comments on commit c92753a

Please sign in to comment.