diff --git a/docs/docs/getting-started/offline-mode.md b/docs/docs/getting-started/offline-mode.md new file mode 100644 index 00000000000..71a8cd6554f --- /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. 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, 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 }` + +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 diff --git a/hydra-node/src/Hydra/Chain/Offline.hs b/hydra-node/src/Hydra/Chain/Offline.hs index dd25200f35d..6343d7a3258 100644 --- a/hydra-node/src/Hydra/Chain/Offline.hs +++ b/hydra-node/src/Hydra/Chain/Offline.hs @@ -7,6 +7,8 @@ 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 (..), @@ -36,7 +38,18 @@ offlineHeadId = UnsafeHeadId "offline" offlineHeadSeed :: HeadSeed offlineHeadSeed = UnsafeHeadSeed "offline" +newtype 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. +-- 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 @@ -46,8 +59,11 @@ loadGenesisFile ledgerGenesisFile = now <- getCurrentTime -- TODO: uses internal cardano-api lib pure shelleyGenesisDefaults{sgSystemStart = now} - Just fp -> - readJsonFileThrow (parseJSON @(ShelleyGenesis StandardCrypto)) fp + Just fp -> do + 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 withOfflineChain :: OfflineChainConfig ->