Skip to content

State Transition Functions

Transitioning between states

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:

transitions.ts
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.

transitions.ts
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.

transitions.ts
import { SolidityType } from "@stackr/sdk/machine";
 
const schema = {
  "<INPUT_FIELD_NAME>": SolidityType.
ADDRESS
BOOL
BYTES
BYTES32
STRING
UINT
} 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:

  1. state
  2. msgSender
  3. inputs
  4. signature
  5. block
    • block.height
    • block.timestamp
    • block.parentHash
  6. emit
transitions.ts
// Without Destructuring
const superCoolStf = CounterState.STF({
  schema,
  handler: (props) => {
    const state = props.
block
emit
inputs
msgSender
signature
state
const height = props.block.
height
parentHash
timestamp
}, }); // With Destructuring const anotherSuperCoolStf = CounterState.STF({ schema, handler: ({ s
signature
state
}) => {
}, });

State

  1. 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.

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.

  1. msgSender : The address of the sender of the transaction.

  2. inputs : The inputs passed as per the schema.

  3. signature : The signature of the msgSender on the inputs.

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.

  1. block.height : The height of the block in which the transaction is included.

  2. block.timestamp : The timestamp of the block in which the transaction is included.

  3. 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:

transitions.ts
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.