Only this pageAll pages
Powered by GitBook
1 of 39

General

Getting Started

Loading...

Loading...

onboarding

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

core

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Appendix

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Tutorial

Learn how to build, deploy, and play with the Walnut App, your first Seismic-powered shielded contract game, in this hands-on tutorial.

Setting Up Your Walnut App Project

In this chapter, you’ll set up the foundation for the Walnut App using a clean and modular monorepo-style workspace. This structure separates concerns into distinct areas, making your project easier to navigate and maintain. You’ll create a contracts directory to house your Seismic smart contracts and tests, and a cli directory to serve as the command-line interface for interacting with those contracts. By the end of this chapter, you’ll have a fully initialized project, complete with dependencies, formatting tools, and a seamless environment for both contract development and interaction.

Installation

Setting up your local machine to develop with Seismic


System requirements

Before you begin, make sure your machine meets the following requirements:

  • x84_64 or arm64 architecture

  • MacOS, Ubuntu, or Windows


Install the local development suite

The local development suite uses sforge as the testing framework, sanvil as the local node, and ssolc as the compiler.

  1. Install and on your machine if you don't already have them. Default installation works well.

  1. Download and execute the sfoundryup installation script.

  1. Install sforge, sanvil, ssolc. Expect this to take between 5-20 minutes depending on your machine.

  1. (Optional) Remove old build artifacts in existing projects. You can ignore this step if you aren't working with existing foundry projects.


Set up the VSCode extension

We recommend adding syntax highlighting via the (or for Open VSX) extension from the VSCode marketplace. If you already have the solidity extension, you'll have to disable it while writing Seismic code.

Welcome

You've arrived at the Seismic docs!


What is Seismic?

Seismic is a privacy enabled blockchain for fintechs. The blockchain is EVM, so developer experience is approximately the same as Ethereum, with a bit of extra syntax to control privacy settings.

It also comes with additional modules commonly needed by fintechs, such as compliance tooling and on-/off-ramps.

Whether you're an individual developer or part of a larger team, Seismic can help you build crypto powered products while protecting the privacy of your users.


How to use the docs

The docs are organized into 4 sections:

  • : You are here.

  • : Shortcut to getting your hands dirty.

  • : Walkthrough of core concepts for developing on Seismic.

  • : Detailed technical references.

Use the sidebar to navigate through the sections, or search (Cmd+K) to quickly find a page.


Pre-requisite knowledge

Our documentation assumes some familiarity with blockchain app development. Before getting started, it'll help if you're comfortable with:

If you're new to blockchain app development or need a refresher, we recommend starting out with the tutorial.


Work with us

If you might benefit from direct support from the team, please don't hesitate to reach out to [email protected]. We pride ourselves in fast response time.

You can also check out our for the latest updates.

Quickstart

You're two commands away from running an encrypted protocol

You can play around with stype using our . This assumes you went through everything in .

Getting Started
Onboarding
Core
Appendix
Solidity
Foundry
Viem
CryptoZombies
X account
rust
cargo
seismic
seismic
git clone "https://[email protected]/SeismicSystems/seismic-starter.git"
cd seismic-starter/packages/contracts
sforge test -vv
starter repository
Installation
curl https://sh.rustup.rs -sSf | sh
curl -L \
     -H "Accept: application/vnd.github.v3.raw" \
     "https://api.github.com/repos/SeismicSystems/seismic-foundry/contents/sfoundryup/install?ref=seismic" | bash
source ~/.zshenv  # or ~/.bashrc or ~/.zshrc
sfoundryup
source ~/.zshenv  # or ~/.bashrc or ~/.zshrc
sforge clean  # run in your project's contract directory

Verify devtool installation

Before continuing, ensure that you have completed the steps in the Installation section to install all necessary Seismic developer tools:

• sforge: Framework for testing and deploying smart contracts.

• sanvil : Local Seismic node.

• ssolc: The Seismic Solidity compiler.

Run each of the above commands in your terminal with a --version flag to ensure that they're installed correctly.

Also ensure that you have bun installed on your machine. If you do not have bun installed, follow the instructions here to install it on your machine.

saddress

shielded address

An saddress variable has all address operations supported. As for members, it supports call, delegatecall, staticcall, code, and codehash only. You cannot have saddress payable or have saddress as a transaction signer.

The universal casting rules and restrictions described in Basics apply.

saddress a = saddress(0x123);
saddress b = saddress(0x456);

// == VALID EXAMPLES
a == b  // false
b.call()

// == INVALID EXAMPLES
a.balance
payable(a)

Writing, testing and deploying the contract

This section dives into the heart of the Walnut App—the shielded smart contract that powers its functionality. You’ll start by building the foundational pieces of the Walnut, including the kernel and the protective shell, before implementing more advanced features like rounds, reset mechanisms, and contributor-based access control. By the end of this section, you’ll have a fully functional, round-based Walnut contract that is secure, fair, and replayable.

What You'll Learn

In this section, you’ll:

• Define and initialize the kernel, the hidden value inside the Walnut.

• Build the shell, the protective layer that hides the kernel, and implement a hit()

Clients

Typescript

Seismic maintains , which composes with to make calls to an RPC provider

The documentation for seismic-viem can be found

function to help crack it, and a
shake
function to increment the kernel value by an encrypted amount.

• Add a look() function to conditionally reveal the kernel.

• Implement a reset mechanism to restart the Walnut for multiple rounds.

• Track player contributions in each round, ensuring that only contributors can access the kernel.

Overview of Chapters

  • Chapter 1: Making the Kernel

You’ll define the kernel using a shielded state variable (suint256) and implement a shake() function to increment its value. This chapter introduces shielded writes.

  • Chapter 2: Making the Shell and Revealing the Kernel

Learn how to build the shell, which protects the kernel from being accessed prematurely. You’ll implement the hit()function to crack the shell and the look() function to reveal the kernel once conditions are met.

  • Chapter 3: Reset Mechanism, Rounds, and a more conditional Kernel Reveal

This chapter introduces a reset mechanism to enable multiple rounds of gameplay. You’ll track contributions per round and ensure that only players who helped crack the Walnut in a specific round can reveal the kernel. This chapter introduces shielded reads.

Rust

Seismic maintains seismic-alloy, which contains a crate called seismic-alloy-provider

  • Use SeismicSignedProvider to instantiate a client that can sign transactions (e.g. wallet client)

  • Use SeismicUnsignedProvider for a read-only client (e.g. public)

seismic-viem
viem
here

suint / sint

shielded unsigned integer / shielded integer

All comparisons and operators for suint / sint are functionally identical to uint / int. The universal casting rules and restrictions described in Basics apply.

suint256 a = suint256(10)
suint256 b = suint256(3)

// == EXAMPLES
a > b  // true
a | b  // 11
a << 2  // 40
a % b  // 1

Initialize the contracts subdirectory

  1. Navigate to the contracts subdirectory:

cd packages/contracts
  1. Initialize a project with sforge :

sforge init --no-commit && rm -rf .github

This command will:

  • Create the contract project structure (e.g., src/ , test/, foundry.toml).

  • Automatically install the Forge standard library (forge-std ) as a submodule.

  • Remove the .github workflow folder (not required)

  1. Edit the .gitignore file to be the following:

  1. Delete the default contract, test and script files (Counter.sol and Counter.t.sol and Counter.s.sol ) and replace them with their Walnut counterparts (Walnut.sol , Walnut.t.sol and Walnut.s.sol ):

These files are empty for now, but we will add to them as we go along.

Testnet

Network configuration

Property
Value

Name

Seismic Testnet

Chain ID

5124 (0x1404)

Explorer

Currently in development

Faucet

We will host a faucet at

If you are a third party working with us, please send us an address. We will make sure it is always topped up

Create project structure and monorepo workspace

  1. Create the project folder and navigate into it:

  1. Create the packages directory with subdirectories for contracts and cli

The contracts

Initialize the CLI subdirectory

Now that the contracts subdirectory has been initialized, you should now initialize the cli subdirectory that will be used to interact with the deployed contracts.

  1. Navigate to the contracts subdirectory:

  1. Initialize a new bun project

Deploy tools

Documentation on Seismic's deploy repo

You can find our deploy tools

Documentation for these tools will be published here soon

sbool

shielded boolean

All comparisons and operators for sbool function identically to bool. The universal casting rules and restrictions described in apply.

We recommend reading the point on conditional execution in prior to using sbool since it's easy to accidentally leak information with this type.

Chain type

EVM L1

RPC (HTTP)

https://internal-testnet.seismictest.net/rpc

RPC (WS)

wss://internal-testnet.seismictest.net/ws

Block time

1 block per ~600ms

Finality

1 block (may become 2 blocks)

faucet.seismictest.net
here

Interacting with the contract via a CLI

In this section, you will write a CLI to interact with the deployed Walnut smart contract from scratch, simulating a multiplayer, multi-round game consisting of two players, Alice and Bob. At the end of this section, you will be able to see a full game play out on your local Seismic node! Estimated time: ~30 minutes

.env
broadcast/
cache/
# Remove the Counter files
rm -f src/Counter.sol test/Counter.t.sol script/Counter.s.sol

# Create empty Walnut files in the same locations
touch src/Walnut.sol test/Walnut.t.sol script/Walnut.s.sol
sbool a = sbool(true)
sbool b = sbool(false)

// == EXAMPLES
a && b  // false
!b  // true
Basics
Common mistakes
subdirectory will house the Seismic smart contract(s) and test(s) for the project, while the
cli
will house the interface to interact with the contracts.
  1. Initialize a bun project in the root directory:

We remove the default index.ts and tsconfig.json files created by bun init -y to keep the root directory clean and focused on managing the monorepo structure rather than containing code. We also create a .prettierrc file for consistent code formatting and a .gitmodules file to manage contract submodules.

  1. Replace the default package.json with the following content for a monorepo setup:

  1. Add the following to the .prettierrc file for consistent code formatting:

  1. Replace the.gitignore file with:

  1. Add the following to the .gitmodules file to track git submodules (in our case, only the Forge standard library, forge-std):

mkdir walnut-app
cd walnut-app

Now, create an src/ folder and move index.ts there.

  1. Now, edit package.json to be the following:

  1. Edit .gitignore to be:

Your environment is now set!

# Assuming you are currently in the contracts directory
cd ../cli
bun init -y
mkdir -p src && mv -t src index.ts
mkdir -p packages/contracts packages/cli
bun init -y && rm index.ts && rm tsconfig.json && touch .prettierrc && touch .gitmodules
{
  "workspaces": [
    "packages/**"
  ],
  "dependencies": {},
  "devDependencies": {
    "@trivago/prettier-plugin-sort-imports": "^5.2.1",
    "prettier": "^3.4.2"
  }
}
{
  "semi": false,
  "tabWidth": 2,
  "singleQuote": true,
  "printWidth": 80,
  "trailingComma": "es5",
  "plugins": ["@trivago/prettier-plugin-sort-imports"],
  "importOrder": [
    "<TYPES>^(?!@)([^.].*$)</TYPES>",
    "<TYPES>^@(.*)$</TYPES>",
    "<TYPES>^[./]</TYPES>",
    "^(?!@)([^.].*$)",
    "^@(.*)$",
    "^[./]"
  ],
  "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"],
  "importOrderSeparation": true,
  "importOrderSortSpecifiers": true
}
# Compiler files
cache/
out/

# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env

node_modules/
[submodule "packages/contracts/lib/forge-std"]
	path = packages/contracts/lib/forge-std
	url = https://github.com/foundry-rs/forge-std
{
    "name": "walnut-cli",
    "license": "MIT License",
    "type": "module",
    "scripts": {
        "dev": "bun run src/index.ts"
    },
    "dependencies": {
        "dotenv": "^16.4.7",
        "seismic-viem": "1.0.9",
        "viem": "^2.22.3"
    },
    "devDependencies": {
        "@types/node": "^22.7.6",
        "typescript": "^5.6.3"
    }
}
node_modules

Deploying your contract

In this chapter, you’ll deploy your Walnut contract to a local Seismic node for testing. By the end of this guide, you’ll have a fully deployed contract that you can interact with using your CLI or scripts. Estimated Time: ~15 minutes.

Writing the deploy script

Navigate to the script folder in your Walnut App and open the Walnut.s.sol file located at:

packages/contracts/script

and add the following to it:

This script will deploy a new instance of the Walnut contract with an initial shell strength of 3 and an initial kernel value of 0.

Deploying the contract

  1. In a separate terminal window, run

in order to spin up a local Seismic node.

  1. In packages/contracts , create a .env file and add the following to it:

The RPC_URL denotes the port on which sanvil is running and the PRIVKEY is one of the nine standard sanvil testing private keys.

  1. Now, from packages/contracts, run

Your contract should be up and deployed to your local Seismic node!

Chapter 1: Making the Kernel

In this chapter, you’ll learn to create and initialize the kernel, a hidden value inside the Walnut, and increment it by implementing a shake function. Estimated time: ~10 minutes.

Defining the kernel

The kernel is the hidden number inside the Walnut. Using Seismic’s suint256 type, the kernel is shielded on-chain. Open up packages/contracts/Walnut.sol and define the kernel as a state variable and initialize it in the constructor:

Add a shake function

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import {Walnut} from "../src/Walnut.sol";

contract WalnutScript is Script {
    Walnut public walnut;

    function run() public {
        uint256 deployerPrivateKey = vm.envUint("PRIVKEY");

        vm.startBroadcast(deployerPrivateKey);
        walnut = new Walnut(3, suint256(0));
        vm.stopBroadcast();
    }
}
sanvil
RPC_URL=http://127.0.0.1:8545
PRIVKEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Next, let’s implement a function to increment the kernel. The shake function takes an suint256 parameter, _numShakes which specifies the amount to increment the kernel by.

What's happening here?

Since shake takes in an stype as one of its parameters, it is key that no information about this parameter (in this case, the number of shakes) is leaked at any time during the function call. This means that the value of _numShakes is known only to the function caller and is encrypted on-chain.

The function also updates a state variable (kernel ) and hence constitutes a state transition, which makes a call to this function a shielded write.

// SPDX-License-Identifier: MIT License
pragma solidity ^0.8.13;

contract Walnut {
    suint256 kernel; // The shielded kernel (number inside the Walnut)

    // Constructor to initialize the kernel
    constructor(suint256 _kernel) {
        kernel = _kernel;
    }
}
function shake(suint256 _numShakes) public {
    kernel += _numShakes; // Increment the kernel value using the shielded parameter.
    emit Shake(msg.sender); // Log the shake event.
}
source .env
sforge script script/Walnut.s.sol:WalnutScript \
    --rpc-url $RPC_URL \
    --broadcast

Chapter 2: Writing the core app

In this chapter, you’ll write the core logic to interact with the Walnut contract by creating an App class. This class will initialize player-specific wallet clients and contracts, and provide easy-to-use functions like hit, shake, reset, and look. Estimated time: ~20 minutes

Now, navigate to packages/cli/src/ and create a file called app.ts which will contain the core logic for the CLI:

Import required dependencies

Start by importing all the necessary modules and functions at the top of app.ts:

Define the app configuration

The AppConfig interface organizes all settings for the Walnut App, including player info, wallet setup, and contract details. It supports a multiplayer environment, with multiple players having distinct private keys and contract interactions.

Create the App class

The App class manages player-specific wallet clients and contract instances, providing an easy-to-use interface for multiplayer gameplay.

Add initialization logic to App

The init()method sets up individual wallet clients and contract instances for each player, enabling multiplayer interactions. Each player gets their own wallet client and a direct connection to the contract.

Add helper methods to App

These helper methods ensure that the app fetches the correct wallet client or contract instance for a specific player, supporting multiplayer scenarios.

getWalletClient :

getPlayerContract :

Implement Contract Interaction Methods

reset

Resets the Walnut for the next round. The reset is player-specific and resets the shell and kernel values.

shake

Allows a player to shake the Walnut, incrementing the kernel. This supports multiplayer scenarios where each player’s shakes impact the Walnut. Uses signed writes.

hit :

A player can hit the Walnut to reduce the shell’s strength. Each hit is logged for the respective player.

look :

Reveals the kernel for a specific player if they contributed to cracking the shell. This ensures fairness in multiplayer gameplay. Uses signed reads.

Differences from Ethereum

Overview

The Seismic EVM is approximately a superset of the EVM

What's the same

  • Transaction construction and serialization identical to Ethereum (with one new transaction type)

  • Address generation, gas estimation, and signing work the same as Ethereum

  • RPC methods are identical to reth

  • Standard Solidity bytecode will behave identically on Seismic

  • Seismic supports all of Ethereum's opcodes & precompiles

  • Transaction priority & fees follow EIP-1559 rules

  • Seismic will produce empty blocks when there are no pending transactions

Key differences

  • Shielded storage: Solidity contracts can store private data on-chain

  • Runs in a TEE: Seismic nodes must run in Trusted Execution Environments

  • Seismic transaction: We added a new transaction type that allows you to encrypt your calldata

EVM Compatibility

Opcodes

  • CLOAD – load shielded data from storage

  • CSTORE – write shielded data to storage

  • TIMESTAMP_MS – get the block timestamp in milliseconds

Seismic transaction

The transaction with type 0x4a allows users to encrypt their calldata. These otherwise work just like legacy transactions. We also support the other standard Ethereum transaction types (Legacy, EIP-1559, EIP-2930, EIP-4844, EIP-7702)

Precompiles

All standard Ethereum precompiles are still available. Seismic added 6 new precompiles to our EVM:

  • RNG: 0x64 securely generate a random number

  • ECDH 0x65: Elliptic Curve Diffie-Hellman, for generating a shared secret given a public key and secret key

  • AES-GCM cryptography

Staking

Seismic uses the same staking contract as Ethereum, which is hardcoded into our Genesis block at address 0x00000000219ab540356cbb839cbe05303d7705fa

Block times

We will often produce multiple blocks in the same second, yet Ethereum's block timestamps are expressed in terms of unix seconds. Our solution to this:

  • Block headers and the EVM see timestamps in milliseconds

  • All RPC endpoints will format block timestamps in seconds for Ethereum compatibility (not ms)

  • In Seismic Solidity, block.timestamp returns unix seconds, just like in standard solidity. We added block.timestamp_ms which returns unix milliseconds

RPC compatibility

We support almost every RPC endpoint in Reth, and have added a few more of our own

These methods are in Reth, but will behave differently on Seismic:

  • Calls to tracing endpoints will remove shielded data from the trace

  • Calls to getStorageAt will fail if the requested storage slot holds shielded data

Quick primer: seismic-viem

Before proceeding to write the CLI, you need to be acquainted with some of the functions and utilities used to enable Seismic primitives (e.g. shielded reads, shielded writes etc.) through our client library, seismic-viem , which we will be using heavily to write the CLI. The detailed docs for seismic-viem can be found here. Estimated time: ~15 minutes

Shielded wallet client

The shielded wallet client is the shielded/Seismic counterpart of the wallet client in viem . It is used to enable extended functionality for interacting with shielded blockchain features, wallet operations, and encryption. It can be initialized using the createShieldedWalletClient function as follows:

createShieldedWalletClient takes in the following parameters:

  1. chain : a well-defined object

  2. transport : the method of transport of interacting with the chain (http /ws along with the corresponding RPC URL)

  3. privateKey: the private key to create the client for

Once initialized, it can then be used to perform wallet operations or shielded-specific

Shielded contract

A shielded contract instance provides an interface to interact with a shielded contract onchain. It has extended functionality for performing shielded write operations, signed reads, and contract interaction for a specific contract performed by a specific wallet client that it is initialized with. It can be initialized with the getShieldedContract as follows:

It takes in the following parameters:

  1. abi : the ABI of the contract it is interacting with.

  2. address : the address of the deployed contract it is interacting with.

  3. client : the shielded wallet client that the interactions are to be performed by.

This function extends the base getContract functionality by adding:

  • Shielded write actions for nonpayable and payable functions.

  • Signed read actions for pure and view functions.

  • Proxy-based access to dynamically invoke contract methods.

We will extensively use shielded writes (for shake ) and shielded reads (for look()) in our CLI.

Collections

Using stype variables in arrays and maps

All stype variables can be stored in Solidity collections, much like their unshielded counterparts. They behave normally (as outlined in Basics) when used as values in these collections. It's when they're used as both the keys and values where it gets interesting. This applies to arrays and maps in particular:

suint256[] a;  // stype as value
function f(suint256 idx) {
    a[idx]  // stype as key
    // ...
}

// ==========

mapping(saddress => suint256) m;  // stype as key and value
function d(suint256 k) {
    m[k]
}

What's special here is that you can hold on to a[idx] and m[k] without observers knowing which values in the collection they refer to. You can read from these references:

sbool b = a[idx] < 10;
suint256 s = m[k] + 10;

You can write to these references:

a[idx] *= 3;
m[k] += a[idx];

Observers for any of these operations will not know which elements were read from / written to.

Using an stype as the key and value to a collection shields which element you're using.

In the previous section, we only knew how to shield what was happening for certain elements. Now, we know how to shield which elements are being modified in the first place.

We can take the ERC20 variant discussed in the section and extend it further to shielded balances, transfer amounts, and now recipients.

Chapter 1: Defining the constants and utilities

In this chapter, you will learn about defining and constants and utility functions which we will frequently use throughout the project. Estimated time: ~10 minutes

First, navigate to the root of the directory/monorepo and run bun install to install all the dependencies.

Now, navigate to make a lib folder inside packages/cliwith the files constants.ts and utils.ts and navigate to it:

mkdir -p packages/cli/lib
touch packages/cli/lib/constants.ts packages/cli/lib/utils.ts
cd packages/cli/lib

constants.ts will contain the constants we use throughout the project with utils.ts will contain the necessary utility functions.

Add the following to constants.ts :

This file centralizes key project constants:

• CONTRACT_NAME: The Walnut contract name.

• CONTRACT_DIR: Path to the contracts directory.

Now, add the following to utils.ts :

This file contains utility functions to interact with your Walnut contract:

• getShieldedContractWithCheck: Ensures the contract is deployed and returns a shielded contract instance.

• readContractAddress: Reads the deployed contract’s address from a broadcast file.

• readContractABI: Parses the contract’s ABI from an ABI file for use in interactions.

Links & Contact

Official channels

  • X.com/SeismicSys

  • Join our Discord

Links

  • Seismic

  • documentation

  • Seismic solidity extensions

Contact

For partnerships, contact L@, T@ or M@ seismic (dot) systems

If you would like to work at Seismic, email your resume to M@

Chapter 4: Testing your Walnut contract

In this chapter, you’ll write tests to verify that the Walnut contract behaves as expected under various scenarios. Testing ensures the functionality, fairness, and access control mechanisms of your contract work seamlessly, particularly in multi-round gameplay. Estimated Time: ~15 minutes.

Getting Started

Navigate to the test folder in your Walnut App and open the Walnut.t.sol file located at:

This file is where you’ll write all the test cases for the Walnut contract. Start with the following base code:

The setUp()

Chapter 3: Bringing it all together

Now that we’ve built the core logic for interacting with the Walnut contract, it’s time to tie everything together into a CLI that runs a multiplayer game session with two players - Alice and Bob. In this chapter, you’ll set up the environment variables for multiple players and write index.ts to simulate gameplay. Estimated time: ~20 minutes

Set Up Environment Variables

Before running the Walnut game, we need to define environment variables that store important configurations such as the RPC URL, chain ID, and player private keys.

Create a .env in

Basics

A handle on stype unlocks all shielded computation and storage

Mental model

We assume familiarity with .

Developers communicate to Seismic through the stype. A thorough understanding of this one concept unlocks all shielded computation and storage. The

# Assuming you are in packages/cli/lib
cd ../src
touch app.ts

Encryption 0x66

  • Decryption 0x67

  • HKDF 0x68: generate a cryptographic keys from a parent key

  • Secp256k1 0x69: Sign a message given a secret key

  • Almost all

    in Open VSX

    Website
    GitHub Organization
    Status hub
    seismic client
    in the VSCode Marketplace
    Chain
    actions.
    mapping(saddress => suint256) public balanceOf;  // key is now saddress
    
    function transfer(saddress to, suint256 amount) public {  // recipient now saddress
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
    }
    Basics
    import { join } from 'path'
    
    const CONTRACT_NAME = 'Walnut'
    const CONTRACT_DIR = join(__dirname, '../../contracts')
    
    export { CONTRACT_NAME, CONTRACT_DIR }
    function initializes the Walnut contract for use in all test cases.

    Writing Test Cases

    Start off with testing the basic functionalities, hit , shake , look and reset

    Core functionalities

    1. Basic hit functionality

    Ensures the Walnut’s shell can be cracked by shellStrength number of hits.

    1. Basic shake functionality

    Validates that shaking the Walnut increments the kernel value.

    1. Reset functionality

    Now, test for the restrictive/conditional nature of these basic functionalities.

    Restricting Actions

    1. Preventing hit when shell is cracked

    Ensures that hitting a cracked shell is not allowed.

    1. Preventing shake when shell is cracked

    Ensures that shaking the Walnut after the shell is cracked is not allowed.

    1. Preventing look when shell is intact

    Ensures that the kernel cannot be revealed unless the shell is fully cracked.

    1. Preventing reset when shell is intact

    Validates that the Walnut cannot be reset unless the shell is fully cracked.

    Now, test for more complex scenarios.

    Complex scenarios

    1. Sequence of Multiple Actions

    Ensures that the Walnut behaves correctly under a sequence of hits and shakes.

    1. Prevent Non-Contributors From Using look()

    Ensures that only contributors in the current round can call look() .

    1. Contributor Tracking Across Rounds

    Validates that contributions are tracked independently for each round. The test has one contributor hit both times and crack the shell in the first round, and a different contributor hit and crack the shell in the second round. We check for the fact the second round contributor cannot see the kernel after the first round and the first round contributor cannot see the kernel after the second.

    You can find the entire test file here.

    Test out the file by running the following inside the packages/contracts directory:

    The contract has been tested, time to deploy it!

    packages/cli
    :

    Open .env and paste the following:

    What’s Happening Here?

    • CHAIN_ID=31337 : 31337is the default chain ID for sanvil(your local Seismic node).

    • RPC_URL=http://127.0.0.1:8545 : This is the RPC URL for interacting with the local Seismic node.

    • ALICE_PRIVKEYand BOB_PRIVKEY : These are Alice and Bob’s private keys, allowing them to play the game. (These again are standard test keys provided by sanvil)

    Write index.ts

    Now, we’ll create the main entry point for our game session. This file will simulate gameplay, initializing players and having them interact with the Walnut contract. Open packages/cli/src/index.tsand follow these steps:

    Import Dependencies

    Import the required libraries to read environment variables, define network configurations, and interact with the Walnut contract.

    Define the main() function

    This function initializes the contract and player wallets, then runs the game session.

    Read Contract Details

    The contract’s ABI and deployed address are read from files generated during deployment.

    Select the blockchain network

    Determine whether to use the local sanvilnode (31337) or the Seismic devnet.

    Define players

    Assign Alice and Bob as players with private keys stored in .env .

    Initialize the Game App

    Create an App instance to interact with the Walnut contract.

    Simulate the game round by round

    The following logic executes two rounds of gameplay between Alice and Bob.

    Round 1 - Alice Plays

    Round 2 - Bob Plays

    Alice Tries to Look in Round 2 (we expect this to fail since she has contributed in round 1 but not round 2)

    Execute the main() function

    This ensures that the script runs when executed.

    The entire index.ts file can be found here

    Running the CLI

    Now, run the CLI from packages/cli by running:

    You should see something like this as the output:

    This output logs the events during two rounds of gameplay in the Walnut contract, showing interactions by Alice and Bob, along with a revert error when Alice attempts to call look()in Round 2.

    Congratulations! You've reached the end of the tutorial. You can find the code for the entire project here.

    import {
      type ShieldedContract,
      type ShieldedWalletClient,
      createShieldedWalletClient,
      getShieldedContract,
    } from 'seismic-viem'
    import { Abi, Address, Chain, http } from 'viem'
    import { privateKeyToAccount } from 'viem/accounts'
    import { getShieldedContractWithCheck } from '../lib/utils'
    interface AppConfig {
      players: Array<{
        name: string // Name of the player
        privateKey: string // Private key for the player’s wallet
      }>
      wallet: {
        chain: Chain // Blockchain network (e.g., Seismic Devnet or Anvil)
        rpcUrl: string // RPC URL for blockchain communication
      }
      contract: {
        abi: Abi // The contract's ABI for interaction
        address: Address // The contract's deployed address
      }
    }
    export class App {
      private config: AppConfig // Holds all app configuration
      private playerClients: Map<string, ShieldedWalletClient> = new Map() // Maps player names to their wallet clients
      private playerContracts: Map<string, ShieldedContract> = new Map() // Maps player names to their contract instances
    
      constructor(config: AppConfig) {
        this.config = config
      }
    }
    async init() {
      for (const player of this.config.players) {
        // Create a wallet client for the player
        const walletClient = await createShieldedWalletClient({
          chain: this.config.wallet.chain,
          transport: http(this.config.wallet.rpcUrl),
          account: privateKeyToAccount(player.privateKey as `0x${string}`),
        })
        this.playerClients.set(player.name, walletClient) // Map the client to the player
    
        // Initialize the player's contract instance and ensure the contract is deployed
        const contract = await getShieldedContractWithCheck(
          walletClient,
          this.config.contract.abi,
          this.config.contract.address
        )
        this.playerContracts.set(player.name, contract) // Map the contract to the player
      }
    }
    private getWalletClient(playerName: string): ShieldedWalletClient {
      const client = this.playerClients.get(playerName)
      if (!client) {
        throw new Error(`Wallet client for player ${playerName} not found`)
      }
      return client
    }
    private getPlayerContract(playerName: string): ShieldedContract {
      const contract = this.playerContracts.get(playerName)
      if (!contract) {
        throw new Error(`Shielded contract for player ${playerName} not found`)
      }
      return contract
    }
    async reset(playerName: string) {
      console.log(`- Player ${playerName} writing reset()`)
      const contract = this.getPlayerContract(playerName)
      const walletClient = this.getWalletClient(playerName)
      await walletClient.waitForTransactionReceipt({
        hash: await contract.write.reset([], { gas: 100000n })
      })
    }
    async shake(playerName: string, numShakes: number) {
      console.log(`- Player ${playerName} writing shake()`)
      const contract = this.getPlayerContract(playerName)
      const walletClient = this.getWalletClient(playerName)
      await contract.write.shake([numShakes], { gas: 50000n }) // signed write
      })
    }
    async hit(playerName: string) {
      console.log(`- Player ${playerName} writing hit()`)
      const contract = this.getPlayerContract(playerName)
      const walletClient = this.getWalletClient(playerName)
      await contract.write.hit([], { gas: 100000n })
    }
    async look(playerName: string) {
      console.log(`- Player ${playerName} reading look()`)
      const contract = this.getPlayerContract(playerName)
      const result = await contract.read.look() // signed read
      console.log(`- Player ${playerName} sees number:`, result)
    }
    const walletClient = await createShieldedWalletClient({
      chain: seismicChain,
      transport: httpTransport,
      privateKey: '0xabcdef...',
    })
    const contract = getShieldedContract({
      abi: myContractAbi,
      address: '0x1234...',
      client: shieldedWalletClient,
    })
    // Perform a shielded write
    await contract.write.myFunction([arg1, arg2], { gas: 50000n })
     
    // Perform a signed read
    const value = await contract.read.getValue()
    console.log('Value:', value)
    import fs from 'fs'
    import { type ShieldedWalletClient, getShieldedContract } from 'seismic-viem'
    import { Abi, Address } from 'viem'
    
    async function getShieldedContractWithCheck(
      walletClient: ShieldedWalletClient,
      abi: Abi,
      address: Address
    ) {
      const contract = getShieldedContract({
        abi: abi,
        address: address,
        client: walletClient,
      })
    
      const code = await walletClient.getCode({
        address: address,
      })
      if (!code) {
        throw new Error('Please deploy contract before running this script.')
      }
    
      return contract
    }
    
    function readContractAddress(broadcastFile: string): `0x${string}` {
      const broadcast = JSON.parse(fs.readFileSync(broadcastFile, 'utf8'))
      if (!broadcast.transactions?.[0]?.contractAddress) {
        throw new Error('Invalid broadcast file format')
      }
      return broadcast.transactions[0].contractAddress
    }
    
    function readContractABI(abiFile: string): Abi {
      const abi = JSON.parse(fs.readFileSync(abiFile, 'utf8'))
      if (!abi.abi) {
        throw new Error('Invalid ABI file format')
      }
      return abi.abi
    }
    
    export { getShieldedContractWithCheck, readContractAddress, readContractABI }
    packages/contracts/test/Walnut.t.sol
    // SPDX-License-Identifier: MIT License
    pragma solidity ^0.8.13;
    
    import {Test, console} from "forge-std/Test.sol";
    import {Walnut} from "../src/Walnut.sol";
    
    contract WalnutTest is Test {
        Walnut public walnut;
    
        function setUp() public {
            // Initialize a Walnut with shell strength = 2 and kernel = 0
            walnut = new Walnut(2, suint256(0));
        }
    }
    function test_Hit() public {
        walnut.hit(); // Decrease shell strength by 1
        walnut.hit(); // Fully crack the shell
        assertEq(walnut.look(), 0); // Kernel should still be 0 since no shakes
    }
    function test_Shake() public {
        walnut.shake(suint256(10)); // Shake the Walnut, increasing the kernel
        walnut.hit(); // Decrease shell strength by 1
        walnut.hit(); // Fully crack the shell
        assertEq(walnut.look(), 10); // Kernel should be 10 after 10 shakes
    }
    function test_Reset() public {
        walnut.hit(); // Decrease shell strength by 1
        walnut.shake(suint256(2)); // Shake the Walnut
        walnut.hit(); // Fully crack the shell
        walnut.reset(); // Reset the Walnut
    
        assertEq(walnut.getShellStrength(), 2); // Shell strength should reset to initial value
        walnut.hit(); // Start hitting again
        walnut.shake(suint256(5)); // Shake the Walnut again
        walnut.hit(); // Fully crack the shell again
        assertEq(walnut.look(), 5); // Kernel should reflect the shakes in the new round
    }
    function test_CannotHitWhenCracked() public {
        walnut.hit(); // Decrease shell strength by 1
        walnut.hit(); // Fully crack the shell
        vm.expectRevert("SHELL_ALREADY_CRACKED"); // Expect revert when hitting an already cracked shell
        walnut.hit();
    }
    function test_CannotShakeWhenCracked() public {
        walnut.hit(); // Decrease shell strength by 1
        walnut.shake(suint256(1)); // Shake the Walnut
        walnut.hit(); // Fully crack the shell
        vm.expectRevert("SHELL_ALREADY_CRACKED"); // Expect revert when shaking an already cracked shell
        walnut.shake(suint256(1));
    }
    function test_CannotLookWhenIntact() public {
        walnut.hit(); // Partially crack the shell
        walnut.shake(suint256(1)); // Shake the Walnut
        vm.expectRevert("SHELL_INTACT"); // Expect revert when trying to look at the kernel with the shell intact
        walnut.look();
    }
    function test_CannotResetWhenIntact() public {
        walnut.hit(); // Partially crack the shell
        walnut.shake(suint256(1)); // Shake the Walnut
        vm.expectRevert("SHELL_INTACT"); // Expect revert when trying to reset without cracking the shell
        walnut.reset();
    }
    function test_ManyActions() public {
        uint256 shakes = 0;
        for (uint256 i = 0; i < 50; i++) {
            if (walnut.getShellStrength() > 0) {
                if (i % 25 == 0) {
                    walnut.hit(); // Hit the shell every 25 iterations
                } else {
                    uint256 numShakes = (i % 3) + 1; // Random shakes between 1 and 3
                    walnut.shake(suint256(numShakes));
                    shakes += numShakes;
                }
            }
        }
        assertEq(walnut.look(), shakes); // Kernel should match the total number of shakes
    }
    function test_RevertWhen_NonContributorTriesToLook() public {
        address nonContributor = address(0xabcd);
    
        walnut.hit(); // Decrease shell strength by 1
        walnut.shake(suint256(3)); // Shake the Walnut
        walnut.hit(); // Fully crack the shell
    
        vm.prank(nonContributor); // Impersonate a non-contributor
        vm.expectRevert("NOT_A_CONTRIBUTOR"); // Expect revert when non-contributor calls `look()`
        walnut.look();
    }
    function test_ContributorInRound2() public {
        address contributorRound2 = address(0xabcd); // Contributor for round 2
    
        // Round 1: Cracked by address(this)
        walnut.hit(); // Hit 1
        walnut.hit(); // Hit 2
        assertEq(walnut.look(), 0); // Confirm kernel value
    
        walnut.reset(); // Start Round 2
    
        // Round 2: ContributorRound2 cracks the Walnut
        vm.prank(contributorRound2);
        walnut.hit();
    
        vm.prank(contributorRound2);
        walnut.shake(suint256(5)); // Shake kernel 5 times
    
        vm.prank(contributorRound2);
        walnut.hit();
    
        vm.prank(contributorRound2);
        assertEq(walnut.look(), 5); // Kernel value is 5 for contributorRound2
    
        vm.expectRevert("NOT_A_CONTRIBUTOR"); // address(this) cannot look in round 2
        walnut.look();
    }
    sforge build
    sforge test
    touch .env
    CHAIN_ID=31337
    RPC_URL=http://127.0.0.1:8545
    ALICE_PRIVKEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    BOB_PRIVKEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
    import dotenv from 'dotenv'
    import { join } from 'path'
    import { seismicDevnet } from 'seismic-viem'
    import { anvil } from 'viem/chains'
    
    import { CONTRACT_DIR, CONTRACT_NAME } from '../lib/constants'
    import { readContractABI, readContractAddress } from '../lib/utils'
    import { App } from './app'
    
    // Load environment variables from .env file
    dotenv.config()
    async function main() {
      if (!process.env.CHAIN_ID || !process.env.RPC_URL) {
        console.error('Please set your environment variables.')
        process.exit(1)
      }
      const broadcastFile = join(
        CONTRACT_DIR,
        'broadcast',
        `${CONTRACT_NAME}.s.sol`,
        process.env.CHAIN_ID,
        'run-latest.json'
      )
      const abiFile = join(
        CONTRACT_DIR,
        'out',
        `${CONTRACT_NAME}.sol`,
        `${CONTRACT_NAME}.json`
      )
      const chain =
        process.env.CHAIN_ID === anvil.id.toString() ? anvil : seismicDevnet
      const players = [
        { name: 'Alice', privateKey: process.env.ALICE_PRIVKEY! },
        { name: 'Bob', privateKey: process.env.BOB_PRIVKEY! },
      ]
      const app = new App({
        players,
        wallet: {
          chain,
          rpcUrl: process.env.RPC_URL!,
        },
        contract: {
          abi: readContractABI(abiFile),
          address: readContractAddress(broadcastFile),
        },
      })
    
      await app.init()
      console.log('=== Round 1 ===')
      await app.reset('Alice')
      await app.shake('Alice', 2)
      await app.hit('Alice')
      await app.shake('Alice', 4)
      await app.hit('Alice')
      await app.shake('Alice', 1)
      await app.hit('Alice')
      await app.look('Alice')
      console.log('=== Round 2 ===')
      await app.reset('Bob')
      await app.hit('Bob')
      await app.shake('Bob', 1)
      await app.hit('Bob')
      await app.shake('Bob', 1)
      await app.hit('Bob')
    
      // Bob looks at the number in round 2
      await app.look('Bob')
      // Alice tries to look in round 2, should fail by reverting
      console.log('=== Testing Access Control ===')
      console.log("Attempting Alice's look() in Bob's round (should revert)")
      try {
        await app.look('Alice')
        console.error('❌ Expected look() to revert but it succeeded')
        process.exit(1)
      } catch (error) {
        console.log('✅ Received expected revert')
      }
    }
    
    main()
    bun dev
    === Round 1 ===
    - Player Alice writing shake()
    - Player Alice writing hit()
    - Player Alice writing shake()
    - Player Alice writing hit()
    - Player Alice writing shake()
    - Player Alice writing hit()
    - Player Alice reading look()
    - Player Alice sees number: 7n
    === Round 2 ===
    - Player Bob writing reset()
    - Player Bob writing hit()
    - Player Bob writing shake()
    - Player Bob writing hit()
    - Player Bob writing shake()
    - Player Bob writing hit()
    - Player Bob reading look()
    - Player Bob sees number: 3n
    === Testing Access Control ===
    - Attempting Alice's look() in Bob's round (should revert)
    ✅ Received expected revert
    stype
    consists of three elementary types:
    • suint / sint: shielded integer

    • sbool: shielded boolean

    • saddress: shielded address

    The primary difference between them and their vanilla counterparts is that they're shielded. Any operations you apply to them are carried out as expected, but the values won't be visible to external observers.

    There are special considerations unique to each individual type. These are covered in the next three sections. For now, we'll develop a general understanding of stype that applies to all its component types.

    Here's the mental model you should have for shielded contracts. Whenever a tx is broadcasted by a user, it goes through the same submission, execution, and storage phases as a tx in a regular blockchain. The only difference is that when you look at the tx at these different stages- whether it's as a calldata payload during submission, a trace during execution, or as leaves in the MPT tree during storage- any bytes that represent stype variables are replaced with 0x000.

    Let's step through a concrete example. We'll follow the lifecycle of a transfer() tx for an ERC20 variant. This variant shields user balances and transfer amounts:

    Observers see 0x000 in place of stype variables during transaction submission, execution, and storage.

    Shielding user balances is done by changing the values of the balanceOf array to suint256. Shielding transfer amounts is done by changing the amount parameter in transfer() to suint256. Now we can see what happens at every stage of the tx lifecycle:

    1. Submit. The tx is sitting in the mempool. You know that you're sending 12 tokens to your friend. Observers can look at the calldata and figure out that your friend is the recipient, but will see 0x000 instead of the number 12.

    2. Execute. The tx is processed by a full node, and its trace is open. You know that 12 tokens were removed from your balance and 12 were added to your friend's. Observers know that the same number that was deducted from your balance was added to your friend's, but they see 0x000 instead of the number 12.

    3. Store. The effects of the tx are applied to the state tree of all full nodes. You know that your new balance goes down by 12, to 200. You know that your friend's balance went up by 12, but you only see 0x000 for what its final state is. Observers know that your new balance is down the same amount that your friend's new balance is up, but they see 0x000 for both balances.

    Seismic currently shields a lot more than just the bytes representing stype variables, so the above model is more granular than you technically need to be. However, this will soon stop being the case. You should not fit your contracts to this temporary discrepancy.

    Casting

    You can cast stype variables to their unshielded counterparts, and vice-versa. Only explicit casting is allowed- no implicit. Note that whenever you do this, observers can look at the trace to figure out either the initial (if going from not stype to stype) or final (if going from stype to not stype) value.

    Restrictions

    There are two restrictions in how you can use stype variables:

    1. You can't return them in public or external functions. This also means stype contract variables can't be public, since this automatically generates a getter. If you want to return one, you'll have to cast it into its unshielded counterpart.

    1. You can't use them as constants.

    Solidity

    Understanding the Walnut contract

    Imagine you’re holding a walnut, an unassuming object. Inside it lies a number, a secret only revealed when you crack the shell. There are primarily two actions you can take on this walnut: either shaking it or hitting it. Shaking the walnut some n number of times increments the number inside by n, while hitting the walnut brings it one step closer to the shell cracking. You can only see the number inside if you have contributed to the cracking the shell, i.e., you have hit the walnut at least once. This collaborative challenge is the heart of the Walnut App, and it’s all powered by the Walnut smart contract. Let’s dive into the inner workings of the contract and uncover how each part fuels this game.

    The contract can be found in the packages/contracts/Walnut.sol file of the starter repo.

    State variables

    startShell and shell

    Think of the shell as the Walnut’s durability. startShell is the Walnut’s starting strength, and shell tracks how much of it remains as players hit it.

    startNumber and number

    These are the secret numbers at the heart of the Walnut. startNumber initializes the hidden number, while number evolves as players shake the Walnut. Being suint256 (shielded integers), these numbers remain encrypted on-chain—visible only to authorized participants.

    round

    A counter that increments with each new round/reset, ensuring every round has a fresh Walnut to crack.

    hitsPerRound

    A mapping that records every player’s contribution to the current round, ensuring only participants can peek at the Walnut’s secret.

    Functions

    hit ( )

    This function allows a player to hit the Walnut, reducing its durability and bringing it one step closer to cracking:

    What happens:

    • Checks if the shell is intact (shell>0 )

    • If it is, decrements shell by 1

    • Increases the player who called hit() 's contribution in the current round (hitsPerRound[round][playerAddress]) by 1

    shake (suint256 _numShakes)

    This function allows a player to shake the walnut _numShakes number of times. Since this is a write function that takes in an stype as one of its parameters, calling this function would constitute a Seismic write.

    What happens:

    • Adds _numShakes to number

    • Emits the Shake event.

    look ( )

    This function allows contributors to the current round to view the number inside the walnut. Since this is a view function that reveals an stype, calling this function would constitute a Seismic read.

    What happens:

    • Requires the shell to be cracked (requireCracked modifier)

    • Ensures the function caller contributed to the cracking the walnut for this round (onlyContributor modifier)

    • Returns ("reveals") the number inside the walnut.

    Modifiers

    Modifiers enforce the rules of the game:

    requireCracked

    Ensures that look() can only be called if the Walnut’s shell is completely cracked.

    requireIntact

    Ensures that shake() and hit() can only be called if the Walnut’s shell is intact.

    onlyContributor

    Restricts access to look() , and hence the number being revealed, only to players who contributed at least one hit in the current round.

    Codebases

    A list of all our codebases

    Our consensus client. Built on top of

    mapping(address => suint256) public balanceOf;  // shielded balance
    
    function transfer(address to, suint256 amount) public {  // shielded transfer amount
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
    }
    uint256 number = 100;
    suint256 sNumber = suint256(number);
    /*
     * Throws a compiler error
     */
    suint256 public v;
    
    // ==========
    
    /*
     * Throws a compiler error
     */
    function f() external view returns (suint256) {}
    /*
     * Throws a compiler error
     */
    suint256 constant MY_CONSTANT = 42;

    Emits the Hit event to update all participants.

    Our fork of Solidity. We added a set of types & opcodes for shielded computation

    Enclave

    Codebase for encryption, TEE & on-chain verification

    Execution layer

    Most of the repositories here are forks of the reth stack

    Alloy

    Seismic Alloy Core

    fork of alloy-rs/core

    • This is the repo that depends on nothing else

    • Upstream: version 1.1.2, commit e55993f

    Seismic Alloy

    Analogous to alloy-rs/op-alloy, but not a fork of it

    • Depends on:

      • seismic-alloy-core

      • alloy-rs/alloy

    Seismic Trie

    fork of alloy-rs/trie

    • Depends on seismic-alloy-core

    • Upstream: version 0.8.1, commit a098d3f

    REVM

    Seismic REVM

    fork of bluealloy/revm

    • Depends on:

      • seismic-alloy-core

      • seismic-enclave

    • Upstream: version 23.1.0, commit b287ce02

    Seismic EVM

    fork of alloy-rs/evm

    • Depends on:

      • alloy-rs/alloy

      • seismic-alloy

      • seismic-alloy-core

      • seismic-revm

    • Upstream: version 0.9.1, commit

    Seismic REVM Inspectors

    fork of paradigmxyz/revm-inspectors

    • Depends on:

      • seismic-alloy-core

      • alloy-rs/alloy

      • seismic-revm

    • Upstream: version 0.22.3, commit

    Reth

    fork of paradigmxyz/reth

    • Depends on:

      • seismic-alloy-core

      • seismic-alloy

      • alloy-rs/alloy

      • alloy-trie

      • seismic-revm

      • seismic-evm

      • seismic-revm-inspectors

    • Upstream: version 1.2.1, commit

    Foundry

    Seismic Compilers

    fork of foundry-rs/compilers

    • Depends on:

      • seismic-alloy-core

    • Upstream: version 0.16.1, commit ec745cec

    Seismic Foundry Fork DB

    fork of foundry-rs/foundry-fork-db

    • Depends on:

      • seismic-alloy-core

      • alloy-rs/alloy

      • seismic-revm

      • seismic-alloy (only for seismic-prelude)

    • Upstream: version 0.14.0, commit

    Seismic Foundry

    fork of foundry-rs/foundry

    • Depends on:

      • seismic-alloy-core

      • seismic-alloy

      • alloy-rs/alloy

      • alloy-trie

      • seismic-revm

      • seismic-evm

      • seismic-revm-inspectors

      • seismic-foundry-fork-db

    • Upstream: version 1.2.1, commit

    Seismic Client

    A library for building web applications on Seismic. This repo provides two packages that compose with the viem/wagmi stack to interact with the Seismic network:

    • seismic-viem: composes with viem

    • seismic-react: composes with wagmi

    Deployment

    Deploy

    A repository containing tools to deploy infrastructure

    Yocto build system

    Seismic forked Flashbots' stack for reproducible TEE builds. This includes these repos:

    Meta Seismic

    A Yocto layer that configures how the image runs Summit, Reth & the enclave server

    Yocto Manifests

    Yocto Scripts

    Summit
    Commonware's
    primitives
    Seismic Solidity

    Devnet

    Try out the developer testnet

    Welcome! This walkthrough is quick. It only requires a minute of actual attention, while the rest is waiting. If you run into any issues, please check if it's one of the 10 common errors resolved in the section. You can also hop in and ask questions in the #devnet channel.

    If you end up deploying your own custom contract, please send the github link to on TG! Also note, this is not an incentivized testnet.

    Works on Mac, Linux, and Windows via WSL (see ).

    Chapter 2: Making the Shell and revealing the Kernel

    In this chapter, you’ll build the shell, the protective layer that hides the kernel. You’ll initialize the shell’s strength and implement a hit function to decrement it. Additionally, you’ll add a look() function with a requiredCracked modifier to ensure the kernel can only be viewed once the shell is fully broken. Estimated Time: ~10 minutes.

    Defining the shell

    The shell determines the Walnut’s resilience. It has an integer strength (shellStrength

    Node Operator FAQ

    General

    Seismic nodes have to run inside a TEE. Why? and what does that mean for node operators?

    Seismic nodes run inside TEEs so we can verify that they are running the correct software via remote attestation. If someone were to deploy a node that allowed them to view network secrets, it would be rejected by other nodes, and therefore never receive any sensitive data.

    As a result, all node operators have to be running the exact same versions of the code, including reth parameters. If you are an RPC provider partnering with us, and need nodes to run with specific settings, please contact our team – we'll see how we can help. While we have nothing in place to support this now, we can prioritize features to make it easier for you to run your business

    Is Seismic a ZK Chain?

    No. Seismic uses trusted execution environments (TEE) via Intel TDX for privacy, not zero-knowledge proofs.

    Does Seismic support light nodes, full nodes or archival nodes?

    Seismic currently supports archival nodes only

    How fast does storage grow?

    • Current size: TBD (network has not yet launched)

    • Archive node: 1TB+ storage recommended initially

    • Growth rate: Will depend on network activity; approximately 12 hours of sync time expected for first year of operation

    Detailed storage projections will be published after mainnet launch

    How do I run a node?

    There are instructions to deploy a node in our deploy repo. There are two steps:

    1. Build (optional): you can build the image yourself using our Python scripts in the deploy repo. Alternatively we will be hosting images that we've built, along with the measurements generated. When we do this, you can download the image from the releases page of that repo. The basic command is: python3 -m yocto.cli --build --logs

    2. Deploy: once you have an image, you can deploy it to Azure using our Python tooling. The basic command is: python3 -m yocto.genesis_deploy -a 20251017221200 -n 1

    Soon we will publish more detailed documentation on our Python tooling, which will allow you to customize the deploy

    Hardware requirements

    Cloud hosting

    Seismic uses Azure's Confidential Computing with Intel TDX to run our nodes. We are also planning to support bare metal TDX as well

    Recommended specs

    • CPU: 4+ vCPUs

    • Memory: 16+GB RAM

    • Storage: 1TB

    • Azure Confidential virtual machines (TDX) with secure boot & TPM enabled

      • Example instance: EC4es v5

    • Security: Confidential VM with secure boot and vTPM (NonPersistedTPM)

    • SKU: standard_lrs with ConfidentialVM_NonPersistedTPM security type

    RPC

    Are there rate limits on RPC calls?

    No rate limits are currently imposed by the protocol itself, though node operators may implement their own.

    Is there an RPC parameter to set the maximum fee cap?

    Yes, --rpc.txfeecap. We use reth's default, which is 1.0 units of the native token (e.g. 1.0 ETH on testnet)

    Is there a maximum payload size for RPC requests?

    Yes, this is controlled through the arg --rpc.max-response-size. We use reth's default, which is 160MB

    Is there a limit on the batch count for RPC requests?

    No. Just like in reth, there's no limit on batch count. The only limit comes from total payload size (above)

    What is the maximum size for eth_getLogs responses?

    This is the same as reth's maximum payload size for general RPC requests: 160MB

    Does Seismic support log look back?

    Yes, archival nodes support complete log look back and retrieval of contract events from the beginning of the chain

    What sync mode should I use for fetching logs?

    We only support archival nodes. Make RPC calls to them with block filters

    What are the heaviest RPC methods?

    The most resource-intensive RPC methods are:

    • eth_getLogs with large block ranges or many matching events

    • Tracing calls (e.g., debug_traceTransaction, trace_* methods) with complex geth tracers

    Are block height indicators available?

    Yes, use eth_blockNumber to check current block height and sync progress

    Are there recommended caching rules for RPC methods?

    We haven't thought about this yet

    Operations

    How often are hard forks expected?

    No hard forks have occurred yet (network is pre-mainnet). The frequency of future hard forks is TBD, but all upgrades will be communicated via Twitter, Discord, and direct partner outreach

    All changes will be deployed to testnet before mainnet

    Does the node handle SIGTERM gracefully?

    Individual processes do support this. However because the node has to run inside a TEE, the correct way to restart a node is to reboot the machine. Relevant processes will automatically spawn on boot

    Expected restart time: About 1 minute from machine reboot

    Have there been any major outages?

    Mainnet has not launched yet, so no. In testnet, various incidents have occurred, but these have been resolved prior to public release

    1c4f35c
    dd283db
    3c0b3df8
    0fe2b2a
    b76d4f66
    ), which represents how many hits it can withstand before cracking. Let’s define the shell and initialize it in the constructor:

    Adding the hit function

    Each time the Walnut is hit, the shell strength decreases, simulating damage to the protective shell. This is crucial for revealing the kernel, as the shell must be fully broken for the kernel to be accessed:

    What's happening here?

    • The requireIntact modifier: Ensures that the function cannot be called if the Walnut’s shell is already broken (shellStrength == 0). This prevents unnecessary calls after the shell is fully cracked. We can now also add this modifier to the shake function in order to restrict shake being called even after the shell is broken:

    • Decrementing the shell: Each call to hitdecreases the shell’s strength (shellStrength) by one.

    • Logging the action: The Hit event records the hitter’s address (msg.sender) and the remaining shell strength.

    Example call:

    Here’s how calling the hit function works in practice:

    • Initial State: The shell strength is set to 5.

    • First Hit: A player calls hit(). The shell strength decreases to 4.

    • Subsequent Hits: Each additional hit reduces the shell strength by 1 until it reaches 0

    Revealing the Kernel

    Now that we have implemented the shell’s durability and the ability to break it using the hit function, we can introduce a new condition: the kernel should only be revealed once the shell is fully cracked.

    Currently, there is no way to access the kernel’s value. However, now that we have a shell with a decreasing strength, we can apply a condition that restricts when the kernel can be seen. Specifically:

    • The kernel should remain hidden while the shell is intact.

    • The kernel can only be revealed once the shell’s strength reaches zero, i.e. when it is cracked.

    To enforce this, we will create a function called look() , which will return the kernel’s value, but only if the Walnut has been fully cracked.

    Here’s how we define look() with a requireCracked modifier:

    What's happening here?

    • Restricting Access with a Condition: The requireCracked modifier ensures that look() can only be called if shellStrength == 0, meaning the Walnut has been fully cracked.

    • Revealing the Kernel: Once the condition is met, look() returns the unshielded value of the kernel.

    • Preventing Premature Access: If look() is called before the shell is broken, the function will revert with the error "SHELL_INTACT".

    Updated contract with hit, shake and look

        uint256 shellStrength; // The strength of the Walnut's shell.
    
        constructor(uint256 _shellStrength, suint256 _kernel) {
            shellStrength = _shellStrength; // Set the initial shell strength.
            kernel = _kernel; // Initialize the kernel.
        }
        // Event to log hits
        event Hit(address indexed hitter, uint256 remainingShellStrength);
        
        // Function to hit the walnut shell
        function hit() public {
            shellStrength--; // Decrease the shell strength.
            emit Hit(msg.sender, shellStrength); // Log the hit event.
        }
    
        // Modifier to ensure the shell is not cracked.
        modifier requireIntact() {
            require(shellStrength > 0, "SHELL_ALREADY_CRACKED");
            _;
        }
        function shake(suint256 _numShakes) public requireIntact {
            kernel += _numShakes; // Increment the kernel value using the shielded parameter.
            emit Shake(msg.sender); // Log the shake event.
        }
        // Function to reveal the kernel if the shell is fully cracked.
        function look() public view requireCracked returns (uint256) {
            return uint256(kernel); // Reveal the kernel as a standard uint256.
        }
        
        // Modifier to ensure the shell is fully cracked before revealing the kernel.
        modifier requireCracked() {
            require(shellStrength == 0, "SHELL_INTACT"); // Ensure the shell is broken before revealing the kernel.
            _;
        }
    // SPDX-License-Identifier: MIT License
    pragma solidity ^0.8.13;
    
    contract Walnut {
        uint256 shellStrength; // The strength of the Walnut's shell.
        suint256 kernel; // The hidden kernel (number inside the Walnut).
    
        // Events
        event Hit(address indexed hitter, uint256 remainingShellStrength); // Logs when the Walnut is hit.
        event Shake(address indexed shaker); // Logs when the Walnut is shaken.
    
        // Constructor to initialize the shell and kernel.
        constructor(uint256 _shellStrength, suint256 _kernel) {
            shellStrength = _shellStrength; // Set the initial shell strength.
            kernel = _kernel; // Initialize the kernel.
        }
    
        // Function to hit the Walnut and reduce its shell strength.
        function hit() public requireIntact {
            shellStrength--; // Decrease the shell strength.
            emit Hit(msg.sender, shellStrength); // Log the hit action.
        }
    
        // Function to shake the Walnut and increment the kernel.
        function shake(suint256 _numShakes) public requireIntact {
            kernel += _numShakes; // Increment the kernel by the given number of shakes.
            emit Shake(msg.sender); // Log the shake action.
        }
        
        // Function to reveal the kernel if the shell is fully cracked.
        function look() public view requireCracked returns (uint256) {
            return uint256(kernel); // Reveal the kernel as a standard uint256.
        }
        
        // Modifier to ensure the shell is fully cracked before revealing the kernel.
        modifier requireCracked() {
            require(shellStrength == 0, "SHELL_INTACT"); // Ensure the shell is broken before revealing the kernel.
            _;
        }
        
        // Modifier to ensure the shell is not cracked.
        modifier requireIntact() {
            require(shellStrength > 0, "SHELL_ALREADY_CRACKED");
            _;
        }
        
    }
    Deploy an encrypted contract

    1. Install Rust

    2. Install jq

    For Mac. See instructions for your machine here. Only step that isn't OS agnostic.

    3. Install sfoundryup

    4. Run sfoundryup

    5. Clone repository

    6. Deploy contract

    Interact with an encrypted contract

    1. Install Bun

    2. Install node dependencies

    3. Send transactions

    FAQ

    What if I'm on Windows?

    We recommend using WSL to run commands as if you were on a Linux machine. Run

    Now restart your computer. After booting back up, you should be able to run the below command and follow the rest of the steps like normal

    I'm stuck at 1108/1112 when running sfoundryup .

    Some machines take up to an hour to do this step. If it takes longer, ask a question in our discord's #devnet channel.

    I'm getting Command failed: cargo build --bins --release.

    Means your machine doesn't have cargo. If you're on Linux, run

    I'm getting jq (command not found).

    Means step #2 didn't work. If you're on Linux, run

    I'm getting Address not funded. Please check if your faucet transaction went...

    Means your wallet has no testnet ETH. Please go to the faucet, enter the address the script gave you, and wait for the green confirmation.

    I'm getting Command 'brew' not found.

    Means your machine doesn't have the Homebrew package manager. Run

    I'm getting linker 'cc' not found.

    You can resolve by running

    I'm getting command not found: sfoundryup .

    If this comes up even after you complete step #3 successfully, restart your terminal. Should be able to run it after.

    I'm getting info: aborting installation .

    Means you aren't selecting an option for your Rust installation. Run the curl command again, and press Enter.

    I'm getting Command: 'bun' not found.

    You need to add bun to your PATH. You can either do this temporarily in your current terminal via the below command (you'll have to do it for every new window):

    Or set it properly, by opening up your ~/.bashrc and adding

    View official links

    Item
    Value

    Network Name

    Seismic devnet

    Currency Symbol

    ETH

    Chain ID

    5124

    RPC URL (HTTP)

    RPC URL (WS)

    Explorer

    NOTE: This is a testnet with known decryption keys. Please don't put real information on it!

    FAQ
    our discord
    @lyronc
    FAQ

    Chapter 3: Reset Mechanism, Rounds, and a more conditional Kernel Reveal

    In this chapter, we’ll implement a reset mechanism that allows the Walnut to be reused in multiple rounds, ensuring each game session starts fresh. We’ll also track contributors per round so that only players who participated in cracking the Walnut can call look(). By the end, we’ll have a fully functional round-based walnut game where the kernel remains shielded until conditions are met! Estimated time: ~15 minutes.

    The need for a Reset mechanism

    Right now, once the Walnut is cracked, there’s no way to reset it. If a game session were to continue, we’d have no way to start fresh—the shell would remain at 0, and the kernel would be permanently revealed.

    To solve this, we need to introduce:

    ✅ A reset function that restores the Walnut to its original state.

    ✅ Round tracking, so each reset creates a new round.

    The need for a contributor check

    While the reset mechanism and round tracking allow us to restart the Walnut for continuous gameplay, they still don’t address who should be allowed to call the look() function.

    Right now, any player can call look() once the shell is cracked, even if they didn’t participate in hitting it during the current round. This creates the following issues:

    • Fairness: Players who didn’t contribute should not be able to reap the benefits of seeing the kernel.

    • Incentivizing Contribution: The game needs to encourage active participation by ensuring that only those who helped crack the Walnut in a specific round are rewarded with access to the kernel.

    The solution to this is implementing a conditional check on look() which allows only those players who contributed in hitting the shell for a particular round (i.e., players whose hit count is >0 for that round) to view the kernel after the walnut is cracked.

    Implementing the Reset Mechanism

    The reset mechanism allows the Walnut to be reused for multiple rounds, with each round starting fresh. It restores the Walnut’s shell and kernel to their original states and increments the round counter to mark the beginning of a new round.

    Here’s how we can implement the reset function:

    What’s Happening Here?

    • Condition for Reset (requireCracked): The reset function can only be called once the Walnut’s shell is cracked, enforced by the requireCracked modifier.

    • Restoring Initial State: The shell strength and kernel are reset to their original values (initialShellStrength and initialKernel), ensuring the Walnut starts afresh for the next round.

    Modifying hit() to track contributions

    To enforce fair access to the kernel, we’ll track the number of hits each player contributes in a given round. This is achieved using the hitsPerRound mapping:

    Every time a player calls the hit() function, we update their contribution in the current round:

    What’s Happening Here?

    • Tracking Contributions: The hitsPerRound mapping records each player’s hits in the current round. This ensures we can verify who participated when the Walnut was cracked.

    • Replayable Rounds: Because contributions are tracked by round, the game can fairly reset and start fresh without losing player data from previous rounds.

    Restricting look() with a contributor check

    To ensure only contributors can reveal the kernel, we’ll use a modifier called onlyContributor:

    We’ll then apply this modifier to the look() function:

    Congratulations! You made it through to writing the entire shielded smart contract for a multiplayer, multi-round, walnut app!

    Final Walnut contract

    Now, onto testing the contract!

    wsl --install
    wsl
    sudo apt update && sudo apt install -y build-essential
    sudo apt install cargo -y
    sudo apt-get install jq
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
    sudo apt update && sudo apt install -y build-essential
    sudo apt install cargo -y
    export PATH="/home/$(whoami)/.bun/bin:$PATH"
    PATH="/home/$(whoami)/.bun/bin:$PATH"
    curl https://sh.rustup.rs -sSf | sh  # choose default, just press enter
    . "$HOME/.cargo/env"
    brew install jq
    curl -L \
         -H "Accept: application/vnd.github.v3.raw" \
         "https://api.github.com/repos/SeismicSystems/seismic-foundry/contents/sfoundryup/install?ref=seismic" | bash
    source ~/.bashrc
    sfoundryup  # takes between 5m to 60m, and stalling for a while at 98% normal
    git clone --recurse-submodules https://github.com/SeismicSystems/try-devnet.git
    cd try-devnet/packages/contract/
    bash script/deploy.sh
    curl -fsSL https://bun.sh/install | bash
    cd try-devnet/packages/cli/
    bun install
    bash script/transact.sh

    Faucet

    https://faucet-2.seismicdev.net/

    Starter Repo

    https://github.com/SeismicSystems/seismic-starter

    https://node-2.seismicdev.net/rpc
    wss://node-2.seismicdev.net/ws
    https://explorer-2.seismicdev.net/
    Round Tracking: The round counter increments each time the Walnut is reset, allowing us to distinguish between rounds.
        // The current round number.
        uint256 round; 
        
        // Event to log resets.
        event Reset(uint256 indexed newRound, uint256 shellStrength);
        
        function reset() public requireCracked {
            shellStrength = initialShellStrength; // Restore the shell strength.
            kernel = initialKernel; // Reset the kernel to its original value.
            round++; // Increment the round counter.
            emit Reset(round, shellStrength); // Log the reset action.
        }
        // Mapping to track contributions: hitsPerRound[round][player] → number of hits.
        mapping(uint256 => mapping(address => uint256)) hitsPerRound;
        function hit() public requireIntact {
            shellStrength--; // Decrease the shell strength.
            hitsPerRound[round][msg.sender]++; // Record the player's contribution for the current round.
            emit Hit(round, msg.sender, shellStrength); // Log the hit event.
        }
         modifier onlyContributor() {
            require(hitsPerRound[round][msg.sender] > 0, "NOT_A_CONTRIBUTOR"); // Check if the caller contributed in the current round.
            _;
        }
        // Look at the kernel if the shell is cracked and the caller contributed.
        function look() public view requireCracked onlyContributor returns (uint256) {
            return uint256(kernel); // Return the kernel value.
        }
    // SPDX-License-Identifier: MIT License
    pragma solidity ^0.8.13;
    
    contract Walnut {
        uint256 initialShellStrength; // The initial shell strength for resets.
        uint256 shellStrength; // The current shell strength.
        uint256 round; // The current round number.
    
        suint256 initialKernel; // The initial hidden kernel value for resets.
        suint256 kernel; // The current hidden kernel value.
    
        // Tracks the number of hits per player per round.
        mapping(uint256 => mapping(address => uint256)) hitsPerRound;
    
        // Events to log hits, shakes, and resets.
    
        // Event to log hits.
        event Hit(uint256 indexed round, address indexed hitter, uint256 remaining);
        // Event to log shakes.
        event Shake(uint256 indexed round, address indexed shaker);
        // Event to log resets.
        event Reset(uint256 indexed newRound, uint256 shellStrength);
    
        constructor(uint256 _shellStrength, suint256 _kernel) {
            initialShellStrength = _shellStrength; // Set the initial shell strength.
            shellStrength = _shellStrength; // Initialize the shell strength.
    
            initialKernel = _kernel; // Set the initial kernel value.
            kernel = _kernel; // Initialize the kernel value.
    
            round = 1; // Start with the first round.
        }
    
        // Get the current shell strength.
        function getShellStrength() public view returns (uint256) {
            return shellStrength;
        }
    
        // Hit the Walnut to reduce its shell strength.
        function hit() public requireIntact {
            shellStrength--; // Decrease the shell strength.
            hitsPerRound[round][msg.sender]++; // Record the player's hit for the current round.
            emit Hit(round, msg.sender, shellStrength); // Log the hit.
        }
    
        // Shake the Walnut to increase the kernel value.
        function shake(suint256 _numShakes) public requireIntact {
            kernel += _numShakes; // Increment the kernel value.
            emit Shake(round, msg.sender); // Log the shake.
        }
    
        // Reset the Walnut for a new round.
        function reset() public requireCracked {
            shellStrength = initialShellStrength; // Reset the shell strength.
            kernel = initialKernel; // Reset the kernel value.
            round++; // Move to the next round.
            emit Reset(round, shellStrength); // Log the reset.
        }
    
        // Look at the kernel if the shell is cracked and the caller contributed.
        function look() public view requireCracked onlyContributor returns (uint256) {
            return uint256(kernel); // Return the kernel value.
        }
    
        // Set the kernel to a specific value.
        function set_number(suint _kernel) public {
            kernel = _kernel;
        }
    
        // Modifier to ensure the shell is fully cracked.
        modifier requireCracked() {
            require(shellStrength == 0, "SHELL_INTACT");
            _;
        }
    
        // Modifier to ensure the shell is not cracked.
        modifier requireIntact() {
            require(shellStrength > 0, "SHELL_ALREADY_CRACKED");
            _;
        }
    
        // Modifier to ensure the caller has contributed in the current round.
        modifier onlyContributor() {
            require(hitsPerRound[round][msg.sender] > 0, "NOT_A_CONTRIBUTOR");
            _;
        }
    }

    Mainnet

    Network Configuration

    Property
    Value

    Name

    Seismic

    Chain ID

    5123 (0x1403)

    Genesis date

    The mainnet genesis date will be announced publicly. Follow official channels for updates:

    Explorer

    Currently in development

    Contract verification

    The explorer will support the ability to verify contracts written in Seismic Solidity

    Chain type

    EVM L1

    RPC (HTTP)

    TBA

    RPC (WS)

    TBA

    Block time

    1 block per ~600ms

    Finality

    1 block (may become 2 blocks)