Hekate Kotlin
On-device zero-knowledge proofs on Android: one suspend Kotlin call runs your Rust prover on a dispatcher you choose, and Job.cancel() reaches Rust.
On this page
Async / cancellation facade over the UniFFI-generated Kotlin bindings. Maps Kotlin coroutines (suspend, Job,
structured cancellation) onto the synchronous prove(inputs, cancel) C-ABI call that the prover crate exports through
UniFFI.
The wrapper is one suspending extension function. Two jobs and nothing more: hand the multi-second prove to a dispatcher
you choose, and route coroutine cancellation through the FFI CancelToken to make a structured Job.cancel() actually
reach Rust.
§ Installation
The .aar is produced by cargo xtask android-build and lives at target/aar/<pkg>-release.aar. To consume the
Kotlin facade through Gradle, publish it from kotlin/ to the local Maven repository:
./gradlew :lib:publishToMavenLocal
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenLocal()
}
}
// app/build.gradle.kts
dependencies {
implementation("dev.oumuamua.hekate:hekate:<version>")
}
<version> is the version declared in kotlin/lib/build.gradle.kts.
The .aar carries the generated Kotlin bindings, lib<dev>.so and libhekate_prover_cdylib.so. The app adds it as a
file dependency with JNA next to it and declares MyProver once over the bindings, as the
package README shows.
§ Usage
import dev.oumuamua.hekate.prove
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
viewModelScope.launch {
val output = MyProver.prove(inputs, Dispatchers.Default)
}
That's the surface. No Thread, no Executor, no Handler, no JNI typed-buffer marshalling. The suspend extension
hands the synchronous FFI call to the dispatcher and returns control to the coroutine when the proof is ready.
The dispatcher parameter is configurable for a reason. Dispatchers.Default is sized to CPU cores and is shared with
every other CPU-bound coroutine in the process. A multi-second prove pinned to Default starves that pool when several
proofs run concurrently or alongside other CPU work. For batch proving or background flows, pass Dispatchers.IO (
elastic) or a dedicated newFixedThreadPoolContext("prove", n) sized to the prover's parallelism budget.
§ Cancellation
Job.cancel() on the enclosing coroutine automatically calls CancelToken.request() through
suspendCancellableCoroutine.invokeOnCancellation. That covers every form of cancellation the coroutine machinery
produces:
- parent-driven structured cancellation when a
CoroutineScopeis cancelled, viewModelScopeteardown when itsViewModelis cleared,- an explicit
cancel()from aJobhandle.
The Rust prover stops at its next poll of the token, and the coroutine ends with CancellationException, as a cancelled
coroutine does. There is no manual cancel-token plumbing on the call site.
The propagation is cooperative on both sides: Rust observes the flag at its next poll and returns. The wrapper's
invokeOnCancellation block is the only side that's free, the dispatch thread keeps running the prover until the next
poll.
§ Panic isolation
The Rust prove entry point wraps the prover invocation in catch_unwind(AssertUnwindSafe(...)). If the prover panics
for any reason, a debug assertion, an integer overflow in debug_assertions, a bug in the dev's circuit or witness
builder, catch_unwind captures it, formats the panic payload, and returns Err(ProveError::Panic(message)). UniFFI
names the Rust ProveError enum ProveException in Kotlin, a sealed class, and maps that variant to its Panic case:
try {
val output = MyProver.prove(inputs, Dispatchers.Default)
} catch (e: ProveException.Panic) {
// log + surface; the prove call did NOT corrupt your process
} catch (e: ProveException) {
// every other typed prover error variant
}
No abort() crosses the FFI boundary. The Rust panic unwinds locally inside catch_unwind, the FFI return frame is
intact, and the Kotlin caller sees a typed exception instead of a SIGABRT that takes down the app.
§ Next steps
- Your First ZK Program: write the statement in Rust, a private payment proved and verified end to end.
- Preflight: check a witness against every constraint before the prover runs.
- Verifier Logic: verify the proof on a server and pin the program id.
- Soundness and Security: what a proof guarantees and the checks left to you.