Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Learn how to build, deploy, and play with the Walnut App, your first Seismic-powered shielded contract game, in this hands-on tutorial.
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.
Setting up your local machine to develop with Seismic
Before you begin, make sure your machine meets the following requirements:
x84_64 or arm64 architecture
MacOS, Ubuntu, or Windows
The local development suite uses sforge as the testing framework, sanvil as the local node, and ssolc as the compiler.
Install and on your machine if you don't already have them. Default installation works well.
Download and execute the sfoundryup installation script.
Install sforge, sanvil, ssolc. Expect this to take between 5-20 minutes depending on your machine.
(Optional) Remove old build artifacts in existing projects. You can ignore this step if you aren't working with existing foundry projects.
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.
You've arrived at the Seismic docs!
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.
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.
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.
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.
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 .
git clone "https://[email protected]/SeismicSystems/seismic-starter.git"
cd seismic-starter/packages/contracts
sforge test -vvcurl https://sh.rustup.rs -sSf | shcurl -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 ~/.zshrcsfoundryup
source ~/.zshenv # or ~/.bashrc or ~/.zshrcsforge clean # run in your project's contract directoryBefore 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.
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)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.
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()
shake• 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.
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.
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.
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.
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)
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 // 1Navigate to the contracts subdirectory:
cd packages/contractsInitialize a project with sforge :
sforge init --no-commit && rm -rf .githubThis 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)
Edit the .gitignore file to be the following:
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.
Create the project folder and navigate into it:
Create the packages directory with subdirectories for contracts and cli
The contracts
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.
Navigate to the contracts subdirectory:
Initialize a new bun project
Documentation on Seismic's deploy repo
You can find our deploy tools
Documentation for these tools will be published here soon
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)
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.solsbool a = sbool(true)
sbool b = sbool(false)
// == EXAMPLES
a && b // false
!b // truecliInitialize 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.
Replace the default package.json with the following content for a monorepo setup:
Add the following to the .prettierrc file for consistent code formatting:
Replace the.gitignore file with:
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-appNow, create an src/ folder and move index.ts there.
Now, edit package.json to be the following:
Edit .gitignore to be:
Your environment is now set!
# Assuming you are currently in the contracts directory
cd ../clibun init -ymkdir -p src && mv -t src index.tsmkdir -p packages/contracts packages/clibun 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_modulesIn 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.
Navigate to the script folder in your Walnut App and open the Walnut.s.sol file located at:
packages/contracts/scriptand 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.
In a separate terminal window, run
in order to spin up a local Seismic node.
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.
Now, from packages/contracts, run
Your contract should be up and deployed to your local Seismic node!
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.
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();
}
}sanvilRPC_URL=http://127.0.0.1:8545
PRIVKEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80Next, 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 \
--broadcastIn 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:
Start by importing all the necessary modules and functions at the top of app.ts:
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.
The App class manages player-specific wallet clients and contract instances, providing an easy-to-use interface for multiplayer gameplay.
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.
These helper methods ensure that the app fetches the correct wallet client or contract instance for a specific player, supporting multiplayer scenarios.
getWalletClient :
getPlayerContract :
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.
The Seismic EVM is approximately a superset of the EVM
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
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
CLOAD – load shielded data from storage
CSTORE – write shielded data to storage
TIMESTAMP_MS – get the block timestamp in milliseconds
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)
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
Seismic uses the same staking contract as Ethereum, which is hardcoded into our Genesis block at address 0x00000000219ab540356cbb839cbe05303d7705fa
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
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
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
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:
chain : a well-defined object
transport : the method of transport of interacting with the chain (http /ws along with the corresponding RPC URL)
privateKey: the private key to create the client for
Once initialized, it can then be used to perform wallet operations or shielded-specific
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:
abi : the ABI of the contract it is interacting with.
address : the address of the deployed contract it is interacting with.
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.
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.
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.
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/libconstants.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.
Join our Discord
Seismic
documentation
Seismic solidity extensions
For partnerships, contact L@, T@ or M@ seismic (dot) systems
If you would like to work at Seismic, email your resume to M@
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.
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()
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
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
# Assuming you are in packages/cli/lib
cd ../src
touch app.tsEncryption 0x66
Decryption 0x67
HKDF 0x68: generate a cryptographic keys from a parent key
Secp256k1 0x69: Sign a message given a secret key
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;
}
import { join } from 'path'
const CONTRACT_NAME = 'Walnut'
const CONTRACT_DIR = join(__dirname, '../../contracts')
export { CONTRACT_NAME, CONTRACT_DIR }Start off with testing the basic functionalities, hit , shake , look and reset
Basic hit functionality
Ensures the Walnut’s shell can be cracked by shellStrength number of hits.
Basic shake functionality
Validates that shaking the Walnut increments the kernel value.
Reset functionality
Now, test for the restrictive/conditional nature of these basic functionalities.
Preventing hit when shell is cracked
Ensures that hitting a cracked shell is not allowed.
Preventing shake when shell is cracked
Ensures that shaking the Walnut after the shell is cracked is not allowed.
Preventing look when shell is intact
Ensures that the kernel cannot be revealed unless the shell is fully cracked.
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.
Sequence of Multiple Actions
Ensures that the Walnut behaves correctly under a sequence of hits and shakes.
Prevent Non-Contributors From Using look()
Ensures that only contributors in the current round can call look() .
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/cliOpen .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)
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 the required libraries to read environment variables, define network configurations, and interact with the Walnut contract.
This function initializes the contract and player wallets, then runs the game session.
The contract’s ABI and deployed address are read from files generated during deployment.
Determine whether to use the local sanvilnode (31337) or the Seismic devnet.
Assign Alice and Bob as players with private keys stored in .env .
Create an App instance to interact with the Walnut contract.
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)
This ensures that the script runs when executed.
The entire index.ts file can be found here
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 testtouch .envCHAIN_ID=31337
RPC_URL=http://127.0.0.1:8545
ALICE_PRIVKEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
BOB_PRIVKEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690dimport 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 revertstypesuint / 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:
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:
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.
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.
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.
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.
There are two restrictions in how you can use stype variables:
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.
You can't use them as constants.
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.
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.
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.
A counter that increments with each new round/reset, ensuring every round has a fresh Walnut to crack.
A mapping that records every player’s contribution to the current round, ensuring only participants can peek at the Walnut’s secret.
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
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.
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 enforce the rules of the game:
Ensures that look() can only be called if the Walnut’s shell is completely cracked.
Ensures that shake() and hit() can only be called if the Walnut’s shell is intact.
Restricts access to look() , and hence the number being revealed, only to players who contributed at least one hit in the current round.
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.

Codebase for encryption, TEE & on-chain verification
Most of the repositories here are forks of the reth stack
fork of alloy-rs/core
This is the repo that depends on nothing else
Upstream: version 1.1.2, commit e55993f
Analogous to alloy-rs/op-alloy, but not a fork of it
Depends on:
seismic-alloy-core
alloy-rs/alloy
fork of alloy-rs/trie
Depends on seismic-alloy-core
Upstream: version 0.8.1, commit a098d3f
fork of bluealloy/revm
Depends on:
seismic-alloy-core
seismic-enclave
Upstream: version 23.1.0, commit b287ce02
fork of alloy-rs/evm
Depends on:
alloy-rs/alloy
seismic-alloy
seismic-alloy-core
seismic-revm
Upstream: version 0.9.1, commit
fork of paradigmxyz/revm-inspectors
Depends on:
seismic-alloy-core
alloy-rs/alloy
seismic-revm
Upstream: version 0.22.3, commit
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
fork of foundry-rs/compilers
Depends on:
seismic-alloy-core
Upstream: version 0.16.1, commit ec745cec
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
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
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:
A repository containing tools to deploy infrastructure
Seismic forked Flashbots' stack for reproducible TEE builds. This includes these repos:
A Yocto layer that configures how the image runs Summit, Reth & the enclave server
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 ).
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.
The shell determines the Walnut’s resilience. It has an integer strength (shellStrength
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
No. Seismic uses trusted execution environments (TEE) via Intel TDX for privacy, not zero-knowledge proofs.
Seismic currently supports archival nodes only
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
There are instructions to deploy a node in our deploy repo. There are two steps:
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
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
Seismic uses Azure's Confidential Computing with Intel TDX to run our nodes. We are also planning to support bare metal TDX as well
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
No rate limits are currently imposed by the protocol itself, though node operators may implement their own.
Yes, --rpc.txfeecap. We use reth's default, which is 1.0 units of the native token (e.g. 1.0 ETH on testnet)
Yes, this is controlled through the arg --rpc.max-response-size. We use reth's default, which is 160MB
No. Just like in reth, there's no limit on batch count. The only limit comes from total payload size (above)
This is the same as reth's maximum payload size for general RPC requests: 160MB
Yes, archival nodes support complete log look back and retrieval of contract events from the beginning of the chain
We only support archival nodes. Make RPC calls to them with block filters
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
Yes, use eth_blockNumber to check current block height and sync progress
We haven't thought about this yet
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
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
Mainnet has not launched yet, so no. In testnet, various incidents have occurred, but these have been resolved prior to public release
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:
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.
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
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".
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");
_;
}
}For Mac. See instructions for your machine here. Only step that isn't OS agnostic.
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!

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.
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.
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.
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.
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.
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 --installwslsudo apt update && sudo apt install -y build-essential
sudo apt install cargo -ysudo 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 -yexport 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 jqcurl -L \
-H "Accept: application/vnd.github.v3.raw" \
"https://api.github.com/repos/SeismicSystems/seismic-foundry/contents/sfoundryup/install?ref=seismic" | bash
source ~/.bashrcsfoundryup # takes between 5m to 60m, and stalling for a while at 98% normalgit clone --recurse-submodules https://github.com/SeismicSystems/try-devnet.git
cd try-devnet/packages/contract/bash script/deploy.shcurl -fsSL https://bun.sh/install | bashcd try-devnet/packages/cli/
bun installbash script/transact.shFaucet
Starter Repo


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");
_;
}
}Name
Seismic
Chain ID
5123 (0x1403)
The mainnet genesis date will be announced publicly. Follow official channels for updates:
Currently in development
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)