Skip to content

Commit 44e779b

Browse files
authored
Merge pull request #1242 from IntersectMBO/mgalazyn/feature/rpc-fetchblock-add-timestamp
cardano-rpc: Add timestamp to fetchBlock
2 parents 9297a0f + f4c0241 commit 44e779b

11 files changed

Lines changed: 115 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`.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
project: cardano-api
2+
pr: 1242
3+
kind:
4+
- compatible
5+
description: |
6+
Re-export more consensus types from Cardano.Api.Consensus for node kernel access: getCurrentLedger, TopLevelConfig, configLedger, configBlock, ledgerState, HasHardForkHistory(..), ConfigSupportsNode, mkInterpreter. Add nodeSystemStart helper extracting SystemStart from TopLevelConfig.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
project: cardano-rpc
2+
pr: 1242
3+
kind:
4+
- feature
5+
- breaking
6+
description: |
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
@@ -45,16 +45,20 @@ module Cardano.Api.Consensus
4545
, ProtocolInfoArgs (..)
4646
, ProtocolClient (..)
4747
, ProtocolClientInfoArgs (..)
48+
, nodeSystemStart
4849

4950
-- * Reexports from @ouroboros-consensus@
5051
, BlockComponent (..)
5152
, ByronBlock
5253
, CardanoBlock
5354
, ChainDB.ChainDB
5455
, ChainDB.getBlockComponent
56+
, ChainDB.getCurrentLedger
57+
, ConfigSupportsNode
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/Protocol.hs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,20 @@ module Cardano.Api.Consensus.Internal.Protocol
1717
, ProtocolInfoArgs (..)
1818
, ProtocolClient (..)
1919
, ProtocolClientInfoArgs (..)
20+
, nodeSystemStart
2021
)
2122
where
2223

2324
import Cardano.Api.Consensus.Internal.Mode
2425

26+
import Cardano.Slotting.Time (SystemStart)
2527
import Ouroboros.Consensus.Block.Forging (MkBlockForging (..))
2628
import Ouroboros.Consensus.Byron.ByronHFC (ByronBlockHFC)
2729
import Ouroboros.Consensus.Cardano
2830
import Ouroboros.Consensus.Cardano.Block
2931
import Ouroboros.Consensus.Cardano.Node
32+
import Ouroboros.Consensus.Config (TopLevelConfig, configBlock)
33+
import Ouroboros.Consensus.Config.SupportsNode (ConfigSupportsNode (getSystemStart))
3034
import Ouroboros.Consensus.HardFork.Combinator.Embed.Unary
3135
import Ouroboros.Consensus.Ledger.SupportsProtocol qualified as Consensus (LedgerSupportsProtocol)
3236
import Ouroboros.Consensus.Node.ProtocolInfo (ProtocolClientInfo (..), ProtocolInfo (..))
@@ -143,3 +147,7 @@ data SomeBlockType where
143147
SomeBlockType :: BlockType blk -> SomeBlockType
144148

145149
deriving instance Show SomeBlockType
150+
151+
-- | Extract the network system start time from the node's top-level configuration.
152+
nodeSystemStart :: ConfigSupportsNode blk => TopLevelConfig blk -> SystemStart
153+
nodeSystemStart = getSystemStart . configBlock

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,31 @@ module Cardano.Api.Consensus.Internal.Reexport
22
( BlockComponent (..)
33
, ByronBlock
44
, CardanoBlock
5+
, ConfigSupportsNode
56
, ChainDepState
67
, GenTx (..)
78
, HasHeader
89
, HeaderHash
910
, EraMismatch (..)
1011
, NodeKernel (..)
1112
, OneEraHash (..)
13+
, HasHardForkHistory (..)
1214
, PastHorizonException
1315
, PraosProtocolSupportsNode
1416
, PraosProtocolSupportsNodeCrypto
1517
, RealPoint (..)
1618
, ShelleyGenesisStaking (..)
1719
, StandardCrypto
20+
, TopLevelConfig
21+
, ledgerState
1822
, blockNo
1923
, byronIdTx
24+
, configBlock
25+
, configLedger
2026
, condense
2127
, getOpCertCounters
2228
, interpreterToEpochInfo
29+
, mkInterpreter
2330
, unsafeExtendSafeZone
2431
, txId
2532
)
@@ -29,12 +36,17 @@ import Cardano.Protocol.Crypto (StandardCrypto)
2936
import Ouroboros.Consensus.Block (HasHeader, HeaderHash, RealPoint (..), blockNo)
3037
import Ouroboros.Consensus.Byron.Ledger (ByronBlock, GenTx (..), byronIdTx)
3138
import Ouroboros.Consensus.Cardano.Block (CardanoBlock, EraMismatch (..))
39+
import Ouroboros.Consensus.Config (TopLevelConfig, configBlock, configLedger)
40+
import Ouroboros.Consensus.Config.SupportsNode (ConfigSupportsNode)
41+
import Ouroboros.Consensus.HardFork.Abstract (HasHardForkHistory (..))
3242
import Ouroboros.Consensus.HardFork.Combinator.AcrossEras (OneEraHash (..))
3343
import Ouroboros.Consensus.HardFork.History.EpochInfo (interpreterToEpochInfo)
3444
import Ouroboros.Consensus.HardFork.History.Qry
3545
( PastHorizonException
46+
, mkInterpreter
3647
, unsafeExtendSafeZone
3748
)
49+
import Ouroboros.Consensus.Ledger.Extended (ledgerState)
3850
import Ouroboros.Consensus.Ledger.SupportsMempool (txId)
3951
import Ouroboros.Consensus.Node (NodeKernel (..))
4052
import Ouroboros.Consensus.Protocol.Abstract (ChainDepState)

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: 15 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,33 @@ fetchBlockMethod request = do
4345
throwNotFound =
4446
throwGrpcErrorWithMessage GrpcNotFound $
4547
"block not found at slot " <> tshow (unSlotNo slot)
48+
throwPastHorizon =
49+
throwGrpcErrorWithMessage GrpcInternal $
50+
"cannot convert slot "
51+
<> tshow (unSlotNo slot)
52+
<> " to timestamp: the slot is past the era history horizon;"
53+
<> " check that the requested slot is correct and that the node is fully in sync"
4654
headerHash <-
4755
deserialiseFromRawBytes (proxyToAsType (Proxy @(Hash BlockHeader))) hashBytes
4856
& either (const throwInvalidHash) pure
4957
(rawBytes, BlockNo height) <-
5058
fetchBlock nodeKernelAccess slot headerHash >>= maybe throwNotFound pure
59+
eraHistory <- readEraHistory
60+
timestampMs <-
61+
slotToUTCTime systemStart eraHistory slot
62+
& either (const throwPastHorizon) (pure . round . (* 1000) . utcTimeToPOSIXSeconds)
5163
let blockHeader =
5264
defMessage
5365
& U5c.slot .~ unSlotNo slot
5466
& U5c.hash .~ hashBytes
5567
& U5c.height .~ height
56-
-- TODO: timestamp - needs EraHistory from ledger state (snapshot migration)
5768
pure $
5869
defMessage
5970
& U5c.block
6071
.~ ( defMessage
6172
& U5c.nativeBytes .~ rawBytes
6273
& U5c.cardano . U5c.header .~ blockHeader
74+
& U5c.cardano . U5c.timestamp .~ timestampMs
6375
)
6476

6577
-- 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

0 commit comments

Comments
 (0)