State Transition Functions
State transitions are the functions that move the state machine from one state to another. These contain the logic of the application and determine how the state machine will move from one state to another based on the inputs.
Transition Functions in Stackr
Stackr provides a toolkit to create state transition functions.
A basic transition function in Stackr looks like this:
import { SolidityType, Transitions } from "@stackr/sdk/machine";
import { CounterState } from "./state";
// Note that you should declare schema "as const", if not defining them in place.
// This prevents the types not being inferred correctly.
const schema = {
timestamp: SolidityType.UINT,
} as const;
const increment = CounterState.STF({
schema,
handler: ({ state }) => {
state += 1;
return state;
},
});
const decrement = CounterState.STF({
schema,
handler: ({ state }) => {
state -= 1;
return state;
},
});
export const transitions: Transitions<CounterState> = {
increment,
decrement,
};
In the above example, we have two transition functions increment
and decrement
. These functions take the current state as input and return the new state after applying the logic.
Transition Functions in Detail
The most important part of the transition function is the handler
function. This function takes the current state and any other input as arguments and returns the new state. There are several inputs that handler receives which are discussed in the next section. However the most important thing to note here is that the handler function should be a pure function. It should not have any side effects and should only depend on the inputs.
Mutating the State
The state is passed as a reference to the handler function. This means that you can directly mutate the state inside the handler function. Once the operations on the state are complete, you must return the new state otherwise the state machine will not apply the changes to the state
STF is a pure function
The transition function is a pure function. This means that it should not have any side effects and should only depend on the inputs. This is important because the state is verified by Vulcan after the block is sent to it. Vulcan can only run the STF inside a sandboxed environment and cannot run any async operations or access the network or perform any side effects.
Passing Inputs to an STF
Each STF must have a schema
defined which represents the type definition for the user inputs accepted by the transition function. These are then available via the inputs
argument on the handler.
const increment = CounterState.STF({
schema: {
amount: SolidityType.UINT,
},
handler: ({ state, inputs }) => {
state += inputs.amount;
return state;
},
});
The schema is a key-value object where key is the field name and value being the Solidity type to use for encoding the field's value. The supported types are:
address
bool
bytes
bytes32
string
uint256
The solidity types can be accessed using the SolidityType
enum provided with the SDK.
import { SolidityType } from "@stackr/sdk/machine";
const schema = {
"<INPUT_FIELD_NAME>": SolidityType.ADDRESSBOOLBYTESBYTES32STRINGUINT
} as const;
The reason to use Solidity types here is so the SDK can generate equivalent EIP-712 types for signing purposes.
Examples
Variables available inside an STF
There are 6 special variables which always exist inside the handler of the State transition function. These can be used by the developer to write the logic of the state transition function:
state
msgSender
inputs
signature
block
block.height
block.timestamp
block.parentHash
emit
// Without Destructuring
const superCoolStf = CounterState.STF({
schema,
handler: (props) => {
const state = props.blockemitinputsmsgSendersignaturestate
const height = props.block.heightparentHashtimestamp
},
});
// With Destructuring
const anotherSuperCoolStf = CounterState.STF({
schema,
handler: ({ ssignaturestate }) => {
},
});
State
state
: The entire current state of the state machine in wrapped format. This is the state that the state transition function is supposed to modify.
The STF receives a cloned version of the state, and the state transition function must return the modified state to update the state of the state machine.
Action Data
When an action is dispatched, the inputs are passed to the state transition function. The inputs are the parameters passed to the action schema.
-
msgSender
: The address of the sender of the transaction. -
inputs
: The inputs passed as per the schema. -
signature
: The signature of the msgSender on the inputs.
In the current version of the SDK, the msgSender
is the same as the account that signs the input payload. In future iterations someone could submit the payload on behalf of another account which signed the payload.
Block Properties
A block
object containing the current block height, timestamp and the parent block's hash is passed as an argument to the state transition function. These can be used to generate pseudo-random numbers or to implement time-based logic.
-
block.height
: The height of the block in which the transaction is included. -
block.timestamp
: The timestamp of the block in which the transaction is included. -
block.parentHash
: The hash of the parent block of the block in which the transaction is included.
Custom Execution Logs
The emit
variable passed to STF is a method that can be used to record custom execution logs (later accessible at action.logs
).
Example usage:
const increment = CounterState.STF({
schema,
handler: ({ state, inputs, emit }) => {
state += inputs.amount;
emit({ name: "incremented", value: inputs.amount });
return state;
},
});
Bonus: Pseudo-Random Number Generation
The block.parentHash
can be used to generate pseudo-random numbers. The hash of the parent block is used as a seed to generate pseudo-random numbers. This is useful when you need to generate random numbers in a deterministic way. This is subjected to change with VRF implementation in the future.