Stake Validator Pattern
Overview
The Stake Validator pattern allows you to delegate validation logic to a staking script, significantly reducing script execution costs when processing multiple UTxOs. This is achieved through the "withdraw zero trick" - where spending validators simply check for the presence of a staking credential withdrawal, and the staking validator performs the actual business logic validation once per transaction.
The Problem
When multiple UTxOs from the same script are spent in a transaction, the validator logic runs for each UTxO. This quickly becomes expensive:
The Solution
Move heavy validation logic to a staking validator that runs once per transaction. Spending validators only check that the staking credential is present:
Aiken Implementation
This pattern allows for delegating some computations to a given staking script. The primary application for this is the so-called "withdraw zero trick," which is most effective for validators that need to go over multiple inputs.
With a minimal spending logic (which is executed for each UTxO), and an arbitrary withdrawal logic (which is executed only once), a much more optimized script can be implemented.
The module offers three functions, primarily meant to be implemented under spending endpoints:
validate_withdrawvalidate_withdraw_with_amountvalidate_withdraw_minimal
Use validate_withdraw_minimal if you don't need to perform any validations on either the staking script's redeemer or withdrawal Lovelace quantity.
All three functions go over the withdrawals list in the transaction. However, validate_withdraw and validate_withdraw_with_amount also traverse the redeemers field in order to let you validate against the redeemer (and the withdrawal quantity in case of the latter).
Example
The following example shows a spending validator that uses validate_withdraw_with_amount to delegate to a staking script, and a minimal accompanying withdrawal script:
use aiken/crypto.{ScriptHash}
use aiken_design_patterns/stake_validator
use cardano/address.{Credential}
use cardano/transaction.{OutputReference, Transaction}
pub type ExampleSpendRedeemer {
withdraw_redeemer_index: Int,
withdrawal_index: Int,
}
/// Example for a validator that requires a withdrawal from a given staking
/// script hash.
validator spend(withdraw_script_hash: ScriptHash) {
spend(
_datum,
redeemer: ExampleSpendRedeemer,
own_out_ref: OutputReference,
tx: Transaction,
) {
// Extract needed values from `tx`
let Transaction { withdrawals, redeemers, .. } = tx
// Validate the required staking script is present in transaction, and grab
// its redeemer data and withdraw quantity in Lovelace.
let
redeemer_data,
withdraw_amount,
<-
stake_validator.validate_withdraw_with_amount(
withdraw_script_hash: withdraw_script_hash,
redeemers: redeemers,
withdraw_redeemer_index: redeemer.withdraw_redeemer_index,
withdrawals: withdrawals,
withdrawal_index: redeemer.withdrawal_index,
)
// Example validation, ensuring the staking script has been invoked with
// access to the output reference of the UTxO being spent.
expect out_ref_passed_to_staking_script: OutputReference = redeemer_data
expect out_ref_passed_to_staking_script == own_out_ref
// Another example validation, only allowing withdrawals from the staking
// script as long as no rewards had been accumulated for said script.
withdraw_amount == 0
}
else(_) {
fail
}
}
/// A very minimal example just to show how an accompanying staking script can
/// be defined.
validator withdraw {
withdraw(
redeemer: OutputReference,
_own_credential: Credential,
_tx: Transaction,
) {
let OutputReference { output_index, .. } = redeemer
// A contrived check. Only UTxOs that have an output index of 0 pass this
// script's validation.
output_index == 0
}
else(_) {
fail
}
}
Key Functions
The library provides three functions, all meant to be used in spending endpoints:
validate_withdraw
Helper function for implementing validation for spending UTxOs, essentially delegating their requirements to the given withdrawal validator.
In simpler terms, it says: As long as there is a redeemer with a withdrawal purpose, for the given script in transaction, this UTxO can be spent.
Allows you to validate based on the withdrawal's redeemer, which is mostly useful for ensuring specific endpoints are invoked.
pub fn validate_withdraw(
withdraw_script_hash: ScriptHash,
redeemers: Pairs<ScriptPurpose, Redeemer>,
withdraw_redeemer_index: Int,
withdraw_redeemer_validator: fn(Redeemer) -> Bool,
) -> Bool
validate_withdraw_with_amount
Similar to validate_withdraw, but with the additional steps for extracting the withdrawal amount from the withdrawals field:
pub fn validate_withdraw_with_amount(
withdraw_script_hash: ScriptHash,
redeemers: Pairs<ScriptPurpose, Redeemer>,
withdraw_redeemer_index: Int,
withdrawals: Pairs<Credential, Lovelace>,
withdrawal_index: Int,
withdraw_redeemer_validator: fn(Redeemer, Lovelace) -> Bool,
) -> Bool
validate_withdraw_minimal
A more minimal version of validate_withdraw, where only the presence of a given staking script is checked, regardless of withdraw amount or redeemer:
pub fn validate_withdraw_minimal(
withdraw_script_hash: ScriptHash,
withdrawals: Pairs<Credential, Lovelace>,
withdrawal_index: Int,
) -> Bool
Why "Withdraw Zero"?
The pattern is called "withdraw zero trick" because you can withdraw 0 lovelace from the staking credential to trigger the staking validator - the withdrawal amount is irrelevant to the validation logic.
Submitting the trigger off-chain
The off-chain half of the pattern is an ordinary transaction carrying a zero-amount withdrawal for the staking script, with a redeemer and the script attached. Withdrawal validators must be registered on-chain first; the on-chain logic that decides which registrations, delegations, and reward withdrawals to allow is the certificate and withdrawal handlers in Write a validator.
- Evolution
- Mesh
const tx = await client
.newTx()
.withdraw({
stakeCredential: scriptStakeCredential,
amount: 0n,
redeemer: Data.constr(0n, []),
label: "coordinator-trigger"
})
.attachScript({ script: stakeScript })
.build()
import { MeshTxBuilder, mConStr0 } from "@meshsdk/core"
declare const scriptRewardAddress: string // reward address derived from the stake script hash
declare const stakeScriptCbor: string
const collateral = await wallet.getCollateralMesh()
const unsignedTx = await new MeshTxBuilder({ fetcher: provider })
.withdrawalPlutusScriptV3()
.withdrawal(scriptRewardAddress, "0") // zero-amount withdrawal triggers the validator
.withdrawalScript(stakeScriptCbor)
.withdrawalRedeemerValue(mConStr0([]))
.txInCollateral(collateral[0].input.txHash, collateral[0].input.outputIndex)
.changeAddress(await wallet.getChangeAddressBech32())
.selectUtxosFrom(await wallet.getUtxosMesh())
.complete()
const signedTx = await wallet.signTx(unsignedTx)
await wallet.submitTx(signedTx)
Script control is not limited to the withdrawal trigger. In Evolution every staking operation accepts a redeemer and an attached script, so a script-held credential can also delegate:
- Evolution
import { Credential, Data } from "@evolution-sdk/evolution"
declare const scriptStakeCredential: Credential.Credential
declare const stakeScript: any
const tx = await client
.newTx()
.delegateToPool({
stakeCredential: scriptStakeCredential,
poolKeyHash,
redeemer: Data.constr(0n, []),
label: "delegate-script-stake"
})
.attachScript({ script: stakeScript })
.build()
Mesh's builder takes a redeemer on a script withdrawal (the trigger above) but not on a stake delegation certificate, so script-controlled delegation is Evolution or cardano-cli only.
The script does not have to hold the stake credential itself, either. When locked funds should keep earning for their depositor, the script address carries the depositor's stake credential, and staking needs no script logic at all: production lending pools stake idle liquidity this way, with the validator simply preserving the full address on every continuing output.
Double Satisfaction Protection
When using this pattern with multiple inputs/outputs, protect against double satisfaction attacks by:
- Tagging outputs - Include input OutRef in output datums
- Unique indexing - Use redeemer indices to pair inputs with outputs
- Filtering inputs - Validate only inputs from your script address
See UTxO Indexers for robust input/output pairing patterns.
Example Code
Full working example: stake-validator.ak
Library implementation: stake_validator module
When to Use
Use stake validators when:
- Processing multiple UTxOs in single transactions
- Validation logic is expensive (CPU/memory)
- You need transaction-level validation rather than per-UTxO validation
Registration state as global state
The withdraw zero trick uses a stake credential for its logic, but the credential carries a second, often overlooked property: its registration status. Whether a credential is currently registered is tracked on the account-based side of the ledger, outside the UTxO set. That makes it one bit of global state that any transaction can set, unset, or test:
- Registering the credential sets the bit. Registration fails if the credential is already registered, so a successful registration also proves the bit was unset.
- Deregistering the credential unsets the bit, and proves it was set.
- Registering and then deregistering in the same transaction tests that the bit is unset without leaving it set.
The interesting consequence is proving that an event has not happened. The usual proof-of-occurrence technique mints a token when an event occurs; anyone can later prove the event happened by referencing the UTxO holding that token. It cannot prove the opposite, because no validator can force another transaction to include that UTxO as a reference input. If the event instead requires registering a stake credential, absence becomes checkable: a transaction that registers the credential only succeeds while the event has not occurred.
Because this state lives on the account side of the ledger, reading or flipping it spends no UTxO. There is no contention: many transactions can interact with the same credential within one block without competing for an input.
Two caveats. The certificate operations that make this work are exactly the ones covered in Unconstrained Certificate Operations: an unguarded certificate path lets anyone flip the bit and claim the registration deposit. And while several credentials give several bits, treating them as an integer reintroduces the concurrency problems the technique avoids; the global state write-up in the design patterns repository demonstrates multi-bit counters but labels them a proof of concept.