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...
A vision-centric overview of Seismic
The traditional financial system processes over $100 trillion in sensitive payment volume annually. The scale is clear even when considering just the top three categories: payroll distributions ($57 trillion), commercial revenue ($53 trillion), and housing payments ($24 trillion). Including healthcare, insurance, and treasury management pushes the figure even higher.
Public blockchains are poorly suited to handle this sensitive volume due to their reliance on transparency for consensus. Critical details— such as account balances, transfer amounts, and participant identities— are visible to all observers.
Such transparency is commercially and socially untenable. Businesses lose competitive edge, narrative control, and bargaining power. Consumers, likewise, are exposed to reputational and interpersonal losses.
Developers have long sought to resolve this issue by creating custom protocols that encrypt payment data. These offerings tend to focus on one of two value propositions:
Privacy. Your financial activity cannot be monitored by any third party. Not your payment processor, not your government.
Efficiency. Your transactions can settle in under a second, cost less than a cent, and have global reach.
The challenge is that, for mass market users, traditional financial services are sufficiently private and efficient. While encrypted stablecoin payments offer clear advantages, they are not compelling enough on their own to overcome the network effects of legacy payment systems.
The missing piece is a broader DeFi ecosystem around these encrypted payments. A business is unlikely to become an early adopter if the benefit is a 1% cost reduction on 1% of point-of-sale volume. However, if going on-chain enables access to DeFi services— such as converting revenue into BTC, then borrowing against the position— the experience begins to approach an order-of-magnitude improvement. This is why companies that want to target sensitive payments can’t focus solely on payments. They need to build markets around them.
The shift in perspective has implications beyond payments. It reframes the role of encrypted blockchains altogether. Historically, encryption has struggled to find product-market fit. Many attribute this to timing, or argue that encryption is a feature rather than a product. These critiques hold when encryption is applied to existing crypto volume, primarily one-off stablecoin transfers or memecoin trades, which genuinely don’t need encryption.
But encryption isn’t valuable because it improves services for today’s volume. It’s valuable because it unlocks new volume, the $100 trillion in sensitive TradFi transactions that public blockchains cannot reach. To reach that volume, product development must be centered around markets that provide value to sensitive payments.
This is challenging. Developers looking to act on this insight today must build complex, custom infrastructure using technologies such as zero-knowledge proofs (ZK), multi-party computation (MPC), fully homomorphic encryption (FHE), and trusted execution environments (TEE). Effectively leveraging these tools requires orchestrating provers, managing specialized indexers, distributing heavyweight client-side SDKs, and maintaining relayer clusters.
The technical overhead presents a significant barrier to entry. Today, only a handful of teams possess the expertise required to build encrypted DeFi protocols. And even for these teams, the need to invest months of engineering effort and tens of thousands of dollars in audits imposes meaningful friction on product development and growth.
A product-centric overview of Seismic
Seismic enables everyday developers to build encrypted DeFi.
Seismic is an L1 blockchain capable of encrypting arbitrary smart contracts. Deploying a single Solidity contract is sufficient to launch an encrypted DeFi protocol, no custom infrastructure required.
We achieved this by restructuring the modern blockchain stack around TEEs. This involved forking key components such as Solidity, Reth, and Foundry. By doing so, we were able to fully leverage the confidentiality guarantees of secure enclaves and eliminate the need for additional infrastructure.
This architecture unlocks the three core properties necessary to encrypt arbitrary smart contracts:
Encrypted global state. Allows functions to operate on encrypted state owned by multiple users. Essential for use cases such as matching orders in a dark pool or liquidating encrypted margin positions in a lending protocol.
Encrypted memory access. Enables the modification of elements in a collection without revealing their locations in memory. Essential for use cases like placing bids in a blind auction and transferring money in a shielded pool.
Encrypted orchestration. Supports the consolidation of all protocol logic within a single VM. Without it, developers must split logic between transparent and encrypted components across different environments, introducing significant engineering complexity.
These properties together restore powerful capabilities that are standard in vanilla DeFi, but notoriously difficult to achieve in encrypted environments. This is why developers building on Seismic can encrypt existing EVM contracts with minimal changes to the original.
Seismic is an encrypted blockchain
If you're interested in learning, we suggest:
If you're interested in building, we suggest:
Asking a friend (or @seismicMatt) to add you to our invite-only developer TG group.
If you're interested in community, we suggest:
A tech-centric overview of Seismic
We've restructured the modern blockchain stack around secure hardware. The major components are as follows:
At the core of this stack is the secure enclave. The term describes a set of hardware components that provide confidentiality measures over data in use, protecting it from being read by any outside entity, including the host machine. Our secure enclave of choice is Intel TDX.
Our system uses TDX by cloning all the primary memory segments of the EVM. This results in a set of encrypted segments and a set of transparent segments, where the former can leverage the confidentiality properties of the underlying hardware.
Data flows between these segments with cloned storage opcodes. For example, the vanilla EVM has SLOAD
/ SSTORE
to manage elements in storage, and our EVM adds CLOAD
/ CSTORE
to manage elements in encrypted storage. The same pattern is applied to calldata, transient storage, and memory.
These segments allow us to know whether every element added to the stack is considered transparent or encrypted. Then it becomes a matter of tracking these elements to enforce rules over how they interact and how they should be handled by the execution loop.
Our github repositories hold the v0 implementation for this. Like every v0, it's a far cry from the final spec. Notably, the current implementation only clones the storage segment and does not keep track of element status in the stack. It compensates by treating all memory segments as encrypted. This heavy handed approach has major drawbacks in both UX and liveness. However, it's enough to test our core hypothesis around encrypting protocols while decreasing our time to market, which makes it a great fit for our first release.
We believe these enclaves are sufficient to support entire blockchain ecosystems. And with the current momentum behind confidential compute, we expect the rate of improvement only increases from here.
Setting up your local machine to develop with Seismic
The local development suite uses sforge
as the testing framework, sanvil
as the local node, and ssolc
as the compiler.
Download and execute the sfoundryup installation script.
Install sforge
, sanvil
, ssolc
. Expect this to take between 5-20 minutes depending on your machine.
Remove old build artifacts in existing projects.
Starting with high-level context on the , , and behind Seismic.
Understanding the for interacting with Seismic.
Reading through our implementation on .
Setting up your local via , then deploying via .
Deriving inspiration from our early .
Talking to us in our channel.
If you end up writing a contract with Seismic, please send the github link to on TG! I'd love to chat.
The language is a fork of . We added stype
.
The execution client is a fork of , , and . We added encrypted storage and relevant opcodes.
The consensus middleware is . We used it off-the-shelf.
The consensus client is . We used it off-the-shelf.
The secure hardware build is a fork of manifests from flashbots. We added proxies.
The testing framework is a fork of . We added encrypted storage, along with the relevant opcodes.
The wallet client is an extension of . We added transaction types.
Notice, 99% of our stack is code written by the OS community. We're making sure to maintain this standard, which is why are fully open-source under an MIT License.
Our dependency on secure enclaves comes with strong trust assumptions. The most prominent one is on hardware confidentiality, which leaves us wary of . Our short-term mitigation for this is the restriction to cloud-based validators.
Though this assumption has led to in the past, we're cautiously optimistic about the most recent generation of VM-based enclaves like Intel's TDX and AMD's SEV-SNP. They function with an untrusted hypervisor and patch up major flaws present in the first generation.
Install [ / / / ] on your machine if you don't already have them. Default installations for all work well.
We also recommend adding syntax highlighting via the extension from the VSCode marketplace. If you have the solidity
extension, you'll have to disable it while writing Seismic code.
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.
Create the project folder and navigate into it:
Create the packages
directory with subdirectories for contracts
and cli
The contracts
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.
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.
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
):
Navigate to the contracts subdirectory:
Initialize a project with sforge
:
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)
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.
You're two commands away from running an encrypted protocol
• 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.
You can play around with stype
using our . This assumes you went through everything in .
Before continuing, ensure that you have completed the steps in the section to install all necessary Seismic developer tools:
Also ensure that you have installed on your machine. If you do not have bun
installed, follow the instructions to install it on your machine.
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.
Round Tracking: The round
counter increments each time the Walnut is reset, allowing us to distinguish between rounds.
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!
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
Now, 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!
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()
function initializes the Walnut contract for use in all test cases.
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.
Test out the file by running the following inside the packages/contracts
directory:
The contract has been tested, time to deploy it!
You can find the entire test file .
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
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.
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
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
), which represents how many hits it can withstand before cracking. Let’s define the shell and initialize it in the constructor:
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 hit
decreases 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"
.
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.
Navigate to the script folder in your Walnut App and open the Walnut.s.sol
file located at:
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.
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!
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:
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
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.
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/cli
with the files constants.ts
and utils.ts
and navigate to it:
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.
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()
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.
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.
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:
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.
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 . Estimated time: ~15 minutes
chain
: a well-defined object
Once initialized, it can then be used to perform wallet operations or shielded-specific
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
Emits the Hit
event to update all participants.
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.
shielded unsigned integer / shielded integer
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.
shielded boolean
A handle on stype unlocks all shielded computation and storage
Developers communicate to Seismic through the stype
. A thorough understanding of this one concept unlocks all shielded computation and storage. The 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
.
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.
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 packages/cli
:
Open .env
and paste the following:
What’s Happening Here?
• CHAIN_ID=31337
: 31337
is 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_PRIVKEY
and 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.ts
and 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 sanvil
node (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.
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.
All comparisons and operators for suint
/ sint
are functionally identical to uint
/ int
. The universal casting rules and restrictions described in apply.
The universal casting rules and restrictions described in apply.
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.
We assume familiarity with .
Let's step through a concrete example. We'll follow the lifecycle of a transfer()
tx for an variant. This variant shields user balances and transfer amounts:
The entire index.ts
file can be found
Congratulations! You've reached the end of the tutorial. You can find the code for the entire project .
Try out the developer testnet
Network Name
Seismic devnet
Currency Symbol
ETH
Chain ID
5124
RPC URL (HTTP)
RPC URL (WS)
Explorer
Faucet
Starter Repo
NOTE: This is a testnet with known decryption keys. Please don't put real information on it!
Using stype variables in arrays and maps
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:
You can write to these references:
Observers for any of these operations will not know which elements were read from / written to.
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.
Constructing transactions with stype variables
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 ).
For Mac. See instructions for your machine . Only step that isn't OS agnostic.
We recommend using to run commands as if you were on a Linux machine. Run
Some machines take up to an hour to do this step. If it takes longer, ask a question in #devnet
channel.
Means didn't work. If you're on Linux, run
Means your wallet has no testnet ETH. Please go to the , enter the address the script gave you, and wait for the green confirmation.
Means your machine doesn't have the package manager. Run
If this comes up even after you complete successfully, restart your terminal. Should be able to run it after.
All stype
variables can be stored in Solidity collections, much like their unshielded counterparts. They behave normally (as outlined in ) 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:
We can take the ERC20 variant discussed in the section and extend it further to shielded balances, transfer amounts, and now recipients.
Our viem docs can be found .
stype
as the key and value to a collection shields which element you're using.