ETH Cluj is tomorrow! How to build a blockchain and WASM excerpt
12th May 2026
Hello everyone! ETH Cluj in Romania is tomorrow. I’ve been working on having content ready for the workshop that Jack Smith Suidlander will be speaking at. Jack is scheduled to speak on the first day, the 13th of May, at 11:10AM at the Tech Stage.
I’ve done up a little writeup that accompanies his presentation that I wanted to share here. This article introduces how to build a blockchain and WASM machine, by introducing stack programming and public-private key cryptography. In the below, we’ve included a basic blockchain introduction. Later, the article introduces how to build a full WASM machine progressively using the same function. Not the WASM machine itself, but similar enough in the ways that matter for showing it off (in this author’s humble opinion).
I feel that it’s a fun technical read that provides some background we might take for granted. If you have any feedback, let me know! We’ve been supported by the following groups very graciously:
Gian with ARBuilder has very helpfully provided an AI integration in the website for answering any questions about Stylus!
Tolga from The Wizard has been very supportive with having the Stylus workshop items be supported easily via The Wizard.
Without further ado:
Stylus for beginners
A blockchain is a recursive data structure (or, structured commit log), of cryptographically signed blobs with attribution to whomever added the blob, verifiable with public-private key cryptography. We’ll explain what public-private key cryptography is soon, so don’t worry.
In our example code, commits are referred to as transactions. The recursive data structure (or, state machine), is summed by a program that remembers what it has seen to generate an end application state.
Imagine a calculator state machine that we’ll turn into a blockchain. The calculator state could look like the following:
interface Transaction {
Op: Op; // operation code
No?: number; // optional immediate value
C?: Transaction; // nested next transaction
Before?: any[]; // preceding context items (mixed types)
}With several operations that operate on the previous state:
enum Op {
Push = 0,
Add,
Mul,
Div,
}When the Op.Push operation is used (number 0), we provide a number that is used in our transitive storage of the operations. So, to create a formula of 10 + 20 * 30 (with the end result being 610), our state machine would look like the following:
const example1: Transaction = {
Op: Op.Add,
C: {
Op: Op.Push,
No: 10,
C: {
Op: Op.Mul,
C: {
Op: Op.Push,
No: 20,
C: {
Op: Op.Push,
No: 30,
},
},
},
},
};
The equivalent to the above is:
10 + (20 * 30)A program could pattern match recursively the structure and find the answer pretty quickly. But, the above is just a state machine. How do we turn it into a blockchain? Using our above example, let’s add four more fields:
interface Transaction {
Op: Op; // operation code
No?: number; // optional immediate value
C?: Transaction; // nested next transaction
Before?: any[]; // preceding context items (mixed types)
SubmitterPubKey?: string; // 65-byte public key (hex-encoded)
R?: string; // 32-byte signature R (hex-encoded)
S?: string; // 32-byte signature S (hex-encoded)
}These fields are used to interact with public-private key. Public-private key cryptography is a cryptographic system to prove that someone, known by their public key, created the content that we wanted to verify is theirs.
Public-private key cryptography
In our below example, we’ll work with a type of Elliptic Curve Cryptography (ECC) called the secp256k1 system.
Private keys are random bytes that were chosen by your computer. Private keys create Public keys, a combination of the aforementioned random bytes chosen, combined with the hardcoded Generator point for the Elliptic Curve System in use, for us the secp256k1 system, a very large number.
Public keys are created with this multiplication:
Public keys are used to prove that the private key attested something: imagine that you were to make a proclamation like “I love dogs!”: you could create a cryptographic proof that you said that statement by signing it using your private key.
The multiplication above is a type of operation called modular arithmetic over a finite field. It differs from ordinary multiplication: it’s a completely different system that we won’t elaborate on for now.
So this means that the owner of the private key can sign data, and whoever possesses the public key at the time can validate the public key signed the data. To recap:
Private keys: create public keys and sign content to produce signatures.
Signatures: cryptographic proof that the owner of a public key created the content that was signed.
Public keys: created from the private key
flowchart LR
PrivateKey[Private key]
-->|Signs content, creating a...| Signature
-->|Which is verified with...| PublicKey[Public key]
PrivateKey -->|Creates...| PublicKey
You might imagine that this system is useful for banking and for voting (for example).
What does a signature look like? Let’s write some Go code. Go has great library support for working with the signature type we’re discussing here. The following code will emit private keys, public keys that are compressed in size, and signatures:
package main
import (
"bytes"
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"fmt"
"os"
ethCrypto "github.com/ethereum/go-ethereum/crypto"
)
func main() {
// Read from stdin, saving it to a buffer:
var buf bytes.Buffer
if _, err := buf.ReadFrom(os.Stdin); err != nil {
panic(err)
}
// Use the system randomness to generate random bytes
// for our private key:
prkey, err := ethCrypto.GenerateKey()
if err != nil {
panic(err)
}
// Which we print using hex to stderr:
fmt.Fprintf(os.Stderr,
"private key: %x, pubkey: %x\n",
prkey.D,
ethCrypto.CompressPubkey(&prkey.PublicKey),
)
// Later to create a sha256 signer, which we feed the
// input from stdin into to create a digest:
x := sha256.New()
if _, err := buf.WriteTo(x); err != nil {
panic(err)
}
// Finally, we sign the input we provided from stdin:
r, s, err := ecdsa.Sign(rand.Reader, prkey, x.Sum(nil))
if err != nil {
panic(err)
}
fmt.Printf("r: %v, s: %v\n", r, s)
}We needed the go-ethereum code since the secp256k1 curve is not supported by the Go standard library. Private keys are only able to be used to sign blobs that are hashed, a transformation that we apply to the data called hashing.
Hashing is the process of taking data, then applying transformations on it until it fits a specific size that is unique to the data that was inputted. The most common hashing strategy is sha256, which you may have seen in other places involving file integrity. Hashing isn’t just important for integrity of data: it’s essential to coming up with keyed datastructures including maps and hashtables, pointer protection, and more. With the secp256k1 signature system in the Go code, we use sha256.
If we run this code, the program output would be:
b % ./private-key-picker
hello
private key: 5fe1956dc65de3c7b6cae1cdd4078672c0ce75e9a9138cdd95e39e1fd99e692d, pubkey: 02307380e710633086506f0b5d9878fb91e19f574caf749bcf5ae23cfc50495fe8
r: 46675391592507098461744368311622675236959728188104541593922010388651068885271, s: 71421556357399114972604334027290112648460025933014628472725999421029190949770This Go program, using the randomness source on whatever operating system it runs on (in my case, the nonblocking virtual file /dev/urandom ) reads some random data that’s then used to create the private key. After creating the private key, it also created a public key using finite field arithmetic. In the Go code that we wrote from above, we’re making use of a cryptographic library called BoringSSL, a fork of OpenSSL, that the Go code FFI’s into.
Once it’s done that, it takes the standard input that we provided to the program (in our case, “hello\n”), and signs it. We won’t touch on signing here, but note that it’s an involved mathematical operation.
Let’s note that with secp256k1, uncompressed public keys are 65 bytes (520 bits) large. Private keys are 32 bytes (or 256-bit) large. In our above program, we’ve returned an abbreviated public key that’s 33 bytes large, after the compression pass that took place. The first byte is an indicator about how to derive the public key, which again we won’t dive into.
A signature is made up R and S values. We use these two values to recover a signature to get the public key of the signature (this operation is known as ecrecover). When it comes to R and S, they’re similarly large (256 bits) large. To keep the signature scheme hard to defeat where someone could get the public key by estimating private keys, this signature scheme is hard for processors to derive public keys with.
How could we apply this program and signature scheme to the system from above? Let’s sign the input to the machine above, using JSON as the encoding method:
b% echo -n '{"op":0,"no":30}' | ./private-key-picker
private key: 8b58f9b91384a9b4edcd33e87b5ee4b06d99e0840ff7e55d9efe93422699f633, pubkey: 02f3e02d856255ed62a63d31d60d3097a343fb9d57b1820034ab5b1a911e9ed8a8
r: 96160680372721079114515954144798968965976538873393357704671109455747708845508, s: 106340575424520363008010994198042967461407413579147027606774442838620301825513This would let us create the transaction object from before like this:
const example2: Transaction = {
Op: Op.Push,
No: 30,
SubmitterPubKey: "02f3e02d856255ed62a63d31d60d3097a343fb9d57b1820034ab5b1a911e9ed8a8",
R: "96160680372721079114515954144798968965976538873393357704671109455747708845508",
S: "106340575424520363008010994198042967461407413579147027606774442838620301825513",
};The R and S fields are base 10, though they might have been encoded with hex for the same effect (we just haven’t here). Anyone can make use of the R and S fields within this system to validate that the author of each Transaction blob for the calculator, the public key, created the op and number here for addition.
How do we give our blockchain meaning? We chose the calculator example, since it’s a state machine everyone understands that may or may not depend on being commutative (able to take arguments independently). Let’s extrapolate it further.
What are transactions?
We need to figure out a system to guarantee the order of operations prior to someone adding to the history. We can’t have someone supplying an operation to perform a division when the inputs aren’t what we expect!
The simplest solution is to ask the users to sign the content or the hash of the previous blob in the chain, enforcing ordering to keep our system consistent.
If we were to do the sign the final step in the operation chain from before, but this time unlike the previous example of just the inputs the user is immediately responsible for we include everything that came before:
b% echo -n '{"op":1,"c":{"op":0,"no":10,"c":{"op":2,"c":{"op":0,"no":20,"c":{"op":0,"no":30}}}}}' | ./private-key-picker
private key: c5406d7b5e55aa41352a96f5772641e6cb446b0a4109e451555acfdb8ca320fb, pubkey: 027956e423088cbf5cd3896d979abaf681c153c02292a8d29856fc07358e022a7c
r: 109490587954065224018665916114696439326893613653236139668140228769916996904930, s: 52847444640315674408619378992306125231504692620405526333529832338143970682812We could only append to the chain with a valid signature so it’s not possible to reconstruct our step in the calculator chain with something that the signer didn’t expect. Using the previous input, including the signature guarantees some level of consistency here with history.
This works well, but there’s an issue with this system: these steps form the basis of what takes place in our chain (”on-chain”), but they require “liveness” from the senders of the data here. The signer of the step in the calculator chain must be online to sign the previous interaction into their signed blob.
If we could do this in a way where the signer identified constraints instead of specifying literally what it wants before it, it would be better. This way we’re not beholden to a specific configuration beyond the scope of what the user is presumably trying to add. Someone could easily specify that they’re expecting a specific number was supplied before.
We would need to translate our calculator system to resemble the following:
const example3: Transaction = {
Op: Op.Add,
Before: [Op.Push, 10],
C: {
Op: Op.Push,
No: 10,
Before: [Op.Mul],
C: {
Op: Op.Mul,
Before: [Op.Push, 20],
C: {
Op: Op.Push,
No: 20,
Before: [Op.Push, 30],
C: {
Op: Op.Push,
No: 30,
},
},
},
},
};This affords us an opportunity to say what came before our step in the chain of interactions. But the problem here is that we can’t constraint perfectly what came before without knowing ahead of time clearly what we expect. Over time the inputs would blow out exponentially.
Let’s flatten the structure to explain how we might improve on this by improving the flexibility of the constraints we use. Let’s add another layer of type above it to act as a container, a “block” for the recursive transaction type by redefining it:
interface Transaction {
Op: Op; // Operation code
No?: number; // Optional immediate value
Before?: any[]; // Constraints for previous calculator operations
SubmitterPubKey?: string; // Transaction public key (hex encoded)
R?: string; // R part of the signature for this transaction
S?: string; // S part of the signature for this transaction
}
interface Block {
Transaction: Transaction[]; // Transactions in theb lock
B?: Block; // Next block
BlockProducerPubkey: string; // Hex encoded public key
R: string; // R part of the signature
S: string; // S part of the signature
}This new container type is the Block, the essential container for a blockchain, containing transactions. This is better, this improves on the existing chain of transactions that we made up by specialising a specific producer of the history, the Block producer (the one who signs the blocks).
It affords the transaction signer the opportunity to work non-interactively with this system by not requiring liveness (they can sign a blob and disseminate it with the other people interested in this chain): they can just sign the transaction they want like this:
b % echo -n '{"op":0,"no":20,"before":[0,30]}' | ./private-key-picker
private key: f58dd4d235754c9fc2750d2e55e1ba67638a0a5f3d41cf931400209fc6b92f6, pubkey: 024f476e1b9368ae50ab34573bc7b0be7c590c9f634796d4e0ad072d8b21108ba1
r: 13701538462231091527866200836950950743407758947180296657773133663066990370872, s: 113336431400841648482223458012748904845724013047034353917738642742656731282990Then share it to the Block producer:
# Note that I've not included in the transaction object the public key
# for the signer for brevity reasons:
b % echo -n '{"transaction":[{"op":2,"before":[0,30]}],"b":{"transaction":[{"op":0,"no":20,"before":[0,30]}],"block_producer_pubkey":"0385550d6b0cb13676a6769db36b775b8d49a1be71f6449f8785cd73bc1ffd8e3d","r":"86030860734043180344371311626322149138169356980983536493285126474363242170697","s":"86030860734043180344371311626322149138169356980983536493285126474363242170697"}}' | ./private-key-picker
private key: 5c8b95f23979f3f6e3c70f502ddfdd8f26eb94cb5c9ec8e82d514f79785f6f54, pubkey: 0306271c7a9957cef4b34be8627b9a56580593a187d799c478324e2dbae64ca704
r: 58746628405342652628129852483027334912560882418649680529364128475182863919908, s: 37375918562291893242652039551230332408939357744305368896508701819625797352813
Which would result in this layout:
const block: Block = {
Transaction: [
{
No: Op.Mul,
Before: [Op.Push, 30],
SubmitterPubkey: "...",
R: "...",
S: "...",
},
],
B: {
Transaction: [
{
No: Op.Push,
SubmitterPubkey: "...",
R: "...",
S: "...",
},
],
R: "...",
S: "...",
},
R: "...",
S: "...",
};Users of the calculator system still struggle with the problem of the chain of operations being able to be reordered in a way that distorts the calculator’s desired state by the signers.
With the block structure though, we trust the submitter to order things in a way that we like. But how can we improve the granularity of the constraints we want to establish with the calculator?
How can we have programmability on our blockchain?
Let’s discuss programming languages on our blockchain. We want our blockchain to have some way of checking if the state in the blockchain is what we expect. The easiest way is to introduce a machine for people to program with that we emulate on the fly to enforce the constraints and even add to the current state of the operations.
One of the best machines we can implement is a stack machine. A stack machine is a programming paradigm where a CPU (on the metal, or otherwise) reads from a tape of operations (the code), which it uses to manipulate a separate queue of operations, aka the stack. It’s one of the best options to implement since it’s one of the simplest.
A simple stack machine that functions like a calculator might be the following:
PUSH 0x1e0f3
PUSH 0x1c8
ADD
RETURNThis stack machine puts two items on the stack, the number 123123 (0x1e0f3) and 456 (0x1c8), and it adds them together. An interpreter for our simple stack machine might work like this in Python, also supporting some branching:
def execute(code, stack = None, pc = 0):
(op, imm) = code[pc]
match op:
case "PUSH":
return execute(code, (imm, stack), pc + 1)
case "ADD":
x = stack[0]
y = stack[1][0]
return execute(code, (x + y, stack[1][1]), pc + 1)
case "RETURN":
return stackThe Python uses tuples and recursion to implement our simple machine simply. If we were to invoke it like this:
if __name__ == "__main__":
code = [
("PUSH", 123123),
("PUSH", 456),
("ADD", None),
("RETURN", None)
]
print(execute(code))We would get the result 123579. To achieve our goal of regulating the chain’s state using the stack machine, let’s add to our stack interpreter to add jumps with branching, and a feature to explicitly set the program counter (where in the list of instructions we read from).
Our new stack feature adds a GT operation which checks if the value at the top of the stack is larger than the value next on the stack. Our stack program also returns the return status of the program as a tuple now, which we can use to check if the program executed correctly.
def execute(code, stack = None, pc = 0):
(op, imm) = code[pc]
match op:
case "PUSH":
return execute(code, (imm, stack), pc + 1)
case "ADD":
x = stack[0]
y = stack[1][0]
return execute(code, (x + y, stack[1][1]), pc + 1)
case "RETURN":
return (stack, True)
case "GT":
x = stack[0]
y = stack[1][0]
return execute(code, (x > y, stack[1][1]), pc + 1)
case "JUMPIF":
dst = stack[0]
x = stack[1][0]
pc = pc + 1
if x:
pc = dst
return execute(code, stack[1][1], pc)
case "REVERT":
return (stack, False)We could now implement a program that checks if the head of the stack is larger than 10:
if __name__ == "__main__":
code = [
("PUSH", 2),
("PUSH", 10),
("GT", None),
("PUSH", 6),
("JUMPIF", None),
("RETURN", None),
("REVERT", None)
]
print(execute(code))Which returns (None, False)after jumping to REVERT. If revert did not come to pass (the value is less than 10), we would’ve reached the return statement by simply passing the jump operation.
This example is interesting, but it is deterministic and has no value for us. We need some way to pass in code from the broader blockchain context. Perhaps some lookback features to ask what was in the previous transactions?
Let’s add a feature that lets us check what the currently accumulating value in the calculator is, using an operation called CURBLOCKVALS.
def execute(calc_acc, code, stack = None, pc = 0):
def loop(stack, pc):
(op, imm) = code[pc]
match op:
case "PUSH":
return loop((imm, stack), pc + 1)
case "ADD":
x = stack[0]
y = stack[1][0]
return loop((x + y, stack[1][1]), pc + 1)
case "RETURN":
return (stack, True)
case "GT":
x = stack[0]
y = stack[1][0]
return loop((x > y, stack[1][1]), pc + 1)
case "JUMPIF":
dst = stack[0]
x = stack[1][0]
pc = pc + 1
if x:
pc = dst
return loop(stack[1][1], pc)
case "REVERT":
return (stack, False)
case "CURBLOCKVALS":
return loop((calc_acc, stack), pc + 1)
return loop(stack, pc)Note that in the above we add a new argument, calc_acc. calc_acc is the accumulated value on the blockchain of the previous calculator operations. Armed with the above, we could amend our example from earlier to include this operation:
if __name__ == "__main__":
cur_calc_state = 30
code = [
("PUSH", 10),
("CURBLOCKVALS", None),
("GT", None),
("PUSH", 6),
("JUMPIF", None),
("RETURN", None),
("REVERT", None)
]
print(execute(cur_calc_state, code))
This way of getting information in from the outside world lets us implement code that acts as a restriction for the state that enters the blockchain. This could be very expressive, depending on what we support. Indeed, the Solidity and Vyper programming languages compile to a stack-based language, and as you’ve seen, the sky’s the limit with what’s possible to build!
Returning to our example, what if we actually changed it that the transactions no longer include the value we want to append to the blockchain state, but instead the stack language appends the state itself?
That would be a very interesting idea, as it would expand the scope of the functionality our machine supports. We could have the code RETURN the value that it has, using the last item on the stack. That would invert the calculator concept! This adds feasibly infinite programmability to our machine.
Let’s add a simple operation that just adds to the chain a number:
PUSH 10
RETURNThis would simply add “10” to the chain. Let’s bake this stack language into our transaction system from before:
interface Stack {
Op: string;
Val: number;
}
interface Transaction {
Code: Stack[];
SubmitterPubKey: string;
R: string;
S: string;
}Note that Transaction now no longer includes the Op and Val fields, only now having a stack. It would look like this possibly:
const tx: Transaction = {
Code: [
{ Op: "PUSH", Val: 10 },
{ Op: "RETURN", Val: 0 },
],
SubmitterPubkey: "...",
R: "...",
S: "...",
};So if we wanted to add some code that multiplies the current machine by 100 if the number is more than 123, we could leverage a MUL operation that we’ll just assume exists here:
// We'll emit from below the R, S, V, and public keys:
const block: Block = {
Transaction: [
{
Code: [
{ Op: "CURBLOCKVALS", Val: 0 },
{ Op: "PUSH", Val: 123 },
{ Op: "GT", Val: 0 },
{ Op: "PUSH", Val: 6 },
{ Op: "JUMP", Val: 0 },
{ Op: "REVERT", Val: 0 },
{ Op: "PUSH", Val: 100 },
{ Op: "MUL", Val: 0 },
{ Op: "RETURN", Val: 0 },
],
SubmitterPubKey: "",
R: "",
S: "",
},
],
B: {
Transaction: [
{
Code: [
{ Op: "PUSH", Val: 1000 },
{ Op: "RETURN", Val: 0 },
],
SubmitterPubKey: "",
R: "",
S: "",
},
],
BlockProducerPubkey: "",
R: "",
S: "",
},
BlockProducerPubkey: "",
R: "",
S: "",
};So this would set 1000 to the blockchain state, which the following program would then check to see if it’s greater than 100, and if it is, then it would multiply it by 100. But only if RETURN ends up taking place! If REVERT happened, then we could presumably just NOT add to the history.
With this stack-based system, we can see it’s possible to create circumstances which separate the participant from their liveness in the system. Looking back at what we’ve shown off here:
In a blockchain, people sign and share transactions, where they’re included in a history of things that took place, before being aggregated together to produce a final state: blockchains are state machines where the history is signed by people participating.
We’ve established that a specialised producer of history for the blockchain is titled a block producer, and they aggregate transactions, which are loaded sequentially by whoever is responsible for the history at the time. 3. We’ve established that we can implement a stack machine to have programming on our blockchain, to have state that’s predicated on what’s been included before.
That’s all for now! Continue from here: https://stylus-developers-guild.github.io/ethcluj-2026/chapter-3.html#introducing-a-real-blockchain-arbitrum for an introduction to Arbitrum, but first, how to build more exotic machines that more closely resemble WASM. Also how the Stylus machine works under the hood, and why.

