Simple scripts
Cardano is designed to support multiple script languages, and most features that are related to scripts work the same irrespective of the script language (or version of a script language).
Since Shelley era, Cardano supports the Simple script language, which can be used for multi-signature addresses.
The Allegra era extends the simple script language with a feature to make scripts conditional on time (token locking). This can be used to make address with "time locks", where the funds cannot be withdrawn from a script address until after a certain point in time.
The Alonzo era brought support for Plutus scripts, these are out of the scope of this section.
Script addresses
In general, addresses (both payment addresses and stake addresses) specify the authorisation conditions that must be met for the address to be used. For payment addresses, this means the authorisation conditions for funds to be withdrawn. For stake addresses, this means the authorisation conditions for setting a delegation choice or rewards withdrawal.
Both payment and stake addresses come in two flavours: single-key based or script based. The key-based addresses use a single cryptographic key per address. The authorisation condition to use the address is that one holds the secret (signing) part of the cryptographic key (for that address) and thus be able to make a cryptographic signature for that key.
The script-based addresses use a script per address. The authorisation condition to use the address is that the evaluation of the script for the address results in success. The script expresses the authorisation conditions and evaluation of the script tests if those conditions are met. For example, a very simple script might express the condition that one holds a particular cryptographic signing key. The script would express that condition by testing if the transaction has a cryptographic signature from the appropriate cryptographic verification key. Such a script would, of course, be exactly equivalent to an ordinary single-key based address: the authorisation conditions would be the same! Thus, we can see that single-key based addresses are in some sense just a (very common) special case and that script addresses are the general case.
When using an address (payment or stake) in a transaction, the transaction must contain the information needed to show that the authorisation conditions for the use of the address are met. This information is known as a transaction witness. We say that it witnesses the validity of the transaction using the address. The addresses themselves have a credential which is information sufficient to check that a witness is the right witness.
Specifically, there are two types of such credentials, for key and script addresses:
-
Key credential - a key credential is constructed using a verification key (vk) (which has corresponding signing key (sk)). The credential is the cryptographic hash of the verification key H(vk).
The transaction witness for a key credential consists of the verification key vk and the signature of transaction body hash using the signing key sk.
-
Script credential - a script credential is the hash of the script.
The transaction witness for a script credential is the script itself. There are no other inputs for the very simple script language introduced in the Shelley era. Scripts in the Plutus language (once that is available) will require additional inputs which will also form part of the witness.
Multi-signature scripts
In Shelley and later eras, multisig scripts are used to make script addresses where the authorisation condition for a transaction to use that address is that the transaction has signatures from multiple cryptographic keys. Examples include M of N schemes, where a transaction can be authorized if at least M distinct keys, from a set of N keys, sign the transaction.
As with all scripts, the transaction witness for a multisig script address includes the script itself. The multisig language is so simple that this is the entire witness: there are no other script data inputs. Although the script itself is the witness, the script has conditions that must be satisfied. The conditions for multisig scripts are that the transaction has other ordinary key witnesses. Which combination of key witnesses are acceptable is, of course, determined by the script.
The multisig script language is an expression language. Its scripts form an
expression tree. The evaluation of the script produces either true
or false
.
In BNF notation, the script expressions follow the following abstract syntax:
<script> ::= <RequireSignature> <vkeyhash>
| <RequireAllOf> <script>*
| <RequireAnyOf> <script>*
| <RequireMOf> <num> <script>*
Note that it is recursive. There are no constraints on the nesting or size, except that imposed by the overall transaction size limit (given that the script must be included in the transaction in a script witnesses).
In more detail, the four multisig constructors are:
-
RequireSignature: has the hash of a verification key.
This expression evaluates to
true
if (and only if) the transaction also includes a valid key witness where the witness verification key hashes to the given hash. In other words, this checks that the transaction is signed by a particular key, identified by its verification key hash. -
RequireAllOf: has a list of multisig sub-expressions.
This expression evaluates to
true
if (and only if) all the sub-expressions evaluate totrue
. Following standard mathematical convention, the degenerate case of an empty list evaluates totrue
. -
RequireAnyOf: has a list of multisig sub-expressions.
This expression evaluates to
true
if (and only if) any the sub-expressions evaluate totrue
. That is, if one or more evaluate totrue
. The degenerate case of an empty list evaluates tofalse
. -
RequireMOf: has a number M and a list of multisig sub-expressions.
This expression evaluates to
true
if (and only if) at least M of the sub-expressions evaluate totrue
.
Time locking
In the Allegra and later eras, the simple script language above is extended with two additional terms for expressing conditions on the time.
This extension can be used to make script addresses where it is not possible to spend from them until after a certain time. Or, in combination with the existing features, one could make a script where up until a certain time one group of keys (perhaps M-of-N) is allowed to spend, but after a time another group of keys is allowed instead.
On the Cardano blockchain, time is expressed via slot numbers, since time slots correspond to time. The extra constructors allow expressing that the current time slot number must be before a certain slot, or after a certain slot.
The BNF notation for the abstract syntax is:
<script> ::= <RequireSignature> <vkeyhash>
| <RequireTimeBefore> <slotno>
| <RequireTimeAfter> <slotno>
| <RequireAllOf> <script>*
| <RequireAnyOf> <script>*
| <RequireMOf> <num> <script>*
The interpretation of "before" and "after" is somewhat subtle, and interacts with another transaction feature: transaction validity intervals.
In the Allegra and later eras transactions have validity intervals. This is the range of slots in which they are valid; the range of slots in which they may be included into blocks on the blockchain. The transaction can specify the first slot in which the transaction can become valid, and the first slot in which the transaction becomes invalid. This is a "half-open" interval: the lower bound is inclusive and the upper bound is exclusive. Both the lower and the upper ends of this interval are optional, in which case the interpretation is zero or positive infinity respectively.
With this in mind, we can understand the interpretation of the new expressions:
-
RequireTimeBefore: has a slot number X.
This expression evaluates to
true
if (and only if) the upper bound of the transaction validity interval is a slot number Y, and Y < X.This condition guarantees that the actual slot number in which the transaction is included is (strictly) less than slot number X.
-
RequireTimeAfter: has a slot number X.
This expression evaluates to
true
if (and only if) the lower bound of the transaction validity interval is a slot number Y, and Y >= X.This condition guarantees that the actual slot number in which the transaction is included is greater than or equal to slot number X.
One might reasonably wonder why use this two-stage check via the validity interval rather than more straightforwardly check the actual slot time. The reason is to give deterministic script evaluation, which becomes crucial for Plutus scripts. In this context, by deterministic we mean that the result of the script evaluation depends only on the transaction itself and not any other context or state of the system. This property is less crucial for this simple script language, but it is better if all the languages behave the same in this regard. Even for the simple script language it does still have the advantage that the script itself can be evaluated without needing to know the current slot number.
JSON script syntax
Multi-signature scripts can be written using JSON syntax. This is the format
that the cardano-cli
tool accepts.
There is a JSON form corresponding to each of the six constructors described above. Of course, for the Shelley era, only the basic four are available.
Type "sig"
This corresponds to the "RequireSignature" expression above. It specifies the type "sig" and the key hash in hex.
{
"type": "sig",
"keyHash": "e09d36c79dec9bd1b3d9e152247701cd0bb860b5ebfd1de8abb6735a"
}
Type "all"
This corresponds to the "RequireAllOf" expression above. It specifies the type "all" and a list of scripts as the sub-expressions.
This example requires signatures from all of three keys, but remember that the sub-expressions could be any script type, not just type "sig".
{
"type": "all",
"scripts":
[
{
"type": "sig",
"keyHash": "e09d36c79dec9bd1b3d9e152247701cd0bb860b5ebfd1de8abb6735a"
},
{
"type": "sig",
"keyHash": "a687dcc24e00dd3caafbeb5e68f97ca8ef269cb6fe971345eb951756"
},
{
"type": "sig",
"keyHash": "0bd1d702b2e6188fe0857a6dc7ffb0675229bab58c86638ffa87ed6d"
},
]
}
Type "any"
This corresponds to the "RequireAnyOf" expression above. It specifies the type "any" and a list of scripts as the sub-expressions.
This example requires signatures from any of three keys, but remember that the sub-expressions could be any script type, not just type "sig".
{
"type": "any",
"scripts":
[
{
"type": "sig",
"keyHash": "d92b712d1882c3b0f75b6f677e0b2cbef4fbc8b8121bb9dde324ff09"
},
{
"type": "sig",
"keyHash": "4d780ed1bfc88cbd4da3f48de91fe728c3530d662564bf5a284b5321"
},
{
"type": "sig",
"keyHash": "3a94d6d4e786a3f5d439939cafc0536f6abc324fb8404084d6034bf8"
}
]
}
Type "atLeast"
This corresponds to the "RequireMOf" expression above. It specifies the type "atLeast", the required number and a list of scripts as the sub-expressions.
This example requires signatures from two of three keys, but remember that the sub-expressions could be any script type, not just type "sig".
{
"type": "atLeast",
"required": 2,
"scripts":
[
{
"type": "sig",
"keyHash": "2f3d4cf10d0471a1db9f2d2907de867968c27bca6272f062cd1c2413"
},
{
"type": "sig",
"keyHash": "f856c0c5839bab22673747d53f1ae9eed84afafb085f086e8e988614"
},
{
"type": "sig",
"keyHash": "b275b08c999097247f7c17e77007c7010cd19f20cc086ad99d398538"
},
]
}
Type "after"
This corresponds to the "RequireTimeAfter" expression above. It specifies the type "after" and the slot number.
This example requires a signature from a single key and requires that the time be after slot 1000.
{
"type": "all",
"scripts":
[
{
"type": "after",
"slot": 1000
},
{
"type": "sig",
"keyHash": "966e394a544f242081e41d1965137b1bb412ac230d40ed5407821c37"
}
]
}
Note that transactions spending from scripts that use type after
must provide
the lower bound for the transaction validity interval. See below for examples of
how to do this using the cardano-cli
.
Type "before"
This corresponds to the "RequireTimeBefore" expression above. It specifies the type "before" and the slot number.
This example allows a signature from one key at any time, or before slot 3000 it also allows a signature from a second key. That is, up to slot 3000 either key is acceptable, but at slot 3000 and thereafter only one key is acceptable.
{
"type": "any",
"scripts":
[
{
"type": "sig",
"keyHash": "b275b08c999097247f7c17e77007c7010cd19f20cc086ad99d398538"
},
{
"type": "all",
"scripts":
[
{
"type": "before",
"slot": 3000
},
{
"type": "sig",
"keyHash": "966e394a544f242081e41d1965137b1bb412ac230d40ed5407821c37"
}
]
}
]
}
Note that transactions spending from scripts that use type before
must provide
the lower bound for the transaction validity interval. See below for examples of
how to do this using the cardano-cli
.
Example of using a script for multi-signatures
Below is an example that shows how to use a script. This is a step-by-step process involving:
- the creation of a script address
- sending ada to that address
- gathering required witnesses in order to spend ada from the script address.
The example is based on using an all
script.
Sending ada to a script address
Step 1 - create a script
First, generate the keys that you require witnesses from using the
cardano-cli address key-gen
command. Then, construct a script in JSON syntax
as described above. For this example, we will describe the process using an
all
multisig script (allMultiSigScript.json
) as follows:
{
"type": "all",
"scripts":
[
{
"type": "sig",
"keyHash": "e09d36c79dec9bd1b3d9e152247701cd0bb860b5ebfd1de8abb6735a"
},
{
"type": "sig",
"keyHash": "a687dcc24e00dd3caafbeb5e68f97ca8ef269cb6fe971345eb951756"
},
{
"type": "sig",
"keyHash": "0bd1d702b2e6188fe0857a6dc7ffb0675229bab58c86638ffa87ed6d"
}
]
}
Step 2 - create a script address
A script address is required in order to use a script. Construct this as follows:
cardano-cli address
--payment-script-file allMultiSigScript
--out-file script.addr
Step 3 - construct and submit a transaction (tx) to the script address
To construct and submit a tx to send ada to the script address, construct the transaction body:
cardano-cli transaction build-raw
--invalid-hereafter 1000
--fee 0
--tx-in utxoinput
--tx-out "$(< script.addr) ${amount}"
--out-file txbody
Where amount
is a shell variable you have defined, or replace it with a
literal value.
Create the transaction witness:
cardano-cli transaction witness
--tx-body-file txbody
--signing-key-file utxoSignKey
--out-file utxoWitness
Assemble the transaction witness and the tx body to create the transaction:
cardano-cli transaction assemble
--tx-body-file txbody
--witness-file utxoWitness
--out-file allWitnessesTx
After submitting the above tx, the inputs associated with the script address will be "guarded" by the script.
Sending ada from a script address
Step 1 - construct the tx body
cardano-cli transaction build-raw \
--invalid-hereafter 1000 \
--fee 0 \
--tx-in (txin of script address)
--tx-out yourspecifiedtxout \
--out-file spendScriptTxBody
Step 2 - construct required witnesses
To construct the script witness and three key witnesses required by the example
all
script, run the following commands:
cardano-cli transaction witness \
--tx-body-file spendScriptTxBody \
--script-file allMultiSigScript \
--out-file scriptWitness
cardano-cli transaction witness \
--tx-body-file spendScriptTxBody \
--signing-key-file paySignKey1 \
--out-file key1witness
cardano-cli transaction witness \
--tx-body-file spendScriptTxBody \
--signing-key-file paySignKey2 \
--out-file key2witness
cardano-cli transaction witness \
--tx-body-file spendScriptTxBody \
--signing-key-file paySignKey3 \
--out-file key3witness
Step 3 - construct and submit the transaction
To construct and submit a transaction, you must assemble it with the script witness and all the other required key witnesses.
cardano-cli transaction assemble \
--tx-body-file spendScriptTxBody \
--witness-file scriptWitness \
--witness-file key1witness \
--witness-file key2witness \
--witness-file key3witness \
--out-file spendMultiSig
You can now submit this tx via cardano-cli transaction submit
!
Example of using a script for time locking
In this example we will use a script that allows spending from an address with a particular key, but only after a certain time.
It is important to understand the transaction validity interval and how that relates to the script: make sure you have read the time locking section above.
Previously in Shelley we had a --invalid-hereafter
flag in the cli which was an upper bound
on when a transaction can be valid i.e the transaction was valid up until that
slot number. This has been replaced with the --invalid-before
and
--invalid-hereafter
flags as described below.
If you specify only a lower bound on a transaction, with --invalid-before=X
,
it is valid in slot X and thereafter. The valid slot interval is:
[X, infinity)
If you specify an upper bound on a transaction, with --invalid-hereafter=X
,
it is valid up to but not including slot X. The valid slot interval is:
[0, X)
If you specify both bounds, with --invalid-before=X --invalid-hereafter=Y
,
then the tx is valid in slot X up to but not including slot Y. The valid slot
interval is:
[X, Y)
Whatever bounds you specify, the slot number in which you submit your
transaction has to be within those bounds. For example if you specify
--invalid-before=600
but you submit your transaction in slot 500 then it will
be rejected.
This interacts with time locking scripts. If you specify a script with
"after": 1000
then you must specify a --invalid-before
of at least 1000
and therefore also submit that transaction in or after slot 1000.
Conversely, if you specify a time lock script with "before": 500
you must
specify an --invalid-hereafter
slot of at most 500 and submit the transaction
before slot 500. Remember the upper bound is exclusive: before: 500
means the
transaction is expired in slot 500.
Once you have generated your time lock script you need to follow all the same steps as above in the multi-signature script example but with a slight modification of step 1 which needs to be amended as follows:
Example time locking script file:
{
"type": "all",
"scripts":
[
{
"type": "after",
"slot": 1000
},
{
"type": "sig",
"keyHash": "966e394a544f242081e41d1965137b1bb412ac230d40ed5407821c37"
}
]
}
For after
scripts we must provide a --invalid-before
slot that is greater
than or equal to the specified slot number in our simple script. In the example
above this means >= 1000.
cardano-cli transaction build-raw \
--invalid-before 1000 \
--fee 0 \
--tx-in (txin of script address)
--tx-out yourspecifiedtxout \
--out-file spendScriptTxBody
A simple example for a before-time script is:
{
"scripts": [
{
"keyHash": "966e394a544f242081e41d1965137b1bb412ac230d40ed5407821c37",
"type": "sig"
},
{
"slot": 3000,
"type": "before"
}
],
"type": "all"
}
Note that this is not really time locking in the normal sense, and indeed this is a very dangerous script to use because any funds left in a script address using this script after time slot 3000 will be locked there permanently!
For before scripts we must provide a --invalid-hereafter
slot that is less
than or equal to the specified slot number in our simple script. In the example
above this means <= 3000
:
cardano-cli transaction build-raw \
--invalid-hereafter 3000\
--fee 0 \
--tx-in (txin of script address)
--tx-out yourspecifiedtxout \
--out-file spendScriptTxBody