Validators

Validators

Handlers

In Aiken, you can promote some functions to validator handlers using the keyword validator.

use cardano/assets.{PolicyId}
use cardano/transaction.{Transaction}
 
validator my_script {
  mint(redeemer: MyRedeemer, policy_id: PolicyId, self: Transaction) {
    todo @"validator logic goes here"
  }
}

As you can see, a validator is a named block that contains one or more handlers. The handler name must match Cardano's well-known purposes:

  • mint, spend, withdraw, publish, vote or propose.

Each handler is a predicate function: they must return True or False. When True, they authorize the action they are validating. Alternatively to returning false, they can also halt using the fail keyword or an invalid expect assignment.

With the exception of the spend handler, each handler is a function with exactly three arguments:

  • A redeemer, which is a user-defined type and value.
  • A target, whose type depends on the purpose.
  • A transaction which represents the script execution context.

The spend handler takes an additional first argument which is an optional datum, also a user-defined type. More on that in the section below.

Datum? Redeemer? If you aren't yet familiar with the eUTxO model yet, we recommend reading through the eUTxO Crash Course.

In fact, handlers acts similarly to a pattern-matching on a ScriptContext (opens in a new tab); pulling out elements for you while also ensuring safety around your validator boundaries. For convenience, here's a table that summarizes the different target types with their corresponding definitions from the standard library:

PurposeTargetWhat for
mintPolicyId (opens in a new tab)minting / burning of assets
spendOutputReference (opens in a new tab)spending of transaction outputs
withdrawCredential (opens in a new tab)withdrawing staking rewards
publishCertificate (opens in a new tab)publishing of delegation certificates
voteVoter (opens in a new tab)voting on governance proposals
proposeProposalProcedure (opens in a new tab)Constitution guardrails, executed when submitting governance proposals

Or, seen in action:

use cardano/address.{Credential}
use cardano/assets.{PolicyId}
use cardano/certificate.{Certificate}
use cardano/governance.{ProposalProcedure, Voter}
use cardano/transaction.{Transaction, OutputReference}
 
validator my_script {
  mint(redeemer: MyMintRedeemer, policy_id: PolicyId, self: Transaction) {
    todo @"mint logic goes here"
  }
 
  spend(datum: Option<MyDatum>, redeemer: MySpendRedeemer, utxo: OutputReference, self: Transaction) {
    todo @"spend logic goes here"
  }
 
  withdraw(redeemer: MyWithdrawRedeemer, account: Credential, self: Transaction) {
    todo @"withdraw logic goes here"
  }
 
  publish(redeemer: MyPublishRedeemer, certificate: Certificate, self: Transaction) {
    todo @"publish logic goes here"
  }
 
  vote(redeemer: MyVoteRedeemer, voter: Voter, self: Transaction) {
    todo @"vote logic goes here"
  }
 
  propose(redeemer: MyProposeRedeemer, proposal: ProposalProcedure, self: Transaction) {
    todo @"propose logic goes here"
  }
}

Notice how every handler can take a different redeemer type and all take a Transaction as last argument.

Managing (Optional) Datum

Spend handlers contain an extra argument: the (optional) datum which may be set with the output when assets get initially locked. Because there's no ways to enforce that the datum is present (you cannot prevent one to send/lock assets into your validator), it always come as an Option<T> where T is a user-defined type that depends on the contract.

Nevertheless, should your contract require a datum to be present, then it is straightforward to enforce this constraint using expect and halt the execution of the validator when the datum is missing.

use cardano/transaction.{Transaction, OutputReference}
 
validator my_script {
  spend(datum_opt: Option<MyDatum>, redeemer: MyRedeemer, input: OutputReference, self: Transaction) {
    expect Some(datum) = datum_opt
    todo @"validator logic goes here"
  }
}

Fallback handler

The keen reader would have noticed that the example validators above are non-exhaustive and only cover one of the six purposes. It may be cumbersome to always define a handler for all purposes, especially if your application isn't expected to work in those contexts.

A special handler can thus serve as a fallback / catch-all with one notable difference: the fallback handler takes a single argument of type ScriptContext (opens in a new tab). It is then your responsibility as a smart contract developer to assert the script purposes and recover your redeemer and/or datum.

use cardano/assets.{PolicyId}
use cardano/transaction.{Transaction, OutputReference}
use cardano/script_context.{ScriptContext}
 
validator my_multi_purpose_script {
  mint(redeemer: MyRedeemer, policy_id: PolicyId, self: Transaction) {
    todo @"validator logic goes here"
  }
 
  spend(datum_opt: Option<MyDatum>, redeemer: MyRedeemer, input: OutputReference, self: Transaction) {
    expect Some(datum) = datum_opt
    todo @"validator logic goes here"
  }
 
  else(_ctx: ScriptContext) {
    fail @"unsupported purpose"
  }
}

There are also scenarios where you might not want the granularity and guardrails offered by Aiken. A typical use-case for example is writing validator on an layer-2 system that uses the Plutus Virtual Machine (e.g. Hydra) but may have different purposes and/or script context.

The fallback handler comes in handy for those situation and allows you to define and use arbitrary script contexts to match any environment. In the long run, a dedicated syntax might be introduced to declare handlers based on some type definition.

Default fallback

When no fallback is explicitly specified, Aiken defaults to a validator that is always rejecting.

validator my_script {
  else(_) {
    fail
  }
}

Parameters

Validators themselves can take parameters, which represent configuration elements that must be provided to create an instance of the validator. Once provided, parameters are embedded within the compiled validator and part of the generated code. Hence they must be provided before any address can be calculated for the corresponding validator.

Parameters are accessible to all handlers from within the validator and can be any serialisable (non-opaque) data-type. For example, it is common to ensure uniqueness of

validators/my_script.ak
use aiken/collection/list
use cardano/assets.{PolicyId}
use cardano/transaction.{Transactions, OutputReference}
 
validator my_script(utxo_ref: OutputReference) {
  mint(redeemer: Data, policy_id: Policy_id, self: Transaction) {
    expect list.any(
      self.inputs,
      fn (input) { input.output_reference == utxo_ref }
    )
    todo @"rest of the logic goes here"
  }
}

Calling handlers

Handlers are your smart contract interface with the rest of the world. However, there are situations where you may want to invoke a handler as a standalone function (e.g. for testing).

Aiken provides a convenient syntax for syntax akin to calling functions from a module:

{validator_name}.{handler_name}

The result is a function that takes the same arguments as the handler, prepended with any parameter from the validator. For example:

test return_true_when_utxo_ref_match() {
  let utxo_ref = todo @"OutputReference"
  let redeemer = todo @"Redeemer"
  let policy_id = todo @"OutputReference"
  let transaction = todo @"Transaction"
  my_script.mint(utxo_ref, redeemer, policy_id, transaction)
}

Importing validators

Should you need to split tests in a different module, Aiken allows the import of validators with a few restrictions:

  1. Only test modules can import validators. A module is considered a test module if it doesn't export any public definition.
  2. Validators cannot be imported as unqualified objects. They must be used as qualified imports.

So, assuming the validator above, we could imagine writing a test module like:

use my_script
 
test return_true_when_utxo_ref_match() {
  let utxo_ref = todo @"OutputReference"
  let redeemer = todo @"Redeemer"
  let policy_id = todo @"OutputReference"
  let transaction = todo @"Transaction"
  my_script.my_script.mint(utxo_ref, redeemer, policy_id, transaction)
}