# 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.

- URL: https://oumuamua.dev/mobile/kotlin
- Published: 2026-05-22T15:06:54+07:00
- Updated: 2026-10-12T18:30:17+07:00

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:

```bash
./gradlew :lib:publishToMavenLocal
```

```kotlin build.gradle.kts
// 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](https://github.com/oumuamua-labs/hekate-mobile/blob/main/kotlin/README.md#add-your-prover) shows.

## Usage

```kotlin
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 `CoroutineScope` is cancelled,
- `viewModelScope` teardown when its `ViewModel` is cleared,
- an explicit `cancel()` from a `Job` handle.

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:

```kotlin
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](https://oumuamua.dev/hekate/docs/getting-started/your-first-zk-program): write the statement in Rust, a private
  payment proved and verified end to end.
- [Preflight](https://oumuamua.dev/hekate/docs/basics/preflight): check a witness against every constraint before the prover runs.
- [Verifier Logic](https://oumuamua.dev/hekate/docs/basics/verifier-logic): verify the proof on a server and pin the program id.
- [Soundness and Security](https://oumuamua.dev/hekate/docs/advanced/soundness-and-security): what a proof guarantees and the checks left
  to you.