Skip to content

Commit 39fdbc2

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 39fdbc2

7 files changed

Lines changed: 238 additions & 46 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
project: cardano-api
2+
pr: 1205
3+
kind:
4+
- feature
5+
description: |
6+
Add `evaluateTransaction` and `evaluateSignedTx` to `Cardano.Api.Experimental`, composing script evaluation, fee computation, and balance checking into a single pure function for signed transactions.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
project: cardano-rpc
2+
pr: 1205
3+
kind:
4+
- refactoring
5+
description: |
6+
Refactor `evalTxMethod` to use the new `Exp.evaluateTransaction` from cardano-api instead of inlining the script evaluation, fee computation, and balance checking pipeline.

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: 47 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,41 @@ 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 is needed here to bring ShelleyLedgerEra era ~ LedgerEra era
361+
-- into scope, unifying SignedTx's ShelleyLedgerEra with evaluateTransaction's LedgerEra.
362+
obtainCommonConstraints (useEra @era) $
363+
evaluateTransaction
364+
systemStart
365+
epochInfo
366+
protocolParams
367+
poolIds
368+
stakeDelegDeposits
369+
drepDelegDeposits
370+
utxo
371+
tx
372+
328373
-- Compatibility related. Will be removed once the old api has been deprecated and deleted.
329374

330375
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: 79 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_evaluateSignedTx_balanced_mempty
122+
]
111123
]
112124

113125
-- ---------------------------------------------------------------------------
@@ -736,6 +748,73 @@ 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 whose fee was set by 'calcMinFeeRecursive' has mempty
764+
-- balance when checked by 'evaluateSignedTx', because the generator
765+
-- produces a tx whose inputs exactly cover outputs plus the computed fee.
766+
-- Witnesses are irrelevant for balance checking, so we wrap the ledger tx
767+
-- directly in 'SignedTx' without calling 'signTx'.
768+
prop_evaluateSignedTx_balanced_mempty :: Property
769+
prop_evaluateSignedTx_balanced_mempty = H.property $ do
770+
(unsignedTx, utxo, changeAddr) <- H.forAll $ genFundedSimpleTx Exp.ConwayEra
771+
Exp.UnsignedTx balancedLedgerTx <-
772+
H.leftFail $
773+
Exp.calcMinFeeRecursive changeAddr unsignedTx utxo exampleProtocolParams mempty mempty mempty 0
774+
let signedTx = Exp.SignedTx balancedLedgerTx
775+
systemStart = Api.SystemStart $ Time.posixSecondsToUTCTime 0
776+
epochInfo =
777+
Api.LedgerEpochInfo $
778+
Slotting.fixedEpochInfo (Slotting.EpochSize 100) (Slotting.mkSlotLength 1000)
779+
result =
780+
Exp.evaluateSignedTx
781+
systemStart
782+
epochInfo
783+
exampleProtocolParams
784+
mempty
785+
mempty
786+
mempty
787+
utxo
788+
signedTx
789+
Exp.txEvalBalance result H.=== mempty
790+
791+
-- | Evaluate a simple signed transaction, returning the result and UTxO.
792+
evalSimpleTx
793+
:: Gen
794+
( Exp.TxEvaluationResult (Exp.LedgerEra Exp.ConwayEra)
795+
, L.UTxO (Exp.LedgerEra Exp.ConwayEra)
796+
, Exp.UnsignedTx (Exp.LedgerEra Exp.ConwayEra)
797+
, L.Addr
798+
)
799+
evalSimpleTx = do
800+
(unsignedTx, utxo, changeAddr) <- genFundedSimpleTx Exp.ConwayEra
801+
let signedTx = Exp.signTx Exp.ConwayEra [] [] unsignedTx
802+
systemStart = Api.SystemStart $ Time.posixSecondsToUTCTime 0
803+
epochInfo =
804+
Api.LedgerEpochInfo $
805+
Slotting.fixedEpochInfo (Slotting.EpochSize 100) (Slotting.mkSlotLength 1000)
806+
result =
807+
Exp.evaluateSignedTx
808+
systemStart
809+
epochInfo
810+
exampleProtocolParams
811+
mempty
812+
mempty
813+
mempty
814+
utxo
815+
signedTx
816+
pure (result, utxo, unsignedTx, changeAddr)
817+
739818
-- | Regression test for the bug where 'mapScriptWitnessesCertificates' silently
740819
-- dropped certs stored with a @Nothing@ witness (e.g. shelley stake registration
741820
-- certificates) when rebuilding 'TxCertificates' during fee balancing.

0 commit comments

Comments
 (0)