From dbef520a736250bce2ab418d7b11c799aa1ea8e0 Mon Sep 17 00:00:00 2001 From: card Date: Fri, 3 May 2024 12:06:07 -0400 Subject: [PATCH 1/7] document offline mode --- docs/docs/getting-started/offline-mode.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/docs/getting-started/offline-mode.md diff --git a/docs/docs/getting-started/offline-mode.md b/docs/docs/getting-started/offline-mode.md new file mode 100644 index 00000000000..ff5ef0d27d7 --- /dev/null +++ b/docs/docs/getting-started/offline-mode.md @@ -0,0 +1,9 @@ +Hydra supports an offline mode, which allows for disabling the Layer 1 interface (that is, the underlying Cardano blockchain which Hydra heads use to seed funds and ultimately funds are withdrawn to). Disabling Layer 1 interactions allows use-cases which would otherwise require running and configuring an entire Layer 1 private devnet. + +In this offline mode, only the Layer 2 ledger is run. Therefore, ledger genesis parameters that normally influence things like time-based transaction validation, may be set to defaults that aren't reflective of mainnet. To set this, set --ledger-protocol-parameters to a non-zero file. TODO: show how to obtain this. + +To initialize the Layer 2 ledger's UTXO state, offline mode takes an obligatory --initial-utxo parameter, which points to a JSON encoded UTXO file. This UTXO is independent of Event Source loaded events, and the latter are validated against this UTXO. The UTXO follows the following schema `{ txout : {address, value : {asset : quantity}, datum, datumhash, inlinedatum, referenceScript }` + +An example UTXO: +```json +{"1541287c2598ffc682742c961a96343ac64e9b9030e6b03a476bb18c8c50134d#0":{"address":"addr_test1vqg9ywrpx6e50uam03nlu0ewunh3yrscxmjayurmkp52lfskgkq5k","datum":null,"datumhash":null,"inlineDatum":null,"referenceScript":null,"value":{"lovelace":100000000}},"39786f186d94d8dd0b4fcf05d1458b18cd5fd8c6823364612f4a3c11b77e7cc7#0":{"address":"addr_test1vru2drx33ev6dt8gfq245r5k0tmy7ngqe79va69de9dxkrg09c7d3","datum":null,"datumhash":null,"inlineDatum":null,"referenceScript":null,"value":{"lovelace":100000000}}}``` \ No newline at end of file From 09bed81292a026cc7532103b3bddfacb89072815 Mon Sep 17 00:00:00 2001 From: card Date: Wed, 8 May 2024 12:03:51 -0400 Subject: [PATCH 2/7] better error message --- hydra-node/src/Hydra/Chain/Offline.hs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hydra-node/src/Hydra/Chain/Offline.hs b/hydra-node/src/Hydra/Chain/Offline.hs index dd25200f35d..0c671c661b2 100644 --- a/hydra-node/src/Hydra/Chain/Offline.hs +++ b/hydra-node/src/Hydra/Chain/Offline.hs @@ -48,6 +48,12 @@ loadGenesisFile ledgerGenesisFile = pure shelleyGenesisDefaults{sgSystemStart = now} Just fp -> readJsonFileThrow (parseJSON @(ShelleyGenesis StandardCrypto)) fp + `catch` \(e :: IOException) -> do + putStrLn $ "Failed to parse initial UTXO" <> fp + putStrLn $ "Example UTXO: " <> " + {\"1541287c2598ffc682742c961a96343ac64e9b9030e6b03a476bb18c8c50134d#0\":{\"address\":\"addr_test1vqg9ywrpx6e50uam03nlu0ewunh3yrscxmjayurmkp52lfskgkq5k\",\"datum\":null,\"datumhash\":null,\"inlineDatum \":null,\"referenceScript\":null,\"value\":{\"lovelace\":100000000}},\"39786f186d94d8dd0b4fcf05d1458b18cd5fd8c6823364612f4a3c11b77e7cc7#0\":{\"address\":\"addr_test1vru2drx33ev6dt8gfq245r5k0tmy7ngqe79va69de9dxkrg09c7d3\",\"datum\":null,\"datumhash\":null,\"inlineDatum\":null,\"referenceScript\":null,\"value\":{\"lovelace\":100000000}}} + " + e withOfflineChain :: OfflineChainConfig -> From 887995738ffc0128b76fedc33f7e369f40b95560 Mon Sep 17 00:00:00 2001 From: card Date: Wed, 8 May 2024 12:13:36 -0400 Subject: [PATCH 3/7] rethrow parse exception for genesis file --- hydra-node/src/Hydra/Chain/Offline.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hydra-node/src/Hydra/Chain/Offline.hs b/hydra-node/src/Hydra/Chain/Offline.hs index 0c671c661b2..2f0f0e31e4f 100644 --- a/hydra-node/src/Hydra/Chain/Offline.hs +++ b/hydra-node/src/Hydra/Chain/Offline.hs @@ -53,7 +53,7 @@ loadGenesisFile ledgerGenesisFile = putStrLn $ "Example UTXO: " <> " {\"1541287c2598ffc682742c961a96343ac64e9b9030e6b03a476bb18c8c50134d#0\":{\"address\":\"addr_test1vqg9ywrpx6e50uam03nlu0ewunh3yrscxmjayurmkp52lfskgkq5k\",\"datum\":null,\"datumhash\":null,\"inlineDatum \":null,\"referenceScript\":null,\"value\":{\"lovelace\":100000000}},\"39786f186d94d8dd0b4fcf05d1458b18cd5fd8c6823364612f4a3c11b77e7cc7#0\":{\"address\":\"addr_test1vru2drx33ev6dt8gfq245r5k0tmy7ngqe79va69de9dxkrg09c7d3\",\"datum\":null,\"datumhash\":null,\"inlineDatum\":null,\"referenceScript\":null,\"value\":{\"lovelace\":100000000}}} " - e + throwIO e withOfflineChain :: OfflineChainConfig -> From 753635fff253a7730510b50609bc2811e5b9d16a Mon Sep 17 00:00:00 2001 From: card Date: Wed, 8 May 2024 18:37:40 -0400 Subject: [PATCH 4/7] improve initial utxo parse error --- hydra-node/src/Hydra/Chain/Offline.hs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/hydra-node/src/Hydra/Chain/Offline.hs b/hydra-node/src/Hydra/Chain/Offline.hs index 2f0f0e31e4f..c67e670dfc2 100644 --- a/hydra-node/src/Hydra/Chain/Offline.hs +++ b/hydra-node/src/Hydra/Chain/Offline.hs @@ -2,6 +2,9 @@ module Hydra.Chain.Offline where import Hydra.Prelude +import Data.Aeson qualified as Json +import Data.Aeson.Types qualified as Json + import Cardano.Api.Genesis (shelleyGenesisDefaults) import Cardano.Api.GenesisParameters (fromShelleyGenesis) import Cardano.Ledger.Slot (unSlotNo) @@ -36,6 +39,15 @@ offlineHeadId = UnsafeHeadId "offline" offlineHeadSeed :: HeadSeed offlineHeadSeed = UnsafeHeadSeed "offline" +data InitialUTxOParseException = InitialUTxOParseException String + deriving stock (Show) + +instance Exception InitialUTxOParseException where + displayException (InitialUTxOParseException err) + = "Failed to parse initial UTXO: " <> err <> ". Example UTXO: " + <> "{\"1541287c2598ffc682742c961a96343ac64e9b9030e6b03a476bb18c8c50134d#0\":{\"address\":\"addr_test1vqg9ywrpx6e50uam03nlu0ewunh3yrscxmjayurmkp52lfskgkq5k\",\"datum\":null,\"datumhash\":null,\"inlineDatum \":null,\"referenceScript\":null,\"value\":{\"lovelace\":100000000}},\"39786f186d94d8dd0b4fcf05d1458b18cd5fd8c6823364612f4a3c11b77e7cc7#0\":{\"address\":\"addr_test1vru2drx33ev6dt8gfq245r5k0tmy7ngqe79va69de9dxkrg09c7d3\",\"datum\":null,\"datumhash\":null,\"inlineDatum\":null,\"referenceScript\":null,\"value\":{\"lovelace\":100000000}}}" + + -- | Load the given genesis file or use defaults specific to the offline mode. loadGenesisFile :: Maybe FilePath -> IO (GenesisParameters ShelleyEra) loadGenesisFile ledgerGenesisFile = @@ -46,14 +58,11 @@ loadGenesisFile ledgerGenesisFile = now <- getCurrentTime -- TODO: uses internal cardano-api lib pure shelleyGenesisDefaults{sgSystemStart = now} - Just fp -> - readJsonFileThrow (parseJSON @(ShelleyGenesis StandardCrypto)) fp - `catch` \(e :: IOException) -> do - putStrLn $ "Failed to parse initial UTXO" <> fp - putStrLn $ "Example UTXO: " <> " - {\"1541287c2598ffc682742c961a96343ac64e9b9030e6b03a476bb18c8c50134d#0\":{\"address\":\"addr_test1vqg9ywrpx6e50uam03nlu0ewunh3yrscxmjayurmkp52lfskgkq5k\",\"datum\":null,\"datumhash\":null,\"inlineDatum \":null,\"referenceScript\":null,\"value\":{\"lovelace\":100000000}},\"39786f186d94d8dd0b4fcf05d1458b18cd5fd8c6823364612f4a3c11b77e7cc7#0\":{\"address\":\"addr_test1vru2drx33ev6dt8gfq245r5k0tmy7ngqe79va69de9dxkrg09c7d3\",\"datum\":null,\"datumhash\":null,\"inlineDatum\":null,\"referenceScript\":null,\"value\":{\"lovelace\":100000000}}} - " - throwIO e + Just fp -> do + jsonVal <- Json.eitherDecodeFileStrict fp >>= either fail pure -- just crash if we can't read the file + case Json.parseEither (parseJSON @(ShelleyGenesis StandardCrypto)) jsonVal of + Right a -> pure a + Left e -> throwIO $ InitialUTxOParseException e withOfflineChain :: OfflineChainConfig -> From 7d90a4f35173ff75916026ec8b2a02fedb943a88 Mon Sep 17 00:00:00 2001 From: card Date: Thu, 16 May 2024 11:55:59 -0400 Subject: [PATCH 5/7] revise offline mode docs to link ledger parameters, more --- docs/docs/getting-started/offline-mode.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/getting-started/offline-mode.md b/docs/docs/getting-started/offline-mode.md index ff5ef0d27d7..71a8cd6554f 100644 --- a/docs/docs/getting-started/offline-mode.md +++ b/docs/docs/getting-started/offline-mode.md @@ -1,6 +1,6 @@ -Hydra supports an offline mode, which allows for disabling the Layer 1 interface (that is, the underlying Cardano blockchain which Hydra heads use to seed funds and ultimately funds are withdrawn to). Disabling Layer 1 interactions allows use-cases which would otherwise require running and configuring an entire Layer 1 private devnet. +Hydra supports an offline mode, which allows for disabling the Layer 1 interface (that is, the underlying Cardano blockchain which Hydra heads use to seed funds and ultimately funds are withdrawn to). Disabling Layer 1 interactions allows use-cases which would otherwise require running and configuring an entire Layer 1 private devnet. For example, the offline mode can be used to quickly validate a series of transactions against a UTxO, without having to spin up an entire Layer 1 Cardano node. -In this offline mode, only the Layer 2 ledger is run. Therefore, ledger genesis parameters that normally influence things like time-based transaction validation, may be set to defaults that aren't reflective of mainnet. To set this, set --ledger-protocol-parameters to a non-zero file. TODO: show how to obtain this. +In this offline mode, only the Layer 2 ledger is run, along with the Hydra API and persistence, to support interacting with the offline Hydra. Therefore, ledger genesis parameters that normally influence things like time-based transaction validation, may be set to defaults that aren't reflective of mainnet. To set this, set --ledger-protocol-parameters to a non-zero file, as described [here](https://hydra.family/head-protocol/unstable/docs/configuration/#ledger-parameters). To initialize the Layer 2 ledger's UTXO state, offline mode takes an obligatory --initial-utxo parameter, which points to a JSON encoded UTXO file. This UTXO is independent of Event Source loaded events, and the latter are validated against this UTXO. The UTXO follows the following schema `{ txout : {address, value : {asset : quantity}, datum, datumhash, inlinedatum, referenceScript }` From 243bec855f73255b4154722110f33df950dd90fa Mon Sep 17 00:00:00 2001 From: card Date: Thu, 16 May 2024 12:02:34 -0400 Subject: [PATCH 6/7] lint --- hydra-node/src/Hydra/Chain/Offline.hs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/hydra-node/src/Hydra/Chain/Offline.hs b/hydra-node/src/Hydra/Chain/Offline.hs index c67e670dfc2..d4db8eb86c3 100644 --- a/hydra-node/src/Hydra/Chain/Offline.hs +++ b/hydra-node/src/Hydra/Chain/Offline.hs @@ -43,10 +43,11 @@ data InitialUTxOParseException = InitialUTxOParseException String deriving stock (Show) instance Exception InitialUTxOParseException where - displayException (InitialUTxOParseException err) - = "Failed to parse initial UTXO: " <> err <> ". Example UTXO: " - <> "{\"1541287c2598ffc682742c961a96343ac64e9b9030e6b03a476bb18c8c50134d#0\":{\"address\":\"addr_test1vqg9ywrpx6e50uam03nlu0ewunh3yrscxmjayurmkp52lfskgkq5k\",\"datum\":null,\"datumhash\":null,\"inlineDatum \":null,\"referenceScript\":null,\"value\":{\"lovelace\":100000000}},\"39786f186d94d8dd0b4fcf05d1458b18cd5fd8c6823364612f4a3c11b77e7cc7#0\":{\"address\":\"addr_test1vru2drx33ev6dt8gfq245r5k0tmy7ngqe79va69de9dxkrg09c7d3\",\"datum\":null,\"datumhash\":null,\"inlineDatum\":null,\"referenceScript\":null,\"value\":{\"lovelace\":100000000}}}" - + displayException (InitialUTxOParseException err) = + "Failed to parse initial UTXO: " + <> err + <> ". Example UTXO: " + <> "{\"1541287c2598ffc682742c961a96343ac64e9b9030e6b03a476bb18c8c50134d#0\":{\"address\":\"addr_test1vqg9ywrpx6e50uam03nlu0ewunh3yrscxmjayurmkp52lfskgkq5k\",\"datum\":null,\"datumhash\":null,\"inlineDatum \":null,\"referenceScript\":null,\"value\":{\"lovelace\":100000000}},\"39786f186d94d8dd0b4fcf05d1458b18cd5fd8c6823364612f4a3c11b77e7cc7#0\":{\"address\":\"addr_test1vru2drx33ev6dt8gfq245r5k0tmy7ngqe79va69de9dxkrg09c7d3\",\"datum\":null,\"datumhash\":null,\"inlineDatum\":null,\"referenceScript\":null,\"value\":{\"lovelace\":100000000}}}" -- | Load the given genesis file or use defaults specific to the offline mode. loadGenesisFile :: Maybe FilePath -> IO (GenesisParameters ShelleyEra) @@ -58,7 +59,7 @@ loadGenesisFile ledgerGenesisFile = now <- getCurrentTime -- TODO: uses internal cardano-api lib pure shelleyGenesisDefaults{sgSystemStart = now} - Just fp -> do + Just fp -> do jsonVal <- Json.eitherDecodeFileStrict fp >>= either fail pure -- just crash if we can't read the file case Json.parseEither (parseJSON @(ShelleyGenesis StandardCrypto)) jsonVal of Right a -> pure a From 3a7b50f5b79fc3052cec7f34bda387549e2571e3 Mon Sep 17 00:00:00 2001 From: Sebastian Nagel Date: Fri, 17 May 2024 10:16:15 +0200 Subject: [PATCH 7/7] Resolve hlint issues --- hydra-node/src/Hydra/Chain/Offline.hs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/hydra-node/src/Hydra/Chain/Offline.hs b/hydra-node/src/Hydra/Chain/Offline.hs index d4db8eb86c3..6343d7a3258 100644 --- a/hydra-node/src/Hydra/Chain/Offline.hs +++ b/hydra-node/src/Hydra/Chain/Offline.hs @@ -2,14 +2,13 @@ module Hydra.Chain.Offline where import Hydra.Prelude -import Data.Aeson qualified as Json -import Data.Aeson.Types qualified as Json - import Cardano.Api.Genesis (shelleyGenesisDefaults) import Cardano.Api.GenesisParameters (fromShelleyGenesis) import Cardano.Ledger.Slot (unSlotNo) import Cardano.Slotting.Time (SystemStart (SystemStart), mkSlotLength) import Control.Monad.Class.MonadAsync (link) +import Data.Aeson qualified as Aeson +import Data.Aeson.Types qualified as Aeson import Hydra.Cardano.Api (GenesisParameters (..), ShelleyEra, ShelleyGenesis (..), StandardCrypto, Tx) import Hydra.Chain ( Chain (..), @@ -39,7 +38,7 @@ offlineHeadId = UnsafeHeadId "offline" offlineHeadSeed :: HeadSeed offlineHeadSeed = UnsafeHeadSeed "offline" -data InitialUTxOParseException = InitialUTxOParseException String +newtype InitialUTxOParseException = InitialUTxOParseException String deriving stock (Show) instance Exception InitialUTxOParseException where @@ -50,6 +49,7 @@ instance Exception InitialUTxOParseException where <> "{\"1541287c2598ffc682742c961a96343ac64e9b9030e6b03a476bb18c8c50134d#0\":{\"address\":\"addr_test1vqg9ywrpx6e50uam03nlu0ewunh3yrscxmjayurmkp52lfskgkq5k\",\"datum\":null,\"datumhash\":null,\"inlineDatum \":null,\"referenceScript\":null,\"value\":{\"lovelace\":100000000}},\"39786f186d94d8dd0b4fcf05d1458b18cd5fd8c6823364612f4a3c11b77e7cc7#0\":{\"address\":\"addr_test1vru2drx33ev6dt8gfq245r5k0tmy7ngqe79va69de9dxkrg09c7d3\",\"datum\":null,\"datumhash\":null,\"inlineDatum\":null,\"referenceScript\":null,\"value\":{\"lovelace\":100000000}}}" -- | Load the given genesis file or use defaults specific to the offline mode. +-- Throws: 'InitialUTxOParseException' if the initial UTXO file could not be parsed. loadGenesisFile :: Maybe FilePath -> IO (GenesisParameters ShelleyEra) loadGenesisFile ledgerGenesisFile = -- TODO: uses internal cardano-api lib @@ -60,8 +60,8 @@ loadGenesisFile ledgerGenesisFile = -- TODO: uses internal cardano-api lib pure shelleyGenesisDefaults{sgSystemStart = now} Just fp -> do - jsonVal <- Json.eitherDecodeFileStrict fp >>= either fail pure -- just crash if we can't read the file - case Json.parseEither (parseJSON @(ShelleyGenesis StandardCrypto)) jsonVal of + jsonVal <- Aeson.eitherDecodeFileStrict fp >>= either fail pure -- just crash if we can't read the file + case Aeson.parseEither (parseJSON @(ShelleyGenesis StandardCrypto)) jsonVal of Right a -> pure a Left e -> throwIO $ InitialUTxOParseException e