Context
Follow-up from investigation in #1367. This issue scopes the first implementation step: an online transaction validate command that runs phase 1 (ledger rules via applyTx) and phase 2 (Plutus scripts via evaluateTransactionExecutionUnits) against the current ledger state, without submitting the transaction.
Scope
- New command:
cardano-cli latest transaction validate --tx-file signed.tx --socket-path node.socket
- Phase 1 and phase 2 run independently — user sees all errors even if phase 1 fails
- Online only (requires node connection)
- Text output for now (structured JSON is a follow-up)
Key design decisions
constructGlobals needs ShelleyGenesis + EpochInfo. The existing QueryGenesisParameters returns GenesisParameters which contains all the fields needed for Globals.
We need to add a mkGlobalsFromGenesisParameters :: GenesisParameters era -> EpochInfo (Either Text) -> Globals function in cardano-api that constructs Globals from these fields (with minor type conversions: Int → Word64, Rational → ActiveSlotCoeff, Coin → Word64) and calls computeStabilityWindow/computeRandomnessStabilisationWindow for the derived fields.
Getting the ledger state for applyTx
mkMempoolEnv and mkMempoolState both only access nesEs (the EpochState inside NewEpochState). So we don't necessarily need the full NewEpochState.
Provisional approach: Use QueryCurrentEpochState — returns EpochState directly. Construct MempoolEnv/MempoolState from it manually (mirroring what mkMempoolEnv/mkMempoolState do, but skipping the NewEpochState unwrap). This is lighter than QueryDebugLedgerState.
Problem: On mainnet, transferring the full EpochState over the socket and deserializing it is slow.
Potential solution: Run applyTx inside the node — add a new query (or a validate-only variant of the LocalTxSubmission mini-protocol) in ouroboros-consensus / cardano-node that takes a Tx, runs applyTx server-side against the in-memory ledger state, and returns Either ApplyTxError () without adding the tx to the mempool. This avoids transferring state entirely, gives the same performance as submit, and guarantees state consistency. Requires changes in ouroboros-consensus, ouroboros-network, cardano-node, cardano-api, but each change is small and well-defined.
Independent phase 1 + phase 2
Phase 2 (evaluateTransactionExecutionUnits) already exists and works standalone. We run it alongside phase 1 and merge results. Phase 2 gives richer script errors (per-script traces, execution units) than applyTx's ValidationTagMismatch (PlutusFailure Text ByteString).
Output format
# Both pass:
Transaction is valid.
Phase 1: passed
Phase 2: passed (3 scripts evaluated)
SpendingScript 0: passed (mem: 234000, steps: 89000000)
MintingScript 0: passed (mem: 120000, steps: 45000000)
SpendingScript 1: passed (mem: 180000, steps: 67000000)
# Phase 1 fails, phase 2 passes:
Transaction validation failed.
Phase 1: FAILED
FeeTooSmallUTxO: minimum fee is 300000 lovelace, transaction specifies 170000
Phase 2: passed (1 script evaluated)
SpendingScript 0: passed (mem: 234000, steps: 89000000)
# Both fail:
Transaction validation failed.
Phase 1: FAILED
ValueNotConservedUTxO: consumed 5000000, produced 6000000
Phase 2: FAILED
SpendingScript 0: FAILED
ScriptErrorEvaluationFailed: ...
Known limitations
Context
Follow-up from investigation in #1367. This issue scopes the first implementation step: an online
transaction validatecommand that runs phase 1 (ledger rules viaapplyTx) and phase 2 (Plutus scripts viaevaluateTransactionExecutionUnits) against the current ledger state, without submitting the transaction.Scope
cardano-cli latest transaction validate --tx-file signed.tx --socket-path node.socketKey design decisions
Getting
GlobalsforapplyTxconstructGlobalsneedsShelleyGenesis+EpochInfo. The existingQueryGenesisParametersreturnsGenesisParameterswhich contains all the fields needed forGlobals.We need to add a
mkGlobalsFromGenesisParameters :: GenesisParameters era -> EpochInfo (Either Text) -> Globalsfunction incardano-apithat constructsGlobalsfrom these fields (with minor type conversions:Int→Word64,Rational→ActiveSlotCoeff,Coin→Word64) and callscomputeStabilityWindow/computeRandomnessStabilisationWindowfor the derived fields.Getting the ledger state for
applyTxmkMempoolEnvandmkMempoolStateboth only accessnesEs(theEpochStateinsideNewEpochState). So we don't necessarily need the fullNewEpochState.Provisional approach: Use
QueryCurrentEpochState— returnsEpochStatedirectly. ConstructMempoolEnv/MempoolStatefrom it manually (mirroring whatmkMempoolEnv/mkMempoolStatedo, but skipping theNewEpochStateunwrap). This is lighter thanQueryDebugLedgerState.Problem: On mainnet, transferring the full
EpochStateover the socket and deserializing it is slow.Potential solution: Run
applyTxinside the node — add a new query (or a validate-only variant of theLocalTxSubmissionmini-protocol) inouroboros-consensus/cardano-nodethat takes aTx, runsapplyTxserver-side against the in-memory ledger state, and returnsEither ApplyTxError ()without adding the tx to the mempool. This avoids transferring state entirely, gives the same performance assubmit, and guarantees state consistency. Requires changes inouroboros-consensus,ouroboros-network,cardano-node,cardano-api, but each change is small and well-defined.Independent phase 1 + phase 2
Phase 2 (
evaluateTransactionExecutionUnits) already exists and works standalone. We run it alongside phase 1 and merge results. Phase 2 gives richer script errors (per-script traces, execution units) thanapplyTx'sValidationTagMismatch (PlutusFailure Text ByteString).Output format
Known limitations