Skip to content

Commit 21534da

Browse files
authored
Merge pull request #1214 from IntersectMBO/mgalazyn/fix/guard-utxo-whole-set
cardano-rpc: guard against fetching entire UTXO set.
2 parents 98b7d19 + aabade5 commit 21534da

4 files changed

Lines changed: 55 additions & 32 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
project: cardano-rpc
2+
pr: 1214
3+
kind:
4+
- breaking
5+
description: |
6+
gRPC: Guard against fetching the entire UTxO set. ReadUtxos now returns an empty response when no keys are provided. SearchUtxos now rejects predicates that cannot be narrowed to specific addresses with INVALID_ARGUMENT, instead of falling back to QueryUTxOWhole.

cardano-rpc/cardano-rpc.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ library
7575
aeson,
7676
base,
7777
bytestring,
78-
cardano-api >=10.17,
78+
cardano-api >=11.2,
7979
cardano-binary,
8080
cardano-ledger-api,
8181
cardano-ledger-conway,

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
module Cardano.Rpc.Server.Internal.Error
1313
( throwEither
1414
, throwExceptT
15+
, throwGrpcErrorWithMessage
1516
, RpcException (..)
1617
)
1718
where
@@ -21,6 +22,7 @@ import Cardano.Api
2122
import RIO
2223

2324
import GHC.Stack
25+
import Network.GRPC.Spec (GrpcError, GrpcException (..))
2426

2527
throwEither :: (Error e, HasCallStack, MonadIO m, Show e, Typeable e) => Either e a -> m a
2628
throwEither = withFrozenCallStack $ either (throwIO . RpcException) pure
@@ -39,3 +41,15 @@ instance Exception RpcException where
3941
[ show (prettyError e)
4042
, prettyCallStack callStack
4143
]
44+
45+
-- | Throw a 'GrpcException' with the given error code and message.
46+
-- grapesy converts this to proper gRPC trailers before it reaches 'serverTopLevel'.
47+
throwGrpcErrorWithMessage :: MonadIO m => GrpcError -> Text -> m a
48+
throwGrpcErrorWithMessage err message =
49+
throwIO
50+
GrpcException
51+
{ grpcError = err
52+
, grpcErrorMessage = Just message
53+
, grpcErrorDetails = Nothing
54+
, grpcErrorMetadata = []
55+
}

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

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -74,35 +74,36 @@ readParamsMethod _req = do
7474
-- | Handle the @ReadUtxos@ RPC method.
7575
-- Looks up specific UTxO entries by their 'TxIn' keys and returns them
7676
-- along with the ledger tip.
77+
-- Returns an empty response when no keys are provided, matching other
78+
-- UTxO RPC implementations (Dolos, cardano-node-api, Dingo).
7779
readUtxosMethod
7880
:: MonadRpc e m
7981
=> Proto UtxoRpc.ReadUtxosRequest
8082
-> m (Proto UtxoRpc.ReadUtxosResponse)
81-
readUtxosMethod req = do
82-
utxoFilter <-
83-
if not (null $ req ^. U5c.keys)
84-
then QueryUTxOByTxIn . fromList <$> mapM txoRefToTxIn (req ^. U5c.keys)
85-
else pure QueryUTxOWhole
86-
87-
nodeConnInfo <- grab
88-
AnyCardanoEra era <- liftIO . throwExceptT $ determineEra nodeConnInfo
89-
eon <- forEraInEon @Era era (error "Minimum Conway era required") pure
90-
91-
let target = VolatileTip
92-
(utxo, chainPoint, blockNo, systemStart, eraHistory) <- liftIO . (throwEither =<<) $ executeLocalStateQueryExpr nodeConnInfo target $ do
93-
utxo <- throwEither =<< throwEither =<< queryUtxo (convert eon) utxoFilter
94-
chainPoint <- throwEither =<< queryChainPoint
95-
blockNo <- throwEither =<< queryChainBlockNo
96-
systemStart <- throwEither =<< querySystemStart
97-
eraHistory <- throwEither =<< queryEraHistory
98-
pure (utxo, chainPoint, blockNo, systemStart, eraHistory)
99-
100-
timestamp <- slotToTimestamp systemStart eraHistory chainPoint
101-
102-
pure $
103-
defMessage
104-
& U5c.ledgerTip .~ mkChainPointMsg chainPoint blockNo timestamp
105-
& U5c.items .~ obtainCommonConstraints eon (utxoToUtxoRpcAnyUtxoData utxo)
83+
readUtxosMethod req
84+
| null $ req ^. U5c.keys = pure defMessage
85+
| otherwise = do
86+
utxoFilter <- QueryUTxOByTxIn . fromList <$> mapM txoRefToTxIn (req ^. U5c.keys)
87+
88+
nodeConnInfo <- grab
89+
AnyCardanoEra era <- liftIO . throwExceptT $ determineEra nodeConnInfo
90+
eon <- forEraInEon @Era era (error "Minimum Conway era required") pure
91+
92+
let target = VolatileTip
93+
(utxo, chainPoint, blockNo, systemStart, eraHistory) <- liftIO . (throwEither =<<) $ executeLocalStateQueryExpr nodeConnInfo target $ do
94+
utxo <- throwEither =<< throwEither =<< queryUtxo (convert eon) utxoFilter
95+
chainPoint <- throwEither =<< queryChainPoint
96+
blockNo <- throwEither =<< queryChainBlockNo
97+
systemStart <- throwEither =<< querySystemStart
98+
eraHistory <- throwEither =<< queryEraHistory
99+
pure (utxo, chainPoint, blockNo, systemStart, eraHistory)
100+
101+
timestamp <- slotToTimestamp systemStart eraHistory chainPoint
102+
103+
pure $
104+
defMessage
105+
& U5c.ledgerTip .~ mkChainPointMsg chainPoint blockNo timestamp
106+
& U5c.items .~ obtainCommonConstraints eon (utxoToUtxoRpcAnyUtxoData utxo)
106107
where
107108
txoRefToTxIn :: MonadRpc e m => Proto UtxoRpc.TxoRef -> m TxIn
108109
txoRefToTxIn r = do
@@ -111,8 +112,8 @@ readUtxosMethod req = do
111112

112113
-- | Handle the @SearchUtxos@ RPC method.
113114
-- Filters the UTxO set by a predicate and returns a paginated result.
114-
-- When the predicate contains exact address matches, the query is narrowed
115-
-- to those addresses; otherwise the entire UTxO set is fetched.
115+
-- The predicate must contain exact address matches so the query can be
116+
-- narrowed; broad predicates are rejected with @INVALID_ARGUMENT@.
116117
searchUtxosMethod
117118
:: MonadRpc e m
118119
=> Proto UtxoRpc.SearchUtxosRequest
@@ -123,10 +124,12 @@ searchUtxosMethod req = do
123124
maxItems = req ^. U5c.maxItems
124125
startToken = req ^. U5c.maybe'startToken
125126

126-
-- Determine query strategy: use address-based query if possible, otherwise fetch whole UTxO
127-
let utxoFilter = case mPredicate >>= extractAddressesFromPredicate of
128-
Just addrs -> QueryUTxOByAddress addrs
129-
_ -> QueryUTxOWhole
127+
utxoFilter <- case mPredicate >>= extractAddressesFromPredicate of
128+
Just addrs -> pure $ QueryUTxOByAddress addrs
129+
Nothing ->
130+
throwGrpcErrorWithMessage
131+
GrpcInvalidArgument
132+
"predicate too broad: must contain exact address match to avoid fetching the entire UTxO set"
130133

131134
nodeConnInfo <- grab
132135
AnyCardanoEra era <- liftIO . throwExceptT $ determineEra nodeConnInfo

0 commit comments

Comments
 (0)