Stateful Contracts
sCrypt contracts use the Unspent Transaction Output (UTXO) model: contracts reside within a UTXO and define how the Bitcoin within the UTXO can be spent. When a UTXO is spent (i.e., a public function of the sCrypt contract is successfully called), the contract is terminated. To maintain the contract's state and allow it to be called multiple times with variable states, follow these steps.
State Modifiers
Use the modifier @state
to declare any attribute as part of the state. State attributes can be used like regular
attributes.
contract Counter {
@state
int counter;
constructor(int counter) {
this.counter = counter;
}
}
Updating State
By storing the state in the locking script, the contract can maintain state across chained transactions. In the following example, the contract transitions from state0 to state1, and then to state2. Transaction tx1's input spends the UTXO from tx0, and tx2 spends tx1.
Maintaining State
When you are ready to pass the new state to the next UTXO, simply call the built-in function this.updateState()
with
two parameters:
txPreimage
: The preimage of the current spending transaction. It must have a single output that contains the new state.amount
: The number of satoshis in that single output.
This function is automatically generated for every stateful contract (i.e., a contract with at least one property
decorated with @state
). If you need to customize the sighash type (different from the
default SigHash.ALL | SigHash.FORKID
), you can use updateStateSigHashType
.
Here is an example contract that records the number of increment()
calls:
contract Counter {
@state
int counter;
public function increment(SigHashPreimage txPreimage, int amount) {
// Change state
this.counter++;
require(this.updateState(txPreimage, amount));
// Custom sighash type
// require(this.updateStateSigHashType(txPreimage, amount, SigHash.SINGLE | SigHash.FORKID));
}
}
Advanced
If you need more granular control over the state, such as having multiple outputs in the spending transaction, you can
call another built-in function this.getStateScript()
to get the locking script containing the latest state properties.
Next, use OP_PUSH_TX
to ensure the output containing the state is included in the current spending transaction. This
is equivalent to using this.updateState()
in the contract above.
contract Counter {
@state
int counter;
public function increment(SigHashPreimage txPreimage, int amount) {
// Change state
this.counter++;
require(Tx.checkPreimage(txPreimage));
// Get the locking script containing the latest state properties
bytes outputScript = this.getStateScript();
// Construct an output from the locking script and the satoshi amount
bytes output = Utils.buildOutput(outputScript, amount);
// Use only one input here
require(hash256(output) == SigHash.hashOutputs(txPreimage));
}
}
Limitations
For any public function accessing stateful properties, a SigHashPreimage parameter verified by Tx.checkPreimage()
must
be included, using OP_PUSH_TX
. This does not apply to any non-public functions, including constructors.
contract Counter {
@state
int counter;
constructor(int counter) {
// Allowed: Non-public function
this.counter = counter;
}
public function increment(SigHashPreimage txPreimage, int amount) {
// Allowed
this.counter++;
require(Tx.checkPreimage(txPreimage));
}
public function foo(SigHashPreimage txPreimage, int amount) {
require(Tx.checkPreimageOpt(txPreimage));
// Allowed
this.counter++;
require(true);
}
public function bar(SigHashPreimage txPreimage) {
// Not allowed: Missing Tx.checkPreimage*()
this.counter++;
require(true);
}
public function baz(int i) {
// Not allowed: Missing SigHashPreimage
this.counter++;
require(true);
}
function baz() : int {
// Allowed: Non-public function
return this.counter;
}
}