Skip to main content

System overview

Sender device              Canonical runtime                 Operator
    │                             │                               │
    ├─ generate TransferNonce ────┤                               │
    │   transfer_id = H(nonce)    │                               │
    │                             │                               │
    ├─ compute nullifier ─────────┤                               │
    │   H(wallet_addr, tx_id)     │                               │
    │                             │                               │
    ├─ POST /transfers ───────────►                               │
    │   { asset_id,               │                               │
    │     sender_commitment,      ├─ verify sender balance        │
    │     recipient_commitment,   ├─ check nullifier not used     │
    │     amount, nullifier,      ├─ store TransferRecord (L1)    │
    │     transfer_nonce }        ├─ store ParticipantRecord (L2) │
    │                             ├─ update AccountRecord         │
    │◄── { transfer_id } ─────────┤                               │
    │                             │                               │
    │             subpoena ───────┼───────────────────────────────►
    │                             │              L2 + registry ───┤
The sender receives only transfer_id. The canonical runtime never stores the sender’s wallet address and the authority linkage in the same record. The operator holds the account registry separately; it is accessible only under legal process.

List 1 / List 2 separation

The anonymity guarantee rests on a single structural property: no common key exists between List 1 and List 2.
List 1 — transferRecords (LevelDB prefix "tx:")
  key:   transfer_id = H(TransferNonce)   ← random, chosen by sender
  value: TransferRecord { transfer_id, asset_id, amount, timestamp, height }

List 2 — participants (LevelDB prefix "participant:")
  key:   nullifier = H(wallet_address, transfer_id)
  value: ParticipantRecord { transfer_id, identity_hash, height }
transfer_id is the hash of a random nonce. nullifier is the hash of the wallet address concatenated with the transfer ID. These two values share no derivation path. Joining them requires knowing the sender’s wallet address — which never leaves their device. This is the same mechanism that separates public ballot direction from identity in the voting and governance embodiments. The invariant is use-case-agnostic.

Nullifier: double-spend prevention

The nullifier H(wallet_address, transfer_id) serves the same function as the ZK nullifier in the voting protocol: it prevents the same input from being used twice, without revealing which account submitted a given transfer.
constraints:
  nullifier = H(wallet_address, transfer_id)
  nullifier not in participants  // double-spend check
In the voting protocol, the nullifier is a ZK-proven MiMC hash (to hide person_secret). In the current shyware scaffold, the nullifier is a plain hash — the wallet address is known to the sender, and the canonical runtime verifies only that the nullifier has not been used. The production circuit will ZK-prove the nullifier derivation to hide the wallet address from the canonical runtime entirely.

Value conservation

In addition to the two-list invariant, transfers must conserve value:
sender.Balance    -= Amount
recipient.Balance += Amount
TotalSupply        unchanged  (no Mint or Burn occurred)
This is enforced by state.ExecuteTransfer before every transfer is committed. A transfer where sender.Balance < Amount is rejected with ErrorInsufficientBalance. Current scaffold (plaintext amounts): Amount and Balance are stored as int64. The canonical runtime reads balances to verify conservation, meaning the runtime can observe individual account balances. This is the TODO(circuit) state — acceptable for a licensed enterprise deployment where the operator controls the runtime, not for a permissionless public deployment. Production circuit (Pedersen commitments): Amount becomes an AmountCommitment (Pedersen commitment over BN254). The ZK proof proves Σ inputs == Σ outputs and sender_balance >= amount using range proofs, without revealing either value to the canonical runtime. This is established cryptography (Confidential Transactions, Monero RingCT) — an engineering addition to the existing circuit infrastructure.

Count-match invariant

At every committed block:
len(transferRecords) == len(participants)
This invariant is enforced by state.ExecuteTransfer. A missing or extra record in either list causes the transaction to be rejected. It is the transfer-domain equivalent of the voting protocol’s count-match: len(voteDirections) == len(voterRegistry).

Supply invariant

For every asset:
TotalSupply == TotalMinted - TotalBurned
Only the operator can Mint or Burn. Every TxTypeMint increases TotalMinted and credits the recipient account. Every TxTypeBurn increases TotalBurned and debits the source account. The supply total is publicly queryable; individual balances are not.

Account commitments

Accounts are identified on-chain by account_commitment = H(wallet_address). The wallet address itself is never stored on-chain. The commitment is a one-way function: the ABCI node can store and look up balances by commitment without knowing the underlying address. A TxTypeRegisterAccount transaction establishes the commitment on-chain, authenticated by a WalletProof — an ECDSA signature proving ownership of the wallet address. After registration, the wallet address is only needed locally to compute nullifiers.

Consensus layer

The current reference implementation uses CometBFT-style BFT consensus, aligned with the voting deployments. A 4-validator deployment tolerates 1 Byzantine fault. The canonical application implements:
  • CheckTx — stateless validation (type, signature, required fields)
  • FinalizeBlock — stateful execution (balance check, nullifier dedup, List 1/2 writes, count-match)
  • Query — supply, transfer count, account balance (commitment → balance only)
The signer interface abstracts the signing backend. Production deployments use the shared managed signing boundary: AWS KMS as the application-facing plane and Azure Managed HSM as the external root / attestation tier, with FIPS 140-3 validated cryptographic modules at Level 3 where available. Development may use a local software signer.

What’s deferred (TODO circuit)

The following are marked TODO(circuit) in the source and represent the gap between the current scaffold and a fully private permissionless deployment:
FieldCurrentProduction
TransferRecord.Amountint64 plaintextPedersen commitment
AccountRecord.Balanceint64 plaintextPedersen commitment
TransferData.SenderProofunusedGroth16 range proof
Nullifier derivationplain H(wallet, tx_id)ZK-proven (hide wallet from node)
For enterprise deployments where the operator controls the runtime (stablecoin issuer, neobank), the plaintext scaffold provides sufficient privacy — the operator already holds the account registry for regulatory compliance. The circuit is required for permissionless deployments.