Implementing Marlowe in Plutus
So far these tutorials have dealt with Marlowe as a 'stand alone' artifact; this tutorial describes how Marlowe is implemented on blockchain, using the 'mockchain' that provides a high-fidelity simulation of the Cardano SL layer.
Implementation
To implement Marlowe contracts we use the PlutusTx compiler, which compiles Haskell code into serialized Plutus Core code, to create a Cardano validator script that ensures the correct execution of the contract. This form of implementation relies on the extensions to the UTXO model that are described in this paper.
Marlowe contract execution on the blockchain consists of a chain of transactions where, at each stage, the remaining contract and its state are passed through the data script, and actions and inputs (i.e. choices and oracle values) are passed as redeemer scripts. Each step in contract execution is a transaction that spends a Marlowe contract script output by providing a valid input in a redeemer script, and produces a transaction output with a Marlowe contract as continuation (remaining contract) in addition to other inputs and outputs.
Design space
There are several ways to implement Marlowe contracts on top of Plutus. We could write a Marlowe to Plutus compiler that would convert each Marlowe contract into a specific Plutus script. Instead, we chose to implement an interpreter of Marlowe contracts. This approach has a number of advantages:
- It is simple: we implement a single Plutus script that can be used for all Marlowe contracts, thus making it easier to implement, review, and test what we have done.
- It is close to the semantics of Marlowe, as described in the earlier tutorial, so making it easier to validate.
- It means that the same implementation can be used for both on- and off-chain (wallet) execution of Marlowe code.
- It allows client-side contract evaluation, where we reuse the same code to do contract execution emulation (e.g. in IDE), and compile it to WASM/JavaScript on client side (e.g. in the Plutus or Marlowe Playground).
- Having a single interpreter for all (or a particular group of) Marlowe contracts allows to monitor the blockchain for these kinds of contract, if desired.
- Finally, there is a potential to special-case this sort of script, and implement a specialized, highly effective interpreter in Cardano CL itself.
In our implementation, we store the remaining contract in the data script (see Section 4), which makes it visible to everyone. This simplifies contract reflection and retrospection.
Contract lifecycle on extended UTXO model
The current implementation works on the mockchain, as described in TODO. We expect to have to make only minimal changes to run on the production implementation because the mockchain is designed to be faithful to that.
As we described above, the Marlowe interpreter is realised as a validation script. We can divide the execution of a Marlowe Contract into three phases: initialization/creation, execution and completion.
Initialization/Creation. Contract initialization and creation is realised as a transaction with at least one script output (currently it must be the first output), with the particular Marlowe contract in the data script, and protected by the Marlowe validator script. The transaction has to put some money (at least one Lovelace) on that transaction output, in order for it to become an unspent transaction output (UTXO). We consider this value a contract deposit, which can be spent during the completion phase. Note that we do not place any restriction on the transaction inputs, which could use any other transaction outputs, including scripts. It is possible to initialize a contract with a particular state, containing a number of commitments, as shown here.
Execution. Marlowe contract execution consists of a chain of transactions, where the remaining contract and state are passed through the data script, and actions and inputs (i.e. choices and oracle values) are passed as redeemer scripts.
Each step is a transaction that spends a Marlowe contract script output by providing a valid input in a redeemer script, and produces a transaction output with a Marlowe contract as continuation, as can be seen here.
The Marlowe interpreter first validates the current contract and state. That is, we check that the contract correctly uses identifiers, and holds at least what it should, namely the deposit and the outstanding commitments.
We then evaluate the continuation contract and its state, using the
eval
function,
eval :: Input -> Slot -> Ada -> Ada -> State -> Contract -> (State,Contract,Bool)
using the current slot number and at the same time checking that the
input makes sense and that payments are within committed bounds; if the
input is valid then it returns the new State
and Contract
and the
Boolean True
; otherwise it returns the current State
and Contract
,
unchanged, together with the value False
.
In a little more detail, in the type of eval
above, Input
is a
combination of contract participant actions (i.e. Commit
, Payment
,
Redeem
), oracle values, and choices made. The two Ada parameters are
the current contract value, and the result contract value. So, for
example, if the contract is to perform a 20 Ada Payment and the input
current amount is 100 Ada, then the result value will be 80 Ada. The
Contract
and State
values are the current contract and its State
,
respectively, taken from the data script.
In general, on-chain code cannot generate transaction outputs, but can
only validate whatever a user provides in a transaction. Every step in
contract evaluation is created by a user, either manually or
automatically (by a wallet, say), and published as a transaction. A user
may therefore provide any Contract
and its State
as continuation.
For example, consider the following contract
Commit id Alice 100 (Both (Pay Alice to Bob 30 Ada) (Pay Alice to Charlie 70 Ada))
Alice
commits 100 Ada and then both Bob
and Charlie
can collect 30
and 70 Ada each by issuing the relevant transaction. After Alice
has
made a commitment the contract becomes
Both (Pay Alice to Bob 30 Ada) (Pay Alice to Charlie 70 Ada)
Bob
can now issue a transaction with a Payment
input in the redeemer
script, and a script output with 30 Ada less value, protected by the
Marlowe validator script and with data script containing the evaluated
continuation contract
Pay Alice to Charlie 70 Ada
Charlie
can then issue a similar transaction to receive remaining 70
Ada.
Ensuring execution validity. Looking again at this example, suppose
instead that Bob
chooses, maliciously, to issue a transaction with the
following continuation:
Pay Alice to Bob 70 Ada
and take all the money, as in here, making Charlie reasonably disappointed with all those smart contracts.
To avoid this we must ensure that the continuation contract we evaluate is equal to the one in the data script of its transaction output.
This is the tricky part of the implementation, because we only have the hash of the data script of transaction outputs available during validator script execution. If we were able to access the data script directly, we could simply check that the expected contract was equal to the contract provided. But that would further complicate things, because we would need to know types of all data scripts in a transaction, which is not possible in general.
The trick is to require the input redeemer script
and the
output data script
to be equal. Both the redeemer script and the data
script have the same structure, namely a pair (Input, MarloweData)
where
- The Input contains contract actions (i.e.
Payment
,Redeem
),Choices
andOracle
values. MarloweData
contains the remainingContract
and itsState
.- The
State
here is a set ofCommits
plus a set ofChoices
made.
To spend a transaction output secured by the Marlowe validator script, a
user must provide a redeemer script, which is a tuple of an Input
and
the expected output of interpreting a Marlowe contract for the given
Input
, i.e. a Contract
, State
pair. The expected contract and
state can be precisely evaluated beforehand using eval
function.
To ensure that the user provides valid remaining Contract
and State
,
the Marlowe validator script will compare the evaluated contract and
state with those provided by the user, and will reject a transaction if
those do not match. To ensure that the remaining contract's data script
has the same Contract
and State
as was passed with the redeemer
script, we check that data script hash is the same as that of the
redeemer script.
Completion. When a contract evaluates to Null
, and all expired
Commits
are redeemed, the initial contract deposit can be spent,
removing the contract from the set of unspent transaction outputs.
Exercise
Advanced. Explore running Marlowe contracts in Plutus. In order to be able to do this you will need to use the latest version of Marlowe, rather than
v1.3
.