Skip to content

Conversation

@brenzi
Copy link
Member

@brenzi brenzi commented Feb 9, 2026

Overview

This PR introduces the pallet-encointer-offline-payment which enables privacy-preserving offline payments using Groth16
zero-knowledge proofs on the BN254 curve.

Use Case

Encointer community currencies are designed for local economies where internet connectivity may be unreliable. The offline payment
system allows:

  1. Sender registers an offline identity (Poseidon commitment) while online
  2. Sender generates a ZK proof offline proving they authorized a payment without revealing their secret
  3. Either the sender or the receiver (or anyone else) submits the proof on-chain for settlement
  4. Chain verifies the proof and transfers funds, using a nullifier to prevent double-spending

This enables POS payments in areas with spotty connectivity - the payment can be generated offline and settled later when connectivity is restored.

Design Choices

Groth16 on BN254:

  • Constant-size proofs (128 bytes compressed) regardless of circuit complexity
  • Fast verification (~2ms) suitable for on-chain execution
  • BN254 is widely supported and has efficient arithmetic

Backgound:
image

criteria:

  • fast mobile proving because old phones may struggle
  • small proof size because it has to fit a QR code on rather small screens
  • no binary libs: f-droid accepts no binary libs. If we need to work with a native lib and ffi, prefer pure-rust

Why not noir?

Noir is architecturally superior (especially the no-trusted-setup property) but requires building the Flutter FFI bridge to Barretenberg and writing a custom Substrate UltraPlonk verifier — both significant efforts.

Poseidon Hash:

  • ZK-friendly hash function with minimal constraints (~300 R1CS constraints vs ~25,000 for SHA256)
  • Used for both commitment (H(zk_secret)) and nullifier (H(zk_secret, nonce)) computation
  • Parameters: t=3, rate=2, full_rounds=8, partial_rounds=57

Commitment Scheme:

  • commitment = Poseidon(zk_secret) - registered on-chain, binds identity
  • nullifier = Poseidon(zk_secret, nonce) - unique per payment, prevents replay
  • zk_secret derived deterministically from account seed for wallet recovery

Trusted Setup:

  • Test setup uses deterministic seed 0xDEADBEEFCAFEBABE for reproducibility
  • Production deployment requires MPC ceremony for security

Specification

Storage:

  • OfflineIdentities: Map<AccountId, [u8; 32]> - Poseidon commitments
  • UsedNullifiers: Map<[u8; 32], ()> - spent nullifiers
  • VerificationKey: Option<BoundedVec> - Groth16 VK (424 bytes)

Extrinsics:

  • register_offline_identity(commitment) - register Poseidon commitment
  • submit_offline_payment(proof, sender, recipient, amount, cid, nullifier) - settle payment
  • set_verification_key(vk_bytes) - root-only, set Groth16 VK

Circuit Public Inputs:

  1. commitment - sender's registered commitment
  2. recipient_hash - Blake2 hash of recipient AccountId
  3. amount - payment amount as field element
  4. cid_hash - Blake2 hash of community identifier
  5. nullifier - unique payment identifier

Circuit Constraints:

  • Proves knowledge of zk_secret such that Poseidon(zk_secret) == commitment
  • Proves nullifier == Poseidon(zk_secret, nonce) for witness nonce

Files Added

  • src/circuit.rs - Poseidon config and R1CS circuit definition
  • src/prover.rs - Trusted setup and Groth16 proof generation (std only)
  • src/verifier.rs - On-chain proof verification
  • src/lib.rs - Pallet with storage, events, errors, and extrinsics
  • src/tests.rs - 33 unit tests including full e2e ZK payment test

Dependencies

ark-bn254 = "0.4.0" # BN254 curve
ark-groth16 = "0.4.0" # Groth16 proving system
ark-crypto-primitives = "0.4.0" # Poseidon sponge
ark-r1cs-std = "0.4.0" # R1CS gadgets

Testing

cargo test -p pallet-encointer-offline-payment
33 tests pass, including e2e_zk_payment_works

@brenzi brenzi marked this pull request as ready for review February 10, 2026 08:09
@brenzi
Copy link
Member Author

brenzi commented Feb 10, 2026

I'm going to merge this to enable testing on Gesell. added a README which explicitly warns not to use in prod

@brenzi brenzi merged commit 078ec0a into master Feb 10, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant