Overview
A Scalus validator is an object that extends the Validator trait, annotated with @Compile to compile your Scala code to Plutus Core bytecode.
When defining a contract, you can define logic for 6 distinct purposes by overriding a corresponding method. Each method corresponds to a respective Plutus Redeemer Tag type:
spendmintrewardcertifyvotepropose
A typical contract defines these functions, often just one. A successful contract returns
Unit, while errors are indicated by throwing exceptions (we’ll explore this in more detail below).
Sample Validator
Let’s take a look at an example contract, HTLCValidator, to illustrate the concepts above.
import scalus.*
import scalus.compiler.compile
import scalus.builtin.Builtins.sha3_256
import scalus.builtin.Data.{FromData, ToData}
import scalus.builtin.{ByteString, Data, FromData, ToData}
import scalus.cardano.blueprint.{Application, Blueprint}
import scalus.ledger.api.v3.*
import scalus.prelude.*
import scalus.uplc.Program
type Preimage = ByteString
type Image = ByteString
type PubKeyHash = ByteString
// Contract Datum
case class State(
committer: PubKeyHash,
receiver: PubKeyHash,
image: Image,
timeout: PosixTime
) derives FromData, ToData
// Redeemer
enum Action derives FromData, ToData:
case Timeout
case Reveal(preimage: Preimage)
@Compile
object HtlcValidator extends Validator:
/*
* Spending script purpose validation
*/
override def spend(datum: Option[Data], redeemer: Data, tx: TxInfo, ownRef: TxOutRef): Unit = {
val State(committer, receiver, image, timeout) =
datum.map(_.to[State]).getOrFail(InvalidDatum)
redeemer.to[Action] match
case Action.Timeout =>
require(tx.isSignedBy(committer), UnsignedCommitterTransaction)
require(tx.validRange.isAfter(timeout), InvalidCommitterTimePoint)
case Action.Reveal(preimage) =>
require(tx.isSignedBy(receiver), UnsignedReceiverTransaction)
require(!tx.validRange.isAfter(timeout), InvalidReceiverTimePoint)
require(sha3_256(preimage) === image, InvalidReceiverPreimage)
}
// Error messages
inline val InvalidDatum =
"Datum must be a HtlcValidator.ContractDatum(committer, receiver, image, timeout)"
inline val UnsignedCommitterTransaction = "Transaction must be signed by a committer"
inline val UnsignedReceiverTransaction = "Transaction must be signed by a receiver"
inline val InvalidCommitterTimePoint = "Committer Transaction must be exclusively after timeout"
inline val InvalidReceiverTimePoint = "Receiver Transaction must be inclusively before timeout"
inline val InvalidReceiverPreimage = "Invalid receiver preimage"
end HtlcValidatorImport section and type definitions
At the very top of the file, you can see imports, which are present in most Scala programs. Then, we declare type aliases, which enhance our types with semantics relevant to the contract.
After that, we define the Datum and Redeemer types. Derives syntax allows us to convert the Data values from
the script context into State and Action types to utilize the Scala type system.
Validator code
In the body of the object HtlcValidator, where object is a Scala keyword that defines a singleton instance,
we can see the override spend declaration, which contains the logic of the contract.
Decoding
First, we turn the redeemer Data instance into the type that we’re going to be working with: Action. This is available
thanks to the FromData that we derived automatically. Scalus can automatically derive FromData instances for most
types that you’re going to use for Datums and Redeemers.
The behavior of the validator then branches based on the redeemer type. To implement the branching, we use pattern matching.
Pattern matching
Pattern matching is a Scala language feature that, for enumerable types with known variants, such as Action,
allows to handle every possible option. In this case, it’s Timeout and Reveal.
require()
For each of the two redeemers, we define the requirements necessary for the Spend transaction to be successful.
If any of the required conditions don’t hold true, the validator execution ends with an error. The list of all errors
is enumerated at the bottom of the contract, each containing a message describing why the spending was forbidden.
require is an inline function, meaning that the compiler will not generate a method call and instead insert
the body of require directly into validator body.
Summary
Each validator is, in essence, a boolean function — it either allows performing the desired action (e.g., spending the script-locked funds) or forbids it.
Thus, the logic is just a sequence of binary checks.
In Scalus, this usually means a series of require calls, where the first parameter is the boolean invariant to check,
and the second parameter is the error that is thrown if the condition is false.
In the HtlcValidator, you can see this clearly: the timeout is only valid if the initiating transaction is signed by the
correct key and the necessary amount of time has passed.
This is neatly expressed in 2 lines of Scala code:
require(tx.isSignedBy(committer), UnsignedCommitterTransaction)
require(tx.validRange.isEntirelyAfter(timeout), InvalidCommitterTimePoint)