Skip to content

Commit 0b2a841

Browse files
committed
Add Query.Reward.
New sub-module for reward-related queries (non-myopic member rewards, per-pool reward info, reward provenance). Adds Ord instances to RewardInfoPool and RewardParams in Wallet.hs to support the QueryResultRewardInfoPools stable type.
1 parent c2ff5f1 commit 0b2a841

8 files changed

Lines changed: 267 additions & 3 deletions

File tree

eras/shelley/impl/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 1.19.0.0
44

5+
* Add `Ord` instance for `RewardInfoPool` and `RewardParams`.
56
* Add `Shelley.API.Forecast` and `Shelley.Forecast`:
67
- Add `EraForecast` and `ShelleyEraForecast` typeclasses to deprecate `GetLedgerView` from `cardano-ledger-tpraos`.
78
- Add `currentForecast` and `futureForecast` functions to deprecate `currentLedgerView` and `futureLedgerView`.

eras/shelley/impl/src/Cardano/Ledger/Shelley/API/Wallet.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ data RewardInfoPool = RewardInfoPool
284284
-- Can be larger than @1.0@ for pool that gets lucky.
285285
-- (If some pools get unlucky, some pools must get lucky.)
286286
}
287-
deriving (Eq, Show, Generic)
287+
deriving (Eq, Ord, Show, Generic)
288288

289289
instance NoThunks RewardInfoPool
290290

@@ -305,7 +305,7 @@ data RewardParams = RewardParams
305305
, totalStake :: Coin
306306
-- ^ Maximum lovelace supply minus treasury
307307
}
308-
deriving (Eq, Show, Generic)
308+
deriving (Eq, Ord, Show, Generic)
309309

310310
instance NoThunks RewardParams
311311

libs/cardano-ledger-api/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 1.14.0.0
44

5+
* Add new `Query.Reward` sub-module with `QueryResultRewardInfoPools`, `queryNonMyopicMemberRewards`, `queryRewardInfoPools`, `queryRewardProvenance` (deprecated).
56
* Add new `Query.UTxO` sub-module with `queryUTxOFull`, `queryUTxOByAddress`, `queryUTxOByTxIn`.
67
* Add new `Query.Debug` sub-module with `queryDebugEpochState`, `queryDebugNewEpochState`, `queryProposedPParamsUpdates` (deprecated).
78
* Add to `Query.Pool`: `queryStakePools`, `queryStakePoolDistrByTotalSupply`, `queryStakePoolDistrFromSnapshot`, `queryStakePoolRelays`.

libs/cardano-ledger-api/cardano-ledger-api.cabal

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,15 @@ library
4242
hs-source-dirs: src
4343
other-modules:
4444
Cardano.Ledger.Api.Scripts.ExUnits
45+
Cardano.Ledger.Api.State.Query.Debug
4546
Cardano.Ledger.Api.State.Query.Epoch
4647
Cardano.Ledger.Api.State.Query.Governance
4748
Cardano.Ledger.Api.State.Query.PParams
4849
Cardano.Ledger.Api.State.Query.Pool
50+
Cardano.Ledger.Api.State.Query.Reward
4951
Cardano.Ledger.Api.State.Query.Snapshot
5052
Cardano.Ledger.Api.State.Query.StakeDelegation
5153
Cardano.Ledger.Api.State.Query.UTxO
52-
Cardano.Ledger.Api.State.Query.Debug
5354

5455
default-language: Haskell2010
5556
ghc-options:
@@ -114,6 +115,7 @@ library testlib
114115
cardano-ledger-binary:{cardano-ledger-binary, testlib},
115116
cardano-ledger-core:{cardano-ledger-core, testlib},
116117
cardano-ledger-dijkstra:testlib,
118+
cardano-ledger-shelley,
117119
data-default,
118120
generic-random,
119121
prettyprinter,

libs/cardano-ledger-api/src/Cardano/Ledger/Api/State/Query.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ module Cardano.Ledger.Api.State.Query (
1313
-- * Pool queries
1414
module Cardano.Ledger.Api.State.Query.Pool,
1515

16+
-- * Reward queries
17+
module Cardano.Ledger.Api.State.Query.Reward,
18+
1619
-- * @GetStakeSnapshots@
1720
module Cardano.Ledger.Api.State.Query.Snapshot,
1821

@@ -31,6 +34,7 @@ import Cardano.Ledger.Api.State.Query.Epoch
3134
import Cardano.Ledger.Api.State.Query.Governance
3235
import Cardano.Ledger.Api.State.Query.PParams
3336
import Cardano.Ledger.Api.State.Query.Pool
37+
import Cardano.Ledger.Api.State.Query.Reward
3438
import Cardano.Ledger.Api.State.Query.Snapshot
3539
import Cardano.Ledger.Api.State.Query.StakeDelegation
3640
import Cardano.Ledger.Api.State.Query.UTxO
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE DeriveAnyClass #-}
3+
{-# LANGUAGE DeriveGeneric #-}
4+
{-# LANGUAGE FlexibleContexts #-}
5+
{-# LANGUAGE NamedFieldPuns #-}
6+
{-# LANGUAGE ScopedTypeVariables #-}
7+
{-# LANGUAGE StandaloneDeriving #-}
8+
9+
module Cardano.Ledger.Api.State.Query.Reward (
10+
-- * Stable query result types
11+
QueryResultRewardInfoPools (..),
12+
13+
-- * Queries
14+
queryNonMyopicMemberRewards,
15+
queryRewardInfoPools,
16+
queryRewardProvenance,
17+
) where
18+
19+
import Cardano.Ledger.Api.State.Query.Snapshot (queryCurrentSnapshot)
20+
import Cardano.Ledger.BaseTypes (Globals (..), epochInfoPure, unNonZero, (%?))
21+
import Cardano.Ledger.Binary (DecCBOR (..), EncCBOR (..))
22+
import Cardano.Ledger.Binary.Coders (Decode (..), Encode (..), decode, encode, (!>), (<!))
23+
import Cardano.Ledger.Coin (Coin (..))
24+
import Cardano.Ledger.Compactible (fromCompact)
25+
import Cardano.Ledger.Core (ppA0L, ppNOptL)
26+
import Cardano.Ledger.Credential (Credential (..))
27+
import Cardano.Ledger.Keys (KeyHash, KeyRole (StakePool, Staking))
28+
import Cardano.Ledger.Shelley.API.Wallet (
29+
RewardInfoPool (..),
30+
RewardParams (..),
31+
)
32+
import Cardano.Ledger.Shelley.LedgerState (
33+
EpochState (..),
34+
NewEpochState (..),
35+
RewardUpdate,
36+
circulation,
37+
createRUpd,
38+
curPParamsEpochStateL,
39+
)
40+
import Cardano.Ledger.Shelley.PoolRank (
41+
NonMyopic (..),
42+
PerformanceEstimate (..),
43+
calcNonMyopicMemberReward,
44+
getTopRankedPools,
45+
percentile',
46+
)
47+
import Cardano.Ledger.Shelley.RewardProvenance (RewardProvenance)
48+
import Cardano.Ledger.Shelley.Rewards (StakeShare (..))
49+
import Cardano.Ledger.Slot (epochInfoSize)
50+
import Cardano.Ledger.State (
51+
EraCertState,
52+
EraGov,
53+
EraStake,
54+
spssCost,
55+
spssMargin,
56+
spssPledge,
57+
spssSelfDelegatedOwnersStake,
58+
spssStake,
59+
ssStakePoolsSnapShot,
60+
)
61+
import qualified Cardano.Ledger.State as LS
62+
import Control.DeepSeq (NFData)
63+
import Control.Monad.Trans.Reader (runReader)
64+
import Data.Aeson (ToJSON)
65+
import Data.Default (Default (def))
66+
import Data.Map.Strict (Map)
67+
import qualified Data.Map.Strict as Map
68+
import Data.Set (Set)
69+
import qualified Data.VMap as VMap
70+
import Data.Word (Word64)
71+
import GHC.Generics (Generic)
72+
import Lens.Micro ((^.))
73+
import NoThunks.Class (NoThunks)
74+
75+
-- | Stable query result for per-pool reward information.
76+
data QueryResultRewardInfoPools = QueryResultRewardInfoPools
77+
{ qrripParams :: !RewardParams
78+
, qrripPools :: !(Map (KeyHash StakePool) RewardInfoPool)
79+
}
80+
deriving (Show, Eq, Ord, Generic)
81+
82+
deriving instance ToJSON QueryResultRewardInfoPools
83+
84+
instance NFData QueryResultRewardInfoPools
85+
86+
instance NoThunks QueryResultRewardInfoPools
87+
88+
instance EncCBOR QueryResultRewardInfoPools where
89+
encCBOR (QueryResultRewardInfoPools params pools) =
90+
encode $
91+
Rec QueryResultRewardInfoPools
92+
!> To params
93+
!> To pools
94+
95+
instance DecCBOR QueryResultRewardInfoPools where
96+
decCBOR =
97+
decode $
98+
RecD QueryResultRewardInfoPools
99+
<! From
100+
<! From
101+
102+
-- | Query non-myopic pool member rewards for a set of credentials.
103+
-- Source: ouroboros-consensus:ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Ledger/Query.hs:415
104+
-- answerPureBlockQuery case for GetNonMyopicMemberRewards
105+
--
106+
-- For each given credential (or hypothetical stake amount), returns a map from
107+
-- each stake pool to the estimated non-myopic member reward for that pool.
108+
-- Wallets use this to answer "how much would I earn if I delegated to pool X?".
109+
--
110+
-- "Non-myopic" means rewards are estimated under the assumption that stake
111+
-- will eventually converge to a Nash equilibrium across pools, rather than
112+
-- using today's distribution naively. This avoids overestimating rewards for
113+
-- currently under-saturated pools.
114+
--
115+
-- The input set contains @'Either' 'Coin' ('Credential' 'Staking')@ values:
116+
--
117+
-- * @'Left' coin@ — a hypothetical stake amount (for "what if I delegated
118+
-- this much?" queries).
119+
-- * @'Right' cred@ — an existing credential whose current active stake is
120+
-- looked up from the ledger state.
121+
--
122+
-- This uses a fresh snapshot derived from the current ledger state, not the
123+
-- stored mark\/set\/go snapshots.
124+
queryNonMyopicMemberRewards ::
125+
(EraGov era, EraStake era, EraCertState era) =>
126+
-- | maxLovelaceSupply from genesis config
127+
Word64 ->
128+
NewEpochState era ->
129+
Set (Either Coin (Credential Staking)) ->
130+
Map
131+
(Either Coin (Credential Staking))
132+
(Map (KeyHash StakePool) Coin)
133+
queryNonMyopicMemberRewards maxLovelaceSupply ss = Map.fromSet nmmRewards
134+
where
135+
maxSupply = Coin . fromIntegral $ maxLovelaceSupply
136+
totalStakeCoin@(Coin totalStake) = circulation es maxSupply
137+
toShare (Coin x) = StakeShare $ x %? totalStake
138+
memShare (Right cred) =
139+
toShare $
140+
maybe mempty (fromCompact . unNonZero . LS.swdStake) $
141+
VMap.lookup cred (LS.unActiveStake activeStake)
142+
memShare (Left coin) = toShare coin
143+
es = nesEs ss
144+
pp = es ^. curPParamsEpochStateL
145+
NonMyopic {likelihoodsNM = ls, rewardPotNM = rPot} = esNonMyopic es
146+
LS.SnapShot {LS.ssActiveStake = activeStake, LS.ssStakePoolsSnapShot = stakePoolsSnapShot} = queryCurrentSnapshot ss
147+
calcNMMRewards t poolId spss
148+
| spssPledge <= spssSelfDelegatedOwnersStake =
149+
calcNonMyopicMemberReward pp rPot poolId spssCost spssMargin s sigma t topPools hitRateEst
150+
| otherwise = mempty
151+
where
152+
LS.StakePoolSnapShot {spssSelfDelegatedOwnersStake, spssPledge, spssCost, spssMargin} = spss
153+
s = toShare spssPledge
154+
hitRateEst = percentile' (histLookup poolId)
155+
sigma = toShare (fromCompact (LS.spssStake spss))
156+
157+
nmmRewards cred = VMap.toMap $ VMap.mapWithKey (calcNMMRewards $ memShare cred) stakePoolsSnapShot
158+
histLookup k = VMap.findWithDefault mempty k ls
159+
topPools =
160+
getTopRankedPools rPot totalStakeCoin pp $
161+
Map.intersectionWith (,) (VMap.toMap (VMap.map percentile' ls)) $
162+
VMap.toMap stakePoolsSnapShot
163+
164+
-- | Query per-pool reward information from the current stake distribution.
165+
-- Source: ouroboros-consensus:ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Ledger/Query.hs:447
166+
-- answerPureBlockQuery case for GetRewardInfoPools
167+
--
168+
-- This is the primary query for wallets and tools that display pool ranking,
169+
-- saturation levels, and estimated returns. It returns per-pool data (stake,
170+
-- pledge, costs, margins, performance estimates) together with global reward
171+
-- parameters (a0, nOpt, totalStake, rPot).
172+
--
173+
-- Uses a fresh snapshot from 'queryCurrentSnapshot' rather than the stored
174+
-- epoch-boundary snapshots, so the data reflects the most recent ledger state.
175+
queryRewardInfoPools ::
176+
(EraGov era, EraStake era, EraCertState era) =>
177+
-- | maxLovelaceSupply from genesis config
178+
Word64 ->
179+
NewEpochState era ->
180+
QueryResultRewardInfoPools
181+
queryRewardInfoPools maxLovelaceSupply nes =
182+
QueryResultRewardInfoPools rewardParams poolInfos
183+
where
184+
es = nesEs nes
185+
pp = es ^. curPParamsEpochStateL
186+
NonMyopic {likelihoodsNM = ls, rewardPotNM = rPot} = esNonMyopic es
187+
histLookup poolId = VMap.findWithDefault mempty poolId ls
188+
189+
LS.SnapShot {ssStakePoolsSnapShot} = queryCurrentSnapshot nes
190+
191+
rewardParams =
192+
RewardParams
193+
{ a0 = pp ^. ppA0L
194+
, nOpt = pp ^. ppNOptL
195+
, totalStake =
196+
let supply = Coin (fromIntegral maxLovelaceSupply)
197+
in circulation es supply
198+
, rPot = rPot
199+
}
200+
poolInfos = VMap.toMap $ VMap.mapWithKey mkRewardInfoPool ssStakePoolsSnapShot
201+
mkRewardInfoPool poolId LS.StakePoolSnapShot {spssStake, spssSelfDelegatedOwnersStake, spssPledge, spssMargin, spssCost} =
202+
RewardInfoPool
203+
{ stake = fromCompact spssStake
204+
, ownerStake = spssSelfDelegatedOwnersStake
205+
, ownerPledge = spssPledge
206+
, margin = spssMargin
207+
, cost = spssCost
208+
, performanceEstimate =
209+
unPerformanceEstimate $ percentile' $ histLookup poolId
210+
}
211+
212+
-- | Query reward provenance by computing a reward update (internally based on the go snapshot).
213+
-- Source: ouroboros-consensus:ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Ledger/Query.hs:441
214+
-- answerPureBlockQuery case for GetRewardProvenance
215+
--
216+
-- __Note:__ The 'RewardProvenance' component of the result is always 'def'
217+
-- (empty\/default). Only the 'RewardUpdate' is meaningful. This matches the
218+
-- consensus implementation, which also discards provenance.
219+
--
220+
-- This function requires the full 'Globals' because it internally runs
221+
-- 'createRUpd' in a 'Reader' monad that needs access to 'epochInfo',
222+
-- 'activeSlotCoeff', 'securityParameter', and 'maxLovelaceSupply'.
223+
queryRewardProvenance ::
224+
(EraGov era, EraCertState era) =>
225+
Globals ->
226+
NewEpochState era ->
227+
(RewardUpdate, RewardProvenance)
228+
queryRewardProvenance globals newEpochState =
229+
( runReader
230+
(createRUpd slotsPerEpoch blocksMade epochState maxSupply asc secparam)
231+
globals
232+
, def
233+
)
234+
where
235+
epochState = nesEs newEpochState
236+
maxSupply = Coin (fromIntegral (maxLovelaceSupply globals))
237+
blocksMade = nesBprev newEpochState
238+
epochNo = nesEL newEpochState
239+
slotsPerEpoch = epochInfoSize (epochInfoPure globals) epochNo
240+
asc = activeSlotCoeff globals
241+
secparam = securityParameter globals
242+
{-# DEPRECATED
243+
queryRewardProvenance
244+
"Wallets should prefer 'queryRewardInfoPools' for up-to-date reward information based on the current stake distribution."
245+
#-}

libs/cardano-ledger-api/test/Test/Cardano/Ledger/Api/State/QuerySpec.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ latestErasSpec =
7979
roundTripEraExpectation @era @QueryResultDRepState
8080
prop "QueryResultDRepStates" $
8181
roundTripEraExpectation @era @QueryResultDRepStates
82+
prop "QueryResultRewardInfoPools" $ roundTripEraExpectation @era @QueryResultRewardInfoPools
8283
describe "Queries" $ do
8384
committeeMembersStateSpec @era
8485
queryStakeSnapshotsSpec @era

libs/cardano-ledger-api/testlib/Test/Cardano/Ledger/Api/Arbitrary.hs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
module Test.Cardano.Ledger.Api.Arbitrary () where
44

55
import Cardano.Ledger.Api.State.Query
6+
import Cardano.Ledger.Shelley.API.Wallet (RewardInfoPool, RewardParams)
67
import Generic.Random (genericArbitraryU)
78
import Test.Cardano.Ledger.Common
89
import Test.Cardano.Ledger.Dijkstra.Arbitrary ()
@@ -34,6 +35,15 @@ instance Arbitrary QueryResultDelegsAndRewards where
3435
instance Arbitrary QueryResultDRepStates where
3536
arbitrary = genericArbitraryU
3637

38+
instance Arbitrary QueryResultRewardInfoPools where
39+
arbitrary = genericArbitraryU
40+
41+
instance Arbitrary RewardInfoPool where
42+
arbitrary = genericArbitraryU
43+
44+
instance Arbitrary RewardParams where
45+
arbitrary = genericArbitraryU
46+
3747
instance Arbitrary QueryResultPoolState where
3848
arbitrary = QueryResultPoolState <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
3949

0 commit comments

Comments
 (0)