Skip to content

Commit 499fff2

Browse files
committed
cardano-rpc: Implement ReadEraSummary gRPC method
Serve era summaries from the node kernel's hard-fork history: era name, start and end boundaries (Unix-epoch milliseconds, slot, epoch). The current era's end is left unset (no well-defined ending yet) and per-era protocol parameters are left unset (the node keeps no historical parameters; use ReadParams instead).
1 parent c4fa4d9 commit 499fff2

10 files changed

Lines changed: 241 additions & 2 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
project: cardano-rpc
2+
pr: 1325
3+
kind:
4+
- feature
5+
- breaking
6+
description: |
7+
Implement the `ReadEraSummary` gRPC method, returning the era name and the start and end boundaries (Unix-epoch milliseconds, slot, epoch) of every era in the chain's history. The `NodeKernelAccess` type is now abstract; use the functions of `Cardano.Rpc.Server.NodeKernelAccess` instead of its record fields.

cardano-rpc/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Use a dedicated chain indexing service for those.
2121
| [ReadData](https://utxorpc.org/query/spec/#readdatarequest) | ❌ Not supported, needs a chain indexer |
2222
| [ReadTx](https://utxorpc.org/query/spec/#queryservice) | ❌ Not supported, needs a chain indexer |
2323
| [ReadGenesis](https://utxorpc.org/query/spec/#queryservice) | ✅ Supported |
24-
| [ReadEraSummary](https://utxorpc.org/query/spec/#queryservice) | ⬜ Not supported |
24+
| [ReadEraSummary](https://utxorpc.org/query/spec/#queryservice) | ✅ Supported |
2525
| [ReadState](https://utxorpc.org/query/spec/#queryservice) | ⬜ Not supported |
2626

2727
### [SubmitService](https://utxorpc.org/submit/spec/)

cardano-rpc/cardano-rpc.cabal

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ library
7171
Cardano.Rpc.Server.Internal.UtxoRpc.Type.Byron
7272
Cardano.Rpc.Server.Internal.UtxoRpc.Type.Certificate
7373
Cardano.Rpc.Server.Internal.UtxoRpc.Type.ChainPoint
74+
Cardano.Rpc.Server.Internal.UtxoRpc.Type.EraSummary
7475
Cardano.Rpc.Server.Internal.UtxoRpc.Type.Genesis
7576
Cardano.Rpc.Server.Internal.UtxoRpc.Type.Governance
7677
Cardano.Rpc.Server.Internal.UtxoRpc.Type.PlutusData
@@ -110,6 +111,7 @@ library
110111
cardano-ledger-dijkstra,
111112
cardano-ledger-shelley,
112113
cardano-rpc:gen,
114+
cardano-slotting,
113115
containers,
114116
contra-tracer,
115117
data-default,
@@ -131,6 +133,7 @@ library
131133
proto-lens-protobuf-types,
132134
random,
133135
rio,
136+
sop-extras,
134137
strict-sop-core,
135138
text,
136139
time,
@@ -185,6 +188,7 @@ test-suite cardano-rpc-test
185188
cardano-ledger-shelley,
186189
cardano-ledger-shelley:testlib,
187190
cardano-rpc,
191+
cardano-slotting,
188192
containers,
189193
formatting,
190194
grpc-spec,
@@ -193,10 +197,12 @@ test-suite cardano-rpc-test
193197
hedgehog-quickcheck,
194198
memory,
195199
mtl,
200+
ouroboros-consensus,
196201
ouroboros-consensus:cardano,
197202
proto-lens,
198203
rio,
199204
scientific,
205+
sop-extras,
200206
tasty,
201207
tasty-hedgehog,
202208
text,
@@ -211,6 +217,7 @@ test-suite cardano-rpc-test
211217
build-tool-depends: tasty-discover:tasty-discover
212218
other-modules:
213219
Test.Cardano.Rpc.ByronTx
220+
Test.Cardano.Rpc.EraSummary
214221
Test.Cardano.Rpc.Eval
215222
Test.Cardano.Rpc.FetchBlockTx
216223
Test.Cardano.Rpc.FollowTipStream

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ methodsUtxoRpc
7171
=> Methods m (ProtobufMethodsOf UtxoRpc.QueryService)
7272
methodsUtxoRpc =
7373
UnsupportedMethod -- readData
74-
. UnsupportedMethod -- readEraSummary
74+
. Method (mkNonStreaming $ wrapInSpan TraceRpcQueryReadEraSummarySpan . readEraSummaryMethod)
7575
. Method (mkNonStreaming $ wrapInSpan TraceRpcQueryReadGenesisSpan . readGenesisMethod)
7676
. Method (mkNonStreaming $ wrapInSpan TraceRpcQueryParamsSpan . readParamsMethod)
7777
. UnsupportedMethod -- readState

cardano-rpc/src/Cardano/Rpc/Server/Internal/Tracing.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ data TraceRpcQuery
3737
TraceRpcQuerySearchUtxosSpan TraceSpanEvent
3838
| -- | Span trace marking ReadGenesis query
3939
TraceRpcQueryReadGenesisSpan TraceSpanEvent
40+
| -- | Span trace marking ReadEraSummary query
41+
TraceRpcQueryReadEraSummarySpan TraceSpanEvent
4042
deriving Show
4143

4244
instance Pretty TraceRpc where
@@ -70,6 +72,8 @@ instance Pretty TraceRpcQuery where
7072
TraceRpcQuerySearchUtxosSpan (SpanEnd _) -> "Finished query search UTXO method"
7173
TraceRpcQueryReadGenesisSpan (SpanBegin _) -> "Started query read genesis method"
7274
TraceRpcQueryReadGenesisSpan (SpanEnd _) -> "Finished query read genesis method"
75+
TraceRpcQueryReadEraSummarySpan (SpanBegin _) -> "Started query read era summary method"
76+
TraceRpcQueryReadEraSummarySpan (SpanEnd _) -> "Finished query read era summary method"
7377

7478
instance Error TraceRpcQuery where
7579
prettyError = pretty

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module Cardano.Rpc.Server.Internal.UtxoRpc.Query
1616
, readUtxosMethod
1717
, searchUtxosMethod
1818
, readGenesisMethod
19+
, readEraSummaryMethod
1920
, paginateByTxIn
2021
)
2122
where
@@ -264,6 +265,21 @@ readShelleyGenesisWithInitialFunds shelleyGenesisFile@(File path) bootGenesisHas
264265
<> ": "
265266
<> Text.pack reason
266267

268+
-- | Handle the @ReadEraSummary@ RPC method.
269+
-- Returns the node's hard-fork era summary: one entry per era the node's
270+
-- ledger state has seen so far, with name and start/end boundaries. See
271+
-- 'eraSummariesToProto' for exactly which fields are populated.
272+
readEraSummaryMethod
273+
:: MonadRpc e m
274+
=> Proto UtxoRpc.ReadEraSummaryRequest
275+
-> m (Proto UtxoRpc.ReadEraSummaryResponse)
276+
readEraSummaryMethod _req = do
277+
-- TODO: field masks are ignored for now (same as readParamsMethod)
278+
nodeKernelAccess <- grabNodeKernelAccess
279+
summary <- readHardForkSummary nodeKernelAccess
280+
pure $
281+
defMessage & U5c.cardano .~ eraSummariesToProto (nodeKernelSystemStart nodeKernelAccess) summary
282+
267283
-- | The CAIP-2 chain identifier for a Cardano network, keyed on the Shelley
268284
-- network magic.
269285
-- This follows Dolos, the reference UTxO RPC implementation: the three

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
module Cardano.Rpc.Server.Internal.UtxoRpc.Type
66
( utxoRpcPParamsToProtocolParams
77
, genesisBundleToProto
8+
, eraSummariesToProto
89
, utxoToUtxoRpcAnyUtxoData
910
, txInTxOutToAnyUtxoData
1011
, anyUtxoDataUtxoRpcToUtxo
@@ -31,6 +32,7 @@ where
3132

3233
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.BigInt
3334
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.ChainPoint
35+
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.EraSummary
3436
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.Genesis
3537
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.PlutusData
3638
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.ProtocolParameters
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
{-# LANGUAGE LambdaCase #-}
2+
3+
-- | Conversion of the node's hard-fork era summary to the UTxO RPC
4+
-- 'U5c.EraSummaries' message.
5+
module Cardano.Rpc.Server.Internal.UtxoRpc.Type.EraSummary
6+
( eraSummariesToProto
7+
)
8+
where
9+
10+
import Cardano.Api (AnyCardanoEra (..), SystemStart, docToText, pretty, unEpochNo, unSlotNo)
11+
import Cardano.Api.Consensus qualified as Consensus
12+
import Cardano.Rpc.Proto.Api.UtxoRpc.Query qualified as U5c
13+
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.ChainPoint (utcTimeToMs)
14+
15+
import Cardano.Slotting.Time (fromRelativeTime)
16+
import Ouroboros.Consensus.Cardano.Block (CardanoEras)
17+
import Ouroboros.Consensus.HardFork.History qualified as History
18+
19+
import RIO
20+
21+
import Data.ProtoLens (defMessage)
22+
import Data.SOP.NonEmpty (nonEmptyToList)
23+
import Data.Text qualified as Text
24+
import Network.GRPC.Spec
25+
26+
-- | Convert the node's hard-fork era summary to the UTxO RPC
27+
-- 'U5c.EraSummaries' message.
28+
--
29+
-- Every era except the last gets its 'U5c.maybe''end' populated from the
30+
-- confirmed era transition. The last era's end is always left unset, even
31+
-- when consensus already supplies a bound for it: consensus cannot
32+
-- distinguish a confirmed transition from the safe-zone forecast horizon, so
33+
-- the spec's "if the era has a well-defined ending" only ever holds for
34+
-- non-final eras here. 'History.EraUnbounded' likewise maps to unset.
35+
--
36+
-- 'U5c.protocolParams' is left unset for every era: the node does not keep
37+
-- historical per-era protocol parameters. Use @ReadParams@ for the current
38+
-- era's parameters.
39+
eraSummariesToProto
40+
:: SystemStart
41+
-> History.Summary (CardanoEras Consensus.StandardCrypto)
42+
-> Proto U5c.EraSummaries
43+
eraSummariesToProto systemStart summary =
44+
defMessage & U5c.summaries .~ zipWith3 mkEraSummary eraNames isLastEra eraEntries
45+
where
46+
-- All eras in chronological order, i.e. the same order as the summary's
47+
-- entries: 'History.Summary' has no era name field, an entry's era is its
48+
-- position, so the names are zipped in positionally.
49+
eraNames :: [Text]
50+
eraNames =
51+
[ Text.toLower . docToText $ pretty era
52+
| AnyCardanoEra era <- [minBound .. maxBound]
53+
]
54+
55+
eraEntries :: [History.EraSummary]
56+
eraEntries = nonEmptyToList (History.getSummary summary)
57+
58+
-- 'eraEntries' is always non-empty ('Summary' wraps a non-empty list), so
59+
-- this always ends in exactly one 'True'.
60+
isLastEra :: [Bool]
61+
isLastEra = replicate (length eraEntries - 1) False <> [True]
62+
63+
mkEraSummary :: Text -> Bool -> History.EraSummary -> Proto U5c.EraSummary
64+
mkEraSummary name isLast entry =
65+
defMessage
66+
& U5c.name .~ name
67+
& U5c.start .~ boundToProto (History.eraStart entry)
68+
& U5c.maybe'end .~ if isLast then Nothing else endToProto (History.eraEnd entry)
69+
70+
endToProto :: History.EraEnd -> Maybe (Proto U5c.EraBoundary)
71+
endToProto = \case
72+
History.EraEnd bound -> Just (boundToProto bound)
73+
History.EraUnbounded -> Nothing
74+
75+
boundToProto :: History.Bound -> Proto U5c.EraBoundary
76+
boundToProto bound =
77+
defMessage
78+
& U5c.time .~ boundTimeMs bound
79+
& U5c.slot .~ unSlotNo (History.boundSlot bound)
80+
& U5c.epoch .~ unEpochNo (History.boundEpoch bound)
81+
82+
-- Reuses 'utcTimeToMs', the same millisecond conversion 'mkChainPointMsg'
83+
-- and 'mkTipBlockRef' use for their proto timestamps, for consistency
84+
-- across the API. 'fromRelativeTime' adds the boundary's 'RelativeTime' to
85+
-- the system start with 'Pico'-precision arithmetic throughout.
86+
boundTimeMs :: History.Bound -> Word64
87+
boundTimeMs bound = utcTimeToMs (fromRelativeTime systemStart (History.boundTime bound))

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module Cardano.Rpc.Server.NodeKernelAccess
1212
, securityParam
1313
, genesisConfig
1414
, readEraHistory
15+
, readHardForkSummary
1516
, readChainTipHeader
1617
, GenesisBundle (..)
1718
, mkNodeKernelAccess
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
{-# LANGUAGE TypeApplications #-}
4+
5+
module Test.Cardano.Rpc.EraSummary where
6+
7+
import Cardano.Api (EpochNo (..), SlotNo (..), SystemStart (..))
8+
import Cardano.Api.Consensus qualified as Consensus
9+
import Cardano.Rpc.Proto.Api.UtxoRpc.Query qualified as U5c
10+
import Cardano.Rpc.Server.Internal.UtxoRpc.Type (eraSummariesToProto)
11+
12+
import Cardano.Ledger.BaseTypes (knownNonZeroBounded)
13+
import Cardano.Slotting.Time (RelativeTime (..))
14+
import Ouroboros.Consensus.BlockchainTime.WallClock.Types (slotLengthFromSec)
15+
import Ouroboros.Consensus.Cardano.Block (CardanoEras)
16+
import Ouroboros.Consensus.HardFork.History qualified as History
17+
18+
import RIO
19+
20+
import Data.SOP.NonEmpty (NonEmpty (..))
21+
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
22+
23+
import Hedgehog as H
24+
import Hedgehog.Extras qualified as H
25+
26+
-- | Placeholder era parameters: 'eraSummariesToProto' never reads them, but
27+
-- an 'History.EraSummary' fixture still needs one.
28+
dummyEraParams :: History.EraParams
29+
dummyEraParams =
30+
History.defaultEraParams
31+
(Consensus.SecurityParam (knownNonZeroBounded @2160))
32+
(slotLengthFromSec 1)
33+
34+
mkBound :: SlotNo -> EpochNo -> RelativeTime -> History.Bound
35+
mkBound slot epoch time =
36+
History.Bound
37+
{ History.boundTime = time
38+
, History.boundSlot = slot
39+
, History.boundEpoch = epoch
40+
, History.boundPerasRound = History.NoPerasEnabled
41+
}
42+
43+
mkEraSummary :: History.Bound -> History.EraEnd -> History.EraSummary
44+
mkEraSummary start end =
45+
History.EraSummary
46+
{ History.eraStart = start
47+
, History.eraEnd = end
48+
, History.eraParams = dummyEraParams
49+
}
50+
51+
-- | Two eras: the boundary between them uses a fractional-second
52+
-- 'RelativeTime' to prove the millisecond conversion is exact (rounded to
53+
-- the nearest millisecond, never routed through 'Double'). The second era is
54+
-- last and carries a real 'History.EraEnd' bound, but its end must still
55+
-- come out unset.
56+
hprop_era_summary_multi_era :: Property
57+
hprop_era_summary_multi_era = H.propertyOnce $ do
58+
let systemStart = SystemStart (posixSecondsToUTCTime 0)
59+
60+
byronStart = mkBound (SlotNo 0) (EpochNo 0) (RelativeTime 0)
61+
-- 172800.6789s proves the ms conversion is exact fixed-point via the
62+
-- shared 'utcTimeToMs' (nearest-ms rounding): .6789s -> 679ms. A
63+
-- Double-based path, or a floor instead of a round, would give 678.
64+
transition = mkBound (SlotNo 21600) (EpochNo 1) (RelativeTime 172800.6789)
65+
shelleyEnd = mkBound (SlotNo 43200) (EpochNo 2) (RelativeTime 259200)
66+
67+
byronSummary = mkEraSummary byronStart (History.EraEnd transition)
68+
shelleySummary = mkEraSummary transition (History.EraEnd shelleyEnd)
69+
70+
summary :: History.Summary (CardanoEras Consensus.StandardCrypto)
71+
summary = History.Summary (NonEmptyCons byronSummary (NonEmptyOne shelleySummary))
72+
73+
proto = eraSummariesToProto systemStart summary
74+
entries = proto ^. U5c.summaries
75+
76+
length entries === 2
77+
78+
byronEntry <- H.nothingFail $ listToMaybe entries
79+
shelleyEntry <- H.nothingFail . listToMaybe $ drop 1 entries
80+
81+
byronEntry ^. U5c.name === "byron"
82+
byronEntry ^. U5c.start . U5c.time === 0
83+
byronEntry ^. U5c.start . U5c.slot === 0
84+
byronEntry ^. U5c.start . U5c.epoch === 0
85+
H.assertWith (byronEntry ^. U5c.maybe'end) isJust
86+
byronEntry ^. U5c.end . U5c.time === 172800679
87+
byronEntry ^. U5c.end . U5c.slot === 21600
88+
byronEntry ^. U5c.end . U5c.epoch === 1
89+
90+
shelleyEntry ^. U5c.name === "shelley"
91+
shelleyEntry ^. U5c.start . U5c.time === 172800679
92+
shelleyEntry ^. U5c.start . U5c.slot === 21600
93+
shelleyEntry ^. U5c.start . U5c.epoch === 1
94+
-- Last era: end must be unset even though the fixture supplies a real bound.
95+
H.assertWith (shelleyEntry ^. U5c.maybe'end) isNothing
96+
97+
-- | A single-era summary (only Byron) has exactly one entry, and that entry
98+
-- has no end, whether or not consensus reports the era as unbounded.
99+
hprop_era_summary_single_era_no_end :: Property
100+
hprop_era_summary_single_era_no_end = H.propertyOnce $ do
101+
let systemStart = SystemStart (posixSecondsToUTCTime 0)
102+
byronStart = mkBound (SlotNo 0) (EpochNo 0) (RelativeTime 0)
103+
byronSummary = mkEraSummary byronStart History.EraUnbounded
104+
105+
summary :: History.Summary (CardanoEras Consensus.StandardCrypto)
106+
summary = History.Summary (NonEmptyOne byronSummary)
107+
108+
proto = eraSummariesToProto systemStart summary
109+
entries = proto ^. U5c.summaries
110+
111+
length entries === 1
112+
113+
byronEntry <- H.nothingFail $ listToMaybe entries
114+
byronEntry ^. U5c.name === "byron"
115+
H.assertWith (byronEntry ^. U5c.maybe'end) isNothing

0 commit comments

Comments
 (0)