zkSDK: Automated ZK Backend Selection
- zkSDK is a modular framework for zero-knowledge proof development that automates backend selection using workload profiling and user-defined criteria.
- The system decouples frontend programming from backend-specific proving, leveraging a custom language and dynamic selection to optimize performance tradeoffs.
- Empirical evaluations demonstrate that zkSDK effectively minimizes proving and verification costs, reducing backend lock-in for developers.
Searching arXiv for the cited zkSDK paper and closely related zero-knowledge tooling papers for support. zkSDK is a modular framework for zero-knowledge application development that seeks to decouple frontend programming from backend proving-system choice by automating backend selection. Introduced in "zkSDK: Streamlining zero-knowledge proof development through automated trace-driven ZK-backend selection" (Law, 5 Jul 2025), it addresses a recurring problem in contemporary ZK software engineering: developers face a fragmented ecosystem of proving backends, programming models, setup assumptions, verifier-cost profiles, and hardware constraints, and therefore often become tied to an early backend choice. zkSDK proposes a different workflow: a developer writes a program in a single frontend language, the system profiles the program’s computational structure, combines that profile with user-defined criteria, and selects a supported backend before code generation.
1. Background, motivation, and design objective
The motivating context for zkSDK is the proliferation of heterogeneous ZK development environments. The source paper situates this fragmentation in a landscape that includes Risc Zero’s Rust/C++-based zkVM, Circom-style circuit tooling, Libsnark, Cairo, and Gnark, each with distinct execution models and performance tradeoffs (Law, 5 Jul 2025). The practical consequence, as described by the author, is a steep learning curve for new entrants and a strong tendency toward backend lock-in.
The framework’s central claim is that backend choice should be made as a function of workload characteristics rather than developer familiarity. Integer-heavy programs, SHA-heavy programs, and workloads dominated by ZK-friendly hash functions can exhibit materially different performance across proving systems. In that setting, a manual backend choice made early in development can impose avoidable proving-time or verification-cost penalties. zkSDK therefore defines its core problem not as multi-backend support in the abstract, but as automated backend selection driven by program traces and user preferences.
The paper identifies its novelty as the combination of two elements: a backend-agnostic high-level language, called Presto, and an automatic backend-selection pipeline based on source-level workload profiling. It explicitly contrasts this approach with systems such as Leo and o1js, which are described as being tied to their own ecosystems, and with ZoKrates, which supports multiple backend choices but leaves the final selection to the developer. The closest conceptual precedent identified in the paper is ZØ, which also used cost-model-based backend selection, but for older backends. On the author’s account, zkSDK differs by combining backend-agnostic source programming with automated selection based on workload traces.
2. System organization and compilation workflow
zkSDK is organized as a modular toolchain whose implemented components are a benchmark package, an AST parser, an Interpreter that profiles workload intensity, a DynamicSelect package implementing the Dynamic Backend Selection Mechanism (DBSM), and backend-specific code generators (Law, 5 Jul 2025). In the implementation described in the paper, the initial supported backends are Risc Zero and Consensys Gnark, with Gnark represented in the benchmark and selection tables through its Groth16 and Plonk options.
The operational workflow is source-driven. A developer writes a program in Presto. zkSDK parses the program into an AST, traverses the AST to characterize computational workload, combines the resulting usage summary with user-supplied preferences, runs backend selection, and then emits backend-specific code. This design separates workload analysis from backend code generation and makes backend selection a compiler-stage decision rather than a manual design commitment.
Implementation-wise, the system is split into four packages: Benchmarks, Contracts, Compiler, and DBSM. Benchmarks contains Rust and Go code for measuring proof-generation time and generating inputs for verifier-gas tests. Contracts uses Foundry to benchmark Ethereum gas usage. The Compiler is written in Rust and includes the AST parser, Interpreter, and Codegen. DBSM is also written in Rust and consumes user preferences together with profiling results. The paper reports approximately 1,200 lines of code for Benchmarks and 3,300 lines for the compiler.
A plausible implication is that zkSDK treats the frontend language, the performance model, and the backend adapters as separable concerns. That separation is not formalized as an intermediate representation beyond the AST/context pair, but it structures the framework as if backend selection were a pluggable optimization phase.
3. Presto as frontend language and internal representation
Presto is the frontend language introduced to support zkSDK’s profiling and compilation workflow. It is described as a custom Python-like language that was created primarily to give the compiler precise control over AST structure and make profiling easier, though the paper explicitly notes that a custom language was not strictly necessary (Law, 5 Jul 2025). The language is statically typed and intentionally minimal.
The feature set reported in the paper includes custom functions, variable declarations, mappings, binary and integer operations, byte manipulation, conditionals, loops, and structs, together with built-in precompiles such as ECDSA, SHA256, Keccak256, and MiMC. A program begins with the program keyword and a program name. The grammar distinguishes statements, which require semicolons and include variable declarations, mappings, or custom function calls, from oscs ("optional semicolon statements"), which include if-statements, struct declarations, function declarations, and for-loops.
The parser is built with Pest and then wrapped by custom Rust parsing logic through a PrestoNode<T> trait. The paper presents the parser interface as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
trait PrestoNode<T> {
fn parse(pair: Pair<Rule>, i: usize) -> Result<T>;
fn ast_type() -> Vec<Rule>;
}
fn try_parse<F>(pair: Pair<Rule>) -> Result<F>
where
F: PrestoNode<F>,
{
let r = pair.as_rule();
for (i, rtype) in F::ast_type().enumerate() {
if r.eq(rtype) {
let res = F::parse(pair, i)?;
return Ok(res);
}
}
} |
This parsing layer produces reusable Rust AST objects while maintaining compiler context. The context stores metadata including function declarations, structs, mappings, imports, constraints, accessed keys, whether constraints should be added, and the program name:
1 2 3 4 5 6 7 8 9 10 |
struct Context {
funcs: HashMap<String, FunctionDeclaration>,
structs: HashMap<String, StructStatement>,
mappings: HashMap<String, MappingStatement>,
imports: HashSet<String>,
constraints: HashSet<String>,
keys: HashSet<String>,
add_constraint: bool,
program_name: String,
} |
The paper does not provide a complete formal grammar, type system, or operational semantics in mathematical notation. It also does not define a distinct lower-level IR beyond the AST together with this context metadata. Accordingly, the compiler’s analyzable representation is best understood as a custom Rust AST plus associated contextual state rather than as a multi-stage lowering pipeline.
4. Trace-driven profiling and the Dynamic Backend Selection Mechanism
The phrase "trace-driven" in zkSDK refers to AST interpretation rather than low-level runtime instrumentation. The Interpreter package recursively traverses the Presto AST and derives a computational usage summary by counting workload-relevant operations (Law, 5 Jul 2025). The interpretation interface is presented through a trait structurally analogous to the parser:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
trait Interpret<F, R>
where
R: PrestoNode<R>,
{
fn interpret(ctx: Context, r: R) -> Result<F>;
}
fn try_interpret<R, I>(ctx: Context, r: R) -> Result<I>
where
R: PrestoNode<R>,
I: Interpret<I, R>,
{
I::interpret(ctx, r)
}
fn interpret_ast(ctx: Context, ast: PrestoFile) -> Result<HashMap<String, u64>> {
let statements = ast.statements;
let res = try_interpret::<Statements, StatementsInterpreter>(ctx, statements)
.usage_table;
Ok(res)
} |
Each interpreted node produces metadata that includes a usage_table: HashMap<String, u64> and a uses_storage boolean. The usage table records operation categories and counts. The Interpreter searches for integer operations and specific built-ins such as sha256, mimc, and keccak256; the paper also mentions "sha3_keccak256" and placeholders for other known functions. It recursively traverses custom functions, computes per-function usage tables, and multiplies these by the number of call sites. It additionally traverses all function declarations even if unused and processes conditionals naively without evaluating branch predicates. For mappings and storage, it tracks whether storage is used and counts unique keys accessed.
The implemented DBSM consumes three inputs: the Interpreter’s workload profile, benchmark data, and user preferences. The initial preference model has three weighted criteria: proof generation time, proof verification cost, and hardware acceleration. The preference vector is defined as:
1 2 3 4 5 6 7 8 9 |
impl Preference {
fn to_matrix(&self) -> Vector3<f64> {
Vector3::new(
self.proof_generation,
self.proof_verification,
self.hardware_acceleration,
)
}
} |
The paper states that zkSDK converts the interpreter results into a 1 x 11 matrix, normalizes the benchmark results, and then performs matrix multiplication:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
fn dynamic_select(interpret: HashMap<String, u64>, p: &Preference) -> Backend {
let usage = get_interpret_matrix(interpret);
let normalized = get_table_normalized();
let pref_vec = p.to_matrix();
let scores = normalized * usage * pref_vec;
// Find row with smallest value
let row = scores
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b));
// Find corresponding col
let col = row
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b));
match col {
0 => Backend::RiscZero,
1 => Backend::GnarkGroth16,
2 => Backend::GnarkPlonk,
}
} |
Conceptually, the normalized benchmark matrix captures backend behavior across measured workload classes and selection criteria, the usage vector captures the program’s observed workload mix, and the preference vector weights the criteria. The selected backend is the one with the smallest score. The paper does not specify the exact normalization formula, the full dimensional layout of the scoring matrix, or a more formal optimization objective beyond the expression scores = normalized * usage * pref_vec.
As future work, the author sketches an extended DBSM in which proof-verification constraints are expressed as [curve]-[zk_schema]-[vm]:[max_gas], hardware preferences as [benchmark]-[backend]-[device]:[num_cores], and wildcard * is permitted. In that version, the algorithm first filters backend options by verifier constraints and gas limits, then considers hardware preferences, falls back to baseline devices where needed, and interpolates proof-generation time when benchmark data for requested hardware is missing. The interpolation formula is given as
for a target device lying between devices and in hardware performance.
5. Supported backends, code generation, and empirical behavior
The implemented backends are Risc Zero and Gnark. Risc Zero is treated as a zkVM backend in which developers write ordinary-style code for a RISC-V virtual machine and the proving system attests to the execution trace. The paper notes that Risc Zero supports Rust and C++, reports total zkVM cycles in development mode, and can produce succinct receipts locally, whereas Groth16 receipts require x86 Linux or Bonsai remote proving because of Circom/x86 dependencies (Law, 5 Jul 2025). In zkSDK’s architecture, Risc Zero represents the VM-oriented proving style, especially for workloads involving conventional cryptographic code such as SHA-256.
Gnark is treated as a circuit-oriented backend embedded in Go. It supports Groth16 and Plonk over curves such as BN254 and BLS12-381, with APIs for setup, prove, verify, and witness handling. In zkSDK’s comparison, the concrete quantitative dimensions are proof generation time and Ethereum verification gas, while total zkVM cycles for Risc Zero and total constraint counts for Gnark serve as indirect indicators of computational intensity.
Code generation differs significantly across the two backends. For Risc Zero, most Presto constructs map almost one-to-one into Rust guest code because Presto is statically typed and Risc Zero supports a general-purpose programming style. Variable declarations, arithmetic, control flow, bytes, functions, and structs translate directly. Mappings are emitted as BTreeMaps, consistent with the paper’s note that Risc Zero recommends them over HashMaps for performance. The author also notes that guest-side storage is ephemeral because each execution creates a new guest instance; the proposed but not fully implemented remedy is to pass mappings from the host into the guest via ExecutorEnv.
Gnark code generation is more circuit-specific. Global-scope Presto variables become frontend.Variables on a Go circuit struct, custom functions become circuit methods that accept api frontend.API, and the global program logic is emitted into a Define method. The paper explicitly remarks that typing everything naively as frontend.Variable is simplistic and may not be optimal. For mappings, because Go maps cannot directly be circuit variables, zkSDK uses the Interpreter’s count of unique storage keys to generate public Keys and Values arrays sized by the number of unique keys, thereby modeling externally supplied mapping state. The paper also records an important implementation limitation: although the benchmark study includes Plonk and BLS12-381, the emitted Gnark proving logic supports only BN254 with Groth16 in code generation.
The benchmark results establish the workload dependence that motivates automated selection. For integer operations, 100,000 iterations of y += x**3 + y + 5 took 26.576 s in Risc Zero, 8.688 s in Gnark Groth16 BN254, and 4.091 s in Gnark Plonk BN254; corresponding Ethereum verification gas values were about 194,955 for Gnark Groth16 BN254, 262,869 for Gnark Plonk BN254, and roughly 229,894 for a Risc Zero Groth16 proof. For ECDSA verification, Risc Zero took 267 s, while Gnark Groth16 BN254 took 42.679 s and Gnark Plonk BN254 took 63.697 s; verification gas was approximately 317,105 for Gnark Groth16 BN254, 281,458 for Gnark Plonk BN254, and 230,618 for Risc Zero Groth16. For 100 MiMC hashes, however, Risc Zero was fastest at 1,576.162 ms, versus 2,446.640 ms for Gnark Groth16 BN254 and 2,144.795 ms for Gnark Plonk BN254. For SHA-2/SHA-256 and SHA-3/Keccak workloads over increasing job sizes, the paper reports that Risc Zero is generally much faster than Gnark, especially relative to Plonk, and often faster than Groth16 as sizes grow, though Groth16 BN254 is competitive at some small Keccak sizes.
These measurements are the empirical basis for the framework’s selection logic. They show that no single backend dominates across all workload classes and that proof-generation and verification-cost tradeoffs vary with both computation type and proving schema.
6. Evaluation, limitations, and broader significance
The evaluation environment reported in the paper consists of a MacBook M1 Pro with 32 GB RAM and macOS 14.6.1, using Rust 1.84.0, Go 1.23.4, Risc Zero 1.2.0, Gnark 0.11.0, and Foundry/forge 0.2.0 for Solidity verification benchmarking (Law, 5 Jul 2025). The target benchmark time was 60 seconds. Because local STARK-to-SNARK proving was unavailable on arm64, Risc Zero Groth16 proofs were generated using Bonsai.
Beyond microbenchmarks, the paper evaluates zkSDK on two "real-world" style workloads written in Presto. The first is a token contract-like example involving minting and transferring balances, mappings, arithmetic, and conditional logic. Since memory access had not been benchmarked across backends, the author assumes storage or memory access is relatively cheap and characterizes the program as primarily integer-operation-heavy. Running the program directly yielded proof generation times of 109.038 ms ± 7.441 ms for Gnark and 1590 ms ± 70 ms for Risc Zero. Under preference weights (1, 0, 0), meaning the system favors proof generation time, the Interpreter reported:
1 |
{"int_ops": 6, "get_balance": 7, "set_balance": 6} |
and selected GnarkGroth16.
The second workload is a Merkle tree example using SHA-256 to compute a Merkle root, intended to represent a blockchain-style historical data structure. Direct execution yielded proof generation times of 3401 ms ± 239 ms for Gnark and 2700 ms ± 60 ms for Risc Zero. Under the same preference setting (1,0,0), the Interpreter reported
0
and selected Risc Zero.
The paper interprets these examples as evidence that zkSDK can select the empirically faster backend among its supported options when workload characteristics align with the benchmark corpus. That claim is narrow. The evaluation does not report cross-validation over a larger program set, ablation studies against simpler heuristics, profiling overhead, or regret relative to an oracle selector. It also does not provide a richer formal model of workload beyond operation counts and benchmark normalization.
Several limitations are explicitly acknowledged. Backend coverage is narrow: although the motivation discusses a wider ecosystem including Circom, Libsnark, and Cairo, the implementation supports only Risc Zero and Gnark, and emitted Gnark code currently targets only BN254/Groth16. Profiling is simplistic: the Interpreter traverses all functions whether or not they are called and processes conditionals without evaluating guards, so usage counts can overapproximate actual work. Memory-access behavior was not benchmarked. The selection algorithm is intentionally simple and excludes dimensions such as proof size, setup universality, trust assumptions, and recursion support from its quantitative objective. Verifier gas numbers for Risc Zero are reported as approximate because proof-verification tests were failing for reasons the author could not resolve. Hardware generalization is weak because all benchmarks were run on one machine, while the richer hardware-aware logic remains future work. Finally, the real-world evaluation consists of only two example programs.
The broader significance of zkSDK lies in the architectural proposition it advances. Rather than treating proving-system choice as a one-time manual decision that structures the entire software stack, zkSDK models it as a profiled compilation decision mediated by source-level workload analysis and policy. This suggests a tooling direction in which ZK developers target a common high-level interface and backend specialization becomes a compiler responsibility. The present implementation is limited and exploratory, but it frames backend selection as an optimization problem over workload traces, verification costs, and user priorities rather than as a purely ecosystem-level commitment.