2. Build the witness input (A, sk, PointA) from the bech32 keys

August 14, 2026 · View on GitHub

Cardano Addresses


Coding Standards Haskell CI using Cabal Docs

Overview

This module provides mnemonic (backup phrase) creation, and conversion of a mnemonic to seed for wallet restoration, and address derivation functionalities.

Documentation

Haddock documentation

Haddock API documentation is available here.

Docusaurus-powered documentation

Powered by Docusaurus logo

CLI documentation is available here

Ownership proving using ZKP (experimental)

⚠️ Experimental. The circuits, CLIs, and workflows below are developed in the cardano-foundation/bls repository and are not part of the cardano-addresses release artifacts. The ceremonies used are single-party dev ceremonies — do not use them in production without a proper multi-party trusted setup. Expect breaking changes.

cardano-addresses derives the keys; it does not itself generate ZK proofs. Proving ownership of a Cardano key with a zero-knowledge proof is provided by the cardano-foundation/bls project, which hosts the Circom circuits and the Rust CLIs that drive them. The goal is to prove knowledge of the private Ed25519 scalar sk such that the public key A = [sk]·G matches a given compressed key — without ever revealing sk. A real Cardano payment key is derived with cardano-address (CIP-1852 path 1852H/1815H/0H/0/0); in BIP32-Ed25519 the first 32 bytes of the extended signing key (kL) already hold the clamped scalar the circuit needs as its private witness. The circuit is compiled once to .r1cs + .wasm with circom on the BLS12-381 field and reused for any key — only the per-user input.json changes. snarkjs turns input.json + .wasm into a witness, the trusted-setup CLI runs a single-party Groth16 ceremony, and the groth16 CLI proves and verifies. Two circuit families exist: CardanoKeyOwnership proves ownership of a single public key, while CardanoKeyOwnershipSMT additionally proves that the key is a member of an authorized key set committed into a Sparse Merkle Tree — so the verifier only trusts and stores the single Merkle root instead of an O(N) key list. Both come in a monolithic (~1.97M constraints) and a Nova step-chain variant that splits the scalar multiplication into 255 × 7.7K-constraint steps, cutting the ceremony from ~8 minutes to ~3 seconds and the proving key from 1.2 GB to 5 MB. The pipeline is driven by three Rust CLIs in clis/trusted-setup, smt, and nova — which are described below.

The CLIs

All CLIs are Rust binaries in the cardano-foundation/bls/clis directory:

CLIBuildPurpose
trusted-setupcd clis/trusted-setup && cargo build --releaseGroth16 trusted-setup ceremonies on BLS12-381: ceremony-dev (single-party dev ceremony, --sparse / --h-scalar) and phase2 (multi-party MPC on a public .ptau SRS)
groth16cd clis/groth16 && cargo build --releaseProof generation (prove), verification (verify), and verifying-key export (export-vk)
smtcd clis/smt && cargo build --releaseSparse Merkle Tree operations for CardanoKeyOwnershipSMT: smt key (Ed25519 decompression + limb chunking + MiMC leaf), smt leaf, smt insert, smt digest, smt path, smt verify, and smt cardano-input (full circuit-input assembly)
novacd clis/nova && cargo build --releaseNova IVC step-chain flow: nova params, nova ceremony, nova fold, nova compress, nova verify

CardanoKeyOwnership — proving ownership of a single key

The circuit proves A = PointCompress([sk]·G) on Curve25519, where sk is the clamped scalar from a real Cardano payment key and A its compressed public key.

BLS=<path-to-bls-repo>
cd $BLS/circom/CardanoKeyOwnership

# 1. Derive a real Cardano payment key
cardano-address recovery-phrase generate --size 15 > phrase.prv
cardano-address key from-recovery-phrase Shelley < phrase.prv > root.xsk
cardano-address key child 1852H/1815H/0H/0/0 < root.xsk > pay.xsk
cardano-address key public --without-chain-code < pay.xsk > pay.vk

# 2. Build the witness input (A, sk, PointA) from the bech32 keys
python3 gen_cardano_address_input.py --xsk pay.xsk --vk pay.vk -o input.json

# 3. Compile the circuit (once, reused for any key)
circom --prime bls12381 -l ../Ed25519Verify/node_modules/circomlib/circuits \
  cardano_ed25519_ownership.circom --r1cs --wasm --sym

# 4. Generate the witness
snarkjs wtns calculate \
  cardano_ed25519_ownership_js/cardano_ed25519_ownership.wasm \
  input.json witness_ownership.wtns

# 5. Single-party dev ceremony (once per circuit, ~8 min; --sparse --h-scalar required)
cd $BLS/clis/trusted-setup && cargo build --release
target/release/trusted-setup ceremony-dev --sparse --h-scalar \
  --circuit $BLS/circom/CardanoKeyOwnership/cardano_ed25519_ownership.r1cs \
  --proving-key cko.pk --verifying-key cko.vk

# 6. Prove
cd $BLS/clis/groth16
cargo run --release -- prove --sparse \
  --circuit $BLS/circom/CardanoKeyOwnership/cardano_ed25519_ownership.r1cs \
  --witness $BLS/circom/CardanoKeyOwnership/witness_ownership.wtns \
  --proving-key cko.pk --out proof.bin

# 7. Verify
cargo run --release -- verify \
  --proof proof.bin --public proof.pub --verifying-key cko.vk
# → Verification result: VALID

The same statement can be split into 255 identical 7,724-constraint steps and folded with Nova (Implementation 8). The ceremony drops to ~3 s and the proving key to 5 MB — at the cost of O(N) verification and a sequential step chain.

cd $BLS/clis/nova && cargo build --release

cd $BLS/circom/CardanoKeyOwnership
circom --prime bls12381 -l ../Ed25519Verify/node_modules/circomlib/circuits \
  cardano_ed25519_ownership_nova.circom --r1cs --wasm --sym

# Inspect the step circuit (must report n_pub_in == n_pub_out == 24)
$BLS/clis/nova/target/release/nova params --circuit cardano_ed25519_ownership_nova.r1cs

# One ceremony for the step circuit (seconds, reusable)
$BLS/clis/nova/target/release/nova ceremony \
  --circuit cardano_ed25519_ownership_nova.r1cs \
  --proving-key cko255.pk --verifying-key cko255.vk

# Generate 255 step witnesses step_0000.wtns … step_0254.wtns iteratively
# (dblIn := extended(G), addIn := extended(O), sel := (sk >> i) & 1, LSB-first)
# via: snarkjs wtns calculate cardano_ed25519_ownership_nova_js/cardano_ed25519_ownership_nova.wasm

# Fold — prove each step, check the state chain, accumulate the transcript
$BLS/clis/nova/target/release/nova fold \
  --circuit cardano_ed25519_ownership_nova.r1cs \
  --proving-key cko255.pk --steps <witness-dir> --out cko255_ivc.json

# Verify — re-checks every pairing, the state chain, and the transcript
$BLS/clis/nova/target/release/nova verify --ivc cko255_ivc.json --verifying-key cko255.vk
# → Verified 255 steps: 255 pairings OK, state chain OK, transcript OK

CardanoKeyOwnershipSMT — set-based authorization with a Merkle root

The SMT variant combines Ed25519 key ownership with Sparse Merkle Tree membership: authorized public keys are committed (as one-way MiMC leaves, never raw keys) into an SMT, and a single proof shows that the prover owns A and that A is authorized by the tree root. The verifier stores only the root, so the authorized-set state stays constant regardless of N. Key rotation and revocation are root updates.

cd $BLS/clis/smt && cargo build --release

cd $BLS/circom/CardanoKeyOwnershipSMT

# Derive a real Cardano payment key (as above)
cardano-address recovery-phrase generate --size 15 > phrase.prv
cardano-address key from-recovery-phrase Shelley < phrase.prv > root.xsk
cardano-address key child 1852H/1815H/0H/0/0 < root.xsk > pay.xsk
cardano-address key public --without-chain-code < pay.xsk > pay.vk

# Build the full circuit input (A, sk, PointA, smt_root, siblings, directions);
# all crypto is done by the smt CLI, not by Python
./gen_input.sh --xsk pay.xsk --vk pay.vk --depth 4 --output input.json \
  --smt-cli $BLS/clis/smt/target/release/smt

# Compile, witness, ceremony, prove, verify — same shape as CardanoKeyOwnership,
# with cardano_key_ownership_smt.circom
circom --prime bls12381 -l ../Ed25519Verify/node_modules/circomlib/circuits \
  cardano_key_ownership_smt.circom --r1cs --wasm --sym
snarkjs wtns calculate cardano_key_ownership_smt_js/cardano_key_ownership_smt.wasm \
  input.json witness.wtns

$BLS/clis/trusted-setup/target/release/trusted-setup ceremony-dev --sparse --h-scalar \
  --circuit cardano_key_ownership_smt.r1cs --proving-key smt.pk --verifying-key smt.vk

cd $BLS/clis/groth16 && cargo build --release
target/release/groth16 prove --sparse \
  --circuit $BLS/circom/CardanoKeyOwnershipSMT/cardano_key_ownership_smt.r1cs \
  --witness $BLS/circom/CardanoKeyOwnershipSMT/witness.wtns \
  --proving-key smt.pk --out proof.bin
target/release/groth16 verify --proof proof.bin --public proof.pub --verifying-key smt.vk
# → Verification result: VALID

The SMT step-chain (Nova) variant cardano_key_ownership_smt_nova.circom folds the scalar multiplication into 255 steps exactly as in the CardanoKeyOwnership Nova flow; the SMT-membership half stays in the monolithic circuit.

CardanoKeyOwnership vs CardanoKeyOwnershipSMT

FeatureCardanoKeyOwnershipCardanoKeyOwnershipSMT
Proves key ownership
Proves set membership
Verifier trust / statePer public key ASingle SMT root
Authorized set size1Any N (constant verification state)
Public inputsA[256]A[256], smt_root
Hides which key✗ (A public)✗ (A public) — hides path/index only
Circuit size (monolithic)1,967,405 constraints1,971,079 constraints (+0.2 % at depth 4)
Circuit size (Nova)255 × 7,724 constraints255 × 7,724 constraints (SMT part stays monolithic)
Set rotation / revocationn/a (per-key proof)Root update (rebuild SMT)
Needs a key registry✓ (root + Merkle path)
SMT CLI integration

In short: use CardanoKeyOwnership to prove "I own this specific key A", and CardanoKeyOwnershipSMT to prove "I own a key that is authorized in this set" — the set being committed by a single Merkle root that the verifier must trust.

Implementation trade-offs: monolithic Groth16 vs Nova step-chain

Independently of the circuit family, the same statement can be proven as a single monolithic Groth16 proof (Implementation 7) or decomposed into 255 small steps and folded with Nova IVC (Implementation 8). The two paths have very different trade-offs, measured on the same machine with the same key:

PhaseMonolithic Groth16Nova step-chain
Circuit1,967,405 constraints255 × 7,724 constraints
Witness generation~10 s255 steps: ~133 s (sequential)
Ceremony (one-time, reusable)~8 min~3 s
Prove / fold~74 s~179 s
Verify~1.5 s (one pairing)~3.2 s (255 pairings, O(N))
e2e first run (incl. ceremony)~9.7 min~5.2 min
e2e steady state (ceremony amortized)~86 s~312 s
Proving key1.2 GB5 MB
Verifying key178 MB719 KB
Peak memory~4.5 GiBper-step

Takeaways:

  • First run — Nova wins. The monolithic ceremony dominates (~8 min) and needs ~4.5 GiB of RAM, while the Nova ceremony is ~3 s and the fold uses per-step memory. The proving key shrinks from 1.2 GB to 5 MB.
  • Steady state — monolithic Groth16 wins (~3.6×). Once the ceremony is amortized, a new key costs ~86 s (one witness + one proof) vs ~312 s (255 step witnesses + fold). The step chain is inherently sequential — each step feeds the next — so it cannot be parallelized.
  • Verification. The monolithic proof verifies with a single Groth16 pairing in ~1.5 s and is a standalone, constant-size artifact — the natural fit for on-chain verification. Nova verification is O(N): it re-checks all 255 pairings plus the state chain and the transcript (~3.2 s), and the bundle grows linearly with the number of steps.
  • Deployment footprint. Nova's ceremony is trivial but verification is not constant-time; Groth16's ceremony is heavy but is run once per circuit and then amortized across unlimited keys.
  • Both prove the same statement. The point-compression and addOut == 2·PointA checks are done by the application outside the Nova fold; a single monolithic proof encodes them in-circuit.

Rule of thumb: Nova if you prove a fresh key once (or run on constrained hardware), monolithic Groth16 if you prove many keys against a pre-computed ceremony or need constant-size, cheap on-chain verification.

Further reading

Also available as a dedicated documentation page.

Supported platforms

cardano-addresses is officially supported on the following operating systems:

  • Linux - Ubuntu 20.04+, Debian 11+, Fedora 38+, and other major distributions
  • macOS - version 11 (Big Sur) and later
  • Windows - Windows 10 and Windows 11

cardano-addresses comes with CLI for Linux, MacOS and Windows. See releases to get respective pre-compiled binaries. There is also straightforward way to build Docker image.

Building/testing from source using nix

Prerequisites: Install Nix with flakes enabled.

Enter development shell

nix develop

Inside the development shell:

# building
cabal build all

# testing
cabal test cardano-addresses:unit

# installing executable locally
cabal install cardano-address

Build using nix directly

# Build the Linux x86_64 binary
nix build .

# Run the built binary
./result/bin/cardano-address

Building for different platforms

# Linux x86_64
nix build .

# Darwin x86_64
nix build .#packages.x86_64-darwin.default

# Darwin aarch64 (Apple Silicon)
nix build .#packages.aarch64-darwin.default

# Linux aarch64
nix build .#packages.aarch64-linux.default

# Linux x86_64 to Windows (cross-compilation)
nix build .#packages.x86_64-linux.default

Building the Docker image

nix build .#packages.x86_64-linux.docker-image
docker load < result

Override command for cross-compilation

We have now fixed cross-compilation (from Linux to Windows) by replacing runtime git call in System.Git.TH with CPP macro (ie., GITREV) defaulting to "unknown" but allowing overriding via -DGITREV as below:

cabal build all --ghc-option=-DGITREV=\"$(git rev-parse HEAD)\"

Preparation steps before uploading to hackage

cabal build all
cabal haddock
cabal sdist

Note: Make sure proper version is set in cardano-addresses.cabal

Docker Image

Please make sure you have just installed as justfile is used for building Docker image.

Build

just clean-build-docker

Run

Use the auto-remove flag --rm when running commands.

docker run --rm cardano-address recovery-phrase generate --size 15

Use the interactive flag -i when piping stdin:

echo "addr1gqtnpvdhqrtpd4g424fcaq7k0ufuzyadt7djygf8qdyzevuph3wczvf2dwyx5u" | docker run --rm -i cardano-addresses address inspect

Javascript support

Javascript support was discontinued and dropped. One could look at the following now:

  1. MeshJS
  2. blaze-cardano

Alternatively one could lean back on release 3.9.0 where Javascript was still present.

WebAssembly

The library compiles to WebAssembly via GHC's WASM backend, producing a single cardano-addresses.wasm binary that runs in the browser or any WASI runtime.

Build

nix build github:IntersectMBO/cardano-addresses#wasm
ls result/cardano-addresses.wasm   # 7.0MB

Commands

The binary reads JSON from stdin and writes JSON to stdout. A cmd field selects the operation:

# Address inspection
echo '{"cmd":"inspect","address":"addr1..."}' | wasmtime result/cardano-addresses.wasm

# Key derivation (CIP-1852 Shelley)
# Hardened indexes can use the legacy 'H' suffix or the standard single-quote (') suffix.
# An optional 'm/' prefix denotes absolute derivation from the master key.
echo '{"cmd":"derive","mnemonic":"word1 word2 ...","path":"1852H/1815H/0H/0/0"}' | wasmtime result/cardano-addresses.wasm
# Equivalent with standard notation:
# echo '{"cmd":"derive","mnemonic":"word1 word2 ...","path":"m/1852'/1815'/0'/2/0"}' | wasmtime result/cardano-addresses.wasm

# Address construction
echo '{"cmd":"make-address","type":"enterprise","network":"testnet","payment_key":"hex..."}' | wasmtime result/cardano-addresses.wasm

# Ed25519 signing and verification
echo '{"cmd":"sign","key":"hex...","message":"hex..."}' | wasmtime result/cardano-addresses.wasm
echo '{"cmd":"verify","key":"hex...","message":"hex...","signature":"hex..."}' | wasmtime result/cardano-addresses.wasm

# Legacy bootstrap addresses (Byron/Icarus)
echo '{"cmd":"bootstrap-address","style":"icarus-from-mnemonic","protocol_magic":764824073,...}' | wasmtime result/cardano-addresses.wasm

Browser integration

Use @bjorn3/browser_wasi_shim to run the WASM binary client-side:

import { WASI, File, OpenFile, ConsoleStdout } from "@bjorn3/browser_wasi_shim";

const mod = await WebAssembly.compile(await (await fetch("cardano-addresses.wasm")).arrayBuffer());

async function call(input) {
  let out = "";
  const fds = [
    new OpenFile(new File(new TextEncoder().encode(input))),
    ConsoleStdout.lineBuffered(l => out += l + "\n"),
    ConsoleStdout.lineBuffered(() => {}),
  ];
  const wasi = new WASI([], [], fds, { debug: false });
  wasi.start(await WebAssembly.instantiate(mod, { wasi_snapshot_preview1: wasi.wasiImport }));
  return JSON.parse(out.trim());
}

Benchmarked: ~9ms compile (one-time), ~3ms per Shelley call, ~13ms for legacy.

A live demo is available at: https://IntersectMBO.github.io/cardano-addresses/browser/

Nix integration

Downstream flakes consume the WASM as a package:

{
  inputs.cardano-addresses.url = "github:IntersectMBO/cardano-addresses";

  outputs = { cardano-addresses, ... }: {
    packages.wasm = cardano-addresses.packages.x86_64-linux.wasm;
    # result/cardano-addresses.wasm
  };
}

Contributing

Pull requests are welcome.

When creating a pull request, please make sure that your code adheres to our coding standards.