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

Add blockfrost mode to hydra-chain-observer #1631

Merged
merged 43 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
bea35a7
Draft new options to run with BlockFrost
ffakenz Sep 13, 2024
18f3969
Draft new NodeClient handle for ouroborus
ffakenz Sep 13, 2024
216c3da
Draft new Blockfrost NodeClient
ffakenz Sep 13, 2024
7ba0608
Draft roll forward and backward
ffakenz Sep 13, 2024
016d2c5
Draft adapters to convert blockfrost-api types into cardano-api
ffakenz Sep 18, 2024
7c95078
Draft naive loop through latest head observations
ffakenz Sep 23, 2024
6b4b97d
Enhance Draft by using ExceptT
ffakenz Sep 23, 2024
e17d570
Enhance Draft with retry policy
ffakenz Sep 23, 2024
0237f3d
Derive network id from blockfrost project
ffakenz Sep 23, 2024
579c62b
Fetch the genesis block hash from blockfrost project
ffakenz Sep 23, 2024
cff93ea
Enhance error handking when converting CBOR to Cardano API Tx
ffakenz Sep 24, 2024
f1e2921
Fix getTxCBOR API call to use latest release
ffakenz Sep 24, 2024
d8fcb9d
Refactor options
ffakenz Sep 24, 2024
4dc6d12
Draft README
ffakenz Sep 24, 2024
1b2b870
Remove unnecessary helper
ffakenz Sep 24, 2024
d667b09
Minor formatting changes
ffakenz Sep 24, 2024
6915caa
Enhance error handling
ffakenz Sep 24, 2024
933bf34
Update CHANGELOG
ffakenz Sep 24, 2024
5870cd0
Minor formatting changes
ffakenz Sep 24, 2024
8f70483
Break down loop into roll-forward and iterative processes
ffakenz Sep 24, 2024
0285b8b
Minor formatting changes
ffakenz Sep 24, 2024
372be9a
Fix getTxCBOR API call to use latest release
ffakenz Sep 26, 2024
7e6818c
Enhance observer options
ffakenz Sep 26, 2024
862ef43
Add blockfrost mode in explorer
ffakenz Sep 27, 2024
c8b7136
Refetch block on roll forward and use constant retry delay
ch1bo Sep 27, 2024
2aff6d3
update flake lock
ffakenz Oct 1, 2024
cb79451
Fix specs to use latest options for observer and explorer
ffakenz Oct 1, 2024
dffcec8
Take a Chain Point from the options directly
ffakenz Oct 1, 2024
8c58a8f
Replace ExceptT by MonadThrow
ffakenz Oct 2, 2024
caa7cd0
Fix to not error (and retry) after observing already
ffakenz Oct 2, 2024
137c243
Made safe nbr of block confirmations configurable
ffakenz Oct 2, 2024
136934c
Fix retry mech after refactoring towards MonadThrow
ffakenz Oct 3, 2024
4196663
Fix blockfrost block hash derivation from chain point
ffakenz Oct 3, 2024
ff16daa
fix after rebase
ffakenz Oct 7, 2024
0d3d8ad
Enhance blockfrost mode invocation sample
ffakenz Oct 7, 2024
e13614a
Update CHANGELOG
ffakenz Oct 7, 2024
05bb872
Refactor options to made them more type-safety
ffakenz Oct 7, 2024
579f0c0
Remove spy calls
ffakenz Oct 7, 2024
b08a384
Extend smoke tests with check observations
ffakenz Oct 7, 2024
9358126
Add blockfrost smoke test
ffakenz Oct 7, 2024
49cea28
Revert "Add blockfrost smoke test"
ffakenz Oct 8, 2024
e97bd04
Revert "Extend smoke tests with check observations"
ffakenz Oct 8, 2024
4793f95
Remove mandatory networkId from follow function
noonio Oct 8, 2024
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
3 changes: 2 additions & 1 deletion .github/workflows/explorer/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ services:
ports:
- "80:8080"
command:
[ "--node-socket", "/data/node.socket"
[ "direct"
, "--node-socket", "/data/node.socket"
, "--testnet-magic", "2"
, "--api-port", "8080"
# NOTE: Block in which current master scripts were published
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ changes.
- Overall this results in transactions still to be submitted once per client,
but requires signifanctly less book-keeping on the client-side.

- Add **Blockfrost Mode** to `hydra-chain-observer`, to follow the chain via Blockfrost API.

## [0.19.0] - 2024-09-13

- Tested with `cardano-node 9.1.1` and `cardano-cli 9.2.1.0`
Expand All @@ -54,7 +56,6 @@ changes.

- Add a demo mode to hydra-cluster to facilitate network resiliance tests [#1552](https://github.com/cardano-scaling/hydra/pull/1552)


## [0.18.1] - 2024-08-15

- New landing page and updated documentation style. [#1560](https://github.com/cardano-scaling/hydra/pull/1560)
Expand Down
4 changes: 2 additions & 2 deletions cabal.project
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ repository cardano-haskell-packages

-- See CONTRIBUTING.md for information about when and how to update these.
index-state:
, hackage.haskell.org 2024-09-23T15:45:50Z
, cardano-haskell-packages 2024-09-20T19:39:13Z
, hackage.haskell.org 2024-09-25T13:28:12Z
, cardano-haskell-packages 2024-09-23T21:46:49Z

packages:
hydra-prelude
Expand Down
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 28 additions & 5 deletions hydra-chain-observer/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
# Hydra Chain Observer

A small executable which connects to a chain like the `hydra-node`, but puts any
observations as traces onto `stdout`.
A lightweight executable designed to connect to a blockchain, such as the `hydra-node`, and streams chain observations as traces to `stdout`.
It supports two modes of operation: **Direct** connection to a node via socket, and connection through **Blockfrost** API.

To run, pass a `--node-socket`, corresponding network id and optionally
`--start-chain-from`. For example:
## Direct Mode

To run the observer in Direct Mode, provide the following arguments:
- `--node-socket`: path to the node socket file.
- network id: `--testnet-magic` (with magic number) for the testnet or `--mainnet` for the mainnet.
- (optional) `--start-chain-from`: specify a chain point (SLOT.HEADER_HASH) to start observing from.

For example:

``` shell
hydra-chain-observer \
hydra-chain-observer direct \
--node-socket testnets/preprod/node.socket \
--testnet-magic 1 \
--start-chain-from "41948777.5d34af0f42be9823ebd35c2d83d5d879c5615ac17f7158bb9aa4ef89072455a7"
```


## Blockfrost Mode

To run the observer in Blockfrost Mode, provide the following arguments:
- `--project-path`: file path to your Blockfrost project API token hash.
> expected to be prefixed with environment (e.g. testnetA3C2E...)
- (optional) `--start-chain-from`: specify a chain point (SLOT.HEADER_HASH) to start observing from.

For example:

``` shell
hydra-chain-observer blockfrost \
--project-path $PROJECT_TOKEN_HASH_PATH \
--start-chain-from "41948777.5d34af0f42be9823ebd35c2d83d5d879c5615ac17f7158bb9aa4ef89072455a7"
```

2 changes: 1 addition & 1 deletion hydra-chain-observer/exe/Main.hs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Main where

import Hydra.ChainObserver (defaultObserverHandler)
import Hydra.ChainObserver qualified
import Hydra.ChainObserver.NodeClient (defaultObserverHandler)
import Hydra.Prelude

main :: IO ()
Expand Down
7 changes: 7 additions & 0 deletions hydra-chain-observer/hydra-chain-observer.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,24 @@ library
hs-source-dirs: src
ghc-options: -haddock
build-depends:
, base16-bytestring
, blockfrost-client >=0.9.1.0
, hydra-cardano-api
, hydra-node
, hydra-plutus
, hydra-prelude
, hydra-tx
, io-classes
, optparse-applicative
, ouroboros-network-protocols
, retry

exposed-modules:
Hydra.Blockfrost.ChainObserver
Hydra.ChainObserver
Hydra.ChainObserver.NodeClient
Hydra.ChainObserver.Options
Hydra.Ouroborus.ChainObserver

executable hydra-chain-observer
import: project-config
Expand Down
227 changes: 227 additions & 0 deletions hydra-chain-observer/src/Hydra/Blockfrost/ChainObserver.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
{-# LANGUAGE DuplicateRecordFields #-}

module Hydra.Blockfrost.ChainObserver where

import Hydra.Prelude

import Blockfrost.Client (
BlockfrostClientT,
runBlockfrost,
)
import Blockfrost.Client qualified as Blockfrost
import Control.Concurrent.Class.MonadSTM (
MonadSTM (readTVarIO),
newTVarIO,
writeTVar,
)
import Control.Retry (constantDelay, retrying)
import Data.ByteString.Base16 qualified as Base16
import Hydra.Cardano.Api (
ChainPoint (..),
HasTypeProxy (..),
Hash,
NetworkId (..),
NetworkMagic (..),
SerialiseAsCBOR (..),
SlotNo (..),
Tx,
UTxO,
serialiseToRawBytes,
)
import Hydra.Cardano.Api.Prelude (
BlockHeader (..),
)
import Hydra.Chain.Direct.Handlers (convertObservation)
import Hydra.ChainObserver.NodeClient (
ChainObservation (..),
ChainObserverLog (..),
NodeClient (..),
ObserverHandler,
logOnChainTx,
observeAll,
)
import Hydra.Logging (Tracer, traceWith)
import Hydra.Tx (IsTx (..))

data APIBlockfrostError
= BlockfrostError Text
| DecodeError Text
| NotEnoughBlockConfirmations Blockfrost.BlockHash
| MissingBlockNo Blockfrost.BlockHash
| MissingNextBlockHash Blockfrost.BlockHash
deriving (Show, Exception)

runBlockfrostM ::
(MonadIO m, MonadThrow m) =>
Blockfrost.Project ->
BlockfrostClientT IO a ->
m a
runBlockfrostM prj action = do
result <- liftIO $ runBlockfrost prj action
case result of
Left err -> throwIO (BlockfrostError $ show err)
Right val -> pure val

blockfrostClient ::
Tracer IO ChainObserverLog ->
FilePath ->
Integer ->
NodeClient IO
blockfrostClient tracer projectPath blockConfirmations = do
NodeClient
{ follow = \startChainFrom observerHandler -> do
prj <- Blockfrost.projectFromFile projectPath

Blockfrost.Block{_blockHash = (Blockfrost.BlockHash genesisBlockHash)} <-
runBlockfrostM prj (Blockfrost.getBlock (Left 0))

Blockfrost.Genesis
{ _genesisActiveSlotsCoefficient
, _genesisSlotLength
, _genesisNetworkMagic
} <-
runBlockfrostM prj Blockfrost.getLedgerGenesis

let networkId = fromNetworkMagic _genesisNetworkMagic
traceWith tracer ConnectingToExternalNode{networkId}

chainPoint <-
case startChainFrom of
Just point -> pure point
Nothing -> do
toChainPoint <$> runBlockfrostM prj Blockfrost.getLatestBlock

traceWith tracer StartObservingFrom{chainPoint}

let blockTime = realToFrac _genesisSlotLength / realToFrac _genesisActiveSlotsCoefficient

let blockHash = fromChainPoint chainPoint genesisBlockHash

stateTVar <- newTVarIO (blockHash, mempty)
void $
retrying (retryPolicy blockTime) shouldRetry $ \_ -> do
loop tracer prj networkId blockTime observerHandler blockConfirmations stateTVar
`catch` \(ex :: APIBlockfrostError) ->
pure $ Left ex
}
where
shouldRetry _ = \case
Right{} -> pure False
Left err -> pure $ isRetryable err

retryPolicy blockTime = constantDelay (truncate blockTime * 1000 * 1000)

-- | Iterative process that follows the chain using a naive roll-forward approach,
-- keeping track of the latest known current block and UTxO view.
-- This process operates at full speed without waiting between calls,
-- favoring the catch-up process.
loop ::
(MonadIO m, MonadThrow m, MonadSTM m) =>
Tracer m ChainObserverLog ->
Blockfrost.Project ->
NetworkId ->
DiffTime ->
ObserverHandler m ->
Integer ->
TVar m (Blockfrost.BlockHash, UTxO) ->
m a
loop tracer prj networkId blockTime observerHandler blockConfirmations stateTVar = do
current <- readTVarIO stateTVar
next <- rollForward tracer prj networkId observerHandler blockConfirmations current
atomically $ writeTVar stateTVar next
loop tracer prj networkId blockTime observerHandler blockConfirmations stateTVar

-- | From the current block and UTxO view, we collect Hydra observations
-- and yield the next block and adjusted UTxO view.
rollForward ::
(MonadIO m, MonadThrow m) =>
Tracer m ChainObserverLog ->
Blockfrost.Project ->
NetworkId ->
ObserverHandler m ->
Integer ->
(Blockfrost.BlockHash, UTxO) ->
m (Blockfrost.BlockHash, UTxO)
rollForward tracer prj networkId observerHandler blockConfirmations (blockHash, utxo) = do
[email protected]
{ _blockHash
, _blockConfirmations
, _blockNextBlock
, _blockHeight
} <-
runBlockfrostM prj $ Blockfrost.getBlock (Right blockHash)

-- Check if block within the safe zone to be processes
when (_blockConfirmations < blockConfirmations) $
throwIO (NotEnoughBlockConfirmations _blockHash)

-- Check if block contains a reference to its next
nextBlockHash <- maybe (throwIO $ MissingNextBlockHash _blockHash) pure _blockNextBlock

-- Search block transactions
txHashes <- runBlockfrostM prj . Blockfrost.allPages $ \p ->
Blockfrost.getBlockTxs' (Right _blockHash) p Blockfrost.def

-- Collect CBOR representations
cborTxs <- traverse (runBlockfrostM prj . Blockfrost.getTxCBOR) txHashes

-- Convert to cardano-api Tx
receivedTxs <- mapM toTx cborTxs
let receivedTxIds = txId <$> receivedTxs
let point = toChainPoint block
traceWith tracer RollForward{point, receivedTxIds}

-- Collect head observations
let (adjustedUTxO, observations) = observeAll networkId utxo receivedTxs
let onChainTxs = mapMaybe convertObservation observations
forM_ onChainTxs (traceWith tracer . logOnChainTx)

blockNo <- maybe (throwIO $ MissingBlockNo _blockHash) (pure . fromInteger) _blockHeight
let observationsAt = HeadObservation point blockNo <$> onChainTxs

-- Call observer handler
observerHandler $
if null observationsAt
then [Tick point blockNo]
else observationsAt

-- Next
pure (nextBlockHash, adjustedUTxO)

-- * Helpers

isRetryable :: APIBlockfrostError -> Bool
isRetryable (BlockfrostError _) = True
isRetryable (DecodeError _) = False
isRetryable (NotEnoughBlockConfirmations _) = True
isRetryable (MissingBlockNo _) = True
isRetryable (MissingNextBlockHash _) = True

toChainPoint :: Blockfrost.Block -> ChainPoint
toChainPoint Blockfrost.Block{_blockSlot, _blockHash} =
ChainPoint slotNo headerHash
where
slotNo :: SlotNo
slotNo = maybe 0 (fromInteger . Blockfrost.unSlot) _blockSlot

headerHash :: Hash BlockHeader
headerHash = fromString . toString $ Blockfrost.unBlockHash _blockHash

fromNetworkMagic :: Integer -> NetworkId
fromNetworkMagic = \case
0 -> Mainnet
magicNbr -> Testnet (NetworkMagic (fromInteger magicNbr))

toTx :: MonadThrow m => Blockfrost.TransactionCBOR -> m Tx
toTx (Blockfrost.TransactionCBOR txCbor) =
case decodeBase16 txCbor of
Left decodeErr -> throwIO . DecodeError $ "Bad Base16 Tx CBOR: " <> decodeErr
Right bytes ->
case deserialiseFromCBOR (proxyToAsType (Proxy @Tx)) bytes of
Left deserializeErr -> throwIO . DecodeError $ "Bad Tx CBOR: " <> show deserializeErr
Right tx -> pure tx

fromChainPoint :: ChainPoint -> Text -> Blockfrost.BlockHash
fromChainPoint chainPoint genesisBlockHash = case chainPoint of
ChainPoint _ headerHash -> Blockfrost.BlockHash (decodeUtf8 . Base16.encode . serialiseToRawBytes $ headerHash)
ChainPointAtGenesis -> Blockfrost.BlockHash genesisBlockHash
Loading