Counter Rollup
This tutorial will show you how to create a Counter Rollup using the Stackr SDK.
Define a State
We define a very basic state that will hold a single number. This state will be used to store the counter value.
Create State class
We do this by extending the State
class and passing the type of the state as number
a generic parameter.
import { BytesLike, State, StateMachine } from "@stackr/sdk/machine";
export class CounterState extends State<number> {
constructor(state: number) {
super(state);
}
getRootHash(): BytesLike {
return solidityPackedKeccak256(["uint256"], [this.state]);
}
}
Define the hashing function
Next we need to define the hashing function that will be used to calculate the root hash of the state. Over here we just use the solidityPackedKeccak256
function from ethers.js to hash the state as it is a simple number.
import { BytesLike, State, StateMachine } from "@stackr/sdk/machine";
export class CounterState extends State<number> {
constructor(state: number) {
super(state);
}
getRootHash(): BytesLike {
return solidityPackedKeccak256(["uint256"], [this.state]);
}
}
Define State Transition Functions (STF)
Next we define the state transition functions that will be used to update the state by submitting actions to the rollup.
We define two functions, one for incrementing the counter and one for decrementing the counter.
Define input schema
Each STF must have a schema
defined which represents the type definition for the inputs accepted by the transition function. These schemas are used to generate equivalent EIP-712 types for signing purposes.
Over here, both increment and decrement actions can be performed using the same input schema.
import { SolidityType } from "@stackr/sdk/machine";
const schema = {
timestamp: SolidityType.UINT,
} as const;
/* ... */
To learn more on how to write schemas, refer to Passings Inputs to an STF.
Increment
We define a state transition function that increments the counter value by 1. It reads the state from the input and returns an updated state.
import { REQUIRE, SolidityType, Transitions } from "@stackr/sdk/machine";
import { CounterState } from "./state";
const schema = {
timestamp: SolidityType.UINT,
} as const;
const increment = CounterState.STF({
schema,
handler: ({ state }) => {
state += 1;
return state;
},
});
/* ... */
Decrement
Next we define a state transition function that decrements the counter value by 1. It reads the state from the input and returns an updated state.
import { REQUIRE, SolidityType, Transitions } from "@stackr/sdk/machine";
import { CounterState } from "./state";
const schema = {
timestamp: SolidityType.UINT,
} as const;
/* ... */
const decrement = CounterState.STF({
schema,
handler: ({ state }) => {
state -= 1;
REQUIRE(state > 0, "Cannot decrement below 0");
return state;
},
});
/* ... */
Note that we also add a check to ensure that the counter value does not go below 0. it is done using the REQUIRE
function from the SDK. It works similarly to the require
function in Solidity.
Export the transitions
Finally we export the transitions so that they can be used in the state machine.
import { REQUIRE, SolidityType, Transitions } from "@stackr/sdk/machine";
import { CounterState } from "./state";
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;
REQUIRE(state > 0, "Cannot decrement below 0");
return state;
},
});
export const transitions: Transitions<CounterState> = {
increment,
decrement,
};
Define genesis state
Next we define the initial state of the state machine. This is the state that will be used to initialize the state machine. We just set the counter value to 0.
{
"state": 0
}
Creating the State Machine
Next we create the state machine using the state, action schema and transitions.
We need to import the genesis-state.json
file and the transitions
object that we defined earlier.
Then we can instantiate the state machine using the StateMachine
class from the SDK.
The machine takes 4 parameters:
id
: A unique identifier for the state machine.stateClass
: The class of the state that the machine will use. This is the same class that we defined earlier.initialState
: The initial state of the state machine. This is the state that we defined in thegenesis-state.json
file.on
: The transitions that the state machine can perform. This is the transitions object that we defined earlier.
import { BytesLike, StateMachine } from "@stackr/sdk/machine";
import * as genesis from "../../genesis-state.json";
import { CounterState } from "./state";
import { transitions } from "./transitions";
const machine = new StateMachine({
id: "counter",
stateClass: CounterState,
initialState: genesis.state,
on: transitions,
});
export { machine };
Hence exporting the machine as a export machine
would not work.
Set the config
Next we define the configuration object for the rollup. This is used to tell the rollup how to connect to connect to the parent chain, block time, sync time etc.
This file is autogenerated if the @stackr/cli
is used to setup the project.
import { KeyPurpose, SignatureScheme, StackrConfig } from "@stackr/sdk";
import dotenv from "dotenv";
dotenv.config();
const REGISTRY_CONTRACT = "<REGISTRY_CONTRACT_ADDRESS>";
const stackrConfig: StackrConfig = {
isSandbox: true,
sequencer: {
blockSize: 16,
blockTime: 1000,
},
syncer: {
slotTime: 1000,
vulcanRPC: process.env.VULCAN_RPC as string,
L1RPC: process.env.L1_RPC as string,
},
operator: {
accounts: [
{
privateKey: process.env.PRIVATE_KEY as string,
purpose: KeyPurpose.BATCH,
scheme: SignatureScheme.ECDSA,
},
],
},
domain: {
name: "Stackr MVP v0",
version: "1",
salt: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
},
datastore: {
type: "sqlite",
uri: process.env.DATABASE_URI,
},
registryContract: REGISTRY_CONTRACT,
logLevel: "log",
};
export { stackrConfig };
Define the Rollup
Once we are done with the state machine, we can define the rollup using the MicroRollup
class from the SDK which includes all the neccessary components to run the state machine.
The rollup takes 2 parameters:
config
: The configuration object for the rollup.stateMachines
: The state machines that the rollup will use.
import { MicroRollup } from "@stackr/sdk";
import { stackrConfig } from "../stackr.config";
import { machine } from "./machine.ts";
const mru = await MicroRollup({
config: stackrConfig,
stateMachines: [machine],
});
await mru.init();
All Set!
We have now successfully created a Counter Rollup using the Stackr SDK. We have defined the state, state transition functions, genesis state, state machine, configuration and the rollup.
Interacting with the Rollup
Now that we have the rollup setup, we can interact with it by sending it actions. We can use the submitAction
method to send actions to the rollup.
Create an Input
We define the inputs for the action. In this case we just need a timestamp as defined in the action schema.
const inputs = { timestamp: Date.now() };
Sign the input to create a signature
Next we sign the inputs (combined with the STF name) using the wallet.signTypedData
function. This uses EIP-712
domain and types definitions to sign the message.
const name = "increment";
const inputs = { timestamp: Date.now() };
const domain = mru.config.domain;
const types = mru.getStfSchemaMap()[name];
const signature = await wallet.signTypedData(domain, types, { name, inputs });
Create an action from the inputs
Next we construct the action parameters from the inputs, signature and the sender address.
const name = "increment";
const inputs = { timestamp: Date.now() };
const domain = mru.config.domain;
const types = mru.getStfSchemaMap()[name];
const signature = await wallet.signTypedData(domain, types, { name, inputs });
const incrementActionParams = {
name,
inputs,
signature,
msgSender: wallet.address,
};
Submit the action to the rollup
Finally we submit the action to the rollup using the submitAction
method. This method takes the action parameters and returns an acknowledgement.
const name = "increment";
const inputs = { timestamp: Date.now() };
const domain = mru.config.domain;
const types = mru.getStfSchemaMap()[name];
const signature = await wallet.signTypedData(domain, types, { name, inputs });
const incrementActionParams = {
name,
inputs,
signature,
msgSender: wallet.address,
};
const ack = await mru.submitAction(incrementActionParams);
console.log(ack)
Complete
We have now successfully created a Counter Rollup using the Stackr SDK and interacted with it by sending actions.