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 withSignUpWithOracle.
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:
- Accept bribe, vote for A with old key
- Secretly change key
- Vote for B with new key
- Only B is valid in the end, briber cannot verify
Message Encryption
Voting messages use ECDH + Poseidon encryption:
- Voter private key × Operator public key = Shared key
- Encrypt vote content with shared key
- 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
| Mode | Who Can Register | Voice Credits | Anonymity |
|---|---|---|---|
SignUpWithStaticWhitelist | Pre-listed addresses only | Fixed (Unified) | Low |
SignUpWithOracle | Any address with valid certificate | Fixed or oracle-defined (Dynamic) | Low |
PrePopulated | Anyone with pre-generated key + ZK proof | Fixed (Unified) | High |
deactivateEnabled + AddNewKey | Anyone who previously deactivated | Inherited | High |
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.