Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ instance LogFormatting TraceRpc where
[ "queryName" .= String "ReadUtxos"
, spanToObject s
]
TraceRpcQuerySearchUtxosSpan s ->
[ "queryName" .= String "SearchUtxos"
, spanToObject s
]
TraceRpcSubmit submitTrace ->
["kind" .= String "SubmitService"]
<> case submitTrace of
Expand All @@ -50,6 +54,7 @@ instance LogFormatting TraceRpc where
-- query names here are taken from UTXORPC spec: https://utxorpc.org/query/intro/#operations
TraceRpcQuery (TraceRpcQueryParamsSpan (SpanBegin _)) -> [CounterM "rpc.request.QueryService.ReadParams" Nothing]
TraceRpcQuery (TraceRpcQueryReadUtxosSpan (SpanBegin _)) -> [CounterM "rpc.request.QueryService.ReadUtxos" Nothing]
TraceRpcQuery (TraceRpcQuerySearchUtxosSpan (SpanBegin _)) -> [CounterM "rpc.request.QueryService.SearchUtxos" Nothing]
TraceRpcSubmit (TraceRpcSubmitSpan (SpanBegin _)) -> [CounterM "rpc.request.SubmitService.SubmitTx" Nothing]
_ -> []

Expand All @@ -63,6 +68,7 @@ instance MetaTrace TraceRpc where
: case queryTrace of
TraceRpcQueryParamsSpan _ -> ["ReadParams", "Span"]
TraceRpcQueryReadUtxosSpan _ -> ["ReadUtxos", "Span"]
TraceRpcQuerySearchUtxosSpan _ -> ["SearchUtxos", "Span"]
TraceRpcSubmit submitTrace ->
"SubmitService"
: case submitTrace of
Expand All @@ -76,6 +82,7 @@ instance MetaTrace TraceRpc where
["Error"] -> Just Debug -- those are normal operation errors, like request errors, hide them by default
["QueryService", "ReadParams", "Span"] -> Just Debug
["QueryService", "ReadUtxos", "Span"] -> Just Debug
["QueryService", "SearchUtxos", "Span"] -> Just Debug
["SubmitService", "SubmitTx", "Span"] -> Just Debug
["SubmitService", "N2cConnectionError"] -> Just Warning -- this is a more serious error, this shouldn't happen
["SubmitService", "TxDecodingError"] -> Just Debug -- request error
Expand All @@ -87,6 +94,7 @@ instance MetaTrace TraceRpc where
["Error"] -> Just "Normal operation errors such as request errors. Those are not harmful to the RPC server itself."
["QueryService", "ReadParams", "Span"] -> Just "Span for the ReadParams UTXORPC method."
["QueryService", "ReadUtxos", "Span"] -> Just "Span for the ReadUtxos UTXORPC method."
["QueryService", "SearchUtxos", "Span"] -> Just "Span for the SearchUtxos UTXORPC method."
["SubmitService", "SubmitTx", "Span"] -> Just "Span for the SubmitTx UTXORPC method."
["SubmitService", "N2cConnectionError"] ->
Just
Expand All @@ -100,6 +108,8 @@ instance MetaTrace TraceRpc where
[("rpc.request.QueryService.ReadParams", "Span for the ReadParams UTXORPC method.")]
["QueryService", "ReadUtxos", "Span"] ->
[("rpc.request.QueryService.ReadUtxos", "Span for the ReadUtxos UTXORPC method.")]
["QueryService", "SearchUtxos", "Span"] ->
[("rpc.request.QueryService.SearchUtxos", "Span for the SearchUtxos UTXORPC method.")]
["SubmitService", "SubmitTx", "Span"] ->
[("rpc.request.SubmitService.SubmitTx", "Span for the SubmitTx UTXORPC method.")]
_ -> []
Expand All @@ -110,6 +120,7 @@ instance MetaTrace TraceRpc where
, ["Error"]
, ["QueryService", "ReadParams", "Span"]
, ["QueryService", "ReadUtxos", "Span"]
, ["QueryService", "SearchUtxos", "Span"]
, ["SubmitService", "SubmitTx", "Span"]
, ["SubmitService", "N2cConnectionError"]
, ["SubmitService", "TxDecodingError"]
Expand Down
1 change: 1 addition & 0 deletions cardano-testnet/cardano-testnet.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ test-suite cardano-testnet-test
Cardano.Testnet.Test.Gov.TreasuryGrowth
Cardano.Testnet.Test.Gov.TreasuryWithdrawal
Cardano.Testnet.Test.Rpc.Query
Cardano.Testnet.Test.Rpc.SearchUtxos
Cardano.Testnet.Test.Rpc.Transaction
Cardano.Testnet.Test.Misc
Cardano.Testnet.Test.Node.Shutdown
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}

module Cardano.Testnet.Test.Rpc.SearchUtxos
( hprop_rpc_search_utxos
)
where

import Cardano.Api
import qualified Cardano.Api.Ledger as L

import Cardano.Rpc.Client (Proto)
import qualified Cardano.Rpc.Client as Rpc
import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Query as U5c
import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Query as UtxoRpc
import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Submit as U5c
import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Submit as UtxoRpc
import Cardano.Rpc.Server.Internal.UtxoRpc.Predicate (serialisePaymentCredential)
import Cardano.Rpc.Server.Internal.UtxoRpc.Type
import Cardano.Testnet

import Prelude

import Control.Monad.Trans.Control (liftBaseOp)
import Data.ByteString (ByteString)
import Data.Default.Class
import GHC.Stack
import Lens.Micro

import Testnet.Components.Query (TestnetWaitPeriod (..), getEpochStateView, retryUntilM)
import Testnet.Property.Util (integrationRetryWorkspace)
import Testnet.Types

import Hedgehog
import qualified Hedgehog as H
import qualified Hedgehog.Extras.Test.Base as H
import qualified Hedgehog.Extras.Test.TestWatchdog as H

-- | E2E test for the SearchUtxos gRPC method.
--
-- Spins up a testnet, submits a transaction to create UTxOs at a known address,
-- waits for them to appear in the UTxO set, then exercises SearchUtxos with
-- exact-address and payment-credential predicates.
--
-- Run with:
-- @TASTY_PATTERN='/RPC SearchUtxos/' cabal test cardano-testnet-test@
hprop_rpc_search_utxos :: Property
hprop_rpc_search_utxos = integrationRetryWorkspace 2 "rpc-search-utxos" $ \tempAbsBasePath' -> H.runWithDefaultWatchdog_ $ do
conf <- mkConf tempAbsBasePath'
let (ceo, eraProxy) =
(conwayBasedEra, asType) :: era ~ ConwayEra => (ConwayEraOnwards era, AsType era)
sbe = convert ceo
options = def{cardanoNodeEra = AnyShelleyBasedEra sbe, cardanoEnableRpc = RpcEnabled}
addressInEra = AsAddressInEra eraProxy

TestnetRuntime
{ configurationFile
, testnetNodes = node0 : _
, wallets = wallet0@(PaymentKeyInfo _ addrTxt0) : (PaymentKeyInfo _ addrTxt1) : _
} <-
createAndRunTestnet options def conf

epochStateView <- getEpochStateView configurationFile $ nodeSocketPath node0
rpcSocket <- H.note . unFile $ nodeRpcSocketPath node0

H.noteShow_ addrTxt0
address0 <- H.nothingFail $ deserialiseAddress addressInEra addrTxt0

H.noteShow_ addrTxt1
address1 <- H.nothingFail $ deserialiseAddress addressInEra addrTxt1

wit0 :: ShelleyWitnessSigningKey <-
H.leftFailM . H.evalIO $
readFileTextEnvelopeAnyOf
[FromSomeType asType WitnessGenesisUTxOKey]
(signingKey $ paymentKeyInfoPair wallet0)

let rpcServer = Rpc.ServerUnix rpcSocket

----------------------
-- Build and submit tx
----------------------
(pparamsResponse, initialSearch) <- H.noteShowM . H.evalIO . Rpc.withConnection def rpcServer $ \conn -> do
pparams' <-
Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "readParams")) def

search' <-
Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "searchUtxos")) $
def & U5c.predicate .~ addressPredicate address0
pure (pparams', search')

pparams <- H.leftFail $ utxoRpcPParamsToProtocolParams (convert ceo) $ pparamsResponse ^. U5c.values . U5c.cardano

txOut0 : _ <- H.noteShow $ initialSearch ^. U5c.items
txIn0 <- txoRefToTxIn $ txOut0 ^. U5c.txoRef

outputCoin <- H.leftFail $ txOut0 ^. U5c.cardano . U5c.coin . to utxoRpcBigIntToInteger
let amount = 200_000_000
fee = 500
change = outputCoin - amount - fee
txOut = TxOut address1 (lovelaceToTxOutValue sbe $ L.Coin amount) TxOutDatumNone ReferenceScriptNone
changeTxOut = TxOut address0 (lovelaceToTxOutValue sbe $ L.Coin change) TxOutDatumNone ReferenceScriptNone
content =
defaultTxBodyContent sbe
& setTxIns [(txIn0, pure $ KeyWitness KeyWitnessForSpending)]
& setTxFee (TxFeeExplicit sbe (L.Coin fee))
& setTxOuts [txOut, changeTxOut]
& setTxProtocolParams (pure . pure $ LedgerProtocolParameters pparams)

txBody <- H.leftFail $ createTransactionBody sbe content
let signedTx = signShelleyTransaction sbe txBody [wit0]

liftBaseOp (Rpc.withConnection def rpcServer) $ \conn -> do
_submitResponse <- H.noteShowM . H.evalIO $
Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.SubmitService "submitTx")) $
def & U5c.tx .~ (def & U5c.raw .~ serialiseToCBOR signedTx)

-------------------------------------------
-- Wait for UTxOs to appear at address1
-------------------------------------------
H.note_ $ "Wait for 2 UTxOs at address " <> show addrTxt1
utxosAtAddress1 <- retryUntilM epochStateView (WaitForBlocks 10)
(do searchResult <- H.evalIO $
Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "searchUtxos")) $
def & U5c.predicate .~ addressPredicate address1
pure $ searchResult ^. U5c.items
)
(\xs -> length xs == 2)

-------------------------------------------
-- Test 1: exact address predicate returns correct amounts
-------------------------------------------
H.note_ "Test 1: Verify exact address search returns correct coin values"
let outputAmounts = map (^. U5c.cardano . U5c.coin) utxosAtAddress1
H.assertWith outputAmounts $ elem (inject amount)

-------------------------------------------
-- Test 2: payment credential predicate
-------------------------------------------
H.note_ "Test 2: Verify payment credential predicate matches same UTxOs"
let paymentCredBytes :: ByteString
paymentCredBytes = case address1 of
AddressInEra ShelleyAddressInEra{} (ShelleyAddress _ payCred _) ->
serialisePaymentCredential $ fromShelleyPaymentCredential payCred
_ -> error "Expected a Shelley address"
paymentPredicate :: Proto UtxoRpc.UtxoPredicate
paymentPredicate =
def
& U5c.match
.~ ( def
& U5c.cardano
.~ (def & U5c.address .~ (def & U5c.paymentPart .~ paymentCredBytes))
)
payCredSearch <- H.noteShowM . H.evalIO $
Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "searchUtxos")) $
def & U5c.predicate .~ paymentPredicate

let payCredUtxos = payCredSearch ^. U5c.items
H.assertWith payCredUtxos $ \xs -> length xs == 2

-------------------------------------------
-- Test 3: search with non-matching address returns empty
-------------------------------------------
H.note_ "Test 3: Verify search with non-existent address returns empty"
let bogusAddressPredicate :: Proto UtxoRpc.UtxoPredicate
bogusAddressPredicate =
def
& U5c.match
.~ ( def
& U5c.cardano
.~ (def & U5c.address .~ (def & U5c.exactAddress .~ "\x00\x01\x02\x03"))
)
emptySearch <- H.noteShowM . H.evalIO $
Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "searchUtxos")) $
def & U5c.predicate .~ bogusAddressPredicate

H.assertWith (emptySearch ^. U5c.items) null

-------------------------------------------
-- Test 4: search without predicate returns all UTxOs
-------------------------------------------
H.note_ "Test 4: Verify search without predicate returns all UTxOs"
allUtxosSearch <- H.noteShowM . H.evalIO $
Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "searchUtxos")) def

H.assertWith (allUtxosSearch ^. U5c.items) $ \xs -> length xs > 2

txoRefToTxIn :: (HasCallStack, MonadTest m) => Proto UtxoRpc.TxoRef -> m TxIn
txoRefToTxIn r = withFrozenCallStack $ do
txId' <- H.leftFail $ deserialiseFromRawBytes AsTxId $ r ^. U5c.hash
pure $ TxIn txId' (TxIx . fromIntegral $ r ^. U5c.index)

addressPredicate :: IsCardanoEra era => AddressInEra era -> Proto UtxoRpc.UtxoPredicate
addressPredicate address =
def
& U5c.match
.~ ( def
& U5c.cardano
.~ (def & U5c.address .~ (def & U5c.exactAddress .~ serialiseToRawBytes address))
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import qualified Cardano.Api.Ledger as L

import Cardano.Rpc.Client (Proto)
import qualified Cardano.Rpc.Client as Rpc
import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Query as U5c hiding (cardano, items, tx)
import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Query as U5c
import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Query as UtxoRpc
import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Submit as U5c
import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Submit as UtxoRpc
Expand All @@ -26,10 +26,8 @@ import Cardano.Testnet

import Prelude

import Control.Monad
import Control.Monad.Trans.Control (liftBaseOp)
import Data.Default.Class
import qualified Data.Text.Encoding as T
import GHC.Stack
import Lens.Micro

Expand All @@ -42,8 +40,6 @@ import qualified Hedgehog as H
import qualified Hedgehog.Extras.Test.Base as H
import qualified Hedgehog.Extras.Test.TestWatchdog as H

import RIO (ByteString)

-- | Run with:
-- @TASTY_PATTERN='/RPC Transaction Submit/' cabal test cardano-testnet-test@
hprop_rpc_transaction :: Property
Expand Down Expand Up @@ -84,21 +80,18 @@ hprop_rpc_transaction = integrationRetryWorkspace 2 "rpc-tx" $ \tempAbsBasePath'
-- RPC queries
--------------
let rpcServer = Rpc.ServerUnix rpcSocket
(pparamsResponse, utxosResponse) <- H.noteShowM . H.evalIO . Rpc.withConnection def rpcServer $ \conn -> do
pparams' <- do
let req = def
Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "readParams")) req
(pparamsResponse, searchResponse) <- H.noteShowM . H.evalIO . Rpc.withConnection def rpcServer $ \conn -> do
pparams' <-
Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "readParams")) def

utxos' <- do
let req = def -- & # U5c.keys .~ [T.encodeUtf8 addrTxt0]
Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "readUtxos")) req
pure (pparams', utxos')
search' <-
Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "searchUtxos")) $
def & U5c.predicate .~ addressPredicate addr0
pure (pparams', search')

pparams <- H.leftFail $ utxoRpcPParamsToProtocolParams (convert ceo) $ pparamsResponse ^. U5c.values . U5c.cardano

txOut0 : _ <- H.noteShowM . flip filterM (utxosResponse ^. U5c.items) $ \utxo -> do
utxoAddress <- deserialiseAddressBs addrInEra $ utxo ^. U5c.cardano . U5c.address
pure $ addr0 == utxoAddress
txOut0 : _ <- H.noteShow $ searchResponse ^. U5c.items
txIn0 <- txoRefToTxIn $ txOut0 ^. U5c.txoRef

outputCoin <- H.leftFail $ txOut0 ^. U5c.cardano . U5c.coin . to utxoRpcBigIntToInteger
Expand All @@ -119,7 +112,7 @@ hprop_rpc_transaction = integrationRetryWorkspace 2 "rpc-tx" $ \tempAbsBasePath'
let signedTx = signShelleyTransaction sbe txBody [wit0]
txId' <- H.noteShow . getTxId $ getTxBody signedTx

H.noteShowPretty_ utxosResponse
H.noteShowPretty_ searchResponse

liftBaseOp (Rpc.withConnection def rpcServer) $ \conn -> do
submitResponse <- H.noteShowM . H.evalIO $
Expand All @@ -131,14 +124,12 @@ hprop_rpc_transaction = integrationRetryWorkspace 2 "rpc-tx" $ \tempAbsBasePath'
H.note_ "Ensure that submitTx returns the same transaction ID as the locally computed signed transaction ID"
txId' === submittedTxId

-- TODO use searchUtxos when available
H.note_ $ "Ensure that there are 2 UTXOs in the address " <> show addrTxt1
utxosForAddress <- retryUntilM epochStateView (WaitForBlocks 10)
(do utxos <- H.evalIO $
Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "readUtxos")) def
flip filterM (utxos ^. U5c.items) $ \utxo -> do
utxoAddress <- deserialiseAddressBs addrInEra $ utxo ^. U5c.cardano . U5c.address
pure $ addr1 == utxoAddress
(do searchResult <- H.evalIO $
Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "searchUtxos")) $
def & U5c.predicate .~ addressPredicate addr1
pure $ searchResult ^. U5c.items
)
(\xs -> length xs == 2)

Expand All @@ -151,5 +142,11 @@ txoRefToTxIn r = withFrozenCallStack $ do
txId' <- H.leftFail $ deserialiseFromRawBytes AsTxId $ r ^. U5c.hash
pure $ TxIn txId' (TxIx . fromIntegral $ r ^. U5c.index)

deserialiseAddressBs :: (MonadTest m, SerialiseAddress c) => AsType c -> ByteString -> m c
deserialiseAddressBs addrInEra = H.nothingFail . deserialiseAddress addrInEra <=< H.leftFail . T.decodeUtf8'
addressPredicate :: IsCardanoEra era => AddressInEra era -> Proto UtxoRpc.UtxoPredicate
addressPredicate addr =
def
& U5c.match
.~ ( def
& U5c.cardano
.~ (def & U5c.address .~ (def & U5c.exactAddress .~ serialiseToRawBytes addr))
)
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import qualified Cardano.Testnet.Test.Gov.TreasuryWithdrawal as Gov
import qualified Cardano.Testnet.Test.MainnetParams
import qualified Cardano.Testnet.Test.Node.Shutdown
import qualified Cardano.Testnet.Test.Rpc.Query
import qualified Cardano.Testnet.Test.Rpc.SearchUtxos
import qualified Cardano.Testnet.Test.Rpc.Transaction
import qualified Cardano.Testnet.Test.RunTestnet
import qualified Cardano.Testnet.Test.SanityCheck
Expand Down Expand Up @@ -144,6 +145,7 @@ tests = do
]
, T.testGroup "RPC"
[ ignoreOnWindows "RPC Query Protocol Params" Cardano.Testnet.Test.Rpc.Query.hprop_rpc_query_pparams
, ignoreOnWindows "RPC SearchUtxos" Cardano.Testnet.Test.Rpc.SearchUtxos.hprop_rpc_search_utxos
, ignoreOnWindows "RPC Transaction Submit" Cardano.Testnet.Test.Rpc.Transaction.hprop_rpc_transaction
]
]
Expand Down
Loading