Skip to content

Commit 10cf9be

Browse files
committed
Add evaluateTransaction and evaluateSignedTx to Cardano.Api.Experimental, composing script evaluation, fee computation, and balance checking into a single pure function for signed transactions.
1 parent f1cc7c8 commit 10cf9be

5 files changed

Lines changed: 221 additions & 46 deletions

File tree

cardano-api/src/Cardano/Api/Experimental.hs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,17 @@ module Cardano.Api.Experimental
2222
, obtainCommonConstraints
2323
, eraProtVerHigh
2424
, hashTxBody
25-
, evaluateTransactionExecutionUnits
26-
, evaluateTransactionExecutionUnitsShelley
2725
, AnchorDataFromCertificateError (..)
2826
, getAnchorDataFromCertificate
2927
, mkTxCertificates
3028

29+
-- ** Transaction evaluation
30+
, evaluateTransaction
31+
, evaluateSignedTx
32+
, TxEvaluationResult (..)
33+
, evaluateTransactionExecutionUnits
34+
, evaluateTransactionExecutionUnitsShelley
35+
3136
-- ** Transaction fee related
3237
, FeeCalculationError (..)
3338
, calcMinFeeRecursive

cardano-api/src/Cardano/Api/Experimental/Tx.hs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,14 @@ module Cardano.Api.Experimental.Tx
193193
, getTxScriptWitnessesRequirements
194194
, obtainMonoidConstraint
195195

196+
-- * Transaction evaluation
197+
, evaluateTransaction
198+
, evaluateSignedTx
199+
, TxEvaluationResult (..)
200+
, evaluateTransactionExecutionUnits
201+
196202
-- * Balancing transactions
197203
, calculateMinimumUTxO
198-
, evaluateTransactionExecutionUnits
199204
, makeTransactionBodyAutoBalance
200205
, TxBodyErrorAutoBalance (..)
201206
, TxFeeEstimationError (..)
@@ -212,6 +217,8 @@ module Cardano.Api.Experimental.Tx
212217
)
213218
where
214219

220+
import Cardano.Api.Address (StakeCredential)
221+
import Cardano.Api.Certificate.Internal (PoolId)
215222
import Cardano.Api.Era.Internal.Core qualified as Api
216223
import Cardano.Api.Era.Internal.Eon.ShelleyBasedEra
217224
import Cardano.Api.Experimental.Era
@@ -225,6 +232,7 @@ import Cardano.Api.Ledger.Internal.Reexport qualified as L
225232
import Cardano.Api.Plutus.Internal.Script qualified as Api
226233
import Cardano.Api.Pretty (docToString, pretty)
227234
import Cardano.Api.ProtocolParameters
235+
import Cardano.Api.Query.Internal.Type.QueryInMode (LedgerEpochInfo, SystemStart)
228236
import Cardano.Api.Serialise.Raw
229237
( SerialiseAsRawBytes (..)
230238
, SerialiseAsRawBytesError (SerialiseAsRawBytesError)
@@ -233,16 +241,18 @@ import Cardano.Api.Tx.Internal.Body qualified as Api
233241
import Cardano.Api.Tx.Internal.Sign
234242

235243
import Cardano.Crypto.Hash qualified as Hash
244+
import Cardano.Ledger.Alonzo.Core qualified as Ledger
236245
import Cardano.Ledger.Api qualified as L
237246
import Cardano.Ledger.Binary qualified as Ledger
238-
import Cardano.Ledger.Core qualified as Ledger
247+
import Cardano.Ledger.Credential qualified as Ledger (Credential)
239248
import Cardano.Ledger.Hashes qualified as L hiding (Hash)
240249

241250
import Control.Exception (displayException)
242251
import Data.Bifunctor (bimap)
243252
import Data.ByteString.Lazy (fromStrict)
244253
import Data.Map.Strict (Map)
245254
import Data.Map.Strict qualified as Map
255+
import Data.Set (Set)
246256
import Data.Set qualified as Set
247257
import GHC.Stack
248258
import Lens.Micro
@@ -325,6 +335,39 @@ signTx era bootstrapWits shelleyKeyWits (UnsignedTx unsigned) =
325335
signedTx = unsigned & L.witsTxL .~ (keyWits <> currentScriptWitnesses)
326336
in SignedTx signedTx
327337

338+
-- | Like 'evaluateTransaction' but accepts a 'SignedTx' directly.
339+
evaluateSignedTx
340+
:: forall era
341+
. IsEra era
342+
=> SystemStart
343+
-- ^ Start time of the blockchain
344+
-> LedgerEpochInfo
345+
-- ^ Epoch info for slot/time conversions
346+
-> L.PParams (LedgerEra era)
347+
-- ^ Protocol parameters
348+
-> Set PoolId
349+
-- ^ Registered stake pools
350+
-> Map StakeCredential L.Coin
351+
-- ^ Stake delegation deposits
352+
-> Map (Ledger.Credential Ledger.DRepRole) L.Coin
353+
-- ^ DRep delegation deposits
354+
-> L.UTxO (LedgerEra era)
355+
-- ^ UTxO set for the transaction inputs
356+
-> SignedTx era
357+
-- ^ Signed transaction to evaluate
358+
-> TxEvaluationResult (LedgerEra era)
359+
evaluateSignedTx systemStart epochInfo protocolParams poolIds stakeDelegDeposits drepDelegDeposits utxo (SignedTx tx) =
360+
obtainCommonConstraints (useEra @era) $
361+
evaluateTransaction
362+
systemStart
363+
epochInfo
364+
protocolParams
365+
poolIds
366+
stakeDelegDeposits
367+
drepDelegDeposits
368+
utxo
369+
tx
370+
328371
-- Compatibility related. Will be removed once the old api has been deprecated and deleted.
329372

330373
convertTxBodyToUnsignedTx

cardano-api/src/Cardano/Api/Experimental/Tx/Internal/Fee.hs

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ module Cardano.Api.Experimental.Tx.Internal.Fee
2020
, calcMinFeeRecursive
2121
, collectTxBodyScriptWitnesses
2222
, estimateBalancedTxBody
23+
, evaluateTransaction
24+
, TxEvaluationResult (..)
2325
, evaluateTransactionExecutionUnits
2426
, evaluateTransactionFee
2527
, indexWitnessedTxProposalProcedures
@@ -45,14 +47,15 @@ import Cardano.Api.Experimental.Tx.Internal.Type
4547
import Cardano.Api.Key.Internal qualified as Api
4648
import Cardano.Api.Ledger.Internal.Reexport qualified as L
4749
import Cardano.Api.Plutus.Internal
48-
import Cardano.Api.Plutus.Internal.Script (fromAlonzoExUnits)
50+
import Cardano.Api.Plutus.Internal.Script (fromAlonzoExUnits, toAlonzoExUnits)
4951
import Cardano.Api.Plutus.Internal.Script qualified as Old
5052
import Cardano.Api.Plutus.Internal.ScriptData
5153
import Cardano.Api.Pretty
5254
import Cardano.Api.ProtocolParameters
5355
import Cardano.Api.Query.Internal.Type.QueryInMode
5456
import Cardano.Api.Tx.Internal.Body
5557
( ScriptWitnessIndex (..)
58+
, fromScriptWitnessIndex
5659
, indexCertificatesWith
5760
, renderScriptWitnessIndex
5861
, toScriptIndex
@@ -93,7 +96,7 @@ import Data.Set (Set)
9396
import Data.Set qualified as Set
9497
import GHC.Exts (IsList (..))
9598
import GHC.Stack
96-
import Lens.Micro ((.~), (^.))
99+
import Lens.Micro ((%~), (.~), (^.))
97100
import Prettyprinter (punctuate)
98101

99102
data TxBodyErrorAutoBalance era
@@ -532,6 +535,86 @@ calculateMinimumUTxO pp (TxOut txout) =
532535
let txOutWithMinCoin = L.setMinCoinTxOut pp txout
533536
in txOutWithMinCoin ^. L.coinTxOutL
534537

538+
-- | Result of evaluating a signed transaction against the current ledger state.
539+
data TxEvaluationResult era = Show (L.Value era) => TxEvaluationResult
540+
{ txEvalFee :: L.Coin
541+
-- ^ Computed minimum fee for the transaction
542+
, txEvalExecutionUnits
543+
:: Map ScriptWitnessIndex (Either ScriptExecutionError (EvalTxExecutionUnitsLog, ExecutionUnits))
544+
-- ^ Per-redeemer execution units or script errors
545+
, txEvalBalance :: L.Value era
546+
-- ^ Remaining balance (consumed - produced); mempty when balanced
547+
}
548+
549+
deriving instance Show (TxEvaluationResult era)
550+
551+
-- | Run all scripts, compute the minimum fee, and check the balance.
552+
evaluateTransaction
553+
:: forall era
554+
. IsEra era
555+
=> SystemStart
556+
-- ^ Start time of the blockchain
557+
-> LedgerEpochInfo
558+
-- ^ Epoch info for slot/time conversions
559+
-> L.PParams (LedgerEra era)
560+
-- ^ Protocol parameters
561+
-> Set PoolId
562+
-- ^ Registered stake pools
563+
-> Map StakeCredential L.Coin
564+
-- ^ Stake delegation deposits
565+
-> Map (Ledger.Credential Ledger.DRepRole) L.Coin
566+
-- ^ DRep delegation deposits
567+
-> L.UTxO (LedgerEra era)
568+
-- ^ UTxO set for the transaction inputs
569+
-> L.Tx L.TopTx (LedgerEra era)
570+
-- ^ Signed transaction to evaluate
571+
-> TxEvaluationResult (LedgerEra era)
572+
evaluateTransaction systemStart epochInfo protocolParams poolIds stakeDelegDeposits drepDelegDeposits utxo tx =
573+
obtainCommonConstraints (useEra @era) $ do
574+
let txEvalExecutionUnits =
575+
evaluateTransactionExecutionUnits systemStart epochInfo protocolParams utxo tx
576+
evaluatedExUnitsMap =
577+
Map.fromList
578+
[ (purpose, toAlonzoExUnits units)
579+
| (scriptWitnessIndex, Right (_, units)) <- Map.toList txEvalExecutionUnits
580+
, Just purpose <- [fromScriptWitnessIndex (convert $ useEra @era) scriptWitnessIndex]
581+
]
582+
txWithEvaluatedExUnits =
583+
tx
584+
& L.witsTxL . L.rdmrsTxWitsL
585+
%~ \redeemers ->
586+
L.Redeemers
587+
. Map.mapWithKey
588+
( \purpose (datum, oldExUnits) ->
589+
(datum, Map.findWithDefault oldExUnits purpose evaluatedExUnitsMap)
590+
)
591+
$ L.unRedeemers redeemers
592+
txEvalFee =
593+
L.setMinFeeTxUtxo protocolParams txWithEvaluatedExUnits utxo
594+
^. L.bodyTxL . L.feeTxBodyL
595+
txEvalBalance =
596+
L.evalBalanceTxBody
597+
protocolParams
598+
lookupDelegDeposit
599+
lookupDRepDeposit
600+
isRegPool
601+
utxo
602+
$ txWithEvaluatedExUnits ^. L.bodyTxL
603+
TxEvaluationResult{txEvalFee, txEvalExecutionUnits, txEvalBalance}
604+
where
605+
isRegPool :: Ledger.KeyHash Ledger.StakePool -> Bool
606+
isRegPool keyHash = Api.StakePoolKeyHash keyHash `Set.member` poolIds
607+
608+
lookupDelegDeposit
609+
:: Ledger.Credential Ledger.Staking -> Maybe L.Coin
610+
lookupDelegDeposit stakeCred =
611+
Map.lookup (fromShelleyStakeCredential stakeCred) stakeDelegDeposits
612+
613+
lookupDRepDeposit
614+
:: Ledger.Credential Ledger.DRepRole -> Maybe L.Coin
615+
lookupDRepDeposit drepCred =
616+
Map.lookup drepCred drepDelegDeposits
617+
535618
-- | Compute the total balance of the proposed transaction. Ultimately, a valid
536619
-- transaction must be fully balanced, which means that it has a total value
537620
-- of zero.

cardano-api/test/cardano-api-test/Test/Cardano/Api/Experimental/Fee.hs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,18 @@ tests =
108108
"underfunded transaction fails with TxBodyErrorBalanceNegative"
109109
prop_makeTransactionBodyAutoBalance_balance_negative
110110
]
111+
, testGroup
112+
"evaluateTransaction"
113+
[ testProperty
114+
"well-funded simple tx returns positive fee"
115+
prop_evaluateTransaction_positive_fee
116+
, testProperty
117+
"script-free tx returns empty execution units map"
118+
prop_evaluateTransaction_no_scripts_empty_exunits
119+
, testProperty
120+
"balanced tx has mempty balance"
121+
prop_evaluateTransaction_balanced_mempty
122+
]
111123
]
112124

113125
-- ---------------------------------------------------------------------------
@@ -736,6 +748,70 @@ prop_makeTransactionBodyAutoBalance_balance_negative = H.property $ do
736748
H.annotate "Expected TxBodyErrorBalanceNegative but tx balanced successfully"
737749
>> H.failure
738750

751+
-- | A well-funded transaction returns a positive fee from 'evaluateTransaction'.
752+
prop_evaluateTransaction_positive_fee :: Property
753+
prop_evaluateTransaction_positive_fee = H.property $ do
754+
(result, _utxo, _unsignedTx, _changeAddr) <- H.forAll evalSimpleTx
755+
H.assertWith (Exp.txEvalFee result) (> L.Coin 0)
756+
757+
-- | A script-free transaction returns an empty execution units map.
758+
prop_evaluateTransaction_no_scripts_empty_exunits :: Property
759+
prop_evaluateTransaction_no_scripts_empty_exunits = H.property $ do
760+
(result, _utxo, _unsignedTx, _changeAddr) <- H.forAll evalSimpleTx
761+
H.assertWith (Exp.txEvalExecutionUnits result) Map.null
762+
763+
-- | A transaction balanced by 'calcMinFeeRecursive' has mempty balance
764+
-- when checked by 'evaluateSignedTx'.
765+
prop_evaluateTransaction_balanced_mempty :: Property
766+
prop_evaluateTransaction_balanced_mempty = H.property $ do
767+
(unsignedTx, utxo, changeAddr) <- H.forAll $ genFundedSimpleTx Exp.ConwayEra
768+
Exp.UnsignedTx balancedLedgerTx <-
769+
H.leftFail $
770+
Exp.calcMinFeeRecursive changeAddr unsignedTx utxo exampleProtocolParams mempty mempty mempty 0
771+
let signedTx = Exp.SignedTx balancedLedgerTx
772+
systemStart = Api.SystemStart $ Time.posixSecondsToUTCTime 0
773+
epochInfo =
774+
Api.LedgerEpochInfo $
775+
Slotting.fixedEpochInfo (Slotting.EpochSize 100) (Slotting.mkSlotLength 1000)
776+
result =
777+
Exp.evaluateSignedTx
778+
systemStart
779+
epochInfo
780+
exampleProtocolParams
781+
mempty
782+
mempty
783+
mempty
784+
utxo
785+
signedTx
786+
Exp.txEvalBalance result H.=== mempty
787+
788+
-- | Evaluate a simple signed transaction, returning the result and UTxO.
789+
evalSimpleTx
790+
:: Gen
791+
( Exp.TxEvaluationResult (Exp.LedgerEra Exp.ConwayEra)
792+
, L.UTxO (Exp.LedgerEra Exp.ConwayEra)
793+
, Exp.UnsignedTx (Exp.LedgerEra Exp.ConwayEra)
794+
, L.Addr
795+
)
796+
evalSimpleTx = do
797+
(unsignedTx, utxo, changeAddr) <- genFundedSimpleTx Exp.ConwayEra
798+
let signedTx = Exp.signTx Exp.ConwayEra [] [] unsignedTx
799+
systemStart = Api.SystemStart $ Time.posixSecondsToUTCTime 0
800+
epochInfo =
801+
Api.LedgerEpochInfo $
802+
Slotting.fixedEpochInfo (Slotting.EpochSize 100) (Slotting.mkSlotLength 1000)
803+
result =
804+
Exp.evaluateSignedTx
805+
systemStart
806+
epochInfo
807+
exampleProtocolParams
808+
mempty
809+
mempty
810+
mempty
811+
utxo
812+
signedTx
813+
pure (result, utxo, unsignedTx, changeAddr)
814+
739815
-- | Regression test for the bug where 'mapScriptWitnessesCertificates' silently
740816
-- dropped certs stored with a @Nothing@ witness (e.g. shelley stake registration
741817
-- certificates) when rebuilding 'TxCertificates' during fee balancing.

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

Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -82,35 +82,18 @@ evalTxMethod request = do
8282
obtainCommonConstraints eon $ do
8383
let ledgerUtxo = toLedgerUTxO (convert eon) utxo
8484
epochInfo = toLedgerEpochInfo eraHistory
85-
evalResults =
86-
Exp.evaluateTransactionExecutionUnits
85+
drepDeposits = Map.map (L.fromCompact . L.drepDeposit) drepStates
86+
poolIdSet = Map.keysSet registeredPools
87+
Exp.TxEvaluationResult fee evalUnits balance =
88+
Exp.evaluateTransaction
8789
systemStart
8890
epochInfo
8991
protocolParams
92+
poolIdSet
93+
stakeDelegDeposits
94+
drepDeposits
9095
ledgerUtxo
9196
ledgerTx
92-
evaluatedExUnitsMap =
93-
Map.fromList
94-
[ (purpose, L.ExUnits (executionMemory units) (executionSteps units))
95-
| (swi, Right (_, units)) <- Map.toList evalResults
96-
, Just purpose <- [fromScriptWitnessIndex (convert eon) swi]
97-
]
98-
-- Failed redeemers keep the client-supplied ex-units, so the computed
99-
-- fee may vary with the client's guess. This is acceptable because a
100-
-- failed evaluation means the tx cannot be submitted anyway.
101-
txWithEvaluatedExUnits =
102-
ledgerTx
103-
& L.witsTxL . L.rdmrsTxWitsL
104-
%~ \rdmrs ->
105-
L.Redeemers $
106-
Map.mapWithKey
107-
( \purpose (datum, oldExUnits) ->
108-
(datum, Map.findWithDefault oldExUnits purpose evaluatedExUnitsMap)
109-
)
110-
(L.unRedeemers rdmrs)
111-
fee =
112-
L.setMinFeeTxUtxo protocolParams txWithEvaluatedExUnits ledgerUtxo
113-
^. L.bodyTxL . L.feeTxBodyL
11497
redeemerData =
11598
Map.fromList
11699
[ ( toScriptIndex (convert eon) purpose
@@ -122,22 +105,7 @@ evalTxMethod request = do
122105
| (purpose, (datum, _exUnits)) <-
123106
toList . L.unRedeemers $ ledgerTx ^. L.witsTxL . L.rdmrsTxWitsL
124107
]
125-
txEval = mkProtoTxEval fee evalResults redeemerData
126-
127-
lookupStakeDeposit credential =
128-
Map.lookup (fromShelleyStakeCredential credential) stakeDelegDeposits
129-
lookupDRepDeposit credential =
130-
L.fromCompact . L.drepDeposit <$> Map.lookup credential drepStates
131-
isRegPool poolKeyHash =
132-
StakePoolKeyHash poolKeyHash `Map.member` registeredPools
133-
balance =
134-
L.evalBalanceTxBody
135-
protocolParams
136-
lookupStakeDeposit
137-
lookupDRepDeposit
138-
isRegPool
139-
ledgerUtxo
140-
(txWithEvaluatedExUnits ^. L.bodyTxL)
108+
txEval = mkProtoTxEval fee evalUnits redeemerData
141109
balanceErrors
142110
| balance == mempty = []
143111
| otherwise =

0 commit comments

Comments
 (0)