Skip to content

Commit 73e1a16

Browse files
committed
cardano-rpc: Add timestamp to fetchBlock
1 parent 49d3ec7 commit 73e1a16

8 files changed

Lines changed: 96 additions & 30 deletions

File tree

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
project: cardano-api
2-
pr: 1232
2+
pr: 1242
33
kind:
44
- compatible
55
description: |
6-
Re-export consensus types from Cardano.Api.Consensus for node kernel access: NodeKernel, ChainDB, BlockComponent, RealPoint, OneEraHash, HeaderHash, HasHeader, BlockType, StandardCrypto, CardanoBlock, blockNo.
6+
Re-export consensus types from Cardano.Api.Consensus for node kernel access: NodeKernel, ChainDB, BlockComponent, RealPoint, OneEraHash, HeaderHash, HasHeader, BlockType, StandardCrypto, CardanoBlock, blockNo, getCurrentLedger, TopLevelConfig, configLedger, configBlock, ledgerState, HasHardForkHistory(..), ConfigSupportsNode, nodeSystemStart, mkInterpreter.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
project: cardano-rpc
2-
pr: 1232
2+
pr: 1242
33
kind:
44
- feature
55
- breaking
66
description: |
7-
Implement FetchBlock SyncService method via node kernel access. Fetches blocks from ChainDB with raw CBOR bytes and cardano block header (slot, hash, height). Breaking: `runRpcServer` signature now takes `IORef (Maybe NodeKernelAccess)` for node kernel access.
7+
FetchBlock: add Block.timestamp via slot-to-UTC conversion using EraHistory. Breaking: `mkNodeKernelAccess` now takes `TopLevelConfig`; `NodeKernelAccess` field `nkaChainDB` renamed to `chainDb`.

cardano-api/src/Cardano/Api/Consensus.hs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,13 @@ module Cardano.Api.Consensus
5252
, CardanoBlock
5353
, ChainDB.ChainDB
5454
, ChainDB.getBlockComponent
55+
, ChainDB.getCurrentLedger
56+
, ConfigSupportsNode
57+
, nodeSystemStart
5558
, ChainDepState
5659
, GenTx (..)
5760
, EraMismatch (..)
61+
, HasHardForkHistory (..)
5862
, HasHeader
5963
, HeaderHash
6064
, NodeKernel (..)
@@ -65,11 +69,16 @@ module Cardano.Api.Consensus
6569
, RealPoint (..)
6670
, ShelleyGenesisStaking (..)
6771
, StandardCrypto
72+
, TopLevelConfig
73+
, ledgerState
6874
, blockNo
6975
, byronIdTx
76+
, configBlock
77+
, configLedger
7078
, condense
7179
, getOpCertCounters
7280
, interpreterToEpochInfo
81+
, mkInterpreter
7382
, unsafeExtendSafeZone
7483
, txId
7584
)

cardano-api/src/Cardano/Api/Consensus/Internal/Reexport.hs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,53 @@ module Cardano.Api.Consensus.Internal.Reexport
22
( BlockComponent (..)
33
, ByronBlock
44
, CardanoBlock
5+
, ConfigSupportsNode
6+
, nodeSystemStart
57
, ChainDepState
68
, GenTx (..)
79
, HasHeader
810
, HeaderHash
911
, EraMismatch (..)
1012
, NodeKernel (..)
1113
, OneEraHash (..)
14+
, HasHardForkHistory (..)
1215
, PastHorizonException
1316
, PraosProtocolSupportsNode
1417
, PraosProtocolSupportsNodeCrypto
1518
, RealPoint (..)
1619
, ShelleyGenesisStaking (..)
1720
, StandardCrypto
21+
, TopLevelConfig
22+
, ledgerState
1823
, blockNo
1924
, byronIdTx
25+
, configBlock
26+
, configLedger
2027
, condense
2128
, getOpCertCounters
2229
, interpreterToEpochInfo
30+
, mkInterpreter
2331
, unsafeExtendSafeZone
2432
, txId
2533
)
2634
where
2735

2836
import Cardano.Protocol.Crypto (StandardCrypto)
37+
import Cardano.Slotting.Time (SystemStart)
2938
import Ouroboros.Consensus.Block (HasHeader, HeaderHash, RealPoint (..), blockNo)
3039
import Ouroboros.Consensus.Byron.Ledger (ByronBlock, GenTx (..), byronIdTx)
3140
import Ouroboros.Consensus.Cardano.Block (CardanoBlock, EraMismatch (..))
41+
import Ouroboros.Consensus.Config (TopLevelConfig, configBlock, configLedger)
42+
import Ouroboros.Consensus.Config.SupportsNode (ConfigSupportsNode (getSystemStart))
43+
import Ouroboros.Consensus.HardFork.Abstract (HasHardForkHistory (..))
3244
import Ouroboros.Consensus.HardFork.Combinator.AcrossEras (OneEraHash (..))
3345
import Ouroboros.Consensus.HardFork.History.EpochInfo (interpreterToEpochInfo)
3446
import Ouroboros.Consensus.HardFork.History.Qry
3547
( PastHorizonException
48+
, mkInterpreter
3649
, unsafeExtendSafeZone
3750
)
51+
import Ouroboros.Consensus.Ledger.Extended (ledgerState)
3852
import Ouroboros.Consensus.Ledger.SupportsMempool (txId)
3953
import Ouroboros.Consensus.Node (NodeKernel (..))
4054
import Ouroboros.Consensus.Protocol.Abstract (ChainDepState)
@@ -46,3 +60,7 @@ import Ouroboros.Consensus.Protocol.Praos.Common
4660
import Ouroboros.Consensus.Shelley.Node (ShelleyGenesisStaking (..))
4761
import Ouroboros.Consensus.Storage.Common (BlockComponent (..))
4862
import Ouroboros.Consensus.Util.Condense (condense)
63+
64+
-- | Extract the network system start time from the node's top-level configuration.
65+
nodeSystemStart :: ConfigSupportsNode blk => TopLevelConfig blk -> SystemStart
66+
nodeSystemStart = getSystemStart . configBlock

cardano-rpc/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ It implements [UTxO RPC](https://utxorpc.org/introduction) protobuf communicatio
3333

3434
| Method | Status |
3535
|--------|--------|
36-
| [FetchBlock](https://utxorpc.org/sync/spec/#fetchblockrequest) | 🚧 In progress (missing: `Block.timestamp`, `Block.body.tx`) |
36+
| [FetchBlock](https://utxorpc.org/sync/spec/#fetchblockrequest) | 🚧 In progress (missing: `Block.body.tx`) |
3737
| [DumpHistory](https://utxorpc.org/sync/spec/#dumphistoryrequest) | ⬜ Not supported |
3838
| [FollowTip](https://utxorpc.org/sync/spec/#followtiprequest) | ⬜ Not supported |
3939
| [ReadTip](https://utxorpc.org/sync/spec/#readtiprequest) | ⬜ Not supported |

cardano-rpc/src/Cardano/Rpc/Server/Internal/UtxoRpc/Sync.hs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{-# LANGUAGE DataKinds #-}
22
{-# LANGUAGE FlexibleContexts #-}
3+
{-# LANGUAGE NamedFieldPuns #-}
34
{-# LANGUAGE TypeApplications #-}
45

56
-- | Handlers for the UTxO RPC @SyncService@ - synchronising chain data
@@ -20,7 +21,8 @@ import RIO
2021

2122
import Data.ByteString qualified as BS
2223
import Data.ProtoLens (defMessage)
23-
import Network.GRPC.Spec (GrpcError (GrpcInvalidArgument, GrpcNotFound), Proto)
24+
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
25+
import Network.GRPC.Spec (GrpcError (GrpcInternal, GrpcInvalidArgument, GrpcNotFound), Proto)
2426

2527
-- | Handle the @FetchBlock@ SyncService RPC method.
2628
-- Fetches a block from ChainDB by slot and header hash.
@@ -33,7 +35,7 @@ fetchBlockMethod
3335
-> m (Proto U5c.FetchBlockResponse)
3436
-- ^ Response containing the fetched block with raw CBOR and cardano header
3537
fetchBlockMethod request = do
36-
nodeKernelAccess <- grabNodeKernelAccess
38+
nodeKernelAccess@NodeKernelAccess{systemStart, readEraHistory} <- grabNodeKernelAccess
3739
let blockRef = request ^. U5c.ref
3840
slot = SlotNo $ blockRef ^. U5c.slot
3941
hashBytes = blockRef ^. U5c.hash
@@ -43,23 +45,30 @@ fetchBlockMethod request = do
4345
throwNotFound =
4446
throwGrpcErrorWithMessage GrpcNotFound $
4547
"block not found at slot " <> tshow (unSlotNo slot)
48+
throwPastHorizon =
49+
throwGrpcErrorWithMessage GrpcInternal $
50+
"past horizon converting slot " <> tshow (unSlotNo slot) <> " to timestamp"
4651
headerHash <-
4752
deserialiseFromRawBytes (proxyToAsType (Proxy @(Hash BlockHeader))) hashBytes
4853
& either (const throwInvalidHash) pure
4954
(rawBytes, BlockNo height) <-
5055
fetchBlock nodeKernelAccess slot headerHash >>= maybe throwNotFound pure
56+
eraHistory <- readEraHistory
57+
timestampMs <-
58+
slotToUTCTime systemStart eraHistory slot
59+
& either (const throwPastHorizon) (pure . round . (* 1000) . utcTimeToPOSIXSeconds)
5160
let blockHeader =
5261
defMessage
5362
& U5c.slot .~ unSlotNo slot
5463
& U5c.hash .~ hashBytes
5564
& U5c.height .~ height
56-
-- TODO: timestamp - needs EraHistory from ledger state (snapshot migration)
5765
pure $
5866
defMessage
5967
& U5c.block
6068
.~ ( defMessage
6169
& U5c.nativeBytes .~ rawBytes
6270
& U5c.cardano . U5c.header .~ blockHeader
71+
& U5c.cardano . U5c.timestamp .~ timestampMs
6372
)
6473

6574
-- TODO: cardano.body.tx - needs full block deserialisation + UTxO RPC tx mapping

cardano-rpc/src/Cardano/Rpc/Server/NodeKernelAccess.hs

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import Cardano.Api.Consensus qualified as Consensus
1616
import Cardano.Rpc.Server.Internal.Monad (MonadRpc, grab)
1717
import Cardano.Rpc.Server.NodeKernelAccess.Type
1818

19-
import RIO (throwIO)
19+
import RIO (atomically, throwIO)
2020

2121
import Control.Tracer (Tracer, traceWith)
2222
import Data.ByteString (ByteString)
@@ -28,36 +28,38 @@ import Network.GRPC.Spec
2828
-- | Construct 'NodeKernelAccess' from a consensus 'Consensus.NodeKernel'.
2929
-- Returns 'Nothing' and traces the block type for non-Cardano block types.
3030
mkNodeKernelAccess
31-
:: Tracer IO Text
31+
:: Monad m
32+
=> Tracer m Text
3233
-- ^ Tracer for unsupported block type warnings
3334
-> Consensus.BlockType blk
3435
-- ^ Block type witness
36+
-> Consensus.TopLevelConfig blk
37+
-- ^ Top-level consensus config (for system start and era history)
3538
-> Consensus.NodeKernel IO addrNTN addrNTC blk
3639
-- ^ Consensus node kernel
37-
-> IO (Maybe NodeKernelAccess)
38-
mkNodeKernelAccess tracer blockType kernel = case blockType of
40+
-> m (Maybe NodeKernelAccess)
41+
mkNodeKernelAccess tracer blockType topLevelConfig kernel = case blockType of
3942
Consensus.CardanoBlockType ->
40-
pure $ Just NodeKernelAccess{nkaChainDB = Consensus.getChainDB kernel}
43+
pure $ Just NodeKernelAccess{chainDb, systemStart, readEraHistory}
44+
where
45+
chainDb = Consensus.getChainDB kernel
46+
ledgerConfig = Consensus.configLedger topLevelConfig
47+
systemStart = Consensus.nodeSystemStart topLevelConfig
48+
-- Read the current ledger state (cheap STM TVar read) and recompute
49+
-- the era summary on every call - O(number_of_eras).
50+
-- This is the same approach consensus uses for GetInterpreter queries
51+
-- (interpretQueryHardFork); neither path caches the summary.
52+
-- RunWithCachedSummary exists but is private to the blockchain time thread.
53+
readEraHistory :: MonadIO n => n EraHistory
54+
readEraHistory = liftIO $ do
55+
extLedger <- atomically $ Consensus.getCurrentLedger chainDb
56+
pure . EraHistory . Consensus.mkInterpreter $
57+
Consensus.hardForkSummary ledgerConfig (Consensus.ledgerState extLedger)
4158
_ -> do
59+
-- unsupported block type
4260
traceWith tracer $ pack (show blockType)
4361
pure Nothing
4462

45-
-- | Fetch a raw block and its block number from ChainDB by slot and header hash.
46-
fetchBlock
47-
:: MonadIO m
48-
=> NodeKernelAccess
49-
-- ^ Node kernel access handle
50-
-> SlotNo
51-
-- ^ Block slot number
52-
-> Hash BlockHeader
53-
-- ^ Block header hash
54-
-> m (Maybe (ByteString, BlockNo))
55-
-- ^ Raw CBOR bytes and block number, or 'Nothing' if not found
56-
fetchBlock NodeKernelAccess{nkaChainDB} slot (HeaderHash shortHash) = do
57-
let point = Consensus.RealPoint slot (Consensus.OneEraHash shortHash)
58-
component = (,) <$> fmap BSL.toStrict Consensus.GetRawBlock <*> fmap Consensus.blockNo Consensus.GetBlock
59-
liftIO $ Consensus.getBlockComponent nkaChainDB component point
60-
6163
-- | Grab the current 'NodeKernelAccess' from the environment, or throw
6264
-- gRPC UNAVAILABLE if the node kernel has not yet initialised.
6365
grabNodeKernelAccess
@@ -75,3 +77,19 @@ grabNodeKernelAccess =
7577
}
7678
Just nodeKernelAccess ->
7779
pure nodeKernelAccess
80+
81+
-- | Fetch a raw block and its block number from ChainDB by slot and header hash.
82+
fetchBlock
83+
:: MonadIO m
84+
=> NodeKernelAccess
85+
-- ^ Node kernel access handle
86+
-> SlotNo
87+
-- ^ Block slot number
88+
-> Hash BlockHeader
89+
-- ^ Block header hash
90+
-> m (Maybe (ByteString, BlockNo))
91+
-- ^ Raw CBOR bytes and block number, or 'Nothing' if not found
92+
fetchBlock NodeKernelAccess{chainDb} slot (HeaderHash shortHash) = do
93+
let point = Consensus.RealPoint slot (Consensus.OneEraHash shortHash)
94+
component = (,) <$> fmap BSL.toStrict Consensus.GetRawBlock <*> fmap Consensus.blockNo Consensus.GetBlock
95+
liftIO $ Consensus.getBlockComponent chainDb component point
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
1+
{-# LANGUAGE RankNTypes #-}
12
{-# LANGUAGE NoFieldSelectors #-}
23

34
module Cardano.Rpc.Server.NodeKernelAccess.Type
45
( NodeKernelAccess (..)
56
)
67
where
78

9+
import Cardano.Api (EraHistory, SystemStart)
810
import Cardano.Api.Consensus qualified as Consensus
911

12+
import Control.Monad.IO.Class (MonadIO)
13+
1014
-- | In-process access to the node kernel.
1115
-- Constructed by cardano-node once consensus initialisation completes.
1216
data NodeKernelAccess = NodeKernelAccess
13-
{ nkaChainDB :: Consensus.ChainDB IO (Consensus.CardanoBlock Consensus.StandardCrypto)
17+
{ chainDb :: Consensus.ChainDB IO (Consensus.CardanoBlock Consensus.StandardCrypto)
1418
-- ^ Handle to the consensus chain database
19+
, systemStart :: SystemStart
20+
-- ^ Network system start time, extracted from genesis config
21+
, readEraHistory :: forall m. MonadIO m => m EraHistory
22+
-- ^ Read current era history from the ledger state.
23+
-- This is a separate read from 'chainDb', but the inconsistency is
24+
-- always safe: the ledger state is at or ahead of any block in ChainDB,
25+
-- and era summaries only grow, so the returned history always covers the
26+
-- slot of any block fetched from ChainDB.
1527
}

0 commit comments

Comments
 (0)