Skip to content

Counter Rollup

Tutorial

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.

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

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

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

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

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

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

genesis-state.json
{
  "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:

  1. id: A unique identifier for the state machine.
  2. stateClass: The class of the state that the machine will use. This is the same class that we defined earlier.
  3. initialState: The initial state of the state machine. This is the state that we defined in the genesis-state.json file.
  4. on: The transitions that the state machine can perform. This is the transitions object that we defined earlier.
machine.ts
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.

stackr.config.ts
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:

  1. config: The configuration object for the rollup.
  2. stateMachines: The state machines that the rollup will use.
rollup.ts
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.

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

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

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

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