Skip to Content
ProtocolCore Concepts

Core Concepts

AMACI’s core components, data structures, and key mechanisms.

Registration Mode Configuration

AMACI uses a unified RegistrationModeConfig enum to control how users can register. This replaces the old separate whitelist, oracle, and pre-deactivate fields.

SignUpWithStaticWhitelist (Standard Registration)

The fastest method. Pre-defined addresses call SignUp directly and receive a state index.

// Round created with static whitelist registrationMode: { sign_up_with_static_whitelist: { whitelist: { users: [{ addr: 'dora1abc...' }] } } } // User registers await client.signup({ signer: wallet, address, contractAddress, maciKeypair });

Privacy Level: Low. Operator can correlate through on-chain signup transactions: address dora1abc... → state index 5 → what they voted for.

Use Cases: Public voting where privacy isn’t a concern, or trusted internal environments.

Add-new-key (Dynamic Anonymity, requires deactivateEnabled: true)

Creates new identity through zero-knowledge proofs, completely breaking link to original address.

Core Idea: Prove “I know the private key of some entry in the deactivate tree” without revealing which one.

// 1. Submit deactivate with old identity await client.deactivate({ signer: oldWallet, ... }); // 2. Wait for operator to process deactivate, generating deactivate tree // 3. Generate ZK proof const voterClient = new VoterClient({ secretKey: oldPrivateKey }); const payload = await voterClient.buildAddNewKeyPayload({ operatorPubkey, deactivates, // deactivate tree leaves wasmFile, zkeyFile }); // 4. Submit add-new-key with new wallet await client.addNewKey({ signer: newWallet, // Different address d: payload.d, proof: payload.proof, nullifier: payload.nullifier, newMaciKeypair: newKeypair });

Privacy Level: High. Operator only knows “someone registered with add-new-key” but cannot determine which user in the deactivate tree. Anonymity set size = number of deactivate messages.

Requires: Round must be created with deactivateEnabled: true.

Trade-off: Need to wait for operator to process deactivate (usually a few hours).

PrePopulated (Pre-configured Anonymity)

Users must use PreAddNewKey. Deactivate tree is pre-configured when the round is created. Direct SignUp is disabled.

// Round created in PrePopulated mode registrationMode: { pre_populated: { pre_deactivate_root: preDeactivateData.root, pre_deactivate_coordinator: { x: '...', y: '...' } } } // Platform pre-generates keypairs and distributes to users // Users generate ZK proof and register from any address const payload = await voterClient.buildPreAddNewKeyPayload({ coordinatorPubkey, deactivates: preConfiguredLeaves, wasmFile, zkeyFile }); await client.rawPreAddNewKey({ signer: anyWallet, d: payload.d, proof: payload.proof, nullifier: payload.nullifier, newPubkey: myKeypair.publicKey });

Privacy Level: High. Same as add-new-key, but without waiting.

Trade-off: Requires vote organizer to pre-configure, and secure key distribution needed.

SignUpWithOracle (Oracle-gated Registration)

Any address can register but must present a backend-signed certificate. Used when access should be controlled by an off-chain oracle rather than a fixed address list.

// Round created with oracle registrationMode: { sign_up_with_oracle: { oracle_pubkey: ORACLE_PUBKEY_HEX } } voiceCreditMode: 'dynamic' // Certificate contains the voice credit amount // User registers with oracle certificate await client.signup({ signer: wallet, address, contractAddress, maciKeypair, certificate: oracleCertificate, amount: '150' // included in certificate signature });

Voice Credit Modes

Alongside registration mode, VoiceCreditMode defines how voting power is allocated:

  • Unified { amount }: All users get the same fixed number of voice credits.
  • Dynamic: Each user’s voice credits come from their oracle certificate. Used with SignUpWithOracle.

Poll ID

Each AMACI contract is assigned a unique, sequential Poll ID by the Registry contract. The pollId is embedded in every vote message as part of the packed command element, which the ZK circuit uses to verify message integrity. It can be queried via getPollId().

const pollId = await client.getPollId({ contractAddress }); console.log('Poll ID:', pollId); await client.vote({

Participating Roles

Voters

Responsible for generating keys and submitting voting messages. Can choose registration method: signup (fast but not anonymous), add-new-key (anonymous but requires waiting), or pre-add-new-key (anonymous and instant).

Key Capabilities:

  • Vote multiple times, last one counts
  • Change keys to invalidate previous votes
  • Use add-new-key to create anonymous identity

Operator

Responsible for processing voting messages and generating zero-knowledge proofs.

What They Can Do:

  • Decrypt all voting messages
  • See what each state index voted for

What They Cannot Do (AMACI Protection):

  • Cannot determine which address corresponds to users using add-new-key
  • Cannot tamper with votes (ZK proof constraints)
  • Cannot ignore or hide votes

Operators are a permissioned professional node network managed by Dora Factory. View list: https://vota.dorafactory.org/operators 

Smart Contracts

Stores encrypted messages, verifies ZK proofs, publishes results. Contracts cannot decrypt message content, relying on operators to submit processing results and proofs.

State Management

AMACI uses Merkle trees to manage voter state:

State Tree

Each leaf represents a voter:

State Leaf = { pubKey: [x, y], // MACI public key voiceCreditBalance: 100, // Available voting weight voteOptionTreeRoot: hash, // Vote option tree root nonce: 0, // Message sequence number d1: [0, 0], // AMACI: Anonymization data d2: [0, 0] // AMACI: Anonymization data }

d1/d2 fields are new additions in AMACI, used for re-randomization in add-new-key mechanism.

Deactivate Tree (AMACI-specific)

Stores all deactivate messages, forming anonymity set. Each leaf:

[c1[0], c1[1], c2[0], c2[1], sharedKeyHash]

Add-new-key users prove they know the private key of some entry in this tree without revealing which one.

Vote Option Tree

Each voter has one, recording voting weight for each option:

Root / \ [Opt0] [Opt1] (5vc) (3vc)

Only leaves corresponding to voted options are non-zero.

Message Tree

Stores all voting messages in submission order. Operator must process all messages, message tree root is used to verify completeness.

Key Mechanisms

Nonce Sequence

Each voter has a nonce starting from 0 and incrementing. Operator processes messages in nonce order:

1st message nonce=0 → process, nonce becomes 1 2nd message nonce=1 → process, nonce becomes 2 3rd message nonce=0 → ignore (expired)

This ensures latest vote is valid, old votes are overwritten.

Key Change

Voters can submit special messages to change public key. After public key in state leaf updates, messages signed with old key become invalid.

Anti-bribery principle:

  1. Accept bribe, vote for A with old key
  2. Secretly change key
  3. Vote for B with new key
  4. Only B is valid in the end, briber cannot verify

Message Encryption

Voting messages use ECDH + Poseidon encryption:

  1. Voter private key × Operator public key = Shared key
  2. Encrypt vote content with shared key
  3. Sign with EdDSA to prove message authenticity

Operator generates same shared key with their private key and voter’s public key to decrypt.

Zero-Knowledge Proofs

After processing messages, Operator generates ZK proofs proving:

  • Processed all messages
  • Processed in correct order (nonce)
  • Did not tamper with any votes
  • Correctly updated state tree
  • Correctly tallied results

Contract verifies proofs. Anyone can verify, ensuring operator didn’t cheat.

Voting Modes

One Person One Vote (1P1V)

Voice credits directly consumed as vote count:

voiceCredits = 100 vote = [ { idx: 0, vc: 60 }, // Consume 60 { idx: 1, vc: 40 } // Consume 40 ]

Quadratic Voting (QV)

Square of votes is consumption amount, encourages distributed voting:

voiceCredits = 100 vote = [ { idx: 0, vc: 8 }, // Consume 64 (8²) { idx: 1, vc: 6 } // Consume 36 (6²) ]

Registration and Access Control Summary

ModeWho Can RegisterVoice CreditsAnonymity
SignUpWithStaticWhitelistPre-listed addresses onlyFixed (Unified)Low
SignUpWithOracleAny address with valid certificateFixed or oracle-defined (Dynamic)Low
PrePopulatedAnyone with pre-generated key + ZK proofFixed (Unified)High
deactivateEnabled + AddNewKeyAnyone who previously deactivatedInheritedHigh

Next Steps

Understand Privacy Protection - Read Privacy Protection Mechanisms for deep dive into identity anonymization.

Learn Message Flow - Check Message Flow to understand voting message lifecycle.

Understand Cryptography - View Cryptography Mechanisms to learn EdDSA, Poseidon and other technical details.

Last updated on