Hints stream
The hints stream accelerates proof generation by offloading expensive operations outside the zkVM execution and feeding the results back as verifiable data through a high-performance, parallel pipeline. Hints are preprocessed results that allow operations to be handled externally while remaining fully verifiable inside the VM.
Overview
The system supports two categories of hints:
- Precompile hints — cryptographic operations (SHA-256, Keccak-256, elliptic curve operations, pairings, etc.) that are computationally expensive inside a zkVM.
- Input hints — data that needs to be passed to the zkVM as input during execution.
The pipeline is designed around three core principles:
- Pre-computing results outside the VM — the guest program emits hint requests describing the operation and its inputs.
- Streaming results back — a dedicated pipeline processes these requests in parallel, maintaining order, and feeds the results to the prover via shared memory.
- Verifying inside the VM — the zkVM circuits verify that the precomputed results are correct, avoiding the cost of computing them inside the zkVM.
:::warning Assembly executor only
Hints are only supported by the Assembly executor. The
emulator-based executor does not use the hints pipeline at all,
so the rest of this guide assumes you are building the
ProverClient with .executor(ExecutorKind::Assembly) (or its
.assembly() shorthand) and running setup with .with_hints().
:::
Hint format and protocol
Hint request format
Hints are transmitted as a stream of u64 values. Each hint
request consists of a header (1 u64) followed by data
(N u64 values):
┌─────────────────────────────────────────────────────────────┐
│ Header (u64) │
├·····························································┤
│ Hint Code (32 bits) Length (32 bits) │
├─────────────────────────────────────────────────────────────┤
│ Data[0] (u64) │
├─────────────────────────────────────────────────────────────┤
│ Data[1] (u64) │
├─────────────────────────────────────────────────────────────┤
│ ... │
├─────────────────────────────────────────────────────────────┤
│ Data[N-1] (u64) │
└─────────────────────────────────────────────────────────────┘
where N = ceil(Length / 8)
- Hint Code (upper 32 bits): control code or data hint type.
- Length (lower 32 bits): payload data size in bytes. The
last
u64may contain padding bytes.
Control hint types
Control codes manage the stream lifecycle and do not carry computational data (Length must be zero):
| Code | Name | Description |
|---|---|---|
0x0000 | CTRL_START | Start a new hint stream. Resets processor state and sequence counters. Must be the first hint. |
0x0001 | CTRL_END | End the current hint stream. Blocks until all pending hints complete. Must be the last hint. |
0x0002 | CTRL_CANCEL | [Reserved for future use] Cancel the current stream and stop processing further hints. |
0x0003 | CTRL_ERROR | [Reserved for future use] Signal an external error and stop processing further hints. |
Data hint types
For data hints, the hint code (32 bits) is structured as follows:
- Bit 31 (MSB) — pass-through flag. When set, the data bypasses computation and is forwarded directly to the sink.
- Bits 0–30 — the hint type identifier (control, built-in,
or custom code), e.g.
HINT_SHA256,HINT_BN254_G1_ADD,HINT_SECP256K1_RECOVER.
Example: a SHA-256 hint (0x0100) with a 32-byte input:
Header: 0x00000100_00000020
Data[0]: first_8_input_bytes_as_u64
Data[1]: next_8_input_bytes_as_u64
Data[2]: next_8_input_bytes_as_u64
Data[3]: last_8_input_bytes_as_u64
The same hint with the pass-through flag set (bit 31), forwarding pre-computed data directly to the sink without invoking the SHA-256 handler:
Header: 0x80000100_00000020
Stream batching
The hints protocol supports chunking for individual hints that exceed the transport's message size limit (currently 128 KB). Each message in the stream contains either a single complete hint or one chunk of a larger hint — hints are never combined in the same message.
When a hint exceeds the size limit, it must be split into multiple sequential chunks, each sent as a separate message. Each chunk includes a header specifying the total length of the complete hint, allowing the receiver to reassemble all chunks before processing. For example, a hint with a 300 KB payload would be split into three messages:
Message 1: Header (code + total length), Data[0..N] (first 128 KB chunk)
Message 2: Header (code + total length), Data[0..N] (second 128 KB chunk)
Message 3: Header (code + total length), Data[0..M] (final 44 KB chunk)
The receiver buffers incoming chunks and reassembles them based on the total length specified in the header before invoking the hint handler. This allows the system to handle arbitrarily large hints while respecting transport limitations.
Pass-through hints
When bit 31 of the hint code is set (e.g. 0x8000_0000 | actual_code),
the hint is marked as pass-through:
- The data payload is forwarded directly to the sink without invoking any handler.
- No worker thread is spawned; the data is queued immediately in the reorder buffer.
- Useful for pre-computed results that don't need processing.
Hint code types
| Category | Code range | Description |
|---|---|---|
| Control | 0x0000–0x000F | Stream lifecycle management |
| Built-in | 0x0100–0x0800 | Cryptographic precompile operations |
| Input | 0xF0000 | Input data hints |
| Custom | User-defined | Application-specific handlers |
Custom hint codes can technically use any value not occupied by
control or built-in codes. By convention, codes in the range
0xA000–0xFFFF are recommended for custom use to avoid future
conflicts as new built-in types are added. The processor does not
enforce a range restriction — any unrecognized code is treated
as custom.
Built-in hint types
| Code | Name | Description |
|---|---|---|
0x0100 | Sha256 | SHA-256 hash computation |
0x0200 | Bn254G1Add | BN254 G1 point addition |
0x0201 | Bn254G1Mul | BN254 G1 scalar multiplication |
0x0205 | Bn254PairingCheck | BN254 pairing check |
0x0300 | Secp256k1EcdsaAddressRecover | Secp256k1 ECDSA address recovery |
0x0301 | Secp256k1EcdsaVerifyAddressRecover | Secp256k1 ECDSA verify + address recovery |
0x0380 | Secp256r1EcdsaVerify | Secp256r1 (P-256) ECDSA verification |
0x0400 | Bls12_381G1Add | BLS12-381 G1 point addition |
0x0401 | Bls12_381G1Msm | BLS12-381 G1 multi-scalar multiplication |
0x0405 | Bls12_381G2Add | BLS12-381 G2 point addition |
0x0406 | Bls12_381G2Msm | BLS12-381 G2 multi-scalar multiplication |
0x040A | Bls12_381PairingCheck | BLS12-381 pairing check |
0x0410 | Bls12_381FpToG1 | BLS12-381 map field element to G1 |
0x0411 | Bls12_381Fp2ToG2 | BLS12-381 map field element to G2 |
0x0500 | ModExp | Modular exponentiation |
0x0600 | VerifyKzgProof | KZG polynomial commitment proof verification |
0x0700 | Keccak256 | Keccak-256 hash computation |
0x0800 | Blake2bCompress | Blake2b compression function |
Input hint type
Input hints allow passing data to the zkVM during execution. Unlike precompile hints that are processed by worker threads, input hints are forwarded directly to a separate inputs sink:
| Code | Name | Description |
|---|---|---|
0xF0000 | Input | Input data for the zkVM |
The input hint payload format is:
- First 8 bytes: length of the input data (
u64, little-endian). - Remaining bytes: the actual input data, padded to 8-byte alignment.
Input hints are not processed by the parallel worker pool; instead they are immediately submitted to the inputs sink for consumption by the zkVM.
Custom hint types
Custom hint types let users define their own handlers for
application-specific logic. Register them on the
HintsProcessor builder by providing a mapping from hint code
to a processing function — see
Custom hint handlers. By convention,
codes in 0xA000–0xEFFFF are recommended to avoid conflicts
with current and future built-in types. If a data hint arrives
with an unregistered code, the processor returns an error and
stops processing immediately.
Stream protocol
A valid hint stream follows this protocol:
CTRL_START ← Reset state, begin stream
[Hint_1] [Hint_2] ... [Hint_N] ← Data hints (precompile, input, or custom)
CTRL_END ← Wait for completion, end stream
Consuming hints
Once a guest program has produced a hints binary file (see
Generating hints in guest programs),
you can feed it to the prover either programmatically through the
ZisK SDK or via the cargo-zisk CLI.
SDK
Load the file with ZiskHints::from_file and pass it to
.hints(...) on the executor:
use anyhow::Result;
use zisk_sdk::{ExecutorKind, GuestProgram, ProverClient, ZiskStdin, ZiskHints};
#[tokio::main]
async fn main() -> Result<()> {
let elf_path = "hints/example/zec-reth.elf";
let program = GuestProgram::from_uri(elf_path)?;
let hints_path = "hints/example/24654300_hints.bin";
let hints = ZiskHints::from_file(hints_path)?;
let client = ProverClient::embedded()
.executor(ExecutorKind::Assembly)
.build()?;
client.upload(&program).run()?;
client.setup(&program).with_hints().run()?.await?;
let result = client
.execute(&program, ZiskStdin::new())
.hints(hints)
.executor(ExecutorKind::Assembly)
.run()?
.await?;
println!(
"Program executed successfully: {} cycles in {:.2?} ms",
result.get_execution_steps(),
result.get_execution_time()
);
Ok(())
}
Notes:
- Setup must run with
.with_hints()so the assembly ROM is generated with hint support enabled. Without it, the prover will not consume the hints stream. ZiskHints::from_fileloads the binary produced by the guest's hint generation. The returned value can be reused across multiple.execute(...)/.prove(...)calls.- The same pattern works for
prove,verify-constraints, andstatsoperations exposed byProverClient.
A complete runnable example is available at
examples/hints/host/src/main.rs.
CLI
Four cargo-zisk commands accept a --hints flag pointing to
the hints file: execute, prove, verify-constraints, and
stats. Pass the path with the file:// scheme:
--hints file://path → File stream reader
Example:
cargo-zisk prove --elf program.elf --hints file:///abs/path/hints.bin
--hints is mutually exclusive with --inputs (-i): if you
provide hints, the inputs are recovered from the hint stream
itself rather than from a separate input file.
Hints in distributed execution
In the distributed proving system, hints are received by the coordinator and broadcast to all workers via gRPC. The coordinator runs a relay that validates incoming hint messages, assigns sequence numbers for ordering, and dispatches them to workers asynchronously. Workers buffer incoming messages and reorder them by sequence number before processing. The processed hints are then submitted to the sink in the correct order.
There is another mode where workers can load hints from a local path or URI instead of streaming from the coordinator, which is useful for debugging.
Architecture
When the coordinator receives a hint request from the guest, it
parses the incoming u64 stream, validates control codes,
assigns sequence numbers for ordering, and broadcasts the data
to all workers.
Three message types are sent over gRPC to workers:
StreamMessageKind | When | Payload |
|---|---|---|
Start | On CTRL_START | None |
Data | For each data batch | Sequence number + raw bytes |
End | On CTRL_END | None |
Each worker receives the stream, buffers messages if they arrive
out of order, and sends them to the HintsProcessor for parallel
processing. The HintsProcessor guarantees that results are
submitted to the sink in their original order.
Hints mode configuration
Calling the coordinator with .hints(...) prepares it to receive
hints. A hints system can be configured in two ways:
- Streaming mode — workers receive hints from the coordinator via gRPC. This is the default and recommended mode for production, as it allows real-time processing of hints as they are generated.
- Path mode — workers load hints from a local path or URI. This is useful for debugging or when hints are pre-generated and stored in a file. The coordinator does not send hints to workers; each worker reads the hints directly from the specified path.
Streaming mode
The transport for the live hints stream is chosen on the SDK
side by constructing a ZiskStream and passing it as the hints
source on the execute/prove call:
| Constructor | Transport |
|---|---|
ZiskStream::unix() | Unix domain socket at an auto-assigned path under /tmp/ |
ZiskStream::unix_at("/path") | Unix domain socket at an explicit path |
ZiskStream::quic("quic://host:port") | QUIC transport (use quic://127.0.0.1:0 to let the OS pick a port) |
ZiskStream::grpc() | gRPC push transport (data pushed to the coordinator via PushJobInput) |
Example launching a prove job with hints streamed over a Unix socket:
use anyhow::Result;
use zisk_sdk::{ExecutorKind, GuestProgram, ProverClient, ZiskStdin, ZiskStream};
#[tokio::main]
async fn main() -> Result<()> {
let program = GuestProgram::from_uri("hints/example/zec-reth.elf")?;
let client = ProverClient::remote("http://127.0.0.1:7000").build()?;
let hints = ZiskStream::unix();
let prove_handle = client
.prove(&program, ZiskStdin::new())
.hints(hints.clone())
.executor(ExecutorKind::Assembly)
.run()?;
let proof = prove_handle.await?;
Ok(())
}
Switching transports is a one-line change at the call site —
replace ZiskStream::unix() with ZiskStream::grpc() or
ZiskStream::quic("quic://0.0.0.0:0").
Path (non-streaming) mode
Non-streaming mode is also selected from the SDK call. Instead
of constructing a ZiskStream, build a ZiskHints from a
pre-generated file (or in-memory bytes) and pass it to
.hints(...). The coordinator skips broadcasting in this case —
each worker loads the hints directly from the URI baked into the
ZiskHints value:
| Constructor | Source |
|---|---|
ZiskHints::from_file("/path") | Hints binary on disk (file path or file:// URI) |
ZiskHints::memory(bytes) | Hints already loaded into memory |
ZiskHints::from(&value) | Serializable Rust value (encoded with bincode) |
Example launching a prove job that loads hints from a file:
use anyhow::Result;
use zisk_sdk::{ExecutorKind, GuestProgram, ProverClient, ZiskStdin, ZiskHints};
#[tokio::main]
async fn main() -> Result<()> {
let program = GuestProgram::from_uri("hints/example/zec-reth.elf")?;
let client = ProverClient::remote("http://127.0.0.1:7000").build()?;
let hints = ZiskHints::from_file("/var/lib/zisk/hints/24654300_hints.bin")?;
let proof = client
.prove(&program, ZiskStdin::new())
.hints(hints)
.executor(ExecutorKind::Assembly)
.run()?
.await?;
Ok(())
}
The same ZiskHints value can be reused across multiple
.execute(...) / .prove(...) calls. As with streaming mode,
no coordinator or worker flags are required to switch between
sources — the SDK call decides.
Custom hint handlers
Register custom handlers via the builder pattern:
let processor = HintsProcessor::builder(my_sink)
.custom_hint(0xA000, |data: &[u64]| -> Result<Vec<u64>> {
// Custom processing logic
Ok(vec![data[0] * 2])
})
.custom_hint(0xA001, |data| {
// Another custom handler
Ok(transform(data))
})
.build()?;
Requirements:
- Handler function must be
Fn(&[u64]) -> Result<Vec<u64>> + Send + Sync + 'static. - Custom hint codes should not conflict with built-in codes
(
0x0000–0x0800). By convention, use codes in the range0xA000–0xFFFF.
Generating hints in guest programs
To generate hints from the guest program you need to follow these steps:
- Emit hint requests — patch your code or dependent crates
to call the external FFI hints helper functions that generate
the hint input data later consumed by the
HintsProcessor. See FFI hints helper functions for the list of available built-in helpers, or Custom hints generation for user-defined hints. - Add the
ziskoscrate to your guestCargo.toml. - Initialize and finalize the hint stream — call the hints init/close functions immediately before and after the section of code that executes precompile logic.
- Enable hints at compile time — compile your guest with
RUSTFLAGS='--cfg zisk_hints'for the native target to activate hint code generation and FFI helper functions in theziskoscrate. - Ensure deterministic execution — verify that both the
native execution that generates hints and the guest compiled
for the
zkvm/zisktarget execute deterministically and produce/consume hints in the exact same order. See Deterministic execution requirement.
To illustrate these steps, see the zec-reth guest program,
which executes and verifies Ethereum Mainnet blocks using the
ZisK zkVM:
https://github.com/0xPolygonHermez/zisk-eth-client/tree/main-reth/bin/guest
Emit hint requests
zec-reth relies on reth crates, which expose a Crypto trait
that allows a guest program to override precompile
implementations. This enables zkVM-optimized implementations
while also emitting hints so the computation can be performed
outside the zkVM.
For example, the BN254 elliptic curve addition (bn254_g1_add)
implementation of the Crypto trait is here:
Two target-specific implementations are provided: one for
zkvm/zisk and one for native (non-zkVM) targets. When
compiling with --cfg zisk_hints for the native target, the
zkVM-specific implementation emits a hint request using the FFI
helper:
#[cfg(zisk_hints)]
unsafe {
pub fn hint_bn254_g1_add(p1: *const u8, p2: *const u8);
}
This call generates the hint input data using the exact input
values that will later be consumed by the zkVM when executing
the zkvm/zisk target code. This data is consumed by the
HintsProcessor, allowing the bn254_g1_add computation to be
performed outside the zkVM while remaining fully verifiable
inside the circuit.
After hint generation, execution continues in the native target
code to compute the bn254_g1_add result.
From the guest program we generate hints carrying the input data
for the corresponding zisklib functions (in this example,
bn254_g1_add_c). These zisklib functions may internally
invoke one or more precompiles to produce the final result.
When the hints are processed by the HintsProcessor, it
executes the same zisklib function using the implementation
code for the zkvm/zisk target. This produces the exact
precompile results expected when executing the guest ELF inside
the zkVM. As a result, for each zisklib function invocation
the HintsProcessor may generate one or more precompile hint
results corresponding to the precompile inputs originally
emitted by the guest.
Initialize / finalize the hint stream
When using the ziskos::entrypoint!(main) macro, hint generation
is initialized and finalized automatically around your guest
entry function. You only need to compile with --cfg zisk_hints
(see Enable hints at compile time)
and, optionally, set environment variables to control the output
paths.
The macro expands to roughly:
fn main() {
zkvm_init(); // initialize hints
super::ZISK_ENTRY(); // your guest entry function
zkvm_deinit(); // close hints
}
zkvm_init and zkvm_deinit are also exposed as extern "C"
symbols so they can be called from C guest programs (see
Using hints from C guest programs).
Environment variables
| Variable | Description | Default |
|---|---|---|
ZISK_HINTS_OUTPUT | Path to the hints binary file written by zkvm_init. | ./tmp/hints.bin |
ZISK_INPUT_FILE | Path to the input file consumed by read_input_slice. | build/input.bin |
The ./tmp/ directory is created automatically if it does not
exist.
Manual API
If you need finer control — e.g., streaming hints over a Unix
socket, configuring a debug file, providing a synchronization
signal to the host — call the lower-level functions directly
instead of relying on the entrypoint! macro.
pub fn init_hints_file(
hints_file_path: PathBuf,
ready: Option<oneshot::Sender<()>>,
) -> Result<()>
Stores the generated hints in the file specified by
hints_file_path.
pub fn init_hints_socket(
socket_path: PathBuf,
debug_file: Option<PathBuf>,
write_flush_threshold: Option<usize>,
ready: Option<oneshot::Sender<()>>,
) -> Result<()>
Sends the hints through the Unix socket specified by
socket_path.
- The optional
debug_filestores a copy of the hints sent through the socket, useful for later debugging. - The optional
write_flush_thresholdcontrols the buffered-write flush size;Noneuses the default. - The optional
readyparameter can be used for synchronization with the host when the guest is executed in a separate thread to generate hints in parallel. It signalsreadywhen the writer is ready to start sending hints over the socket.
To close hints generation:
pub fn close_hints() -> Result<()>
Place these calls under #[cfg(zisk_hints)] so they are only
compiled into the native target used for hints generation:
#[cfg(zisk_hints)]
{
// Initialization / finalization code
...
}
You can review how hint generation is initialized and finalized
in the zec-reth guest here:
https://github.com/0xPolygonHermez/zisk-eth-client/blob/main-reth/bin/guest/src/main.rs
Enable hints at compile time
Once the guest program is set up to generate hints for the
native target, it must be compiled with the zisk_hints
configuration flag enabled:
RUSTFLAGS='--cfg zisk_hints' cargo build --release
After compiling, executing the guest program will generate the
hints. By default — when relying on the entrypoint! macro —
the binary file is written to ./tmp/hints.bin; set
ZISK_HINTS_OUTPUT to override the path. If you used the manual
API instead, the file/socket location follows what was passed to
init_hints_file/init_hints_socket.
If a hints file was generated, it can be consumed using the
--hints flag in the cargo-zisk commands that support hints
(see CLI).
If you want to display metrics in the console about the number
of hints generated during native guest execution, you can also
compile the guest with the --cfg zisk_hints_metrics flag.
To enable hint support when executing the guest inside the zkVM
(ELF guest), pass the --hints flag when generating the
assembly ROM with cargo-zisk rom-setup.
:::warning Emulator mode is not supported Hint processing is not supported when executing the guest ELF file in emulation mode. Use the Assembly executor for all hinted runs. :::
Deterministic execution requirement
An important requirement of the hints generation flow is that the native execution that generates the hints must be fully deterministic and always produce hints in the exact same order.
Furthermore, the order of hints generated during native
execution must match the order in which the guest compiled for
the zkvm/zisk target expects to receive them. Since the zkVM
execution is also deterministic, any divergence in hint ordering
between native and zkVM executions will result in incorrect
behavior.
To guarantee deterministic hint generation, the code paths that directly or indirectly generate hints must avoid:
- The use of threads or parallel execution.
- Data structures such as
HashMap(or any structure based on randomized hash seeds) when iterated in loops that directly or indirectly call precompile/hint functions.
Using threads or iterating over non-deterministically ordered data structures may cause the hint generation order to vary between runs, breaking the required alignment between native and zkVM executions.
FFI hints helper functions
| Code | Function |
|---|---|
0x0100 | fn hint_sha256(f_ptr: *const u8, f_len: usize); |
0x0200 | fn hint_bn254_g1_add(p1: *const u8, p2: *const u8); |
0x0201 | fn hint_bn254_g1_mul(point: *const u8, scalar: *const u8); |
0x0205 | fn hint_bn254_pairing_check(pairs: *const u8, num_pairs: usize); |
0x0300 | fn hint_secp256k1_ecdsa_address_recover(sig: *const u8, recid: *const u8, msg: *const u8); |
0x0301 | fn hint_secp256k1_ecdsa_verify_and_address_recover(sig: *const u8, msg: *const u8, pk: *const u8); |
0x0380 | fn hint_secp256r1_ecdsa_verify(msg: *const u8, sig: *const u8, pk: *const u8); |
0x0400 | fn hint_bls12_381_g1_add(a: *const u8, b: *const u8); |
0x0401 | fn hint_bls12_381_g1_msm(pairs: *const u8, num_pairs: usize); |
0x0405 | fn hint_bls12_381_g2_add(a: *const u8, b: *const u8); |
0x0406 | fn hint_bls12_381_g2_msm(pairs: *const u8, num_pairs: usize); |
0x040A | fn hint_bls12_381_pairing_check(pairs: *const u8, num_pairs: usize); |
0x0410 | fn hint_bls12_381_fp_to_g1(fp: *const u8); |
0x0411 | fn hint_bls12_381_fp2_to_g2(fp2: *const u8); |
0x0500 | fn hint_modexp_bytes(base_ptr: *const u8, base_len: usize, exp_ptr: *const u8, exp_len: usize, modulus_ptr: *const u8, modulus_len: usize); |
0x0600 | fn hint_verify_kzg_proof(z: *const u8, y: *const u8, commitment: *const u8, proof: *const u8); |
0x0700 | fn hint_keccak256(input_ptr: *const u8, input_len: usize); |
0x0800 | fn hint_blake2b_compress(...); |
0xF0000 | fn hint_input_data(input_data_ptr: *const u8, input_data_len: usize); |
Custom hints generation
To extend the built-in hints, you can generate custom hints for
new operations. First register the new hint in the
HintsProcessor as explained in
Custom hint handlers. Once the hint is
registered, you can generate hints for it from the guest using
the following FFI function:
fn hint_custom(hint_id: u32, data_ptr: *const u8, data_len: usize, is_result: u8);
The same guidelines described for the built-in FFI hint helpers apply.
Using hints from C guest programs
The ziskos crate is published as both an rlib and a
staticlib, so C guest programs can link against the resulting
.a archive and call the hint lifecycle functions through the
C ABI. Two symbols are exposed:
extern void zkvm_init(void);
extern void zkvm_deinit(void);
zkvm_init initializes the hint stream and zkvm_deinit
finalizes it. They are no-ops when compiled without
--cfg zisk_hints, so the same C code works for both native
(hint generation) and zkVM target builds without modification.
A minimal C guest program looks like:
extern void zkvm_init(void);
extern void zkvm_deinit(void);
int main(void) {
zkvm_init();
// Guest logic, including any FFI hint calls
// (hint_sha256, hint_keccak256, hint_input_data, ...)
zkvm_deinit();
return 0;
}
When linking the C guest against ziskos for native hint
generation, the same environment variables described in
Environment variables
(ZISK_HINTS_OUTPUT, ZISK_INPUT_FILE) control the file paths
used by zkvm_init and the input reader.
The FFI hint helper functions listed in
FFI hints helper functions are
all extern "C" and use the same signatures from C — declare
them with extern in your C source and link against the same
ziskos archive.
Next steps
With hints integrated end-to-end, the natural next step is to take the proof itself further:
- Verifying a proof — work with proofs from disk and verify them programmatically.
- On-chain verification — pass the public values bytes directly to a Solidity verifier contract.