Skip to content

Commit

Permalink
Derive head id and seed from node id in offline mode
Browse files Browse the repository at this point in the history
  • Loading branch information
ch1bo committed Aug 7, 2024
1 parent 1754eff commit b84ac18
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 61 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ changes.

- Moved several pages from "core concepts" into the user manual and developer docs to futher improve user journey.

- Offline mode of `hydra-node` uses `--node-id` to derive an artificial offline `headId`.

## [0.17.0] - 2024-05-20

- **BREAKING** Change `hydra-node` API `/commit` endpoint for committing from scripts [#1380](https://github.com/cardano-scaling/hydra/pull/1380):
Expand Down
45 changes: 35 additions & 10 deletions hydra-cluster/test/Test/OfflineChainSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Control.Lens ((^?))
import Data.Aeson qualified as Aeson
import Data.Aeson.Lens (key, _Number)
import Hydra.Cardano.Api (UTxO)
import Hydra.Chain (ChainCallback, ChainEvent (..), initHistory)
import Hydra.Chain (ChainCallback, ChainEvent (..), OnChainTx (..), initHistory)
import Hydra.Chain.Direct.State (initialChainState)
import Hydra.Chain.Offline (withOfflineChain)
import Hydra.Cluster.Fixture (alice)
Expand All @@ -20,6 +20,30 @@ import System.FilePath ((</>))

spec :: Spec
spec = do
it "does derive head id from node id" $ do
withTempDir "hydra-cluster" $ \tmpDir -> do
Aeson.encodeFile (tmpDir </> "utxo.json") (mempty @UTxO)
let offlineConfig =
OfflineChainConfig
{ initialUTxOFile = tmpDir </> "utxo.json"
, ledgerGenesisFile = Nothing
}
-- XXX: this is weird
let chainStateHistory = initHistory initialChainState

(callback, waitNext) <- monitorCallbacks
headId1 <- withOfflineChain "test1" offlineConfig alice chainStateHistory callback $ \_chain ->
waitMatch waitNext 2 $ \case
Observation{observedTx = OnInitTx{headId}} -> pure headId
_ -> Nothing

headId2 <- withOfflineChain "test2" offlineConfig alice chainStateHistory callback $ \_chain ->
waitMatch waitNext 2 $ \case
Observation{observedTx = OnInitTx{headId}} -> pure headId
_ -> Nothing

headId1 `shouldNotBe` headId2

it "does start on slot 0 with no genesis" $ do
withTempDir "hydra-cluster" $ \tmpDir -> do
Aeson.encodeFile (tmpDir </> "utxo.json") (mempty @UTxO)
Expand All @@ -32,11 +56,11 @@ spec = do
let chainStateHistory = initHistory initialChainState

(callback, waitNext) <- monitorCallbacks
withOfflineChain offlineConfig alice chainStateHistory callback $ \_chain -> do
withOfflineChain "test" offlineConfig alice chainStateHistory callback $ \_chain -> do
-- Expect to see a tick of slot 1 within 2 seconds
waitMatch waitNext 2 $ \case
Tick{chainSlot} -> chainSlot > 0
_ -> False
Tick{chainSlot} -> guard $ chainSlot > 0
_ -> Nothing

it "does not start on slot 0 with real genesis file" $ do
withTempDir "hydra-cluster" $ \tmpDir -> do
Expand All @@ -52,11 +76,11 @@ spec = do
let chainStateHistory = initHistory initialChainState

(callback, waitNext) <- monitorCallbacks
withOfflineChain offlineConfig alice chainStateHistory callback $ \_chain -> do
withOfflineChain "test" offlineConfig alice chainStateHistory callback $ \_chain -> do
-- Should not start at 0
waitMatch waitNext 1 $ \case
Tick{chainSlot} -> chainSlot > 1000
_ -> False
Tick{chainSlot} -> guard $ chainSlot > 1000
_ -> Nothing
-- Should produce ticks on each slot, which is defined by genesis.json
Just slotLength <- readFileBS (tmpDir </> "genesis.json") >>= \bs -> pure $ bs ^? key "slotLength" . _Number
slotTime <-
Expand All @@ -79,7 +103,7 @@ monitorCallbacks = do
pure (callback, waitNext)

-- XXX: Dry with the other waitMatch utilities
waitMatch :: (HasCallStack, ToJSON a) => IO a -> DiffTime -> (a -> Bool) -> IO ()
waitMatch :: (HasCallStack, ToJSON a) => IO a -> DiffTime -> (a -> Maybe b) -> IO b
waitMatch waitNext seconds match = do
seen <- newTVarIO []
timeout seconds (go seen) >>= \case
Expand All @@ -97,5 +121,6 @@ waitMatch waitNext seconds match = do
go seen = do
a <- waitNext
atomically (modifyTVar' seen (a :))
unless (match a) $
go seen
case match a of
Just b -> pure b
Nothing -> go seen
99 changes: 49 additions & 50 deletions hydra-node/src/Hydra/Chain/Offline.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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 Data.ByteString.Char8 qualified as BSC
import Hydra.Cardano.Api (GenesisParameters (..), ShelleyEra, ShelleyGenesis (..), StandardCrypto, Tx)
import Hydra.Chain (
Chain (..),
Expand All @@ -27,16 +28,17 @@ import Hydra.HeadId (HeadId (..), HeadSeed (..))
import Hydra.Ledger (ChainSlot (ChainSlot))
import Hydra.Ledger.Cardano.Configuration (readJsonFileThrow)
import Hydra.Ledger.Cardano.Time (slotNoFromUTCTime, slotNoToUTCTime)
import Hydra.Network (NodeId)
import Hydra.Options (OfflineChainConfig (..), defaultContestationPeriod)
import Hydra.Party (Party)

-- | Hard-coded 'HeadId' for all offline head instances.
offlineHeadId :: HeadId
offlineHeadId = UnsafeHeadId "offline"
-- | Derived 'HeadId' of offline head.
offlineHeadId :: NodeId -> HeadId
offlineHeadId nodeId = UnsafeHeadId $ "offline-" <> BSC.pack (show nodeId)

-- | Hard-coded 'HeadSeed' for all offline head instances.
offlineHeadSeed :: HeadSeed
offlineHeadSeed = UnsafeHeadSeed "offline"
-- | Derived 'HeadSeed' of offline head.
offlineHeadSeed :: NodeId -> HeadSeed
offlineHeadSeed nodeId = UnsafeHeadSeed $ "offline-" <> BSC.pack (show nodeId)

newtype InitialUTxOParseException = InitialUTxOParseException String
deriving stock (Show)
Expand Down Expand Up @@ -66,69 +68,66 @@ loadGenesisFile ledgerGenesisFile =
Left e -> throwIO $ InitialUTxOParseException e

withOfflineChain ::
NodeId ->
OfflineChainConfig ->
Party ->
-- | Last known chain state as loaded from persistence.
ChainStateHistory Tx ->
ChainComponent Tx IO a
withOfflineChain OfflineChainConfig{ledgerGenesisFile, initialUTxOFile} party chainStateHistory callback action = do
initializeOfflineHead chainStateHistory initialUTxOFile party callback
withOfflineChain nodeId OfflineChainConfig{ledgerGenesisFile, initialUTxOFile} party chainStateHistory callback action = do
initializeOfflineHead
genesis <- loadGenesisFile ledgerGenesisFile
withAsync (tickForever genesis callback) $ \tickThread -> do
link tickThread
action chainHandle
where
headId = offlineHeadId nodeId

chainHandle =
Chain
{ submitTx = const $ pure ()
, draftCommitTx = \_ _ -> pure $ Left FailedToDraftTxNotInitializing
, postTx = const $ pure ()
}

initializeOfflineHead ::
ChainStateHistory Tx ->
FilePath ->
Party ->
(ChainEvent Tx -> IO ()) ->
IO ()
initializeOfflineHead chainStateHistory initialUTxOFile ownParty callback = do
let emptyChainStateHistory = initHistory initialChainState
initializeOfflineHead = do
let emptyChainStateHistory = initHistory initialChainState

-- if we don't have a chainStateHistory to restore from disk from, start a new one
when (chainStateHistory == emptyChainStateHistory) $ do
initialUTxO <- readJsonFileThrow parseJSON initialUTxOFile
-- if we don't have a chainStateHistory to restore from disk from, start a new one
when (chainStateHistory == emptyChainStateHistory) $ do
initialUTxO <- readJsonFileThrow parseJSON initialUTxOFile

callback $
Observation
{ newChainState = initialChainState
, observedTx =
OnInitTx
{ headId = offlineHeadId
, headSeed = offlineHeadSeed
, headParameters =
HeadParameters
{ parties = [ownParty]
, -- NOTE: This is irrelevant in offline mode.
contestationPeriod = defaultContestationPeriod
}
, participants = []
}
}
callback $
Observation
{ newChainState = initialChainState
, observedTx =
OnCommitTx
{ party = ownParty
, committed = initialUTxO
, headId = offlineHeadId
}
}
callback $
Observation
{ newChainState = initialChainState
, observedTx = OnCollectComTx{headId = offlineHeadId}
}
callback $
Observation
{ newChainState = initialChainState
, observedTx =
OnInitTx
{ headId
, headSeed = offlineHeadSeed nodeId
, headParameters =
HeadParameters
{ parties = [party]
, -- NOTE: This is irrelevant in offline mode.
contestationPeriod = defaultContestationPeriod
}
, participants = []
}
}
callback $
Observation
{ newChainState = initialChainState
, observedTx =
OnCommitTx
{ party
, committed = initialUTxO
, headId
}
}
callback $
Observation
{ newChainState = initialChainState
, observedTx = OnCollectComTx{headId}
}

tickForever :: GenesisParameters ShelleyEra -> (ChainEvent Tx -> IO ()) -> IO ()
tickForever genesis callback = do
Expand Down
2 changes: 1 addition & 1 deletion hydra-node/src/Hydra/Node/Run.hs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ run opts = do

prepareChainComponent tracer Environment{party} = \case
Offline cfg ->
pure $ withOfflineChain cfg party
pure $ withOfflineChain nodeId cfg party
Direct cfg -> do
ctx <- loadChainContext cfg party
wallet <- mkTinyWallet (contramap DirectChain tracer) cfg
Expand Down

0 comments on commit b84ac18

Please sign in to comment.