Stream: general

Topic: Suspendable, code-private coroutines with Wizer


view this post on Zulip Roman Volosatovs (Sep 25 2026 at 17:14):

Hey everyone,

As some of you already know, I am one of the members of the team working on Starstream at Midnight Foundation, and I would like to share a bit on what we're working on and how we use Wizer.

Starstream is a language that uses coroutines as its core primitive and targets Wasm components.

An example Starstream program looks like this:

import { sha256_u64 } from "../target/wasm32-unknown-unknown/release/sha256lib.wasm";

abi IRequireHashPreimage {
    // `u64` used for illustration purposes because compiler doesn't implement `list<u8>` yet.
    fn consume(preimage: u64);
}

utxo RequireHashPreimage {
    storage {
        let mut _hash: u64;
    }

    main fn create(pub hash: u64) {
        _hash = hash;
        yield(IRequireHashPreimage);
    }

    impl IRequireHashPreimage {
        fn consume(preimage: u64) {
            let hash = sha256_u64(preimage);
            if (hash != _hash) {
                error;
            }
            resume;
        }
    }
}

script fn create_hash(input: u64) -> RequireHashPreimage {
    RequireHashPreimage::create(sha256_u64(input))
}

script fn consume_hash(utxo: RequireHashPreimage, input: u64) {
    utxo.consume(input)
}

The entrypoint into a Starstream program is called a coordination script. A coordination script can receive and construct UTXOs (coroutines). The UTXO coroutine runs until a yield, at which point it exposes a set of methods that can be called on it.

This example program exports a create_hash coordination script, which, given a (secret) u64 input, calls sha256_u64 implemented in Rust (and imported as a Wasm component) to hash it and then constructs and returns a RequireHashPreimage UTXO by calling main fn create with the hash.
After construction, the UTXO will yield, allowing methods to be called on it.
Whoever knows the original input can then call consume on the UTXO: it hashes the input, compares it to the stored hash and resumes back to the original yield point on success, spending the UTXO by reaching the end of main fn create.

From the runtime's perspective, the coordination scripts are just Wasm component function exports. UTXOs are resources exported by components, they are constructed by the function, outlive the call and represent the suspended coroutines.

Since UTXOs are meant to be long-lived and portable, we instantiate the UTXO's component once per constructor (main fn) call and use Wizer to snapshot each UTXO's instance individually.

The UTXO inputs consumed and UTXO outputs produced (along with a proof of Wasm execution, the ZK part, coming soon) then become a transaction object published to the public ledger.

One property we care about is privacy of the code, i.e. we want the runtime state produced by the Wasm component to be public (the suspended coroutine, UTXO), but not the component itself. To achieve that, we do one additional step: we parse the produced Wizer snapshot and extract values of all relevant state: globals, memory sizes and data segments per core module in the component snapshot. This then becomes part of the public, signed transaction object.

Later, when the UTXO is used as an input to another coordination script or a method is called on it, those in possession of the original Wasm component merge the previously extracted state back by rewriting the component.

So the full flow looks something like this:

  1. For each imported component, instrument it with Wizer
  2. For each UTXO input, produce a new Wasm component by injecting state into it
  3. For each constructed UTXO, snapshot the instance with Wizer and extract the state from the snapshot

As a result, we have suspendable guest-defined resources, which can be moved across arbitrary compute while preserving the privacy of code they originated from.

If you are curious to try this yourself, here is how, on https://github.com/LFDT-Nightstream/Starstream/tree/4cc14e26456ed0eab10ed7cf28300d36918b14a3 (current main):

Install binaries:

$ cargo install --locked --path ./starstream-cli
$ cargo install --locked --path ./starstream-ledger-cli
$ cargo install --locked --path ./starstream-ledger-node

Compile the example (the imported Rust library first):

$ cargo build --release --target wasm32-unknown-unknown -p sha256lib
$ starstream wasm -c examples/require_hash_preimage.star --output-component hash.wasm

Write the genesis (initial ledger state) file, which will call create-hash from the example above with an input of 42.

$ cat > genesis.toml <<EOF
[[scripts]]
contract = "$(starstream-ledger-cli digest ./hash.wasm)"
script = "create-hash"
args = ["42"]
EOF

Start the ledger node:

$ starstream-ledger-node --genesis genesis.toml --import ./hash.wasm &

Inspect the genesis:

$ starstream-ledger-cli genesis
[[outputs]]
contract = "bciqknicpvzese3thpr4c7tmpsyk7qgnlhwoaoxpvj5dtsgmfclxjbqa"
instance = "require-hash-preimage"
methods = ["96f39e3569ec878ecb0b6c1713b4e49f5ca0550d892364f385fcde951f92e5de"]
storage = "0182a8c490a982c68fff000182a8c490a982c68fff0002"

[[outputs.state]]
data = []
memories = [1]

[[outputs.state.globals]]
I32 = 1                   # resume point

[[outputs.state.globals]]
I32 = 1                   # utxo-context handle

[[outputs.state.globals]]
I64 = 9160066635133948930 # _hash

...

[[outputs.state]] # the imported Rust sha256 lib core module
memories = [1]

[[outputs.state.data]]
data = "67e6096a85ae67bb72f36e3c3af54fa57f520e518c68059babd9831f19cde05b01000000030000000c00000004000000040000000500000006"
memory_index = 0
offset = 61440

[[outputs.state.data]]
data = "08000000040000000700000008000000090000000a0000000b00000010000000040000000c0000000d0000000e0000000f0000006d5dcbd62c50eb637841a657711b8bb92b815b01bd8651ec0cb4c29ce4c9c704c0002f72757374632f616336386661613230633538636263636430316565373230386266336236653933613764376639362f6c6962726172792f7374642f7372632f7379732f73796e632f72776c6f636b2f6e6f5f746872656164732e7273002f72757374632f616336386661613230633538636263636430316565373230386266336236653933613764376639362f6c6962726172792f616c6c6f632f7372632f7261775f7665632f6d6f642e7273002f727573742f646570732f646c6d616c6c6f632d302e322e31312f7372632f646c6d616c6c6f632e727300617373657274696f6e206661696c65643a207073697a65203e3d2073697a65202b206d696e5f6f76657268656164000045f100002a000000b104000009000000617373657274696f6e206661696c65643a207073697a65203c3d2073697a65202b206d61785f6f76657268656164000045f100002a000000b70400000d00000072776c6f636b206f766572666c6f7765642072656164206c6f636b7396f000005d000000150000002c"
memory_index = 0
offset = 61504

[[outputs.state.data]]
data = "080000000400000010000000030000000c000000040000001100000072776c6f636b20686173206e6f74206265656e206c6f636b656420666f722072656164696e67000096f000005d0000003e000000090000006361706163697479206f766572666c6f77000000f4f00000500000001c00000005"
memory_index = 0
offset = 61984

[[outputs.state.globals]]
I32 = 61440

[[outputs.state.globals]]
I32 = 62589

[[outputs.state.globals]]
I32 = 62592

[[outputs.state]]
data = []
globals = []
memories = []

[[outputs.state]]
data = []
globals = []
memories = []

Write the admin key (defaults to sha256 hash of word admin for development purposes):

$ printf admin | sha256sum | cut -d' ' -f1 > admin.key

Attempt to consume the UTXO with invalid input:

$ starstream-ledger-cli contract script call --key admin.key --import ./hash.wasm $(starstream-ledger-cli digest ./hash.wasm) consume-hash :0 41
Error: failed to call function

Caused by:
    0: failed to call function
    1: error while executing at wasm backtrace:
           0:    0x7f6 - <unknown>!<wasm function 16>
           1:    0x86d - <unknown>!<wasm function 21>
           2:    0x876 - <unknown>!<wasm function 22>
    2: wasm trap: wasm `unreachable` instruction executed

Consume the UTXO with correct input:

$ starstream-ledger-cli contract script call --key admin.key --import ./hash.wasm --output-transaction tx.cbor $(starstream-ledger-cli digest ./hash.wasm) consume-hash :0 42
()

Show the transaction object (also published to the local ledger node):

$ starstream-ledger-cli transaction show tx.cbor
context = "starstream:transaction"
network = "dev"

[payload]
outputs = []
events = []
proof = ""

[[payload.inputs]]
transaction = ""
index = 0

Last updated: Oct 11 2026 at 04:10 UTC