diff --git a/.gitignore b/.gitignore index fa56a0bd882..617e5c62f91 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,5 @@ cardano-tracer/cardano-tracer-test .idea/ .codex + +.serena/ diff --git a/bench/plutus-scripts-bench/plutus-scripts-bench.cabal b/bench/plutus-scripts-bench/plutus-scripts-bench.cabal index b0812288828..679b683e8ef 100644 --- a/bench/plutus-scripts-bench/plutus-scripts-bench.cabal +++ b/bench/plutus-scripts-bench/plutus-scripts-bench.cabal @@ -82,7 +82,7 @@ library -- IOG dependencies -------------------------- build-depends: - , cardano-api ^>=11.0 + , cardano-api ^>=11.3 , plutus-ledger-api ^>=1.63 , plutus-tx ^>=1.63 , plutus-tx-plugin ^>=1.63 diff --git a/bench/tx-generator/tx-generator.cabal b/bench/tx-generator/tx-generator.cabal index ef230e3e003..5ebadb43f08 100644 --- a/bench/tx-generator/tx-generator.cabal +++ b/bench/tx-generator/tx-generator.cabal @@ -109,9 +109,9 @@ library , attoparsec-aeson , base16-bytestring , bytestring - , cardano-api ^>= 11.0 + , cardano-api ^>= 11.3 , cardano-binary - , cardano-cli ^>= 11.0 + , cardano-cli ^>= 11.1 , cardano-crypto-class , cardano-crypto-wrapper , cardano-data diff --git a/cabal.project b/cabal.project index 7d699e8d362..c007aebc814 100644 --- a/cabal.project +++ b/cabal.project @@ -14,7 +14,7 @@ repository cardano-haskell-packages -- you need to run if you change them index-state: , hackage.haskell.org 2026-04-17T09:20:55Z - , cardano-haskell-packages 2026-05-02T16:21:41Z + , cardano-haskell-packages 2026-05-27T09:43:46Z active-repositories: , :rest @@ -90,4 +90,3 @@ allow-newer: -- IMPORTANT -- Do NOT add more source-repository-package stanzas here unless they are strictly -- temporary! Please read the section in CONTRIBUTING about updating dependencies. - diff --git a/cardano-node-chairman/cardano-node-chairman.cabal b/cardano-node-chairman/cardano-node-chairman.cabal index 09db99a60e3..fe0eaccea9c 100644 --- a/cardano-node-chairman/cardano-node-chairman.cabal +++ b/cardano-node-chairman/cardano-node-chairman.cabal @@ -86,5 +86,5 @@ test-suite chairman-tests ghc-options: -threaded -rtsopts "-with-rtsopts=-N -T" build-tool-depends: cardano-node:cardano-node - , cardano-cli:cardano-cli ^>= 11.0 + , cardano-cli:cardano-cli ^>= 11.1 , cardano-node-chairman:cardano-node-chairman diff --git a/cardano-node/cardano-node.cabal b/cardano-node/cardano-node.cabal index da3d9cc7b07..5b8fe5c619e 100644 --- a/cardano-node/cardano-node.cabal +++ b/cardano-node/cardano-node.cabal @@ -138,7 +138,7 @@ library , async , base16-bytestring , bytestring - , cardano-api ^>= 11.0 + , cardano-api ^>= 11.3 , cardano-data , cardano-crypto-class ^>=2.3 , cardano-crypto-wrapper @@ -156,7 +156,7 @@ library , cardano-prelude , cardano-protocol-tpraos >= 1.4 , cardano-slotting >= 0.2 - , cardano-rpc ^>= 10.2 + , cardano-rpc ^>= 11.0 , cborg ^>= 0.2.4 , containers , contra-tracer diff --git a/cardano-node/src/Cardano/Node/Protocol/Shelley.hs b/cardano-node/src/Cardano/Node/Protocol/Shelley.hs index c80f15a9363..5f63b9b3a99 100644 --- a/cardano-node/src/Cardano/Node/Protocol/Shelley.hs +++ b/cardano-node/src/Cardano/Node/Protocol/Shelley.hs @@ -26,6 +26,7 @@ module Cardano.Node.Protocol.Shelley import Cardano.Api hiding (FileError) import qualified Cardano.Api as Api +import Cardano.Api.Experimental.Certificate (OperationalCertificate (..), getHotKey) import qualified Cardano.Crypto.Hash.Class as Crypto import Cardano.Ledger.BaseTypes (ProtVer (..), natVersion) diff --git a/cardano-node/src/Cardano/Node/Run.hs b/cardano-node/src/Cardano/Node/Run.hs index 5298d926c9d..267905144cf 100644 --- a/cardano-node/src/Cardano/Node/Run.hs +++ b/cardano-node/src/Cardano/Node/Run.hs @@ -57,6 +57,7 @@ import Cardano.Node.Protocol.Types import Cardano.Node.Queries import Cardano.Rpc.Server import Cardano.Rpc.Server.Config +import Data.IORef import Cardano.Node.Startup import Cardano.Node.TraceConstraints (TraceConstraints) import Cardano.Node.Tracing.API @@ -541,6 +542,7 @@ handleSimpleNode blockType runP tracers nc networkMagic onKernel = do #endif nForkPolicy <- getForkPolicy $ ncResponderCoreAffinityPolicy nc cForkPolicy <- getForkPolicy $ ncResponderCoreAffinityPolicy nc + nodeAccessRef <- newIORef Nothing void $ let diffusionNodeArguments :: Cardano.Diffusion.CardanoNodeArguments IO diffusionNodeArguments = Cardano.Diffusion.CardanoNodeArguments { @@ -573,7 +575,7 @@ handleSimpleNode blockType runP tracers nc networkMagic onKernel = do (readTVar ledgerPeerSnapshotVar) nc in - withAsync (rpcServerLoop (startupTracer tracers) (rpcTracer tracers) rpcConfigVar networkMagic) $ \_ -> + withAsync (rpcServerLoop (startupTracer tracers) (rpcTracer tracers) rpcConfigVar networkMagic nodeAccessRef) $ \_ -> Node.run nodeArgs { rnNodeKernelHook = \registry nodeKernel -> do @@ -858,8 +860,9 @@ rpcServerLoop :: Tracer IO (StartupTrace blk) -> Tracer IO TraceRpc -> StrictTVar IO RpcConfig -> NetworkMagic + -> IORef (Maybe (NodeAccess IO)) -> IO () -rpcServerLoop startupTracer rpcTracer rpcConfigVar networkMagic = go +rpcServerLoop startupTracer rpcTracer rpcConfigVar networkMagic nodeAccessRef = go where go = do config@RpcConfig{isEnabled = Identity enabled} <- readTVarIO rpcConfigVar @@ -867,7 +870,7 @@ rpcServerLoop startupTracer rpcTracer rpcConfigVar networkMagic = go then race_ (do - runRpcServer rpcTracer (config, networkMagic) + runRpcServer rpcTracer config networkMagic nodeAccessRef traceWith startupTracer RpcForceDisabled disableRpcServer) (waitForRpcConfigChange config) diff --git a/cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs b/cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs index d81458625fb..f9781f5efe6 100644 --- a/cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs +++ b/cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs @@ -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 @@ -42,6 +46,8 @@ instance LogFormatting TraceRpc where TraceRpcSubmitTxDecodingError _ -> [] TraceRpcSubmitTxValidationError _ -> [] TraceRpcSubmitSpan s -> [spanToObject s] + TraceRpcEvalTxDecodingError _ -> [] + TraceRpcEvalTxSpan s -> [spanToObject s] forHuman = docToText . pretty @@ -50,7 +56,9 @@ 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] + TraceRpcSubmit (TraceRpcEvalTxSpan (SpanBegin _)) -> [CounterM "rpc.request.SubmitService.EvalTx" Nothing] _ -> [] instance MetaTrace TraceRpc where @@ -63,6 +71,7 @@ instance MetaTrace TraceRpc where : case queryTrace of TraceRpcQueryParamsSpan _ -> ["ReadParams", "Span"] TraceRpcQueryReadUtxosSpan _ -> ["ReadUtxos", "Span"] + TraceRpcQuerySearchUtxosSpan _ -> ["SearchUtxos", "Span"] TraceRpcSubmit submitTrace -> "SubmitService" : case submitTrace of @@ -70,16 +79,21 @@ instance MetaTrace TraceRpc where TraceRpcSubmitTxDecodingError _ -> ["TxDecodingError"] TraceRpcSubmitTxValidationError _ -> ["TxValidationError"] TraceRpcSubmitSpan _ -> ["SubmitTx", "Span"] + TraceRpcEvalTxDecodingError _ -> ["EvalTxDecodingError"] + TraceRpcEvalTxSpan _ -> ["EvalTx", "Span"] severityFor (Namespace _ nsInner) _ = case nsInner of ["FatalError"] -> Just Error -- RPC server startup errors ["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", "EvalTx", "Span"] -> Just Debug ["SubmitService", "N2cConnectionError"] -> Just Warning -- this is a more serious error, this shouldn't happen ["SubmitService", "TxDecodingError"] -> Just Debug -- request error ["SubmitService", "TxValidationError"] -> Just Debug -- request error + ["SubmitService", "EvalTxDecodingError"] -> Just Debug -- request error _ -> Nothing documentFor (Namespace _ nsInner) = case nsInner of @@ -87,12 +101,15 @@ 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", "EvalTx", "Span"] -> Just "Span for the EvalTx UTXORPC method." ["SubmitService", "N2cConnectionError"] -> Just "Node connection error. This should not happen, as this means that there is an issue in cardano-rpc configuration." ["SubmitService", "TxDecodingError"] -> Just "A regular request error, when submitted transaction decoding fails." ["SubmitService", "TxValidationError"] -> Just "A regular request error, when submitted transaction is invalid." + ["SubmitService", "EvalTxDecodingError"] -> Just "A regular request error, when evalTx transaction decoding fails." _ -> Nothing metricsDocFor (Namespace _ nsInner) = case nsInner of @@ -100,8 +117,12 @@ 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.")] + ["SubmitService", "EvalTx", "Span"] -> + [("rpc.request.SubmitService.EvalTx", "Span for the EvalTx UTXORPC method.")] _ -> [] allNamespaces = @@ -110,10 +131,13 @@ instance MetaTrace TraceRpc where , ["Error"] , ["QueryService", "ReadParams", "Span"] , ["QueryService", "ReadUtxos", "Span"] + , ["QueryService", "SearchUtxos", "Span"] , ["SubmitService", "SubmitTx", "Span"] + , ["SubmitService", "EvalTx", "Span"] , ["SubmitService", "N2cConnectionError"] , ["SubmitService", "TxDecodingError"] , ["SubmitService", "TxValidationError"] + , ["SubmitService", "EvalTxDecodingError"] ] -- helper functions diff --git a/cardano-submit-api/cardano-submit-api.cabal b/cardano-submit-api/cardano-submit-api.cabal index 6ef4ff984f7..a7dbb612790 100644 --- a/cardano-submit-api/cardano-submit-api.cabal +++ b/cardano-submit-api/cardano-submit-api.cabal @@ -39,9 +39,9 @@ library , aeson , async , bytestring - , cardano-api ^>= 11.0 + , cardano-api ^>= 11.3 , cardano-binary - , cardano-cli ^>= 11.0 + , cardano-cli ^>= 11.1 , cardano-crypto-class ^>=2.3 , containers , ekg-core diff --git a/cardano-submit-api/src/Cardano/TxSubmit/Web.hs b/cardano-submit-api/src/Cardano/TxSubmit/Web.hs index 430bee24bbf..37471457805 100644 --- a/cardano-submit-api/src/Cardano/TxSubmit/Web.hs +++ b/cardano-submit-api/src/Cardano/TxSubmit/Web.hs @@ -18,7 +18,7 @@ import Cardano.Api (AllegraEra, AnyCardanoEra (AnyCardanoEra), AsType LocalNodeConnectInfo (LocalNodeConnectInfo, localConsensusModeParams, localNodeNetworkId, localNodeSocketPath), NetworkId, SerialiseAsCBOR (..), ShelleyBasedEra (..), ShelleyEra, SocketPath, ToJSON, Tx, TxId (..), TxInMode (TxInMode), TxValidationErrorInCardanoMode (..), - getTxBody, getTxId, submitTxToNodeLocal) + getTxId, submitTxToNodeLocal) import qualified Cardano.Api import Cardano.Binary (DecoderError (..)) @@ -144,7 +144,7 @@ txSubmitPost trace p@(CardanoModeParams cModeParams) networkId socketPath txByte case res of Cardano.Api.TxSubmitSuccess -> do liftIO $ T.putStrLn "Transaction successfully submitted." - return $ getTxId (getTxBody tx) + let Tx txBody _ = tx in return $ getTxId txBody Cardano.Api.TxSubmitFail e -> left $ TxCmdTxSubmitValidationError e Cardano.Api.TxSubmitError e -> diff --git a/cardano-testnet/cardano-testnet.cabal b/cardano-testnet/cardano-testnet.cabal index 3d24101a0ad..baf3609479b 100644 --- a/cardano-testnet/cardano-testnet.cabal +++ b/cardano-testnet/cardano-testnet.cabal @@ -41,8 +41,8 @@ library , annotated-exception , ansi-terminal , bytestring - , cardano-api ^>= 11.0 - , cardano-cli:{cardano-cli, cardano-cli-test-lib} ^>= 11.0 + , cardano-api ^>= 11.3 + , cardano-cli:{cardano-cli, cardano-cli-test-lib} ^>= 11.1 , cardano-crypto-class ^>=2.3 , cardano-crypto-wrapper , cardano-git-rev ^>= 0.2.2 @@ -238,7 +238,9 @@ test-suite cardano-testnet-test Cardano.Testnet.Test.Gov.TreasuryDonation Cardano.Testnet.Test.Gov.TreasuryGrowth Cardano.Testnet.Test.Gov.TreasuryWithdrawal + Cardano.Testnet.Test.Rpc.Eval Cardano.Testnet.Test.Rpc.Query + Cardano.Testnet.Test.Rpc.SearchUtxos Cardano.Testnet.Test.Rpc.Transaction Cardano.Testnet.Test.Misc Cardano.Testnet.Test.Node.Shutdown diff --git a/cardano-testnet/src/Testnet/Process/Cli/DRep.hs b/cardano-testnet/src/Testnet/Process/Cli/DRep.hs index c7c4ac72ded..d2a7c481163 100644 --- a/cardano-testnet/src/Testnet/Process/Cli/DRep.hs +++ b/cardano-testnet/src/Testnet/Process/Cli/DRep.hs @@ -17,7 +17,7 @@ module Testnet.Process.Cli.DRep , makeActivityChangeProposal ) where -import Cardano.Api hiding (Certificate, TxBody, txId) +import Cardano.Api hiding (TxBody, txId) import Cardano.Api.Experimental (Some (..)) import Cardano.Api.Ledger (EpochInterval (EpochInterval, unEpochInterval)) diff --git a/cardano-testnet/src/Testnet/Process/Cli/SPO.hs b/cardano-testnet/src/Testnet/Process/Cli/SPO.hs index 703ff345b65..6b976ff5a17 100644 --- a/cardano-testnet/src/Testnet/Process/Cli/SPO.hs +++ b/cardano-testnet/src/Testnet/Process/Cli/SPO.hs @@ -16,6 +16,7 @@ module Testnet.Process.Cli.SPO ) where import Cardano.Api hiding (cardanoEra) +import Cardano.Api.Experimental.Certificate (PoolId) import qualified Cardano.Api.Ledger as L import qualified Cardano.Ledger.Shelley.LedgerState as L diff --git a/cardano-testnet/src/Testnet/Process/Cli/Transaction.hs b/cardano-testnet/src/Testnet/Process/Cli/Transaction.hs index 5ecbed60686..6a4c3984feb 100644 --- a/cardano-testnet/src/Testnet/Process/Cli/Transaction.hs +++ b/cardano-testnet/src/Testnet/Process/Cli/Transaction.hs @@ -17,7 +17,7 @@ module Testnet.Process.Cli.Transaction ) where -import Cardano.Api hiding (Certificate, TxBody) +import Cardano.Api hiding (TxBody) import Cardano.Api.Experimental (Some (..)) import Prelude diff --git a/cardano-testnet/src/Testnet/Property/Assert.hs b/cardano-testnet/src/Testnet/Property/Assert.hs index 2c0e6a0afd1..2e610847b86 100644 --- a/cardano-testnet/src/Testnet/Property/Assert.hs +++ b/cardano-testnet/src/Testnet/Property/Assert.hs @@ -16,6 +16,7 @@ module Testnet.Property.Assert import Cardano.Api hiding (Value) +import Cardano.Api.Experimental.Certificate (PoolId) import Prelude hiding (lines) diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/Transaction.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/Transaction.hs index 46265346eaa..af9d4b781d0 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/Transaction.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/Transaction.hs @@ -13,7 +13,6 @@ import qualified Cardano.Api.Ledger as L import Cardano.CLI.Type.Common import Cardano.Crypto.Hash.Class (hashToStringAsHex) -import qualified Cardano.Ledger.Core as L import Cardano.Testnet import Prelude diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/ProposeNewConstitution.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/ProposeNewConstitution.hs index 0e8df21398f..8478e014754 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/ProposeNewConstitution.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/ProposeNewConstitution.hs @@ -232,47 +232,10 @@ hprop_ledger_events_propose_new_constitution = integrationRetryWorkspace 2 "prop retryUntilJustM epochStateView (WaitForEpochs $ EpochInterval 1) $ maybeExtractGovernanceActionIndex governanceActionTxId <$> getEpochState epochStateView - -- Proposal was successfully submitted, now we vote on the proposal and confirm it was ratified - voteFiles <- generateVoteFiles execConfig work "vote-files" - governanceActionTxId governanceActionIndex - [(defaultDRepKeyPair idx, vote) | (vote, idx) <- allVotes] - - -- Submit votes - voteTxBodyFp <- createVotingTxBody execConfig epochStateView sbe work "vote-tx-body" - voteFiles wallet0 - - let signingKeys = Some <$> (paymentKeyInfoPair wallet0:(defaultDRepKeyPair . snd <$> allVotes)) - voteTxFp <- signTx execConfig cEra gov "signed-vote-tx" voteTxBodyFp signingKeys - - submitTx execConfig cEra voteTxFp - - waitForGovActionVotes epochStateView (EpochInterval 1) - - txId <- H.noteShowM $ retrieveTransactionId execConfig signedProposalTx - - -- Count votes before checking for ratification. It may happen that the proposal gets removed after - -- ratification because of a long waiting time, so we won't be able to access votes. - govState <- getGovState epochStateView ceo - govActionState <- H.headM $ govState ^. L.cgsProposalsL . L.pPropsL . to toList - let votes = govActionState ^. L.gasDRepVotesL . to toList - - length (filter ((== L.VoteYes) . snd) votes) === 4 - length (filter ((== L.VoteNo) . snd) votes) === 3 - length (filter ((== L.Abstain) . snd) votes) === 2 - length votes === fromIntegral numVotes - - -- We check that constitution was successfully ratified - void . H.leftFailM . H.evalIO . runExceptT $ - foldEpochState - configurationFile - socketPath - FullValidation - (EpochNo 10) - () - (\epochState _ _ -> foldBlocksCheckConstitutionWasRatified constitutionHash constitutionScriptHash epochState) - + -- Query proposals via CLI before voting to verify proposal structure. + -- This is race-free: the proposal cannot be ratified before votes are cast. proposalsJSON :: Aeson.Value <- execCliStdoutToJson execConfig - [ eraName, "query", "proposals", "--governance-action-tx-id", prettyShow txId + [ eraName, "query", "proposals", "--governance-action-tx-id", prettyShow governanceActionTxId , "--governance-action-index", "0" ] @@ -286,15 +249,7 @@ hprop_ledger_events_propose_new_constitution = integrationRetryWorkspace 2 "prop -- Check TxId returned is the same as the one we used proposalsTxId <- H.evalMaybe $ proposal ^? Aeson.key "actionId" . Aeson.key "txId" . Aeson._String - proposalsTxId === Text.pack (prettyShow txId) - - -- Check that committeeVotes is an empty object - proposalsCommitteeVotes <- H.evalMaybe $ proposal ^? Aeson.key "committeeVotes" . Aeson._Object - proposalsCommitteeVotes === mempty - - -- Check that dRepVotes has the expected number of votes - proposalsDRepVotes <- H.evalMaybe $ proposal ^? Aeson.key "dRepVotes" . Aeson._Object - length proposalsDRepVotes === numVotes + proposalsTxId === Text.pack (prettyShow governanceActionTxId) -- Fetch proposalProcedure and anchor proposalsProcedure <- H.evalMaybe $ proposal ^? Aeson.key "proposalProcedure" @@ -334,9 +289,70 @@ hprop_ledger_events_propose_new_constitution = integrationRetryWorkspace 2 "prop proposalsTag <- H.evalMaybe $ proposalsProcedure ^? Aeson.key "govAction" . Aeson.key "tag" . Aeson._String proposalsTag === "NewConstitution" - -- Check the stake pool votes are empty - proposalsStakePoolVotes <- H.evalMaybe $ proposal ^? Aeson.key "stakePoolVotes" . Aeson._Object - proposalsStakePoolVotes === mempty + -- Proposal was successfully submitted, now we vote on the proposal and confirm it was ratified + voteFiles <- generateVoteFiles execConfig work "vote-files" + governanceActionTxId governanceActionIndex + [(defaultDRepKeyPair idx, vote) | (vote, idx) <- allVotes] + + -- Submit votes + voteTxBodyFp <- createVotingTxBody execConfig epochStateView sbe work "vote-tx-body" + voteFiles wallet0 + + let signingKeys = Some <$> (paymentKeyInfoPair wallet0:(defaultDRepKeyPair . snd <$> allVotes)) + voteTxFp <- signTx execConfig cEra gov "signed-vote-tx" voteTxBodyFp signingKeys + + submitTx execConfig cEra voteTxFp + + waitForGovActionVotes epochStateView (EpochInterval 1) + + -- Count votes before checking for ratification. It may happen that the proposal gets removed after + -- ratification because of a long waiting time, so we won't be able to access votes. + govState <- getGovState epochStateView ceo + govActionState <- H.headM $ govState ^. L.cgsProposalsL . L.pPropsL . to toList + let votes = govActionState ^. L.gasDRepVotesL . to toList + + length (filter ((== L.VoteYes) . snd) votes) === 4 + length (filter ((== L.VoteNo) . snd) votes) === 3 + length (filter ((== L.Abstain) . snd) votes) === 2 + length votes === fromIntegral numVotes + + -- Query proposals via CLI to verify vote counts are reported correctly. + -- The proposal may have been ratified at an epoch boundary between the ledger check above and this + -- CLI query, in which case the proposal is removed from gov-state and the query returns []. + -- The ledger-level vote checks (lines above) already verified correctness, so we skip gracefully. + votedProposalsJSON :: Aeson.Value <- execCliStdoutToJson execConfig + [ eraName, "query", "proposals", "--governance-action-tx-id", prettyShow governanceActionTxId + , "--governance-action-index", "0" + ] + + H.note_ $ Text.unpack . decodeUtf8 $ prettyPrintJSON votedProposalsJSON + + votedProposalsArray <- H.evalMaybe $ votedProposalsJSON ^? Aeson._Array + unless (null votedProposalsArray) $ do + length votedProposalsArray === 1 + let votedProposal = votedProposalsArray Vector.! 0 + + -- Check that dRepVotes has the expected number of votes + proposalsDRepVotes <- H.evalMaybe $ votedProposal ^? Aeson.key "dRepVotes" . Aeson._Object + length proposalsDRepVotes === numVotes + + -- Check that committeeVotes is an empty object + proposalsCommitteeVotes <- H.evalMaybe $ votedProposal ^? Aeson.key "committeeVotes" . Aeson._Object + proposalsCommitteeVotes === mempty + + -- Check the stake pool votes are empty + proposalsStakePoolVotes <- H.evalMaybe $ votedProposal ^? Aeson.key "stakePoolVotes" . Aeson._Object + proposalsStakePoolVotes === mempty + + -- We check that constitution was successfully ratified + void . H.leftFailM . H.evalIO . runExceptT $ + foldEpochState + configurationFile + socketPath + FullValidation + (EpochNo 10) + () + (\epochState _ _ -> foldBlocksCheckConstitutionWasRatified constitutionHash constitutionScriptHash epochState) foldBlocksCheckConstitutionWasRatified :: String -- submitted constitution hash diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Eval.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Eval.hs new file mode 100644 index 00000000000..77d131f26f9 --- /dev/null +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Eval.hs @@ -0,0 +1,417 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +-- | Run with: +-- @TASTY_PATTERN='/RPC Eval Tx/' cabal test cardano-testnet-test@ +module Cardano.Testnet.Test.Rpc.Eval + ( hprop_rpc_eval_tx + ) +where + +import Cardano.Api +import qualified Cardano.Api.Experimental as Exp +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) +import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Submit as U5c +import Cardano.Rpc.Server.Internal.UtxoRpc.Type (utxoRpcBigIntToInteger) +import Cardano.Testnet + +import Prelude + +import Control.Monad (void) +import Control.Monad.Catch (MonadCatch) +import Control.Monad.Trans.Control (MonadBaseControl, liftBaseOp) +import Data.Default.Class +import qualified Data.Map.Strict as Map +import qualified Data.Text as T +import GHC.Stack (HasCallStack, withFrozenCallStack) +import Lens.Micro +import System.FilePath (()) + +import Testnet.Components.Query (TestnetWaitPeriod (..), findLargestUtxoForPaymentKey, + findLargestUtxoWithAddress, getEpochStateView, retryUntilJustM) +import Testnet.Defaults (plutusV3Script) +import Testnet.Process.Run (execCli', mkExecConfig) +import Testnet.Property.Util (integrationRetryWorkspace) +import Testnet.Start.Types (anyEraToString) +import Testnet.Types + +import Hedgehog +import qualified Hedgehog as H +import qualified Hedgehog.Extras.Test.Base as H +import qualified Hedgehog.Extras.Test.File as H +import qualified Hedgehog.Extras.Test.TestWatchdog as H + +-- | Evaluate a Plutus V3 spending transaction via the gRPC evalTx endpoint and +-- verify that the response contains a valid fee, non-zero execution units, one +-- redeemer, and no errors. +hprop_rpc_eval_tx :: Property +hprop_rpc_eval_tx = integrationRetryWorkspace 2 "rpc-eval-tx" $ \tempAbsBasePath' -> H.runWithDefaultWatchdog_ $ do + conf@Conf{tempAbsPath} <- mkConf tempAbsBasePath' + let tempAbsPath' = unTmpAbsPath tempAbsPath + work <- H.createDirectoryIfMissing $ tempAbsPath' "work" + + let tempBaseAbsPath = makeTmpBaseAbsPath $ TmpAbsolutePath tempAbsPath' + era = Exp.ConwayEra + sbe = convert era + anyEra = AnyCardanoEra $ toCardanoEra sbe + creationOptions = def{creationEra = AnyShelleyBasedEra sbe} + runtimeOptions = def{runtimeEnableRpc = RpcEnabled} + + TestnetRuntime + { configurationFile + , testnetMagic + , testnetNodes = node : _ + , wallets = wallet0 : wallet1 : _ + } <- + createAndRunTestnet creationOptions runtimeOptions conf + + poolSprocket <- H.noteShow $ nodeSprocket node + execConfig <- mkExecConfig tempBaseAbsPath poolSprocket testnetMagic + epochStateView <- getEpochStateView configurationFile $ nodeSocketPath node + rpcSocket <- H.note . unFile $ nodeRpcSocketPath node + + let rpcServer = Rpc.ServerUnix rpcSocket + utxoSKeyFile = signingKeyFp $ paymentKeyInfoPair wallet0 + utxoSKeyFile1 = signingKeyFp $ paymentKeyInfoPair wallet1 + + ------------------------------------ + -- Write Plutus V3 always-succeeds script + ------------------------------------ + plutusScriptFile <- H.note $ work "always-succeeds.plutusV3" + H.writeFile plutusScriptFile $ T.unpack plutusV3Script + + plutusSpendingScriptAddr <- + execCli' + execConfig + [ "latest" + , "address" + , "build" + , "--payment-script-file" , plutusScriptFile + ] + + scriptDatumHash <- + filter (/= '\n') + <$> execCli' + execConfig + [ "latest" + , "transaction" + , "hash-script-data" + , "--script-data-value" + , "0" + ] + + -- Send ADA to the script address with a datum hash, creating a script-locked + -- UTxO that the spending transaction can later reference with a redeemer. + ------------------------------------ + -- 1. Fund the script address + ------------------------------------ + txinFund <- findLargestUtxoForPaymentKey epochStateView sbe wallet0 + + let fundTxBody = work "fund-script-tx-body" + fundTx = work "fund-script-tx" + + void $ + execCli' + execConfig + [ anyEraToString anyEra + , "transaction" + , "build" + , "--change-address" , T.unpack $ paymentKeyInfoAddr wallet0 + , "--tx-in" , T.unpack $ renderTxIn txinFund + , "--tx-out" , plutusSpendingScriptAddr <> "+" <> show @Int 5_000_000 + , "--tx-out-datum-hash" , scriptDatumHash + , "--out-file" , fundTxBody + ] + + void $ + execCli' + execConfig + [ "latest" + , "transaction" + , "sign" + , "--tx-body-file" , fundTxBody + , "--signing-key-file" , utxoSKeyFile + , "--out-file" , fundTx + ] + + ------------------------------------ + -- 1b. EvalTx on the funding tx (no scripts) + ------------------------------------ + (fundLedgerTx, fundTxEval) <- evalTxFile sbe rpcServer fundTx + + let fundCliFee = fundLedgerTx ^. L.bodyTxL . L.feeTxBodyL + + H.note_ "EvalTx minimum fee should not exceed the CLI-computed fee" + fundEvalFee <- H.leftFail $ fundTxEval ^. U5c.fee . to utxoRpcBigIntToInteger + H.assertWith fundEvalFee (<= L.unCoin fundCliFee) + + H.note_ "No execution units for a plain key-witnessed transaction" + fundTxEval ^. U5c.exUnits . U5c.steps === 0 + fundTxEval ^. U5c.exUnits . U5c.memory === 0 + + H.note_ "No redeemers for a plain key-witnessed transaction" + fundTxEval ^. U5c.redeemers === [] + + H.note_ "No evaluation errors" + fundTxEval ^. U5c.errors === [] + + void $ + execCli' + execConfig + [ "latest" + , "transaction" + , "submit" + , "--tx-file" , fundTx + ] + + ------------------------------------ + -- 2. Wait for the script UTxO, find collateral + ------------------------------------ + plutusScriptTxIn <- + fmap fst . retryUntilJustM epochStateView (WaitForBlocks 3) $ + findLargestUtxoWithAddress epochStateView sbe $ + T.pack plutusSpendingScriptAddr + + txinCollateral <- findLargestUtxoForPaymentKey epochStateView sbe wallet1 + + ------------------------------------ + -- 3. Build and sign the spending tx + ------------------------------------ + let spendTxBody = work "spend-script-tx-body" + spendTx = work "spend-script-tx" + + void $ + execCli' + execConfig + [ anyEraToString anyEra + , "transaction" + , "build" + , "--change-address" , T.unpack $ paymentKeyInfoAddr wallet1 + , "--tx-in-collateral" , T.unpack $ renderTxIn txinCollateral + , "--tx-in" , T.unpack $ renderTxIn plutusScriptTxIn + , "--tx-in-script-file" , plutusScriptFile + , "--tx-in-datum-value" , "0" + , "--tx-in-redeemer-value" , "0" + , "--out-file" , spendTxBody + ] + + void $ + execCli' + execConfig + [ "latest" + , "transaction" + , "sign" + , "--tx-body-file" , spendTxBody + , "--signing-key-file" , utxoSKeyFile1 + , "--out-file" , spendTx + ] + + ------------------------------------ + -- 4. EvalTx on the spending tx (Plutus V3 always-succeeds) + ------------------------------------ + (ledgerTx, txEval) <- evalTxFile sbe rpcServer spendTx + + let cliFee = ledgerTx ^. L.bodyTxL . L.feeTxBodyL + + H.note_ "EvalTx minimum fee should not exceed the CLI-computed fee" + evalFee <- H.leftFail $ txEval ^. U5c.fee . to utxoRpcBigIntToInteger + H.assertWith evalFee (<= L.unCoin cliFee) + + H.note_ "Execution units should match the transaction" + (_, L.ExUnits txMem txSteps) <- H.headM . Map.elems . L.unRedeemers $ ledgerTx ^. L.witsTxL . L.rdmrsTxWitsL + txEval ^. U5c.exUnits . U5c.steps === fromIntegral txSteps + txEval ^. U5c.exUnits . U5c.memory === fromIntegral txMem + + H.note_ "One redeemer for the spend purpose at index 0" + let redeemers = txEval ^. U5c.redeemers + length redeemers === 1 + redeemer0 <- H.headM redeemers + redeemer0 ^. U5c.purpose === Proto U5c.REDEEMER_PURPOSE_SPEND + redeemer0 ^. U5c.index === 0 + redeemer0 ^. U5c.exUnits . U5c.steps === fromIntegral txSteps + redeemer0 ^. U5c.exUnits . U5c.memory === fromIntegral txMem + + H.note_ "No evaluation errors" + txEval ^. U5c.errors === [] + + ------------------------------------ + -- 5. Failure path: always-fails script + ------------------------------------ + let failScript = PlutusScript PlutusScriptV1 $ examplePlutusScriptAlwaysFails WitCtxTxIn + failScriptFile <- H.note $ work "always-fails.plutusV1" + H.leftFailM . H.evalIO $ + writeFileTextEnvelope (File failScriptFile) Nothing failScript + + failScriptAddr <- + execCli' + execConfig + [ "latest" + , "address" + , "build" + , "--payment-script-file" , failScriptFile + ] + + txinFund2 <- findLargestUtxoForPaymentKey epochStateView sbe wallet0 + + let fundFailTxBody = work "fund-fail-script-tx-body" + fundFailTx = work "fund-fail-script-tx" + + void $ + execCli' + execConfig + [ anyEraToString anyEra + , "transaction" + , "build" + , "--change-address" , T.unpack $ paymentKeyInfoAddr wallet0 + , "--tx-in" , T.unpack $ renderTxIn txinFund2 + , "--tx-out" , failScriptAddr <> "+" <> show @Int 5_000_000 + , "--tx-out-datum-hash" , scriptDatumHash + , "--out-file" , fundFailTxBody + ] + + void $ + execCli' + execConfig + [ "latest" + , "transaction" + , "sign" + , "--tx-body-file" , fundFailTxBody + , "--signing-key-file" , utxoSKeyFile + , "--out-file" , fundFailTx + ] + + void $ + execCli' + execConfig + [ "latest" + , "transaction" + , "submit" + , "--tx-file" , fundFailTx + ] + + failScriptTxIn <- + fmap fst . retryUntilJustM epochStateView (WaitForBlocks 3) $ + findLargestUtxoWithAddress epochStateView sbe $ + T.pack failScriptAddr + + txinCollateral2 <- findLargestUtxoForPaymentKey epochStateView sbe wallet1 + + protocolParamsFile <- H.note $ work "protocol-params.json" + void $ + execCli' + execConfig + [ "latest" + , "query" + , "protocol-parameters" + , "--out-file" , protocolParamsFile + ] + + -- Use build-raw because `transaction build` would reject the always-fails script. + let failSpendTxBody = work "fail-spend-tx-body" + failSpendTx = work "fail-spend-tx" + + void $ + execCli' + execConfig + [ anyEraToString anyEra + , "transaction" + , "build-raw" + , "--tx-in" , T.unpack $ renderTxIn failScriptTxIn + , "--tx-in-collateral" , T.unpack $ renderTxIn txinCollateral2 + , "--tx-in-script-file" , failScriptFile + , "--tx-in-datum-value" , "0" + , "--tx-in-redeemer-value" , "0" + , "--tx-in-execution-units" , "(10000000000,10000000)" + , "--tx-out" , T.unpack (paymentKeyInfoAddr wallet1) <> "+4700000" + , "--fee" , "300000" + , "--protocol-params-file" , protocolParamsFile + , "--out-file" , failSpendTxBody + ] + + void $ + execCli' + execConfig + [ "latest" + , "transaction" + , "sign" + , "--tx-body-file" , failSpendTxBody + , "--signing-key-file" , utxoSKeyFile1 + , "--out-file" , failSpendTx + ] + + ------------------------------------ + -- 5b. EvalTx on the always-fails spending tx + ------------------------------------ + (_, failTxEval) <- evalTxFile sbe rpcServer failSpendTx + + H.note_ "Evaluation errors for always-fails script" + failTxEval ^. U5c.errors /== [] + + ------------------------------------ + -- 6. Unbalanced key-witnessed tx + ------------------------------------ + txinUnbal <- findLargestUtxoForPaymentKey epochStateView sbe wallet0 + + let unbalTxBody = work "unbal-tx-body" + unbalTx = work "unbal-tx" + + void $ + execCli' + execConfig + [ anyEraToString anyEra + , "transaction" + , "build-raw" + , "--tx-in" , T.unpack $ renderTxIn txinUnbal + , "--tx-out" , T.unpack (paymentKeyInfoAddr wallet0) <> "+1000000" + , "--fee" , "200000" + , "--out-file" , unbalTxBody + ] + + void $ + execCli' + execConfig + [ "latest" + , "transaction" + , "sign" + , "--tx-body-file" , unbalTxBody + , "--signing-key-file" , utxoSKeyFile + , "--out-file" , unbalTx + ] + + (_, unbalTxEval) <- evalTxFile sbe rpcServer unbalTx + + H.note_ "Balance error for unbalanced transaction" + unbalTxEval ^. U5c.errors /== [] + unbalError <- H.headM $ unbalTxEval ^. U5c.errors + H.assertWith (unbalError ^. U5c.msg) $ T.isInfixOf "not balanced" + + H.note_ "Non-zero fee is still returned" + unbalEvalFee <- H.leftFail $ unbalTxEval ^. U5c.fee . to utxoRpcBigIntToInteger + H.assertWith unbalEvalFee (> 0) + +-- | Read a signed transaction from a file and evaluate it via the gRPC evalTx +-- endpoint, returning the ledger transaction and the TxEval result. +evalTxFile + :: (HasCallStack, MonadBaseControl IO m, MonadCatch m, MonadIO m, MonadTest m) + => ShelleyBasedEra era + -> Rpc.Server + -> FilePath + -> m (L.Tx L.TopTx (ShelleyLedgerEra era), Proto U5c.TxEval) +evalTxFile sbe' rpcServer txFile = withFrozenCallStack $ do + textEnvelope <- H.leftFailM . H.evalIO $ readTextEnvelopeFromFile txFile + ShelleyTx _ ledgerTx <- H.leftFail $ shelleyBasedEraConstraints sbe' $ deserialiseFromTextEnvelope textEnvelope + let request = def & U5c.tx . U5c.raw .~ textEnvelopeRawCBOR textEnvelope + (response :: Proto U5c.EvalTxResponse) <- + liftBaseOp (Rpc.withConnection def rpcServer) $ \conn -> + H.noteShowPrettyM . H.evalIO $ + Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf U5c.SubmitService "evalTx")) request + pure (ledgerTx, response ^. U5c.report . U5c.cardano) diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Query.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Query.hs index 316917617ea..c21ee856091 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Query.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Query.hs @@ -29,15 +29,20 @@ import Cardano.Testnet import Prelude import Control.Exception +import Control.Monad import qualified Data.ByteString.Short as SBS import Data.Default.Class import qualified Data.Map.Strict as M +import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) +import Data.Word (Word64) +import GHC.Exts (toList) import Lens.Micro import Testnet.Components.Query import Testnet.Process.Run import Testnet.Property.Util (integrationRetryWorkspace) import Testnet.Start.Types +import Testnet.Types (nodeConnectionInfo) import Hedgehog import qualified Hedgehog as H @@ -57,7 +62,7 @@ hprop_rpc_query_pparams = integrationRetryWorkspace 2 "rpc-query-pparams" $ \tem creationOptions = def{creationEra = AnyShelleyBasedEra sbe} runtimeOptions = def{runtimeEnableRpc = RpcEnabled} - TestnetRuntime + tr@TestnetRuntime { testnetMagic , configurationFile , testnetNodes = node0@TestnetNode{nodeSprocket} : _ @@ -80,6 +85,20 @@ hprop_rpc_query_pparams = integrationRetryWorkspace 2 "rpc-query-pparams" $ \tem ChainTipAtGenesis -> H.failure -- impossible ChainTip (SlotNo slot) (HeaderHash hash) (BlockNo blockNo) -> pure (slot, SBS.fromShort hash, blockNo) + ----------------------------------- + -- Compute expected tip timestamp + ----------------------------------- + connectionInfo <- nodeConnectionInfo tr 0 + (systemStart, eraHistory) <- + (H.leftFail <=< H.leftFailM) . H.evalIO $ + executeLocalStateQueryExpr connectionInfo VolatileTip $ do + ss <- querySystemStart + eh <- queryEraHistory + pure $ (,) <$> ss <*> eh + expectedTimestampMs :: Word64 <- H.leftFail $ do + utcTime <- slotToUTCTime systemStart eraHistory (SlotNo slot) + pure . round $ utcTimeToPOSIXSeconds utcTime * 1000 + -------------- -- RPC queries -------------- @@ -90,7 +109,10 @@ hprop_rpc_query_pparams = integrationRetryWorkspace 2 "rpc-query-pparams" $ \tem Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf U5c.QueryService "readParams")) req utxos' <- do - let req = Rpc.defMessage + let req = Rpc.defMessage & U5c.keys .~ + [ def & U5c.hash .~ serialiseToRawBytes tid & U5c.index .~ fromIntegral tix + | (TxIn tid (TxIx tix), _) <- toList utxos + ] Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf U5c.QueryService "readUtxos")) req pure (pparams', utxos') @@ -100,7 +122,7 @@ hprop_rpc_query_pparams = integrationRetryWorkspace 2 "rpc-query-pparams" $ \tem pparamsResponse ^. U5c.ledgerTip . U5c.slot === slot pparamsResponse ^. U5c.ledgerTip . U5c.hash === blockHash pparamsResponse ^. U5c.ledgerTip . U5c.height === blockNo - pparamsResponse ^. U5c.ledgerTip . U5c.timestamp === 0 -- not possible to implement at this moment + H.assertWithinTolerance (pparamsResponse ^. U5c.ledgerTip . U5c.timestamp) expectedTimestampMs 1000 -- https://docs.cardano.org/about-cardano/explore-more/parameter-guide let chainParams = pparamsResponse ^. U5c.values . U5c.cardano @@ -108,9 +130,10 @@ hprop_rpc_query_pparams = integrationRetryWorkspace 2 "rpc-query-pparams" $ \tem pparams ^. L.ppCoinsPerUTxOByteL . to L.unCoinPerByte . to L.fromCompact . to L.unCoin ===^ chainParams ^. U5c.coinsPerUtxoByte . to utxoRpcBigIntToInteger pparams ^. L.ppMaxTxSizeL === chainParams ^. U5c.maxTxSize . to fromIntegral - pparams ^. L.ppTxFeeFixedL ===^ chainParams ^. U5c.minFeeCoefficient . to (fmap L.Coin . utxoRpcBigIntToInteger) - pparams ^. L.ppTxFeePerByteL . to L.unCoinPerByte . to L.fromCompact . to L.unCoin + pparams ^. L.ppTxFeeFixedL . to L.unCoin ===^ chainParams ^. U5c.minFeeConstant . to utxoRpcBigIntToInteger + pparams ^. L.ppTxFeePerByteL . to L.unCoinPerByte . to L.fromCompact . to L.unCoin + ===^ chainParams ^. U5c.minFeeCoefficient . to utxoRpcBigIntToInteger pparams ^. L.ppMaxBBSizeL === chainParams ^. U5c.maxBlockBodySize . to fromIntegral pparams ^. L.ppMaxBHSizeL === chainParams ^. U5c.maxBlockHeaderSize . to fromIntegral pparams ^. L.ppKeyDepositL ===^ chainParams ^. U5c.stakeKeyDeposit . to (fmap L.Coin . utxoRpcBigIntToInteger) diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/SearchUtxos.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/SearchUtxos.hs new file mode 100644 index 00000000000..87b1b7b8e7a --- /dev/null +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/SearchUtxos.hs @@ -0,0 +1,214 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module Cardano.Testnet.Test.Rpc.SearchUtxos + ( hprop_rpc_search_utxos + ) +where + +import Cardano.Api +import qualified Cardano.Api.Experimental as Exp +import qualified Cardano.Api.Experimental.Tx as Exp +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) +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 era = Exp.ConwayEra + sbe = convert era + creationOptions = def{creationEra = AnyShelleyBasedEra sbe} + runtimeOptions = def{runtimeEnableRpc = RpcEnabled} + addressInEra = asAddressInEra sbe + + TestnetRuntime + { configurationFile + , testnetNodes = node0 : _ + , wallets = wallet0@(PaymentKeyInfo _ addressText0) : (PaymentKeyInfo _ addressText1) : _ + } <- + createAndRunTestnet creationOptions runtimeOptions conf + + epochStateView <- getEpochStateView configurationFile $ nodeSocketPath node0 + rpcSocket <- H.note . unFile $ nodeRpcSocketPath node0 + + H.noteShow_ addressText0 + address0 <- H.nothingFail $ deserialiseAddress addressInEra addressText0 + + H.noteShow_ addressText1 + address1 <- H.nothingFail $ deserialiseAddress addressInEra addressText1 + + 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 era $ 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 + mkOut ledgerAddress coin = Exp.obtainCommonConstraints era $ + Exp.TxOut $ L.mkBasicTxOut ledgerAddress $ L.inject $ L.Coin coin + content = + Exp.defaultTxBodyContent + & Exp.setTxIns [(txIn0, Exp.AnyKeyWitnessPlaceholder)] + & Exp.setTxFee (L.Coin fee) + & Exp.setTxOuts [mkOut (toShelleyAddr address1) amount, mkOut (toShelleyAddr address0) change] + & Exp.setTxProtocolParams pparams + + unsignedTx <- H.leftFail $ Exp.makeUnsignedTx era content + let keyWit = Exp.makeKeyWitness era unsignedTx wit0 + Exp.SignedTx signedLedgerTx = Exp.signTx era [] [keyWit] unsignedTx + + 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 .~ serialiseToRawBytes (Exp.SignedTx signedLedgerTx)) + + ------------------------------------------- + -- Wait for UTxOs to appear at address1 + ------------------------------------------- + H.note_ $ "Wait for 2 UTxOs at address " <> show addressText1 + 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: exact address + payment credential predicate + ------------------------------------------- + H.note_ "Test 2: Verify exact address + 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.exactAddress .~ serialiseToRawBytes address1 + & 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: combined address predicate returns UTxOs from both addresses + ------------------------------------------- + H.note_ "Test 4: Verify anyOf predicate with both addresses returns all UTxOs" + allUtxosSearch <- H.noteShowM . H.evalIO $ + Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "searchUtxos")) $ + def & U5c.predicate .~ (def & U5c.anyOf .~ [addressPredicate address0, addressPredicate address1]) + + H.assertWith (allUtxosSearch ^. U5c.items) $ \xs -> length xs > 2 + +asAddressInEra :: ShelleyBasedEra era -> AsType (AddressInEra era) +asAddressInEra s = shelleyBasedEraConstraints s $ AsAddressInEra asType + +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)) + ) diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Transaction.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Transaction.hs index ea88dfe501c..a447f7c4a16 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Transaction.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Transaction.hs @@ -18,7 +18,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 hiding (cardano) 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 @@ -27,10 +27,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 @@ -43,8 +41,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 @@ -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 addressText0] - 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 address0 + pure (pparams', search') pparams <- H.leftFail $ utxoRpcPParamsToProtocolParams era $ pparamsResponse ^. U5c.values . U5c.cardano - txOut0 : _ <- H.noteShowM . flip filterM (utxosResponse ^. U5c.items) $ \utxo -> do - utxoAddress <- deserialiseAddressBs addressInEra $ utxo ^. U5c.cardano . U5c.address - pure $ address0 == utxoAddress + txOut0 : _ <- H.noteShow $ searchResponse ^. U5c.items txIn0 <- txoRefToTxIn $ txOut0 ^. U5c.txoRef outputCoin <- H.leftFail $ txOut0 ^. U5c.cardano . U5c.coin . to utxoRpcBigIntToInteger @@ -119,7 +112,7 @@ hprop_rpc_transaction = integrationRetryWorkspace 2 "rpc-tx" $ \tempAbsBasePath' Exp.SignedTx signedLedgerTx = Exp.signTx era [] [keyWit] unsignedTx txId' <- H.noteShow . Exp.obtainCommonConstraints era . TxId $ Exp.hashTxBody (signedLedgerTx ^. L.bodyTxL) - H.noteShowPretty_ utxosResponse + H.noteShowPretty_ searchResponse liftBaseOp (Rpc.withConnection def rpcServer) $ \conn -> do submitResponse <- H.noteShowM . H.evalIO $ @@ -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 addressText1 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 addressInEra $ utxo ^. U5c.cardano . U5c.address - pure $ address1 == utxoAddress + (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) @@ -154,5 +145,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 addressInEra = H.nothingFail . deserialiseAddress addressInEra <=< H.leftFail . T.decodeUtf8' +addressPredicate :: IsCardanoEra era => AddressInEra era -> Proto UtxoRpc.UtxoPredicate +addressPredicate address = + def + & U5c.match + .~ ( def + & U5c.cardano + .~ (def & U5c.address .~ (def & U5c.exactAddress .~ serialiseToRawBytes address)) + ) diff --git a/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs b/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs index 49fe74b79a4..c1d7ab52310 100644 --- a/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs +++ b/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs @@ -35,7 +35,9 @@ 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.Parser +import qualified Cardano.Testnet.Test.Rpc.Eval 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 @@ -147,7 +149,9 @@ 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 + , ignoreOnWindows "RPC Eval Tx" Cardano.Testnet.Test.Rpc.Eval.hprop_rpc_eval_tx ] , T.testGroup "NodesWithOptions parser" [ H.testPropertyNamed "Roundtrip" (fromString "prop_parseNodeSpecs_roundtrip") diff --git a/cardano-testnet/test/cardano-testnet-test/files/golden/queries/govStateOut.json b/cardano-testnet/test/cardano-testnet-test/files/golden/queries/govStateOut.json index ec713c7fd2a..8984e8ce8d2 100644 --- a/cardano-testnet/test/cardano-testnet-test/files/golden/queries/govStateOut.json +++ b/cardano-testnet/test/cardano-testnet-test/files/golden/queries/govStateOut.json @@ -414,7 +414,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -424,9 +424,9 @@ 42921, 4, 2, - 30623, - 28755, - 75, + 24548, + 29498, + 38, 1, 898148, 27279, @@ -479,7 +479,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -495,7 +495,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -506,7 +506,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -1092,7 +1092,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -1102,9 +1102,9 @@ 42921, 4, 2, - 30623, - 28755, - 75, + 24548, + 29498, + 38, 1, 898148, 27279, @@ -1157,7 +1157,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -1173,7 +1173,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -1184,7 +1184,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -1759,7 +1759,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -1769,9 +1769,9 @@ 42921, 4, 2, - 30623, - 28755, - 75, + 24548, + 29498, + 38, 1, 898148, 27279, @@ -1824,7 +1824,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -1840,7 +1840,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -1851,7 +1851,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -2423,7 +2423,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -2433,9 +2433,9 @@ 42921, 4, 2, - 30623, - 28755, - 75, + 24548, + 29498, + 38, 1, 898148, 27279, @@ -2488,7 +2488,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -2504,7 +2504,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -2515,7 +2515,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, diff --git a/cardano-testnet/test/cardano-testnet-test/files/golden/queries/protocolParametersFileOut.json b/cardano-testnet/test/cardano-testnet-test/files/golden/queries/protocolParametersFileOut.json index e96183cc046..f8156095026 100644 --- a/cardano-testnet/test/cardano-testnet-test/files/golden/queries/protocolParametersFileOut.json +++ b/cardano-testnet/test/cardano-testnet-test/files/golden/queries/protocolParametersFileOut.json @@ -403,7 +403,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -413,9 +413,9 @@ 42921, 4, 2, - 30623, - 28755, - 75, + 24548, + 29498, + 38, 1, 898148, 27279, @@ -468,7 +468,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -484,7 +484,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -495,7 +495,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, diff --git a/cardano-testnet/test/cardano-testnet-test/files/golden/queries/protocolParametersOut.txt b/cardano-testnet/test/cardano-testnet-test/files/golden/queries/protocolParametersOut.txt index e96183cc046..f8156095026 100644 --- a/cardano-testnet/test/cardano-testnet-test/files/golden/queries/protocolParametersOut.txt +++ b/cardano-testnet/test/cardano-testnet-test/files/golden/queries/protocolParametersOut.txt @@ -403,7 +403,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -413,9 +413,9 @@ 42921, 4, 2, - 30623, - 28755, - 75, + 24548, + 29498, + 38, 1, 898148, 27279, @@ -468,7 +468,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -484,7 +484,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, @@ -495,7 +495,7 @@ 7305, -900, 1716, - 960, + 549, 57, 85848, 0, diff --git a/flake.lock b/flake.lock index 07c24c46639..3be1a1293b2 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "CHaP": { "flake": false, "locked": { - "lastModified": 1777742585, - "narHash": "sha256-ZzXz2vOhqethlqPgBExPXEnKWvaTbidsIxh5MGv+pwE=", + "lastModified": 1779876270, + "narHash": "sha256-FA9E1EaQvPITpO/8weQyi7p3KHgyNb9GiwM6F96Aoeo=", "owner": "intersectmbo", "repo": "cardano-haskell-packages", - "rev": "e8a483522ee73c8c9493ea6055553e5c2532e66b", + "rev": "cb63b6483a5d6ce36fb07815736315bd4408162e", "type": "github" }, "original": {