Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changes/20260515_cardano_api_evaluate_transaction.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
project: cardano-api
pr: 1205
kind:
- feature
description: |
Add `evaluateTransaction` and `evaluateSignedTx` to `Cardano.Api.Experimental`, composing script evaluation, fee computation, and balance checking into a single pure function for signed transactions.
6 changes: 6 additions & 0 deletions .changes/20260515_cardano_rpc_use_evaluate_transaction.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
project: cardano-rpc
pr: 1205
kind:
- refactoring
description: |
Refactor `evalTxMethod` to use the new `Exp.evaluateTransaction` from cardano-api instead of inlining the script evaluation, fee computation, and balance checking pipeline.
9 changes: 7 additions & 2 deletions cardano-api/src/Cardano/Api/Experimental.hs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,17 @@ module Cardano.Api.Experimental
, obtainCommonConstraints
, eraProtVerHigh
, hashTxBody
, evaluateTransactionExecutionUnits
, evaluateTransactionExecutionUnitsShelley
, AnchorDataFromCertificateError (..)
, getAnchorDataFromCertificate
, mkTxCertificates

-- ** Transaction evaluation
, evaluateTransaction
, evaluateSignedTx
, TxEvaluationResult (..)
, evaluateTransactionExecutionUnits
, evaluateTransactionExecutionUnitsShelley

-- ** Transaction fee related
, FeeCalculationError (..)
, calcMinFeeRecursive
Expand Down
49 changes: 47 additions & 2 deletions cardano-api/src/Cardano/Api/Experimental/Tx.hs
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,14 @@ module Cardano.Api.Experimental.Tx
, getTxScriptWitnessesRequirements
, obtainMonoidConstraint

-- * Transaction evaluation
, evaluateTransaction
, evaluateSignedTx
, TxEvaluationResult (..)
, evaluateTransactionExecutionUnits

-- * Balancing transactions
, calculateMinimumUTxO
, evaluateTransactionExecutionUnits
, makeTransactionBodyAutoBalance
, TxBodyErrorAutoBalance (..)
, TxFeeEstimationError (..)
Expand All @@ -212,6 +217,8 @@ module Cardano.Api.Experimental.Tx
)
where

import Cardano.Api.Address (StakeCredential)
import Cardano.Api.Certificate.Internal (PoolId)
import Cardano.Api.Era.Internal.Core qualified as Api
import Cardano.Api.Era.Internal.Eon.ShelleyBasedEra
import Cardano.Api.Experimental.Era
Expand All @@ -225,6 +232,7 @@ import Cardano.Api.Ledger.Internal.Reexport qualified as L
import Cardano.Api.Plutus.Internal.Script qualified as Api
import Cardano.Api.Pretty (docToString, pretty)
import Cardano.Api.ProtocolParameters
import Cardano.Api.Query.Internal.Type.QueryInMode (LedgerEpochInfo, SystemStart)
import Cardano.Api.Serialise.Raw
( SerialiseAsRawBytes (..)
, SerialiseAsRawBytesError (SerialiseAsRawBytesError)
Expand All @@ -233,16 +241,18 @@ import Cardano.Api.Tx.Internal.Body qualified as Api
import Cardano.Api.Tx.Internal.Sign

import Cardano.Crypto.Hash qualified as Hash
import Cardano.Ledger.Alonzo.Core qualified as Ledger
import Cardano.Ledger.Api qualified as L
import Cardano.Ledger.Binary qualified as Ledger
import Cardano.Ledger.Core qualified as Ledger
import Cardano.Ledger.Credential qualified as Ledger (Credential)
import Cardano.Ledger.Hashes qualified as L hiding (Hash)

import Control.Exception (displayException)
import Data.Bifunctor (bimap)
import Data.ByteString.Lazy (fromStrict)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Set (Set)
import Data.Set qualified as Set
import GHC.Stack
import Lens.Micro
Expand Down Expand Up @@ -325,6 +335,41 @@ signTx era bootstrapWits shelleyKeyWits (UnsignedTx unsigned) =
signedTx = unsigned & L.witsTxL .~ (keyWits <> currentScriptWitnesses)
in SignedTx signedTx

-- | Like 'evaluateTransaction' but accepts a 'SignedTx' directly.
evaluateSignedTx
:: forall era
. IsEra era
=> SystemStart
-- ^ Start time of the blockchain
-> LedgerEpochInfo
-- ^ Epoch info for slot/time conversions
-> L.PParams (LedgerEra era)
-- ^ Protocol parameters
-> Set PoolId
-- ^ Registered stake pools
-> Map StakeCredential L.Coin
-- ^ Stake delegation deposits
-> Map (Ledger.Credential Ledger.DRepRole) L.Coin
-- ^ DRep delegation deposits
-> L.UTxO (LedgerEra era)
-- ^ UTxO set for the transaction inputs
-> SignedTx era
-- ^ Signed transaction to evaluate
-> TxEvaluationResult (LedgerEra era)
evaluateSignedTx systemStart epochInfo protocolParams poolIds stakeDelegDeposits drepDelegDeposits utxo (SignedTx tx) =
-- obtainCommonConstraints is needed here to bring ShelleyLedgerEra era ~ LedgerEra era
-- into scope, unifying SignedTx's ShelleyLedgerEra with evaluateTransaction's LedgerEra.
obtainCommonConstraints (useEra @era) $
evaluateTransaction
systemStart
epochInfo
protocolParams
poolIds
stakeDelegDeposits
drepDelegDeposits
utxo
tx
Comment thread
carbolymer marked this conversation as resolved.

-- Compatibility related. Will be removed once the old api has been deprecated and deleted.

convertTxBodyToUnsignedTx
Expand Down
87 changes: 85 additions & 2 deletions cardano-api/src/Cardano/Api/Experimental/Tx/Internal/Fee.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ module Cardano.Api.Experimental.Tx.Internal.Fee
, calcMinFeeRecursive
, collectTxBodyScriptWitnesses
, estimateBalancedTxBody
, evaluateTransaction
, TxEvaluationResult (..)
, evaluateTransactionExecutionUnits
, evaluateTransactionFee
, indexWitnessedTxProposalProcedures
Expand All @@ -45,14 +47,15 @@ import Cardano.Api.Experimental.Tx.Internal.Type
import Cardano.Api.Key.Internal qualified as Api
import Cardano.Api.Ledger.Internal.Reexport qualified as L
import Cardano.Api.Plutus.Internal
import Cardano.Api.Plutus.Internal.Script (fromAlonzoExUnits)
import Cardano.Api.Plutus.Internal.Script (fromAlonzoExUnits, toAlonzoExUnits)
import Cardano.Api.Plutus.Internal.Script qualified as Old
import Cardano.Api.Plutus.Internal.ScriptData
import Cardano.Api.Pretty
import Cardano.Api.ProtocolParameters
import Cardano.Api.Query.Internal.Type.QueryInMode
import Cardano.Api.Tx.Internal.Body
( ScriptWitnessIndex (..)
, fromScriptWitnessIndex
, indexCertificatesWith
, renderScriptWitnessIndex
, toScriptIndex
Expand Down Expand Up @@ -93,7 +96,7 @@ import Data.Set (Set)
import Data.Set qualified as Set
import GHC.Exts (IsList (..))
import GHC.Stack
import Lens.Micro ((.~), (^.))
import Lens.Micro ((%~), (.~), (^.))
import Prettyprinter (punctuate)

data TxBodyErrorAutoBalance era
Expand Down Expand Up @@ -532,6 +535,86 @@ calculateMinimumUTxO pp (TxOut txout) =
let txOutWithMinCoin = L.setMinCoinTxOut pp txout
in txOutWithMinCoin ^. L.coinTxOutL

-- | Result of evaluating a signed transaction against the current ledger state.
data TxEvaluationResult era = Show (L.Value era) => TxEvaluationResult
{ txEvalFee :: L.Coin
-- ^ Computed minimum fee for the transaction
, txEvalExecutionUnits
:: Map ScriptWitnessIndex (Either ScriptExecutionError (EvalTxExecutionUnitsLog, ExecutionUnits))
-- ^ Per-redeemer execution units or script errors
, txEvalBalance :: L.Value era
-- ^ Remaining balance (consumed - produced); mempty when balanced
}

deriving instance Show (TxEvaluationResult era)
Comment thread
carbolymer marked this conversation as resolved.

-- | Run all scripts, compute the minimum fee, and check the balance.
evaluateTransaction
:: forall era
. IsEra era
=> SystemStart
-- ^ Start time of the blockchain
-> LedgerEpochInfo
-- ^ Epoch info for slot/time conversions
-> L.PParams (LedgerEra era)
-- ^ Protocol parameters
-> Set PoolId
-- ^ Registered stake pools
-> Map StakeCredential L.Coin
-- ^ Stake delegation deposits
-> Map (Ledger.Credential Ledger.DRepRole) L.Coin
-- ^ DRep delegation deposits
-> L.UTxO (LedgerEra era)
-- ^ UTxO set for the transaction inputs
-> L.Tx L.TopTx (LedgerEra era)
-- ^ Signed transaction to evaluate
-> TxEvaluationResult (LedgerEra era)
Comment thread
carbolymer marked this conversation as resolved.
evaluateTransaction systemStart epochInfo protocolParams poolIds stakeDelegDeposits drepDelegDeposits utxo tx =
obtainCommonConstraints (useEra @era) $ do
let txEvalExecutionUnits =
evaluateTransactionExecutionUnits systemStart epochInfo protocolParams utxo tx
evaluatedExUnitsMap =
Map.fromList
[ (purpose, toAlonzoExUnits units)
| (scriptWitnessIndex, Right (_, units)) <- Map.toList txEvalExecutionUnits
, Just purpose <- [fromScriptWitnessIndex (convert $ useEra @era) scriptWitnessIndex]
]
txWithEvaluatedExUnits =
tx
& L.witsTxL . L.rdmrsTxWitsL
%~ \redeemers ->
L.Redeemers
. Map.mapWithKey
( \purpose (datum, oldExUnits) ->
(datum, Map.findWithDefault oldExUnits purpose evaluatedExUnitsMap)
)
$ L.unRedeemers redeemers
txEvalFee =
L.setMinFeeTxUtxo protocolParams txWithEvaluatedExUnits utxo
^. L.bodyTxL . L.feeTxBodyL
txEvalBalance =
L.evalBalanceTxBody
protocolParams
lookupDelegDeposit
lookupDRepDeposit
isRegPool
utxo
$ txWithEvaluatedExUnits ^. L.bodyTxL
TxEvaluationResult{txEvalFee, txEvalExecutionUnits, txEvalBalance}
where
isRegPool :: Ledger.KeyHash Ledger.StakePool -> Bool
isRegPool keyHash = Api.StakePoolKeyHash keyHash `Set.member` poolIds

lookupDelegDeposit
:: Ledger.Credential Ledger.Staking -> Maybe L.Coin
lookupDelegDeposit stakeCred =
Map.lookup (fromShelleyStakeCredential stakeCred) stakeDelegDeposits

lookupDRepDeposit
:: Ledger.Credential Ledger.DRepRole -> Maybe L.Coin
lookupDRepDeposit drepCred =
Map.lookup drepCred drepDelegDeposits

-- | Compute the total balance of the proposed transaction. Ultimately, a valid
-- transaction must be fully balanced, which means that it has a total value
-- of zero.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ tests =
"underfunded transaction fails with TxBodyErrorBalanceNegative"
prop_makeTransactionBodyAutoBalance_balance_negative
]
, testGroup
"evaluateTransaction"
[ testProperty
"well-funded simple tx returns positive fee"
prop_evaluateTransaction_positive_fee
, testProperty
"script-free tx returns empty execution units map"
prop_evaluateTransaction_no_scripts_empty_exunits
, testProperty
"balanced tx has mempty balance"
prop_evaluateSignedTx_balanced_mempty
]
]

-- ---------------------------------------------------------------------------
Expand Down Expand Up @@ -736,6 +748,73 @@ prop_makeTransactionBodyAutoBalance_balance_negative = H.property $ do
H.annotate "Expected TxBodyErrorBalanceNegative but tx balanced successfully"
>> H.failure

-- | A well-funded transaction returns a positive fee from 'evaluateTransaction'.
prop_evaluateTransaction_positive_fee :: Property
prop_evaluateTransaction_positive_fee = H.property $ do
(result, _utxo, _unsignedTx, _changeAddr) <- H.forAll evalSimpleTx
H.assertWith (Exp.txEvalFee result) (> L.Coin 0)

-- | A script-free transaction returns an empty execution units map.
prop_evaluateTransaction_no_scripts_empty_exunits :: Property
prop_evaluateTransaction_no_scripts_empty_exunits = H.property $ do
(result, _utxo, _unsignedTx, _changeAddr) <- H.forAll evalSimpleTx
H.assertWith (Exp.txEvalExecutionUnits result) Map.null

-- | A transaction whose fee was set by 'calcMinFeeRecursive' has mempty
-- balance when checked by 'evaluateSignedTx', because the generator
-- produces a tx whose inputs exactly cover outputs plus the computed fee.
-- Witnesses are irrelevant for balance checking, so we wrap the ledger tx
-- directly in 'SignedTx' without calling 'signTx'.
prop_evaluateSignedTx_balanced_mempty :: Property
prop_evaluateSignedTx_balanced_mempty = H.property $ do
(unsignedTx, utxo, changeAddr) <- H.forAll $ genFundedSimpleTx Exp.ConwayEra
Exp.UnsignedTx balancedLedgerTx <-
H.leftFail $
Exp.calcMinFeeRecursive changeAddr unsignedTx utxo exampleProtocolParams mempty mempty mempty 0
let signedTx = Exp.SignedTx balancedLedgerTx
systemStart = Api.SystemStart $ Time.posixSecondsToUTCTime 0
epochInfo =
Api.LedgerEpochInfo $
Slotting.fixedEpochInfo (Slotting.EpochSize 100) (Slotting.mkSlotLength 1000)
result =
Exp.evaluateSignedTx
systemStart
epochInfo
exampleProtocolParams
mempty
mempty
mempty
utxo
signedTx
Exp.txEvalBalance result H.=== mempty
Comment thread
carbolymer marked this conversation as resolved.

-- | Evaluate a simple signed transaction, returning the result and UTxO.
evalSimpleTx
:: Gen
( Exp.TxEvaluationResult (Exp.LedgerEra Exp.ConwayEra)
, L.UTxO (Exp.LedgerEra Exp.ConwayEra)
, Exp.UnsignedTx (Exp.LedgerEra Exp.ConwayEra)
, L.Addr
)
evalSimpleTx = do
(unsignedTx, utxo, changeAddr) <- genFundedSimpleTx Exp.ConwayEra
let signedTx = Exp.signTx Exp.ConwayEra [] [] unsignedTx
systemStart = Api.SystemStart $ Time.posixSecondsToUTCTime 0
epochInfo =
Api.LedgerEpochInfo $
Slotting.fixedEpochInfo (Slotting.EpochSize 100) (Slotting.mkSlotLength 1000)
result =
Exp.evaluateSignedTx
systemStart
epochInfo
exampleProtocolParams
mempty
mempty
mempty
utxo
signedTx
pure (result, utxo, unsignedTx, changeAddr)

-- | Regression test for the bug where 'mapScriptWitnessesCertificates' silently
-- dropped certs stored with a @Nothing@ witness (e.g. shelley stake registration
-- certificates) when rebuilding 'TxCertificates' during fee balancing.
Expand Down
Loading
Loading