Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Offline mode implementation #1118

Merged
merged 44 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
98dbb39
initial untested gummiworm-redacted mostly-cleaned-up offline mode im…
cardenaso11 Dec 5, 2023
6424b29
validate offlinemode 0 peers, tick according to genesis, allow using …
cardenaso11 Oct 19, 2023
299bfb2
remove redundant comment
cardenaso11 Oct 19, 2023
54ab84a
allow configuring writing utxo state to file
cardenaso11 Oct 20, 2023
5e7595d
do not require globals file to run offline mode
cardenaso11 Oct 25, 2023
8d8ae6e
fix tests, offline mode is offline, many changes
cardenaso11 Oct 30, 2023
2227368
refactor offline chain into Hydra.Chain.Offline, cleanup
cardenaso11 Nov 1, 2023
96e7beb
organize and cleanup more offlinemode
cardenaso11 Nov 1, 2023
e55a1cf
commit small todos i forgot to push earlier
cardenaso11 Nov 3, 2023
50ce846
clean up draftCommitTx
cardenaso11 Nov 4, 2023
fa1308b
fix remaining todos
cardenaso11 Nov 4, 2023
615b95c
various pr feedback
cardenaso11 Nov 8, 2023
97f64c2
code standard
cardenaso11 Nov 8, 2023
67e8139
dump worwk for the night
cardenaso11 Nov 16, 2023
842f2b9
temp commit
cardenaso11 Nov 21, 2023
6414da2
bugfixes, end to end offline mode tests
cardenaso11 Nov 29, 2023
e322bb9
remove re-checked-in golden test failures, add to gitignore since the…
cardenaso11 Dec 7, 2023
681202a
commit only test fixes
cardenaso11 Dec 12, 2023
5adfe59
Fix compilation, several warnings remain
ch1bo Dec 13, 2023
ecb26de
Minimize diff to master
ch1bo Dec 13, 2023
dcfc0bf
WIP: get tests passing, with debug code left in
cardenaso11 Dec 15, 2023
f641a98
demonstrate offline mode running with arbitrary participant OnChainId
cardenaso11 Dec 15, 2023
a7ad53a
clean up most stuff, get rid of debug code & logs
cardenaso11 Dec 15, 2023
1228373
Revert "demonstrate offline mode running with arbitrary participant O…
cardenaso11 Dec 15, 2023
f81761a
wip: temporary commit for demoing offline mode. Good for demo, do not…
cardenaso11 Dec 15, 2023
c730f27
Revert "wip: temporary commit for demoing offline mode. Good for demo…
cardenaso11 Dec 15, 2023
2c2f4f9
pr feedback
cardenaso11 Dec 15, 2023
65b2357
revert back to old port: see #1218 for macOS port bug
cardenaso11 Dec 15, 2023
510d4ef
more pr feedback
cardenaso11 Dec 15, 2023
7f283ae
more feedback, undo error->die feedback due to introduced test failure
cardenaso11 Dec 15, 2023
e2438b3
wip: separate top-level command and run(offline)options type. needs t…
cardenaso11 Dec 15, 2023
b4d21c4
add to changelog
rrruko Dec 15, 2023
a892dd3
randomize utxo in offline test
rrruko Dec 15, 2023
266fc0a
remove dead code
rrruko Dec 15, 2023
b860d64
remove TODOs
rrruko Dec 15, 2023
f0a8489
Hydra.Options: import toArgs from Online
rrruko Dec 15, 2023
6880688
resolve warnings
rrruko Dec 15, 2023
3500d51
update log schema
rrruko Dec 15, 2023
a10c492
fix test failures
rrruko Dec 15, 2023
e373bce
Fix formatting
ch1bo Dec 18, 2023
bd68a8e
address some feedback
rrruko Dec 18, 2023
0306ce8
deduplicate fromShelleyGenesis
rrruko Dec 18, 2023
6a54f77
move back loadState
rrruko Dec 18, 2023
bea0b76
organize imports
rrruko Dec 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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' ::
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this separation as withOfflineHydraNode' is used in only one place. And even the other 2 functions are clunky and should be refactored.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll cover this in a refactor PR.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could: use scenario values instead of loading/inferring them again here.

For example given my comment above, this could be simplified by just keeping aliceInitialUTxO <- generate $ genUTxOFor aliceCardanoVk around and there is also UTxO.min which will give you one (no filtering needed if we generate only for alice).


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
Loading