Input Format Guide

A practical guide to formatting inputs for Bonsol ZK programs, covering common pitfalls and working solutions for different input scenarios.

Overview

When creating execution requests for Bonsol ZK programs, proper input formatting is crucial for successful execution. This guide covers the practical aspects of formatting inputs that work with your ZK program's expectations.

Understanding Input Format Mismatch

One of the most common issues developers face is input format mismatch between:

  • The manifest file's inputOrder specification

  • The execution request JSON format

  • What the ZK program actually expects to read

Common Error: InputError (0x3)

If you encounter custom program error: 0x3 during execution, this indicates an InputError. Common causes include:

  • Wrong byte count: ZK program expects 8-byte arrays but receives different sizes

  • Wrong input count: Manifest specifies 1 input but you're sending multiple inputs

  • String vs. byte mismatch: Sending string data when ZK program expects binary data

Working Input Formats

Single Combined Byte Input

Best Practice: When your ZK program reads multiple values using env::read_slice(), combine all values into a single input.

Example: Calculator program that reads 3 i64 values

// ZK program code
fn read_i64_input() -> i64 {
    let mut input_bytes = [0u8; 8];
    env::read_slice(&mut input_bytes);
    i64::from_le_bytes(input_bytes)
}

fn main() {
    let operation = read_i64_input();  // First 8 bytes
    let operand_a = read_i64_input();  // Next 8 bytes
    let operand_b = read_i64_input();  // Next 8 bytes
    // ... rest of program
}

Corresponding Rust client code:

// Create individual i64 values as little-endian bytes
let operation_bytes = 2i64.to_le_bytes();  // multiply operation
let operand_a_bytes = 7i64.to_le_bytes();  // first operand
let operand_b_bytes = 6i64.to_le_bytes();  // second operand

// Combine into single 24-byte input
let mut combined_input = Vec::with_capacity(24);
combined_input.extend_from_slice(&operation_bytes);
combined_input.extend_from_slice(&operand_a_bytes);
combined_input.extend_from_slice(&operand_b_bytes);

// Send as single input
let execution_instruction = execute_v1(
    &requester,
    &payer.pubkey(),
    image_id,
    execution_id,
    vec![
        InputRef::public(&combined_input),  // Single 24-byte input
    ],
    // ... other parameters
)?;

Manifest alignment:

{
  "inputOrder": ["Public"]  // Single input, not multiple
}

String Inputs (Limited Support)

Caution: String inputs may work for transaction submission but fail during ZK execution.

What doesn't work:

// This will fail during ZK execution
vec![
    InputRef::public("2".as_bytes()),    // [50] - only 1 byte
    InputRef::public("7".as_bytes()),    // [55] - only 1 byte  
    InputRef::public("6".as_bytes()),    // [54] - only 1 byte
]

Why it fails: The ZK program expects 8-byte arrays, but strings produce variable-length byte arrays.

Debugging Input Issues

Enable Debug Output

Add debug printing to your client to understand exactly what's being sent:

println!("📥 Input being sent:");
println!("   Data: {:?} (length: {})", &input_data, input_data.len());
println!("   Expected by ZK program: {} calls to env::read_slice() with {}-byte arrays", 
         num_reads, bytes_per_read);

Check Transaction vs. Execution

  • Transaction success + execution failure: Input format accepted by Bonsol but incompatible with ZK program

  • Transaction failure: Input format rejected by Bonsol interface

Monitor Bonsol Logs

Watch for these error patterns in the prover logs:

"0 inputs resolved"
"DeserializeUnexpectedEnd"
"Guest panicked"

Best Practices

1. Align with ZK Program Expectations

Do: Format inputs to match exactly what your ZK program reads

// If ZK program does this:
let mut buffer = [0u8; 8];
env::read_slice(&mut buffer);

// Then send this:
let data = 42i64.to_le_bytes();  // Exactly 8 bytes
InputRef::public(&data)

2. Use Single Combined Inputs

Do: When reading multiple values sequentially, combine them into one input

// Multiple sequential reads = single combined input
let combined = [data1, data2, data3].concat();
vec![InputRef::public(&combined)]

Don't: Send separate inputs when ZK program expects sequential reads from one input

// This may not work as expected
vec![
    InputRef::public(&data1),
    InputRef::public(&data2), 
    InputRef::public(&data3),
]

3. Match Manifest Input Count

Ensure your inputOrder in the manifest matches your actual input usage:

{
  "inputOrder": ["Public"]  // One input
}
vec![InputRef::public(&single_combined_input)]  // One input

4. Test with Known Working Formats

Start with the byte format that matches your ZK program's expectations, then experiment with convenience formats.

Example: Working Calculator Client

This example shows a complete working implementation:

// Client code that works
let operation_bytes = 2i64.to_le_bytes();     // [2,0,0,0,0,0,0,0]
let operand_a_bytes = 7i64.to_le_bytes();     // [7,0,0,0,0,0,0,0]  
let operand_b_bytes = 6i64.to_le_bytes();     // [6,0,0,0,0,0,0,0]

let mut combined_input = Vec::with_capacity(24);
combined_input.extend_from_slice(&operation_bytes);
combined_input.extend_from_slice(&operand_a_bytes);
combined_input.extend_from_slice(&operand_b_bytes);

let execution_instruction = execute_v1(
    &requester,
    &payer.pubkey(),
    "5881e972d41fe651c2989c65699528da8b1ed68ab7057350a686b8a64a00fc91",
    "calc_exec_1",
    vec![InputRef::public(&combined_input)],
    1000,
    expiration,
    ExecutionConfig {
        verify_input_hash: false,
        input_hash: None,
        forward_output: true,
    },
    callback_config,
    None,
)?;

Result: ✅ Transaction succeeds + ZK execution succeeds

Troubleshooting Checklist

Further Reading

Last updated