AMACI Contract
AMACI (Anonymous MACI) contract is the core of each voting round, handling user signup, vote message storage, proof verification, and result publication.
Core Functions
The AMACI contract provides four main functions:
Registration Configuration
When creating an AMACI round, two configuration enums must be provided:
VoiceCreditMode
Defines how voting power is allocated to users:
| Mode | Description | Use Case |
|---|---|---|
Unified { amount } | All users receive the same fixed amount of voice credits | Standard AMACI rounds |
Dynamic | Each user’s voice credits equal the amount provided in their signed certificate | Oracle-gated rounds with variable weights |
// Unified mode: everyone gets 100 voice credits
voiceCreditMode: { unified: { amount: "100" } }
// Dynamic mode: voice credits come from oracle certificate
voiceCreditMode: "dynamic"RegistrationModeConfig
Defines how users register and how access is controlled. This single enum replaces the old whitelist, oracle_whitelist_pubkey, pre_deactivate_root, and pre_deactivate_coordinator fields:
| Mode | Access Control | Description |
|---|---|---|
SignUpWithStaticWhitelist { whitelist } | Pre-defined address list | Users call SignUp directly; only whitelisted addresses are allowed |
SignUpWithOracle { oracle_pubkey } | Backend signature | Users call SignUp with an oracle-signed certificate |
PrePopulated { pre_deactivate_root, pre_deactivate_coordinator } | ZK proof | Users must use PreAddNewKey; direct SignUp is disabled |
deactivate_enabled
An optional boolean flag (default: false) that enables the deactivate → AddNewKey flow for dynamic key rotation within the round. When false, only Signup and PreAddNewKey (in PrePopulated mode) are available.
// Create round with configuration
await client.createAMaciRound({
operator: selectedOperator,
maxVoter: 100,
voteOptionMap: ["Option A", "Option B", "Option C"],
voiceCreditMode: { unified: { amount: "100" } },
registrationMode: {
sign_up_with_static_whitelist: {
whitelist: {
users: [
{ addr: "dora1abc..." },
{ addr: "dora1def..." },
]
}
}
},
deactivateEnabled: false,
// ...other params
});User Registration Methods
AMACI supports three registration methods, allowing users to choose based on privacy needs.
1. Signup (Standard Registration)
The simplest and fastest registration method, suitable for scenarios where you don’t mind the Operator knowing your identity.
Signup Message
ExecuteMsg::Signup {
pubkey: PubKey, // User's MACI public key
certificate: Option<String>, // Oracle certificate (required for SignUpWithOracle mode)
amount: Option<Uint256>, // Voice credit amount (required for Dynamic VC mode with SignUpWithOracle)
}Signup Flow
Signup Code Example
// 1. Generate MACI keypair
// Derive EdDSA-Poseidon keypair from dora address
const maciKeypair = await client.genKeypairFromSign({
signer: wallet,
address
});
// 2. Signup (whitelist addresses only)
await client.signup({
signer: wallet,
address: userAddress,
contractAddress: amaciAddress,
maciKeypair
});Registration Mode Determines Signup Behavior:
The behavior of SignUp depends on the round’s RegistrationModeConfig:
SignUpWithStaticWhitelist: Only pre-listed addresses can signup. Nocertificateoramountneeded.SignUpWithOracle: Any address can attempt signup but must provide a backend-signedcertificate. WithDynamicvoice credit mode,amountmust also be provided (it is included in the certificate’s signature).PrePopulated:SignUpis disabled — users must usePreAddNewKeyinstead.
// SignUpWithStaticWhitelist mode: simple signup
await client.signup({
signer: wallet,
address: userAddress,
contractAddress: amaciAddress,
maciKeypair
});
// SignUpWithOracle + Dynamic VC mode: provide certificate and amount
await client.signup({
signer: wallet,
address: userAddress,
contractAddress: amaciAddress,
maciKeypair,
certificate: oracleCertificate, // base64-encoded oracle signature
amount: "150" // voice credits (included in the signed payload)
});Signup Privacy Level
Privacy Level: Low
Operator Can See:
- Wallet address (through on-chain signup transaction)
- MACI public key
- State Index
- Vote content (after decryption)
Risks:
- Operator can fully correlate: wallet address → pubkey → State Index → vote content
- Potential for targeted bribery or retaliation
Suitable For:
- Don’t mind Operator knowing identity
- Quick and simple voting
- Trusted Operator
2. Add-new-key (Dynamic Key Change)
Uses zero-knowledge proofs to create anonymous identity that Operator cannot link to original user.
Add-new-key Message
ExecuteMsg::AddNewKey {
pubkey: PubKey, // New user public key
nullifier: Uint256, // Anti-replay identifier
d: [Uint256; 4], // [d1[0], d1[1], d2[0], d2[1]]
groth16_proof: Groth16ProofType, // ZK proof
}Complete Flow
Old User Deactivate
// Old user submits deactivate message
await client.deactivate({
signer: oldWallet,
address: oldAddress,
contractAddress,
maciKeypair: oldKeypair
});
// This generates a special vote message:
// - voIdx = 0
// - newVotes = 0
// - newPubKey = [0, 0] (indicates last message)Operator Processes Deactivate
// Operator processes all deactivate messages
// Generates deactivate tree
// Each deactivate leaf contains:
// [c1[0], c1[1], c2[0], c2[1], sharedKeyHash]
// Operator submits ProcessDeactivate
await contract.execute({
process_deactivate: {
size: deactivateMessages.length,
new_deactivate_commitment: deactivateCommitment,
new_deactivate_root: deactivateRoot,
groth16_proof: processDeactivateProof
}
});New User Generates ZK Proof
// Use VoterClient to generate payload
import { VoterClient } from '@dorafactory/maci-sdk';
const voterClient = new VoterClient({
network: 'testnet',
secretKey: oldPrivateKeyHex // Old user's private key
});
// Get deactivate data
const deactivates = await client.fetchAllDeactivateLogs(contractAddress);
// Generate add-new-key payload
const payload = await voterClient.buildAddNewKeyPayload({
stateTreeDepth: 10,
operatorPubkey: operatorPubkey,
deactivates: deactivates,
wasmFile, // addNewKey.wasm
zkeyFile // addNewKey.zkey
});
// payload contains:
// {
// proof: { a, b, c }, // Groth16 proof
// d: [d1_0, d1_1, d2_0, d2_1], // Re-randomized values
// nullifier: "0x..." // Anti-replay
// }Submit Add-new-key
// Submit with new wallet (important!)
const newKeypair = genKeypair();
await client.addNewKey({
signer: newWallet, // New wallet
contractAddress,
d: payload.d,
proof: payload.proof,
nullifier: payload.nullifier,
newMaciKeypair: newKeypair, // New MACI keypair
fee: 'auto'
});Contract Verification Flow
pub fn execute_add_new_key(
deps: DepsMut,
env: Env,
pubkey: PubKey,
nullifier: Uint256,
d: [Uint256; 4],
groth16_proof: Groth16ProofType,
) -> Result<Response, ContractError> {
// 1. Check voting period
check_voting_time(env, voting_time)?;
// 2. Check if nullifier already used
if NULLIFIERS.has(deps.storage, nullifier.to_be_bytes()) {
return Err(ContractError::NewKeyExist {});
}
NULLIFIERS.save(deps.storage, nullifier.to_be_bytes(), &true)?;
// 3. Build public inputs
let input = [
DNODES.load(...)?, // deactivate root
COORDINATORHASH.load(...)?, // operator pubkey hash
nullifier,
d[0], d[1], d[2], d[3]
];
let input_hash = hash_256(input) % SNARK_SCALAR_FIELD;
// 4. Verify ZK proof
let is_valid = groth16_verify(&proof, &[input_hash])?;
if !is_valid {
return Err(ContractError::InvalidProof {
step: "AddNewKey".to_string()
});
}
// 5. Create new State Leaf (includes d1, d2)
let state_leaf = StateLeaf {
pub_key: pubkey,
voice_credit_balance: voice_credit_amount,
vote_option_tree_root: Uint256::zero(),
nonce: Uint256::zero(),
}.hash_new_key_state_leaf(d);
// 6. Assign State Index
let state_index = num_sign_ups;
state_enqueue(&mut deps, state_leaf)?;
num_sign_ups += 1;
// 7. Save mapping (pubkey → state index)
SIGNUPED.save(deps.storage, &pubkey_bytes, &state_index)?;
Ok(Response::new()
.add_attribute("action", "add_new_key")
.add_attribute("state_idx", state_index.to_string()))
}ZK Circuit Description
Add-new-key uses a specialized ZK circuit to verify user identity:
template AddNewKey(stateTreeDepth) {
// Public inputs
signal input deactivateRoot; // Deactivate tree root
signal input coordPubKey[2]; // Operator public key
signal input nullifier; // Anti-replay
signal input d1[2]; // Re-randomized values
signal input d2[2];
// Private inputs (critical! not public)
signal input oldPrivateKey; // Old user private key
signal input deactivateIndex; // Position in tree
signal input deactivateLeaf; // Leaf value
signal input c1[2]; // Original encrypted values
signal input c2[2];
signal input randomVal; // Random value for randomization
signal input deactivateLeafPathElements[...]; // Merkle path
// Verification steps:
// 1. Verify nullifier = hash(oldPrivateKey, constant)
// 2. Calculate sharedKeyHash = hash(ecdh(oldPrivKey, coordPubKey))
// 3. Verify deactivateLeaf = hash(c1, c2, sharedKeyHash)
// 4. Verify leaf is in deactivateRoot tree (using deactivateIndex and path)
// 5. Verify d1, d2 = rerandomize(c1, c2, randomVal, coordPubKey)
}Add-new-key Privacy Level
Privacy Level: Highest
Operator Can Only See:
- New wallet address (possibly brand new)
- New MACI public key
- nullifier (just a hash)
- d1, d2 (re-randomized values)
- ZK proof (verified valid)
Operator Cannot Determine:
- Which position in deactivate tree the new user is
- Which wallet address corresponds to
- Which old identity is associated with
- What oldPrivateKey is
Anonymity Set:
- Anonymity set size = number of entries in deactivate tree
- Example: 100 deactivates → Operator knows “one of 100 people”
3. Pre-add-new-key (Pre-configured Key Change)
Uses pre-configured deactivate root to create anonymous identity immediately.
Pre-add-new-key Message
ExecuteMsg::PreAddNewKey {
pubkey: PubKey, // New user public key
nullifier: Uint256, // Anti-replay identifier
d: [Uint256; 4], // [d1[0], d1[1], d2[0], d2[1]]
groth16_proof: Groth16ProofType, // ZK proof
}Differences from Add-new-key
| Feature | Add-new-key | Pre-add-new-key |
|---|---|---|
| Deactivate Source | Dynamically generated during voting | Pre-configured at Round creation |
| Coordinator | Current Operator | Pre-configured Coordinator |
| Wait Time | Need to wait for ProcessDeactivate | Available immediately |
| Flexibility | High (real-time) | Medium (requires pre-configuration) |
Contract Verification Differences
// add-new-key uses dynamically generated root
input[0] = DNODES.load(deps.storage, ...)?; // Dynamic
input[1] = COORDINATORHASH.load(deps.storage)?; // Current operator
// pre-add-new-key uses pre-configured root
input[0] = PRE_DEACTIVATE_ROOT.load(deps.storage)?; // Pre-configured
input[1] = PRE_DEACTIVATE_COORDINATOR_HASH.load(...)?; // May be differentUse Cases
Suitable For:
- Round creation already knows anonymous voting is needed
- Pre-prepared set of deactivate data (e.g., platform distributes keys to users)
- Want users to anonymously participate immediately without waiting
Round Configuration Example:
// Configure when creating Round using PrePopulated mode
await client.createAMaciRound({
// ... other parameters
registrationMode: {
pre_populated: {
pre_deactivate_root: preDeactivateData.root,
pre_deactivate_coordinator: {
x: preCoordPubkey[0].toString(),
y: preCoordPubkey[1].toString()
}
}
},
voiceCreditMode: { unified: { amount: "100" } },
deactivateEnabled: false // PrePopulated mode does not need live deactivation
});Pre-add-new-key Privacy Level
Privacy Level: Highest
Same privacy protection as add-new-key, but without waiting.
4. Process Deactivate (AMACI-specific)
Operator processes deactivate messages to generate deactivate tree for add-new-key.
Process Deactivate Message
ExecuteMsg::ProcessDeactivate {
size: u64, // Number of messages processed
new_deactivate_commitment: Uint256, // New deactivate commitment
new_deactivate_root: Uint256, // New deactivate tree root
groth16_proof: Groth16ProofType, // ZK proof
}Workflow
1. Collect Deactivate Messages
// User submits deactivate
await client.deactivate({
signer: wallet,
contractAddress,
maciKeypair
});
// Generated as special vote message:
// {
// nonce: current_nonce,
// stateIdx: user_state_idx,
// voIdx: 0, // Special marker
// newVotes: 0, // Special marker
// newPubKey: [0, 0], // Indicates last message
// signature: ...
// }2. Operator Processes Deactivate
// Operator decrypts deactivate messages
const deactivateCommands = deactivateMessages.map(msg => {
const sharedKey = ecdh(operatorPrivKey, msg.encPubKey);
const command = poseidonDecrypt(msg.data, sharedKey);
return command;
});
// Generate deactivate tree
const deactivateLeaves = [];
for (const cmd of deactivateCommands) {
const stateIdx = cmd.stateIdx;
const userPubKey = stateTree.getLeaf(stateIdx).pubKey;
// Generate encrypted deactivation flag
const sharedKey = ecdh(operatorPrivKey, userPubKey);
const sharedKeyHash = poseidon(sharedKey);
const randomVal = deterministicRandom(operatorPrivKey, stateIdx);
const { c1, c2 } = encryptDeactivateFlag(
true, // Mark as deactivated
operatorPubKey,
randomVal
);
// Build leaf
const leaf = [c1[0], c1[1], c2[0], c2[1], sharedKeyHash];
deactivateLeaves.push(leaf);
}
// Build Merkle tree
const deactivateTree = new Tree(5, stateTreeDepth + 2, 0n);
deactivateTree.initLeaves(deactivateLeaves.map(l => poseidon(l)));
const deactivateRoot = deactivateTree.root;3. Generate and Submit Proof
// Generate ProcessDeactivate proof
const { proof } = await groth16.fullProve(
processDeactivateInput,
wasmFile,
zkeyFile
);
// Submit to contract
await contract.execute({
process_deactivate: {
size: deactivateMessages.length,
new_deactivate_commitment: deactivateCommitment,
new_deactivate_root: deactivateRoot,
groth16_proof: proof
}
});Process Deactivate Circuit
template ProcessDeactivateMessages(stateTreeDepth, batchSize) {
// Public inputs
signal input newDeactivateRoot;
signal input coordPubKey[2];
signal input batchStartHash;
signal input batchEndHash;
signal input currentDeactivateCommitment;
signal input newDeactivateCommitment;
// Private inputs
signal input coordPrivKey; // Operator private key
signal input msgs[batchSize][7]; // Encrypted messages
signal input encPubKeys[batchSize][2]; // Encryption public keys
signal input currentStateLeaves[batchSize][10]; // Current state
signal input c1[batchSize][2]; // Generated encrypted values
signal input c2[batchSize][2];
// Verification steps:
// 1. Decrypt all deactivate messages
// 2. Verify each message signature
// 3. Generate deactivate entry (c1, c2, hash) for each user
// 4. Build new deactivate tree
// 5. Verify newDeactivateRoot is correct
}Contract Verification
pub fn execute_process_deactivate(
deps: DepsMut,
size: u64,
new_deactivate_commitment: Uint256,
new_deactivate_root: Uint256,
groth16_proof: Groth16ProofType,
) -> Result<Response, ContractError> {
// 1. Check state
// 2. Build public inputs
let input = [
new_deactivate_root,
coord_pubkey_hash,
batch_start_hash,
batch_end_hash,
current_deactivate_commitment,
new_deactivate_commitment,
current_state_root
];
// 3. Verify ZK proof
let is_valid = groth16_verify(&proof, &[input_hash])?;
if !is_valid {
return Err(ContractError::InvalidProof {
step: "ProcessDeactivate".to_string()
});
}
// 4. Update deactivate root
DNODES.save(deps.storage, key, &new_deactivate_root)?;
CURRENT_DEACTIVATE_COMMITMENT.save(..., &new_deactivate_commitment)?;
// 5. Update processing count
PROCESSED_DMSG_COUNT.save(deps.storage, &new_count)?;
Ok(Response::new())
}Purpose of Process Deactivate
Provides Anonymity Set for Add-new-key:
Without ProcessDeactivate:
→ Cannot use add-new-key
→ Can only use signup (low privacy)
With ProcessDeactivate:
→ Generates deactivate tree
→ Users can use add-new-key
→ Anonymity set size = number of deactivate messagesTiming:
- Can be processed any time during voting period
- Usually processed after enough deactivate messages accumulate
- Recommendation: At least 50-100 deactivates before processing (larger anonymity set)
Voting Messages (PublishMessage)
After registration (regardless of method), users can submit encrypted voting messages.
PublishMessage Message
PublishMessage accepts messages in batch — a single call can submit one or more encrypted votes:
ExecuteMsg::PublishMessage {
messages: Vec<MessageData>, // Batch of encrypted vote messages
enc_pub_keys: Vec<PubKey>, // Corresponding ephemeral encryption public keys
}
pub struct MessageData {
pub data: [Uint256; 10], // 10 encrypted fields
}Each messages[i] must have a corresponding enc_pub_keys[i]. A per-message fee of 10 DORA is charged for each message submitted.
Message Format
Each encrypted message contains exactly 10 Uint256 fields:
interface EncryptedMessage {
data: [
bigint, // [0] packaged: nonce(32b)|stateIdx(32b)|voIdx(32b)|newVotes(96b)|pollId(32b)
bigint, // [1] newPubKey.x
bigint, // [2] newPubKey.y
bigint, // [3] salt
bigint, // [4] signature.R8.x
bigint, // [5] signature.R8.y
bigint, // [6] signature.S
bigint, // [7] Poseidon encryption output[0]
bigint, // [8] Poseidon encryption output[1]
bigint, // [9] Poseidon encryption output[2]
];
}Command array layout: Fields [0]–[6] are the plaintext command elements. The
packagedfield at [0] encodes five sub-fields using fixed-width bit positions (see bit layout in Message Packing). Fields [7]–[9] are the Poseidon ciphertext output. Each message is submitted together with its ephemeralenc_pub_key.
Voting Flow
Voting Example
// Get round info (includes coordinator pubkey)
const roundInfo = await client.getRoundInfo({ contractAddress: amaciAddress });
await client.vote({
signer: wallet,
address: userAddress,
contractAddress: amaciAddress,
selectedOptions: [
{ idx: 0, vc: 5 }, // 5 votes for option 0
{ idx: 1, vc: 3 }, // 3 votes for option 1
],
operatorCoordPubKey: [
BigInt(roundInfo.coordinatorPubkeyX),
BigInt(roundInfo.coordinatorPubkeyY)
],
maciKeypair: keypair
});Message Storage
Messages are stored in contract in order:
// Message queue
pub const MESSAGES: Item<Vec<Message>> = Item::new("messages");
// Add message
fn publish_message(
deps: DepsMut,
message: MessageData
) -> Result<Response, ContractError> {
let mut messages = MESSAGES.load(deps.storage)?;
messages.push(Message {
msg_type: Uint256::from(1u128), // 1 = voting message
data: message.data,
});
MESSAGES.save(deps.storage, &messages)?;
Ok(Response::new()
.add_attribute("action", "publish_message")
.add_attribute("message_id", messages.len().to_string()))
}Multiple Votes
Users can call PublishMessage multiple times, later messages override earlier ones:
// First vote
await vote({ options: [{ idx: 0, vc: 5 }], nonce: 0 });
// Change mind, revote
await vote({ options: [{ idx: 1, vc: 5 }], nonce: 1 });
// Change mind again
await vote({ options: [{ idx: 2, vc: 5 }], nonce: 2 });
// During processing, only last vote (option 2) is validMessage Processing (ProcessMessages)
Operator submits zero-knowledge proofs to process voting messages.
ProcessMessages Message
ExecuteMsg::ProcessMessages {
new_state_commitment: Uint256, // New state root
groth16_proof: Groth16ProofType, // Groth16 proof
}Processing Flow
Proof Verification
fn process_messages(
deps: DepsMut,
new_state_commitment: Uint256,
proof: Groth16ProofType
) -> Result<Response, ContractError> {
// 1. Check state
let round_info = ROUND_INFO.load(deps.storage)?;
if round_info.status != RoundStatus::Processing {
return Err(ContractError::InvalidRoundStatus {});
}
// 2. Construct public inputs
let public_inputs = vec![
coordinator_pub_key_x,
coordinator_pub_key_y,
message_root,
current_state_root,
new_state_commitment,
// ... other public inputs
];
// 3. Verify Groth16 proof
let is_valid = verify_groth16_proof(
proof,
public_inputs,
verification_key
)?;
if !is_valid {
return Err(ContractError::ProofVerificationFailed {});
}
// 4. Update state root
STATE_COMMITMENT.save(deps.storage, &new_state_commitment)?;
Ok(Response::new()
.add_attribute("action", "process_messages")
.add_attribute("new_state_root", new_state_commitment.to_string()))
}Result Tallying (ProcessTally)
Operator submits tally proof to publish results.
ProcessTally Message
ExecuteMsg::ProcessTally {
new_tally_commitment: Uint256, // Tally result commitment
groth16_proof: Groth16ProofType, // Groth16 proof
}Tallying Flow
Result Publication
fn process_tally(
deps: DepsMut,
new_tally_commitment: Uint256,
proof: Groth16ProofType
) -> Result<Response, ContractError> {
// 1. Verify proof
let is_valid = verify_groth16_proof(
proof,
public_inputs,
tally_verification_key
)?;
if !is_valid {
return Err(ContractError::ProofVerificationFailed {});
}
// 2. Save tally results
TALLY_COMMITMENT.save(deps.storage, &new_tally_commitment)?;
// 3. Update status
let mut round_info = ROUND_INFO.load(deps.storage)?;
round_info.status = RoundStatus::Tallied;
ROUND_INFO.save(deps.storage, &round_info)?;
Ok(Response::new()
.add_attribute("action", "process_tally")
.add_attribute("tally_commitment", new_tally_commitment.to_string()))
}Query Functions
Round Information
QueryMsg::GetRoundInfo {}
QueryMsg::GetPeriod {}
QueryMsg::GetNumSignUp {}
QueryMsg::GetVotingTime {}Poll ID
Each AMACI instance is assigned a unique poll_id by the Registry at creation time:
QueryMsg::GetPollId {}
// Returns: u64Registration Configuration
// Get the current registration and voice credit configuration
QueryMsg::GetRegistrationConfig {}
// Returns: RegistrationConfigInfo {
// deactivate_enabled: bool,
// voice_credit_mode: VoiceCreditMode,
// registration_mode: RegistrationMode,
// }
// Get whether deactivate feature is enabled
QueryMsg::GetDeactivateEnabled {}
// Returns: boolRegistration Status (replaces IsWhiteList / WhiteBalanceOf)
A unified query that returns registration eligibility and voice credit balance, adapting to the active registration mode:
QueryMsg::QueryRegistrationStatus {
sender: Option<Addr>, // For SignUpWithStaticWhitelist mode
pubkey: Option<PubKey>, // For SignUpWithOracle / PrePopulated mode
certificate: Option<String>,// For SignUpWithOracle mode
amount: Option<Uint256>, // For SignUpWithOracle + Dynamic VC mode
}
// Returns: RegistrationStatus {
// can_sign_up: bool,
// is_register: bool,
// balance: Uint256,
// }// Check registration eligibility (whitelist mode)
const status = await client.queryRegistrationStatus({
contractAddress: amaciAddress,
address: userAddress
});
// Check registration eligibility (oracle mode with dynamic VC)
const status = await client.queryRegistrationStatus({
contractAddress: amaciAddress,
pubkey: { x: pubkeyX, y: pubkeyY },
certificate: oracleCert,
amount: "150"
});
console.log(status.can_sign_up); // Whether user can register
console.log(status.is_register); // Whether already registered
console.log(status.balance); // Voice credits if registeredMessage Queries
QueryMsg::GetMsgChainLength {} // Total number of vote messages
QueryMsg::GetAllResult {} // Final tally for a single option index
QueryMsg::GetAllResults {} // Final tally results for all options (Vec<Uint256>)State Queries
QueryMsg::GetCurrentStateCommitment {}
QueryMsg::GetCurrentDeactivateCommitment {}
QueryMsg::Signuped { pubkey: PubKey } // Returns state index if registered
QueryMsg::GetStateIdxInc { address: Addr }
QueryMsg::GetVoiceCreditBalance { index: String }
QueryMsg::MaxVoteOptions {}
QueryMsg::QueryCircuitType {}
QueryMsg::QueryOracleWhitelistConfig {}Round Status
AMACI contract has the following states:
pub enum PeriodStatus {
Pending, // Contract instantiated, waiting for voting start time
Processing, // Voting period ended; operator is processing messages
Tallying, // Message processing complete; operator is tallying votes
Ended, // Tally submitted and verified; round is complete
}Note: The
Votingstate was removed. The contract determines whether the voting window is open by comparing the current block time against the configuredstart_timeandend_time. There is no explicit state transition into “Voting”.
State Transitions
UpdateRegistrationConfig
After a round is created but before voting starts, the organizer can update registration settings:
ExecuteMsg::UpdateRegistrationConfig {
config: RegistrationConfigUpdate {
deactivate_enabled: Option<bool>, // Toggle deactivate feature
voice_credit_mode: Option<VoiceCreditMode>, // Only changeable when num_signups == 0
registration_mode: Option<RegistrationModeConfig>, // Only changeable when num_signups == 0
}
}This replaces the old SetWhitelists message.
Security Features
Time Verification
fn ensure_voting_period(
env: &Env,
voting_time: &VotingTime
) -> Result<(), ContractError> {
let current_time = env.block.time.seconds();
if current_time < voting_time.start_time {
return Err(ContractError::VotingNotStarted {});
}
if current_time > voting_time.end_time {
return Err(ContractError::VotingEnded {});
}
Ok(())
}Access Control
// Only Coordinator can process messages
fn ensure_coordinator(
sender: &Addr,
coordinator: &Addr
) -> Result<(), ContractError> {
if sender != coordinator {
return Err(ContractError::Unauthorized {});
}
Ok(())
}Reentrancy Protection
// Use state lock to prevent reentrancy
fn process_messages_with_lock(
deps: DepsMut,
// ... parameters
) -> Result<Response, ContractError> {
// Check lock
let is_locked = PROCESSING_LOCK.may_load(deps.storage)?.unwrap_or(false);
if is_locked {
return Err(ContractError::AlreadyProcessing {});
}
// Set lock
PROCESSING_LOCK.save(deps.storage, &true)?;
// Processing logic
let result = process_messages_internal(deps, ...);
// Release lock
PROCESSING_LOCK.save(deps.storage, &false)?;
result
}Registration Method Selection Recommendations
Choose the appropriate registration method based on your privacy needs:
Low Privacy Scenarios
Use Signup:
- Internal community voting
- Non-sensitive decisions
- Trusted Operator
High Privacy Scenarios
Use Add-new-key:
- Large fund allocation
- Sensitive topic voting
- Potentially controversial decisions
- Don’t fully trust Operator
Use Pre-add-new-key:
- Need quick anonymity
- Round already pre-configured
- Emergency voting scenarios
Mixed Usage
In the same Round, different users can use different registration methods:
// User A: Use signup (fast)
await userA.signup({...});
// User B: Use add-new-key (anonymous)
await userB.addNewKey({...});
// User C: Use pre-add-new-key (fast and anonymous)
await userC.rawPreAddNewKey({...});
// All can vote normally
await userA.vote({...});
await userB.vote({...});
await userC.vote({...});Next Steps
After understanding all AMACI contract registration methods, you can:
- Complete Workflow - Understand full process from creation to results
- SDK Usage Guide - Use SDK to interact with AMACI
- Example Code - View complete voting examples