use crate::circuit_params::match_vkeys; use crate::error::ContractError; use crate::groth16_parser::{parse_groth16_proof, parse_groth16_vkey}; use crate::msg::{ DelayConfigResponse, ExecuteMsg, FeeConfigResponse, Groth16ProofType, InstantiateMsg, InstantiationData, QueryMsg, RegistrationConfigInfo, RegistrationConfigUpdate, RegistrationModeConfig, RegistrationStatus, TallyDelayInfo, WhitelistBaseConfig, }; use crate::state::{ Admin, DelayConfig, DelayRecord, DelayRecords, DelayType, FeeConfig, Groth16ProofStr, MaciParameters, MessageData, OracleWhitelistUser, Period, PeriodStatus, PubKey, QuinaryTreeRoot, RegistrationMode, RoundInfo, StateLeaf, VoiceCreditMode, VotingTime, Whitelist, WhitelistConfig, ADMIN, CERTSYSTEM, CIRCUITTYPE, COORDINATORHASH, CREATE_ROUND_WINDOW, CURRENT_DEACTIVATE_COMMITMENT, CURRENT_STATE_COMMITMENT, CURRENT_TALLY_COMMITMENT, DEACTIVATE_COUNT, DEACTIVATE_ENABLED, DELAY_CONFIG, DELAY_RECORDS, DMSG_CHAIN_LENGTH, DMSG_HASHES, DNODES, FEE_CONFIG, FEE_DENOM, FEE_RECIPIENT, FIRST_DMSG_TIMESTAMP, GROTH16_DEACTIVATE_VKEYS, GROTH16_NEWKEY_VKEYS, GROTH16_PROCESS_VKEYS, GROTH16_TALLY_VKEYS, LEAF_IDX_0, MACIPARAMETERS, MACI_DEACTIVATE_MESSAGE, MACI_OPERATOR, MAX_LEAVES_COUNT, MAX_VOTE_OPTIONS, MSG_CHAIN_LENGTH, MSG_HASHES, NODES, NULLIFIERS, NUMSIGNUPS, ORACLE_WHITELIST, PENALTY_RATE, PERIOD, POLL_ID, PRE_DEACTIVATE_COORDINATOR_HASH, PRE_DEACTIVATE_ROOT, PROCESSED_DMSG_COUNT, PROCESSED_MSG_COUNT, PROCESSED_USER_COUNT, QTR_LIB, REGISTRATION_MODE, RESULT, ROUNDINFO, SIGNUPED, STATEIDXINC, STATE_ROOT_BY_DMSG, TALLY_DELAY_MAX_HOURS, TALLY_DELAY_MULTIPLIER, TALLY_TIMEOUT, TALLY_TIMEOUT_EXTRA_SECONDS, TOTAL_RESULT, USED_ENC_PUB_KEYS, VOICECREDITBALANCE, VOICE_CREDIT_AMOUNT, VOICE_CREDIT_MODE, VOTEOPTIONMAP, VOTINGTIME, WHITELIST, ZEROS, ZEROS_H10, }; use cosmwasm_schema::cw_serde; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cw2::set_contract_version; use pairing_ce::bn256::Bn256; use cosmwasm_std::{ attr, coins, to_json_binary, Addr, BankMsg, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Timestamp, Uint128, Uint256, }; use maci_utils::{ hash2, hash5, hash_256_uint256_list, is_on_babyjubjub_curve, uint256_from_hex_string, }; use sha2::{Digest, Sha256}; use bellman_ce_verifier::{prepare_verifying_key, verify_proof as groth16_verify}; use ff_ce::PrimeField as Fr; use hex; use serde_json_wasm as serde_json; // Used by both certificate verification paths; fields must remain in this alphabetical order // to produce JSON identical to what serde_json::json!{} generated with its BTreeMap backing. #[derive(serde::Serialize)] struct VerifyPayload { amount: String, contract_address: String, pubkey_x: String, pubkey_y: String, } /// BN254 scalar field modulus (hex), used to reduce input hashes before proof verification const SNARK_SCALAR_FIELD_HEX: &str = "30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001"; /// Convert Uint256 to a field element for proof verification /// This helper centralizes the conversion logic #[inline] fn uint256_to_field(input: &Uint256) -> Result { F::from_str(&input.to_string()).ok_or_else(|| ContractError::FieldConversionError { value: input.to_string(), }) } /// Decode hex-encoded a/b/c components of a Groth16 proof into byte vectors fn decode_groth16_proof(proof: &Groth16ProofType) -> Result { Ok(Groth16ProofStr { pi_a: hex::decode(&proof.a).map_err(|_| ContractError::HexDecodingError {})?, pi_b: hex::decode(&proof.b).map_err(|_| ContractError::HexDecodingError {})?, pi_c: hex::decode(&proof.c).map_err(|_| ContractError::HexDecodingError {})?, }) } /// Parse and verify a Groth16 proof against a given vkey and input hash. /// Returns an error with the provided step name if verification fails. fn run_groth16_verify( vkey_str: crate::state::Groth16VkeyStr, proof: &Groth16ProofType, input_hash: Uint256, step: &str, ) -> Result<(), ContractError> { let proof_str = decode_groth16_proof(proof)?; let vkey = parse_groth16_vkey::(vkey_str)?; let pvk = prepare_verifying_key(&vkey); let pof = parse_groth16_proof::(proof_str)?; let is_passed = groth16_verify(&pvk, &pof, &[uint256_to_field(&input_hash)?]) .map_err(|_| ContractError::SynthesisError {})?; if !is_passed { return Err(ContractError::InvalidProof { step: step.to_string(), }); } Ok(()) } /// Convert a contract address to Uint256 format /// This function takes the address bytes and converts them to a Uint256 fn address_to_uint256(address: &Addr) -> Uint256 { let address_bytes = address.as_bytes(); // Use SHA256 hash to convert the address to a fixed-length 32-byte format let mut hasher = Sha256::new(); hasher.update(address_bytes); let hash_result = hasher.finalize(); // Convert the hash bytes to Uint256 let mut bytes = [0u8; 32]; bytes.copy_from_slice(&hash_result[..]); // Convert bytes to Uint256 (big-endian) let mut uint256_bytes = [0u8; 32]; for (i, &byte) in bytes.iter().enumerate() { uint256_bytes[31 - i] = byte; // Reverse for little-endian to big-endian conversion } Uint256::from_be_bytes(uint256_bytes) } // version info for migration info const CONTRACT_NAME: &str = "crates.io:cw-amaci"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); /// Validate and process whitelist users into WhitelistConfig /// /// This helper function performs the following validations: /// 1. Address must have 'dora1' prefix /// 2. Address must be a valid chain address (validated via cosmwasm_std::Api) /// 3. For Dynamic VC mode: voice_credit_amount must exist and be non-zero /// 4. For Unified VC mode: uses the provided default_amount for all users /// /// Returns a Vec ready to be stored fn validate_and_process_whitelist( api: &dyn cosmwasm_std::Api, users: &[WhitelistBaseConfig], voice_credit_mode: &VoiceCreditMode, default_amount: cosmwasm_std::Uint256, ) -> Result, ContractError> { let mut processed_users: Vec = Vec::new(); for user_config in users { let addr_str = user_config.addr.as_str(); if !addr_str.starts_with("dora1") { return Err(ContractError::InvalidWhitelistConfig { reason: format!("Must use dora address: {}", addr_str), }); } api.addr_validate(addr_str) .map_err(|_| ContractError::InvalidWhitelistConfig { reason: format!("Invalid address format: {}", addr_str), })?; // Determine amount and validate based on VoiceCreditMode let amount = match voice_credit_mode { VoiceCreditMode::Unified { .. } => { // Unified mode: use the same amount for all users default_amount } VoiceCreditMode::Dynamic => { // Dynamic mode: validate and extract amount from user config match user_config.voice_credit_amount { None => { return Err(ContractError::InvalidWhitelistConfig { reason: format!( "Dynamic VC mode requires voice_credit_amount for user {}", user_config.addr ), }); } Some(amount) if amount == cosmwasm_std::Uint256::zero() => { return Err(ContractError::InvalidWhitelistConfig { reason: format!( "voice_credit_amount must be non-zero for user {}", user_config.addr ), }); } Some(amount) => amount, // Valid: extract the amount } } }; let data = WhitelistConfig { addr: user_config.addr.clone(), is_register: false, voice_credit_amount: amount, }; processed_users.push(data); } Ok(processed_users) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, env: Env, info: MessageInfo, msg: InstantiateMsg, ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; // Create an admin with the sender address let admin = Admin { admin: msg.admin.clone(), }; ADMIN.save(deps.storage, &admin)?; let vote_option_max_amount = Uint256::from_u128( 5u128.pow( msg.parameters .vote_option_tree_depth .to_string() .parse() .map_err(|e| ContractError::ParseError { value: msg.parameters.vote_option_tree_depth.to_string(), reason: format!("{}", e), })?, ), ); let actual_vote_options = Uint256::from_u128(msg.vote_option_map.len() as u128); if actual_vote_options > vote_option_max_amount { return Err(ContractError::MaxVoteOptionsExceeded { current: actual_vote_options, max_allowed: vote_option_max_amount, }); } if msg.voting_time.end_time < env.block.time { return Err(ContractError::WrongTimeSet {}); } // if msg.voting_time.start_time >= msg.voting_time.end_time { // return Err(ContractError::WrongTimeSet {}); // } let create_round_window = Timestamp::from_seconds(10 * 60); // 10 minutes CREATE_ROUND_WINDOW.save(deps.storage, &create_round_window)?; // TODO: check apart time. if msg .voting_time .start_time .plus_seconds(create_round_window.seconds()) >= msg.voting_time.end_time { return Err(ContractError::WrongTimeSet {}); } // Compute the maximum number of leaves based on the state tree depth. // This is needed both for registration mode validation and for tree initialization, // so it is computed once here and reused throughout instantiate. let max_leaves_count = Uint256::from_u128( 5u128.pow( msg.parameters .state_tree_depth .to_string() .parse() .map_err(|e| ContractError::ParseError { value: msg.parameters.state_tree_depth.to_string(), reason: format!("{}", e), })?, ), ); MAX_LEAVES_COUNT.save(deps.storage, &max_leaves_count)?; // Calculate the index of the first leaf in the tree let leaf_idx0 = (max_leaves_count - Uint256::from_u128(1u128)) / Uint256::from_u128(4u128); LEAF_IDX_0.save(deps.storage, &leaf_idx0)?; // ============================================ // Process Registration Mode Configuration // ============================================ // Registration mode combines access control and state initialization // This prevents invalid configuration combinations let registration_mode = match &msg.registration_mode { RegistrationModeConfig::SignUpWithStaticWhitelist { whitelist } => { // Static whitelist mode is limited to 625 max voters (state_tree_depth <= 4). // For larger scales (e.g. 6-3-3-125 with 15625 voters), use SignUpWithOracle // or PrePopulated mode instead. let static_whitelist_max_voters = Uint256::from_u128(625u128); if max_leaves_count > static_whitelist_max_voters { return Err(ContractError::StaticWhitelistScaleExceeded { max_allowed: static_whitelist_max_voters, }); } if Uint256::from_u128(whitelist.users.len() as u128) > max_leaves_count { return Err(ContractError::MaxVoterExceeded { current: Uint256::from_u128(whitelist.users.len() as u128), max_allowed: max_leaves_count, }); } // Validate and process whitelist users let default_amount = match &msg.voice_credit_mode { VoiceCreditMode::Unified { amount } => *amount, VoiceCreditMode::Dynamic => Uint256::zero(), // Will be set from user_config }; let users = validate_and_process_whitelist( deps.api, &whitelist.users, &msg.voice_credit_mode, default_amount, )?; let whitelists = Whitelist { users }; WHITELIST.save(deps.storage, &whitelists)?; // SignUp mode: save default/zero pre_deactivate_root PRE_DEACTIVATE_ROOT.save(deps.storage, &Uint256::zero())?; RegistrationMode::SignUpWithStaticWhitelist } RegistrationModeConfig::SignUpWithOracle { oracle_pubkey } => { // SignUp with Oracle mode (oracle_pubkey = visa/verification pubkey, stored in RegistrationMode) // SignUp mode: save default/zero pre_deactivate_root PRE_DEACTIVATE_ROOT.save(deps.storage, &Uint256::zero())?; RegistrationMode::SignUpWithOracle { oracle_pubkey: oracle_pubkey.clone(), } } RegistrationModeConfig::PrePopulated { pre_deactivate_root, pre_deactivate_coordinator, } => { // PrePopulated mode: bulk import users via PreAddNewKey // IMPORTANT: PrePopulated mode only supports Unified VC mode // because PreAddNewKey ZK proof does not include voice_credit_amount if !matches!(msg.voice_credit_mode, VoiceCreditMode::Unified { .. }) { return Err(ContractError::InvalidRegistrationConfig { reason: "PrePopulated mode only supports Unified VoiceCreditMode. PreAddNewKey ZK proof does not include per-user voice credit amounts.".to_string(), }); } if !is_on_babyjubjub_curve(pre_deactivate_coordinator.x, pre_deactivate_coordinator.y) { return Err(ContractError::InvalidPubKey {}); } // Save pre_deactivate_root for PreAddNewKey proof verification PRE_DEACTIVATE_ROOT.save(deps.storage, pre_deactivate_root)?; // Save pre_deactivate_coordinator hash (required for PreAddNewKey) let coordinator_hash = hash2([pre_deactivate_coordinator.x, pre_deactivate_coordinator.y]); PRE_DEACTIVATE_COORDINATOR_HASH.save(deps.storage, &coordinator_hash)?; RegistrationMode::PrePopulated { pre_deactivate_root: *pre_deactivate_root, pre_deactivate_coordinator: (*pre_deactivate_coordinator).clone(), } } }; REGISTRATION_MODE.save(deps.storage, ®istration_mode)?; // Save the MACI parameters to storage MACIPARAMETERS.save(deps.storage, &msg.parameters)?; // Save poll_id (required, assigned by Registry) POLL_ID.save(deps.storage, &msg.poll_id)?; let qtr_lab = QuinaryTreeRoot { zeros: [ Uint256::from_u128(0u128), uint256_from_hex_string( "2066be41bebe6caf7e079360abe14fbf9118c62eabc42e2fe75e342b160a95bc", ), uint256_from_hex_string( "2a956d37d8e73692877b104630a08cc6840036f235f2134b0606769a369d85c1", ), uint256_from_hex_string( "2f9791ba036a4148ff026c074e713a4824415530dec0f0b16c5115aa00e4b825", ), uint256_from_hex_string( "2c41a7294c7ef5c9c5950dc627c55a00adb6712548bcbd6cd8569b1f2e5acc2a", ), uint256_from_hex_string( "2594ba68eb0f314eabbeea1d847374cc2be7965944dec513746606a1f2fadf2e", ), uint256_from_hex_string( "5c697158c9032bfd7041223a7dba696396388129118ae8f867266eb64fe7636", ), uint256_from_hex_string( "272b3425fcc3b2c45015559b9941fde27527aab5226045bf9b0a6c1fe902d601", ), uint256_from_hex_string( "268d82cc07023a1d5e7c987cbd0328b34762c9ea21369bea418f08b71b16846a", ), // zeros[9..11] — required for state_tree_depth up to 9 // zeros[i] = poseidon5(zeros[i-1] × 5), zero_leaf = 0 uint256_from_hex_string( "2e002d67c30ee0a2bd5fdecc4fb81646ecd6eb0746f5ff2d9b1d1b522a4a3f68", ), // "20806704410832383274034364623685369279680495689837539882650535326035351322472", uint256_from_hex_string( "f14c3fb900b66f523694106f7fc3cbec1f5eee571f047a9eb05bef717d3e064", ), // "6821382292698461711184253213986441870942786410912797736722948342942530789476", uint256_from_hex_string( "d14b45c0e1f64503a143581a25197e022ff9448c190d76938c3567690edac3d", ), // "5916648769022832355861175588931687601652727028178402815013820610204855544893", ], }; // Save the qtr_lib value to storage QTR_LIB.save(deps.storage, &qtr_lab)?; let vkey = match_vkeys(&msg.parameters)?; GROTH16_PROCESS_VKEYS.save(deps.storage, &vkey.process_vkey)?; GROTH16_TALLY_VKEYS.save(deps.storage, &vkey.tally_vkey)?; GROTH16_DEACTIVATE_VKEYS.save(deps.storage, &vkey.deactivate_vkey)?; GROTH16_NEWKEY_VKEYS.save(deps.storage, &vkey.add_key_vkey)?; if !is_on_babyjubjub_curve(msg.coordinator.x, msg.coordinator.y) { return Err(ContractError::InvalidPubKey {}); } // Compute the coordinator hash from the coordinator values in the message let coordinator_hash = hash2([msg.coordinator.x, msg.coordinator.y]); COORDINATORHASH.save(deps.storage, &coordinator_hash)?; // Define an array of zero values for the state tree. // zero_leaf = hash10([0×10]) = hash of an all-zero StateLeaf // zeros_h10[i] = poseidon5(zeros_h10[i-1] × 5) // This supports state_tree_depth up to 9 (requires zeros_h10[0] through zeros_h10[9]) let zeros_h10: [Uint256; 10] = [ uint256_from_hex_string("26318ec8cdeef483522c15e9b226314ae39b86cde2a430dabf6ed19791917c47"), // "17275449213996161510934492606295966958609980169974699290756906233261208992839", uint256_from_hex_string("28413250bf1cc56fabffd2fa32b52624941da885248fd1e015319e02c02abaf2"), // "18207706266780806924962529690397914300960241391319167935582599262189180861170", uint256_from_hex_string("16738da97527034e095ac32bfab88497ca73a7b310a2744ab43971e82215cb6d"), // "10155047796084846065379877743510757035594500557216694906214808863463609584493", uint256_from_hex_string("28140849348769fde6e971eec1424a5a162873a3d8adcbfdfc188e9c9d25faa3"), // "18127908072205049515869530689345374790252438412920611306083118152373728836259", uint256_from_hex_string("1a07af159d19f68ed2aed0df224dabcc2e2321595968769f7c9e26591377ed9a"), // "11773710380932653545559747058052522704305757415195021025284143362529247620506", uint256_from_hex_string("205cd249acba8f95f2e32ed51fa9c3d8e6f0d021892225d3efa9cd84c8fc1cad"), // "14638012437623529368951445143647110672059367053598285839401224214917416754349", uint256_from_hex_string("b21c625cd270e71c2ee266c939361515e690be27e26cfc852a30b24e83504b0"), // "5035114852453394843899296226690566678263173670465782309520655898931824493744", // zeros_h10[7..9] — required for state_tree_depth up to 9 uint256_from_hex_string("7afcc90cde2f45682df00da8e4cc107f9a53881c42ebc49c983c4c28559932b"), // "3476800036588530756460483358565739578209766454141876316818545165759359324971", uint256_from_hex_string("6f5db1bd3b5139e46bb61cbcadb68c90f4c577c4c5c4a771af1f6517f1f91a4"), // "3148266855034193786148184232200661378440218813932266536184598444730954518948", uint256_from_hex_string("1fcdecf7e78d4e167944cf76c1b1d60efeae81c733dc45b7903d013ec4946a7a"), // "14385537449990765079718963953260620244735269748758454223090277993998194076282", ]; ZEROS_H10.save(deps.storage, &zeros_h10)?; NODES.save( deps.storage, Uint256::from_u128(0u128).to_be_bytes().to_vec(), &zeros_h10[msg .parameters .state_tree_depth .to_string() .parse::() .map_err(|e| ContractError::ParseError { value: msg.parameters.state_tree_depth.to_string(), reason: format!("{}", e), })?], // &Uint256::from_u128(0u128), )?; // Define an array of zero values for Merkle tree (5-ary quinary tree, zero_leaf = 0) // zeros[0] = 0 (zero leaf) // zeros[i] = poseidon5(zeros[i-1] × 5) // This supports state_tree_depth up to 9 (deactivate commitment uses zeros[depth+2], // so depth 9 requires zeros[0] through zeros[11]) let zeros: [Uint256; 12] = [ Uint256::from_u128(0u128), uint256_from_hex_string("2066be41bebe6caf7e079360abe14fbf9118c62eabc42e2fe75e342b160a95bc"), // "14655542659562014735865511769057053982292279840403315552050801315682099828156", uint256_from_hex_string("2a956d37d8e73692877b104630a08cc6840036f235f2134b0606769a369d85c1"), // "19261153649140605024552417994922546473530072875902678653210025980873274131905", uint256_from_hex_string("2f9791ba036a4148ff026c074e713a4824415530dec0f0b16c5115aa00e4b825"), // "21526503558325068664033192388586640128492121680588893182274749683522508994597", uint256_from_hex_string("2c41a7294c7ef5c9c5950dc627c55a00adb6712548bcbd6cd8569b1f2e5acc2a"), // "20017764101928005973906869479218555869286328459998999367935018992260318153770", uint256_from_hex_string("2594ba68eb0f314eabbeea1d847374cc2be7965944dec513746606a1f2fadf2e"), // "16998355316577652097112514691750893516081130026395813155204269482715045879598", uint256_from_hex_string("5c697158c9032bfd7041223a7dba696396388129118ae8f867266eb64fe7636"), // "2612442706402737973181840577010736087708621987282725873936541279764292204086", uint256_from_hex_string("272b3425fcc3b2c45015559b9941fde27527aab5226045bf9b0a6c1fe902d601"), // "17716535433480122581515618850811568065658392066947958324371350481921422579201", uint256_from_hex_string("268d82cc07023a1d5e7c987cbd0328b34762c9ea21369bea418f08b71b16846a"), // "17437916409890180001398333108882255895598851862997171508841759030332444017770", // zeros[9..11] — required for state_tree_depth up to 9 uint256_from_hex_string("2e002d67c30ee0a2bd5fdecc4fb81646ecd6eb0746f5ff2d9b1d1b522a4a3f68"), // "20806704410832383274034364623685369279680495689837539882650535326035351322472", uint256_from_hex_string("f14c3fb900b66f523694106f7fc3cbec1f5eee571f047a9eb05bef717d3e064"), // "6821382292698461711184253213986441870942786410912797736722948342942530789476", uint256_from_hex_string("d14b45c0e1f64503a143581a25197e022ff9448c190d76938c3567690edac3d"), // "5916648769022832355861175588931687601652727028178402815013820610204855544893", ]; ZEROS.save(deps.storage, &zeros)?; // Save initial values for message hash, message chain length, processed message count, current tally commitment, // processed user count, and number of signups to storage MSG_HASHES.save( deps.storage, Uint256::from_u128(0u128).to_be_bytes().to_vec(), &Uint256::from_u128(0u128), )?; MSG_CHAIN_LENGTH.save(deps.storage, &Uint256::from_u128(0u128))?; PROCESSED_MSG_COUNT.save(deps.storage, &Uint256::from_u128(0u128))?; CURRENT_TALLY_COMMITMENT.save(deps.storage, &Uint256::from_u128(0u128))?; PROCESSED_USER_COUNT.save(deps.storage, &Uint256::from_u128(0u128))?; NUMSIGNUPS.save(deps.storage, &Uint256::from_u128(0u128))?; MAX_VOTE_OPTIONS.save( deps.storage, &Uint256::from_u128(msg.vote_option_map.len() as u128), )?; // ============================================ // Process Voice Credit Mode Configuration // ============================================ VOICE_CREDIT_MODE.save(deps.storage, &msg.voice_credit_mode)?; match &msg.voice_credit_mode { VoiceCreditMode::Unified { amount } => { // Unified mode: save the fixed voice credit amount VOICE_CREDIT_AMOUNT.save(deps.storage, amount)?; } VoiceCreditMode::Dynamic => { // Dynamic mode: each user provides their own amount at signup // Save zero as placeholder (actual amounts are per-user) VOICE_CREDIT_AMOUNT.save(deps.storage, &Uint256::zero())?; } } // ============================================ // Validate Configuration Consistency // ============================================ // Note: Whitelist validation is now performed inline during whitelist creation // to avoid redundant iteration and ensure validation happens before storage PROCESSED_DMSG_COUNT.save(deps.storage, &Uint256::from_u128(0u128))?; DMSG_CHAIN_LENGTH.save(deps.storage, &Uint256::from_u128(0u128))?; DEACTIVATE_COUNT.save(deps.storage, &0u128)?; let current_dcommitment = &hash2([ zeros[msg .parameters .state_tree_depth .to_string() .parse::() .map_err(|e| ContractError::ParseError { value: msg.parameters.state_tree_depth.to_string(), reason: format!("{}", e), })?], zeros[(msg.parameters.state_tree_depth + Uint256::from_u128(2u128)) .to_string() .parse::() .map_err(|e| ContractError::ParseError { value: (msg.parameters.state_tree_depth + Uint256::from_u128(2u128)).to_string(), reason: format!("{}", e), })?], ]); CURRENT_DEACTIVATE_COMMITMENT.save(deps.storage, current_dcommitment)?; DMSG_HASHES.save( deps.storage, Uint256::from_u128(0u128).to_be_bytes().to_vec(), &Uint256::from_u128(0u128), )?; STATE_ROOT_BY_DMSG.save( deps.storage, Uint256::from_u128(0u128).to_be_bytes().to_vec(), &Uint256::from_u128(0u128), )?; DNODES.save( deps.storage, Uint256::from_u128(0u128).to_be_bytes().to_vec(), &Uint256::from_u128(0u128), )?; VOTEOPTIONMAP.save(deps.storage, &msg.vote_option_map)?; ROUNDINFO.save(deps.storage, &msg.round_info)?; VOTINGTIME.save(deps.storage, &msg.voting_time)?; let period = Period { status: PeriodStatus::Pending, }; // Save the initial period to storage PERIOD.save(deps.storage, &period)?; MACI_OPERATOR.save(deps.storage, &msg.operator)?; FEE_RECIPIENT.save(deps.storage, &msg.fee_recipient)?; // Save deactivate_enabled flag (default: false) DEACTIVATE_ENABLED.save(deps.storage, &msg.deactivate_enabled)?; let circuit_type = if msg.circuit_type == Uint256::from_u128(0u128) { "0" // 1p1v } else if msg.circuit_type == Uint256::from_u128(1u128) { "1" // qv } else { return Err(ContractError::UnsupportedCircuitType {}); }; let certification_system = if msg.certification_system == Uint256::from_u128(0u128) { "groth16" // groth16 } else { return Err(ContractError::UnsupportedCertificationSystem {}); }; CIRCUITTYPE.save(deps.storage, &msg.circuit_type)?; CERTSYSTEM.save(deps.storage, &msg.certification_system)?; // Init penalty rate and timeout let penalty_rate = Uint256::from_u128(50); PENALTY_RATE.save(deps.storage, &penalty_rate)?; // 50% DELAY_RECORDS.save(deps.storage, &DelayRecords { records: vec![] })?; let deactivate_delay = Timestamp::from_seconds(msg.deactivate_delay); let tally_delay_max_hours = 48; // 48 hours TALLY_DELAY_MAX_HOURS.save(deps.storage, &tally_delay_max_hours)?; let tally_timeout = Timestamp::from_seconds(4 * 24 * 60 * 60); // 4 days TALLY_TIMEOUT.save(deps.storage, &tally_timeout)?; // Save fee and delay configuration injected by Registry at round creation time. FEE_CONFIG.save( deps.storage, &FeeConfig { message_fee: msg.message_fee, deactivate_fee: msg.deactivate_fee, signup_fee: msg.signup_fee, }, )?; DELAY_CONFIG.save( deps.storage, &DelayConfig { base_delay: msg.base_delay, message_delay: msg.message_delay, signup_delay: msg.signup_delay, deactivate_delay: msg.deactivate_delay, }, )?; let old_tally_timeout_set = Timestamp::from_seconds(tally_delay_max_hours * 60 * 60); let data: InstantiationData = InstantiationData { caller: info.sender.clone(), parameters: msg.parameters.clone(), coordinator: msg.coordinator.clone(), admin: msg.admin.clone(), operator: msg.operator.clone(), vote_option_map: msg.vote_option_map.clone(), round_info: msg.round_info.clone(), voting_time: msg.voting_time.clone(), circuit_type: circuit_type.to_string(), certification_system: certification_system.to_string(), penalty_rate: penalty_rate.clone(), deactivate_timeout: deactivate_delay.clone(), tally_timeout: old_tally_timeout_set.clone(), poll_id: msg.poll_id, deactivate_enabled: msg.deactivate_enabled, // Unified MACI Configuration voice_credit_mode: msg.voice_credit_mode.clone(), registration_mode, }; let mut attributes = vec![ attr("action", "instantiate"), attr("caller", &info.sender.to_string()), attr("admin", &msg.admin.to_string()), attr("operator", &msg.operator.to_string()), attr( "voting_start", &msg.voting_time.start_time.nanos().to_string(), ), attr("voting_end", &msg.voting_time.end_time.nanos().to_string()), attr("round_title", &msg.round_info.title.to_string()), attr("coordinator_pubkey_x", &msg.coordinator.x.to_string()), attr("coordinator_pubkey_y", &msg.coordinator.y.to_string()), attr("max_vote_options", &msg.vote_option_map.len().to_string()), attr( "state_tree_depth", &msg.parameters.state_tree_depth.to_string(), ), attr( "int_state_tree_depth", &msg.parameters.int_state_tree_depth.to_string(), ), attr( "vote_option_tree_depth", &msg.parameters.vote_option_tree_depth.to_string(), ), attr( "message_batch_size", &msg.parameters.message_batch_size.to_string(), ), attr("circuit_type", &circuit_type.to_string()), attr("certification_system", &certification_system.to_string()), attr("penalty_rate", &penalty_rate.to_string()), // Unified MACI Configuration attr( "voice_credit_mode", &format!("{:?}", data.voice_credit_mode), ), attr( "registration_mode", &format!("{:?}", data.registration_mode), ), attr( "deactivate_timeout", &deactivate_delay.seconds().to_string(), ), attr( "tally_timeout", &old_tally_timeout_set.seconds().to_string(), ), attr("deactivate_enabled", &msg.deactivate_enabled.to_string()), ]; if msg.round_info.description != "" { attributes.push(attr("round_description", msg.round_info.description)) } if msg.round_info.link != "" { attributes.push(attr("round_link", msg.round_info.link)) } Ok(Response::new() .add_attributes(attributes) .set_data(to_json_binary(&data)?)) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { match msg { ExecuteMsg::SetRoundInfo { round_info } => { execute_set_round_info(deps, env, info, round_info) } ExecuteMsg::UpdateRegistrationConfig { config } => { execute_update_registration_config(deps, env, info, config) } ExecuteMsg::SetVoteOptionsMap { vote_option_map } => { execute_set_vote_options_map(deps, env, info, vote_option_map) } // ExecuteMsg::StartVotingPeriod {} => execute_start_voting_period(deps, env, info), ExecuteMsg::SignUp { pubkey, certificate, amount, } => execute_sign_up(deps, env, info, pubkey, certificate, amount), // ExecuteMsg::StopVotingPeriod {} => execute_stop_voting_period(deps, env, info), ExecuteMsg::PublishDeactivateMessage { message, enc_pub_key, } => execute_publish_deactivate_message(deps, env, info, message, enc_pub_key), ExecuteMsg::UploadDeactivateMessage { deactivate_message } => { execute_upload_deactivate_message(deps, env, info, deactivate_message) } ExecuteMsg::ProcessDeactivateMessage { size, new_deactivate_commitment, new_deactivate_root, groth16_proof, } => execute_process_deactivate_message( deps, env, info, size, new_deactivate_commitment, new_deactivate_root, groth16_proof, ), ExecuteMsg::AddNewKey { pubkey, nullifier, d, groth16_proof, } => execute_add_new_key(deps, env, info, pubkey, nullifier, d, groth16_proof), ExecuteMsg::PreAddNewKey { pubkey, nullifier, d, groth16_proof, } => execute_pre_add_new_key(deps, env, info, pubkey, nullifier, d, groth16_proof), ExecuteMsg::PublishMessage { messages, enc_pub_keys, } => execute_publish_message(deps, env, info, messages, enc_pub_keys), ExecuteMsg::StartProcessPeriod {} => execute_start_process_period(deps, env, info), ExecuteMsg::ProcessMessage { new_state_commitment, groth16_proof, } => execute_process_message(deps, env, info, new_state_commitment, groth16_proof), ExecuteMsg::StopProcessingPeriod {} => execute_stop_processing_period(deps, env, info), ExecuteMsg::ProcessTally { new_tally_commitment, groth16_proof, } => execute_process_tally(deps, env, info, new_tally_commitment, groth16_proof), ExecuteMsg::StopTallyingPeriod { results, salt } => { execute_stop_tallying_period(deps, env, info, results, salt) } ExecuteMsg::Claim {} => execute_claim(deps, env, info), } } pub fn execute_set_round_info( deps: DepsMut, _env: Env, info: MessageInfo, round_info: RoundInfo, ) -> Result { if !is_admin(deps.as_ref(), info.sender.as_ref())? { Err(ContractError::Unauthorized {}) } else { if round_info.title == "" { return Err(ContractError::TitleIsEmpty {}); } ROUNDINFO.save(deps.storage, &round_info)?; let mut attributes = vec![attr("action", "set_round_info")]; attributes.push(attr("title", round_info.title)); if round_info.description != "" { attributes.push(attr("description", round_info.description)) } if round_info.link != "" { attributes.push(attr("link", round_info.link)) } Ok(Response::new().add_attributes(attributes)) } } // Helper function to validate registration config update fn validate_registration_config_update( deps: &DepsMut, config: &RegistrationConfigUpdate, num_signups: Uint256, ) -> Result<(), ContractError> { // Check if modifying VC mode or registration_mode with existing signups if num_signups > Uint256::zero() { if config.voice_credit_mode.is_some() || config.registration_mode.is_some() { return Err(ContractError::ConfigModificationAfterSignup { current: num_signups, }); } } // Validate registration_mode configuration if provided if let Some(registration_mode) = &config.registration_mode { match registration_mode { RegistrationModeConfig::SignUpWithStaticWhitelist { whitelist } => { let max_voter_amount = MAX_LEAVES_COUNT.load(deps.storage)?; if Uint256::from_u128(whitelist.users.len() as u128) > max_voter_amount { return Err(ContractError::MaxVoterExceeded { current: Uint256::from_u128(whitelist.users.len() as u128), max_allowed: max_voter_amount, }); } // Validate whitelist users based on VC mode let vc_mode = if let Some(ref new_vc_mode) = config.voice_credit_mode { new_vc_mode } else { &VOICE_CREDIT_MODE.load(deps.storage)? }; let default_amount = match vc_mode { VoiceCreditMode::Unified { amount } => *amount, VoiceCreditMode::Dynamic => Uint256::zero(), }; // Use the existing validation function validate_and_process_whitelist( deps.api, &whitelist.users, vc_mode, default_amount, )?; } RegistrationModeConfig::SignUpWithOracle { oracle_pubkey: _ } => { // SignUpWithOracle mode: oracle_pubkey is already provided in the enum // No additional validation needed here } RegistrationModeConfig::PrePopulated { pre_deactivate_root: _, pre_deactivate_coordinator, } => { // PrePopulated mode requires valid pre_deactivate_coordinator if !is_on_babyjubjub_curve( pre_deactivate_coordinator.x, pre_deactivate_coordinator.y, ) { return Err(ContractError::InvalidRegistrationConfig { reason: "PrePopulated mode requires valid pre_deactivate_coordinator" .to_string(), }); } // PrePopulated mode only supports Unified VC mode let vc_mode = if let Some(ref new_vc_mode) = config.voice_credit_mode { new_vc_mode } else { &VOICE_CREDIT_MODE.load(deps.storage)? }; if !matches!(vc_mode, VoiceCreditMode::Unified { .. }) { // Provide helpful error message if switching to PrePopulated with Dynamic VC let current_vc_mode = VOICE_CREDIT_MODE.load(deps.storage)?; let error_msg = if config.voice_credit_mode.is_none() && matches!(current_vc_mode, VoiceCreditMode::Dynamic) { "Cannot switch to PrePopulated mode: current VoiceCreditMode is Dynamic. \ PrePopulated mode requires Unified VoiceCreditMode because PreAddNewKey ZK proof \ does not include per-user voice credit amounts. \ Please update voice_credit_mode to Unified in the same transaction.".to_string() } else { "PrePopulated mode only supports Unified VoiceCreditMode. \ PreAddNewKey ZK proof does not include per-user voice credit amounts." .to_string() }; return Err(ContractError::InvalidRegistrationConfig { reason: error_msg }); } } } } Ok(()) } // Helper function to apply registration config update fn apply_registration_config_update( deps: DepsMut, config: RegistrationConfigUpdate, ) -> Result, ContractError> { let mut attributes = vec![]; // Update deactivate_enabled if provided if let Some(deactivate_enabled) = config.deactivate_enabled { DEACTIVATE_ENABLED.save(deps.storage, &deactivate_enabled)?; attributes.push(attr("deactivate_enabled", deactivate_enabled.to_string())); } // Update voice_credit_mode if provided if let Some(voice_credit_mode) = config.voice_credit_mode { let vc_amount = match &voice_credit_mode { VoiceCreditMode::Unified { amount } => *amount, VoiceCreditMode::Dynamic => Uint256::zero(), }; VOICE_CREDIT_AMOUNT.save(deps.storage, &vc_amount)?; VOICE_CREDIT_MODE.save(deps.storage, &voice_credit_mode)?; attributes.push(attr("voice_credit_mode", voice_credit_mode.variant_name())); attributes.push(attr("voice_credit_amount", vc_amount.to_string())); } // Update registration_mode if provided if let Some(registration_mode_config) = config.registration_mode { let new_mode = match registration_mode_config { RegistrationModeConfig::SignUpWithStaticWhitelist { whitelist } => { let max_voter_amount = MAX_LEAVES_COUNT.load(deps.storage)?; // Static whitelist mode is limited to 625 max voters (state_tree_depth <= 4). // For larger scales (e.g. 6-3-3-125 with 15625 voters), use SignUpWithOracle // or PrePopulated mode instead. let static_whitelist_max_voters = Uint256::from_u128(625u128); if max_voter_amount > static_whitelist_max_voters { return Err(ContractError::StaticWhitelistScaleExceeded { max_allowed: static_whitelist_max_voters, }); } let vc_mode = VOICE_CREDIT_MODE.load(deps.storage)?; let default_amount = match &vc_mode { VoiceCreditMode::Unified { amount } => *amount, VoiceCreditMode::Dynamic => Uint256::zero(), }; let users = validate_and_process_whitelist( deps.api, &whitelist.users, &vc_mode, default_amount, )?; WHITELIST.save(deps.storage, &Whitelist { users })?; PRE_DEACTIVATE_ROOT.save(deps.storage, &Uint256::zero())?; PRE_DEACTIVATE_COORDINATOR_HASH.remove(deps.storage); RegistrationMode::SignUpWithStaticWhitelist } RegistrationModeConfig::SignUpWithOracle { oracle_pubkey } => { WHITELIST.remove(deps.storage); PRE_DEACTIVATE_ROOT.save(deps.storage, &Uint256::zero())?; PRE_DEACTIVATE_COORDINATOR_HASH.remove(deps.storage); RegistrationMode::SignUpWithOracle { oracle_pubkey } } RegistrationModeConfig::PrePopulated { pre_deactivate_root, pre_deactivate_coordinator, } => { if !is_on_babyjubjub_curve( pre_deactivate_coordinator.x, pre_deactivate_coordinator.y, ) { return Err(ContractError::InvalidRegistrationConfig { reason: "PrePopulated mode requires valid pre_deactivate_coordinator" .to_string(), }); } PRE_DEACTIVATE_ROOT.save(deps.storage, &pre_deactivate_root)?; let coordinator_hash = hash2([pre_deactivate_coordinator.x, pre_deactivate_coordinator.y]); PRE_DEACTIVATE_COORDINATOR_HASH.save(deps.storage, &coordinator_hash)?; RegistrationMode::PrePopulated { pre_deactivate_root, pre_deactivate_coordinator, } } }; REGISTRATION_MODE.save(deps.storage, &new_mode)?; attributes.push(attr("registration_mode", new_mode.variant_name())); // PrePopulated-specific extra attributes if let RegistrationMode::PrePopulated { pre_deactivate_root, pre_deactivate_coordinator, } = &new_mode { attributes.push(attr("pre_deactivate_root", pre_deactivate_root.to_string())); attributes.push(attr( "pre_deactivate_coordinator_x", pre_deactivate_coordinator.x.to_string(), )); attributes.push(attr( "pre_deactivate_coordinator_y", pre_deactivate_coordinator.y.to_string(), )); } } Ok(attributes) } // Main function to update registration configuration pub fn execute_update_registration_config( deps: DepsMut, env: Env, info: MessageInfo, config: RegistrationConfigUpdate, ) -> Result { // 1. Check time constraint: must be before voting starts let voting_time = VOTINGTIME.load(deps.storage)?; if env.block.time >= voting_time.start_time { return Err(ContractError::PeriodError {}); } // 2. Check admin permission if !is_admin(deps.as_ref(), info.sender.as_ref())? { return Err(ContractError::Unauthorized {}); } // 3. Check if there are any signups let num_signups = NUMSIGNUPS.load(deps.storage)?; // 4. Validate the configuration update validate_registration_config_update(&deps, &config, num_signups)?; // 5. Apply the configuration update let mut attributes = apply_registration_config_update(deps, config)?; // 6. Add base action attribute attributes.insert(0, attr("action", "update_registration_config")); // Events emitted: // - Always: "action" = "update_registration_config" // - If deactivate_enabled updated: "deactivate_enabled" = "true" | "false" // - If voice_credit_mode updated: "voice_credit_mode" = "Unified({amount})" | "Dynamic" // - If registration_mode updated: "registration_mode" = "SignUpWithStaticWhitelist" | "SignUpWithOracle" | "PrePopulated" // - For PrePopulated mode: "pre_deactivate_root" = "{root_value}" Ok(Response::new().add_attributes(attributes)) } // in pending pub fn execute_set_vote_options_map( deps: DepsMut, env: Env, info: MessageInfo, vote_option_map: Vec, ) -> Result { let voting_time = VOTINGTIME.load(deps.storage)?; if env.block.time >= voting_time.start_time { return Err(ContractError::PeriodError {}); } if !is_admin(deps.as_ref(), info.sender.as_ref())? { Err(ContractError::Unauthorized {}) } else { let max_vote_options = vote_option_map.len() as u128; let cfg = MACIPARAMETERS.load(deps.storage)?; // An error will be thrown if the number of vote options exceeds the circuit's capacity. let vote_option_max_amount = Uint256::from_u128( 5u128.pow( cfg.vote_option_tree_depth .to_string() .parse() .map_err(|e| ContractError::ParseError { value: cfg.vote_option_tree_depth.to_string(), reason: format!("{}", e), })?, ), ); if Uint256::from_u128(max_vote_options) > vote_option_max_amount { return Err(ContractError::MaxVoteOptionsExceeded { current: Uint256::from_u128(max_vote_options), max_allowed: vote_option_max_amount, }); } VOTEOPTIONMAP.save(deps.storage, &vote_option_map)?; // Save the maximum vote options MAX_VOTE_OPTIONS.save(deps.storage, &Uint256::from_u128(max_vote_options))?; let res = Response::new() .add_attribute("action", "set_vote_option") .add_attribute("vote_option_map", to_json_or(&vote_option_map, "[]")) .add_attribute("max_vote_options", max_vote_options.to_string()); Ok(res) } } // ============================================ // Voting Power Calculation Helper // ============================================ /// Calculate voting power based on amount and configuration // ============================================ // End of Voting Power Calculation Helper // ============================================ // in voting - unified signup for all configuration modes pub fn execute_sign_up( mut deps: DepsMut, env: Env, info: MessageInfo, pubkey: PubKey, certificate: Option, amount: Option, ) -> Result { let voting_time = VOTINGTIME.load(deps.storage)?; check_voting_time(env.clone(), voting_time)?; // ============================================ // Step 1: Check Registration Mode and Access Control // ============================================ let registration_mode = REGISTRATION_MODE.load(deps.storage)?; match ®istration_mode { RegistrationMode::PrePopulated { .. } => { // PrePopulated mode: users cannot signup directly, must use PreAddNewKey return Err(ContractError::Unauthorized {}); } RegistrationMode::SignUpWithStaticWhitelist => { // SignUp with Static whitelist mode: check if sender is in whitelist let whitelist_cfg = WHITELIST.load(deps.storage)?; if !whitelist_cfg.is_whitelist(&info.sender) { return Err(ContractError::Unauthorized {}); } if whitelist_cfg.is_register(&info.sender) { return Err(ContractError::UserAlreadyRegistered {}); } } RegistrationMode::SignUpWithOracle { oracle_pubkey: oracle_pubkey_str, } => { // Oracle verified mode: verify certificate (oracle_pubkey = visa/verification pubkey) let cert = certificate.ok_or(ContractError::CertificateRequired {})?; // Determine the amount for verification based on VC mode let vc_mode = VOICE_CREDIT_MODE.load(deps.storage)?; let verify_amount = match vc_mode { VoiceCreditMode::Unified { amount: vc_amount } => vc_amount, VoiceCreditMode::Dynamic { .. } => { // Dynamic mode requires amount parameter amount.ok_or(ContractError::AmountRequired {})? } }; // Construct verification payload let contract_address_uint256 = address_to_uint256(&env.contract.address); let payload = VerifyPayload { amount: verify_amount.to_string(), contract_address: contract_address_uint256.to_string(), pubkey_x: pubkey.x.to_string(), pubkey_y: pubkey.y.to_string(), }; // Verify signature let msg = serde_json::to_string(&payload) .unwrap_or_default() .into_bytes(); let hash = Sha256::digest(&msg); let certificate_binary = Binary::from_base64(&cert).map_err(|_| ContractError::InvalidBase64 {})?; let oracle_pubkey_binary = Binary::from_base64(oracle_pubkey_str) .map_err(|_| ContractError::InvalidBase64 {})?; let verify_result = deps .api .secp256k1_verify( hash.as_ref(), certificate_binary.as_slice(), oracle_pubkey_binary.as_slice(), ) .map_err(|_| ContractError::VerificationFailed {})?; if !verify_result { return Err(ContractError::InvalidSignature {}); } // Check if already signed up (use pubkey for oracle mode) if ORACLE_WHITELIST.has(deps.storage, &pubkey_key(&pubkey)) { return Err(ContractError::AlreadySignedUp {}); } } } // ============================================ // Step 1.5: Collect Registration Fee // Fee stays in contract balance and is distributed at Claim time. // ============================================ let signup_fee = FEE_CONFIG.load(deps.storage)?.signup_fee; let signup_payment = check_fee_payment(&info, signup_fee)?; // ============================================ // Step 2: Calculate Voice Credit Balance // ============================================ let vc_mode = VOICE_CREDIT_MODE.load(deps.storage)?; let voice_credit_balance = match &vc_mode { VoiceCreditMode::Unified { amount: vc_amount } => *vc_amount, VoiceCreditMode::Dynamic => { // Dynamic mode: amount source depends on registration mode match ®istration_mode { RegistrationMode::SignUpWithStaticWhitelist => { // Static whitelist: read pre-configured amount from whitelist let whitelist = WHITELIST.load(deps.storage)?; let user_config = whitelist .users .iter() .find(|u| u.addr == info.sender) .ok_or(ContractError::Unauthorized {})?; // Amount was set during instantiate (Unified or Dynamic preset) user_config.voice_credit_amount } RegistrationMode::SignUpWithOracle { .. } => { // Oracle verified: use user-provided amount (verified by certificate) amount.ok_or(ContractError::AmountRequired {})? } RegistrationMode::PrePopulated { .. } => { // Already handled above, this branch should not be reached return Err(ContractError::Unauthorized {}); } } } }; if voice_credit_balance == Uint256::zero() { return Err(ContractError::VotingPowerIsZero {}); } // ============================================ // Step 3: Execute Registration // ============================================ let mut num_sign_ups = NUMSIGNUPS.load(deps.storage)?; let max_leaves_count = MAX_LEAVES_COUNT.load(deps.storage)?; // Validate capacity and pubkey if num_sign_ups >= max_leaves_count { return Err(ContractError::StateTreeFull {}); } if !is_on_babyjubjub_curve(pubkey.x, pubkey.y) { return Err(ContractError::InvalidPubKey {}); } // Create state leaf with calculated voice credit balance let state_leaf = StateLeaf { pub_key: pubkey.clone(), voice_credit_balance, vote_option_tree_root: Uint256::zero(), nonce: Uint256::zero(), } .hash_decativate_state_leaf(); let state_index = num_sign_ups; state_enqueue(&mut deps, state_leaf)?; num_sign_ups += Uint256::one(); // Save core registration state VOICECREDITBALANCE.save( deps.storage, state_index.to_be_bytes().to_vec(), &voice_credit_balance, )?; NUMSIGNUPS.save(deps.storage, &num_sign_ups)?; SIGNUPED.save(deps.storage, &pubkey_key(&pubkey), &state_index)?; // ============================================ // Step 4: Update Registration State // ============================================ match ®istration_mode { RegistrationMode::SignUpWithStaticWhitelist => { // Update whitelist to mark user as registered let mut whitelist = WHITELIST.load(deps.storage)?; whitelist.register(&info.sender); WHITELIST.save(deps.storage, &whitelist)?; } RegistrationMode::SignUpWithOracle { .. } => { // Save oracle whitelist record (use pubkey as key) let oracle_user = OracleWhitelistUser { balance: voice_credit_balance, is_register: true, }; ORACLE_WHITELIST.save(deps.storage, &pubkey_key(&pubkey), &oracle_user)?; } RegistrationMode::PrePopulated { .. } => { // Already handled above, this branch should not be reached return Err(ContractError::Unauthorized {}); } } Ok(Response::new() .add_attribute("action", "sign_up") .add_attribute("fee_paid", format!("{}{}", signup_payment, FEE_DENOM)) .add_attribute("state_idx", state_index.to_string()) .add_attribute( "pubkey", format!("{:?},{:?}", pubkey.x.to_string(), pubkey.y.to_string()), ) .add_attribute("balance", voice_credit_balance.to_string()) .add_attribute("registration_mode", format!("{:?}", registration_mode)) .add_attribute("vc_mode", format!("{:?}", vc_mode))) } // in voting pub fn execute_publish_message( deps: DepsMut, env: Env, info: MessageInfo, messages: Vec, enc_pub_keys: Vec, ) -> Result { let voting_time = VOTINGTIME.load(deps.storage)?; check_voting_time(env, voting_time)?; if messages.len() != enc_pub_keys.len() { return Err(ContractError::BatchLengthMismatch { messages_len: messages.len(), enc_pub_keys_len: enc_pub_keys.len(), }); } let batch_size = messages.len(); let message_fee = FEE_CONFIG.load(deps.storage)?.message_fee; let required_fee = message_fee .checked_mul(Uint128::from(batch_size as u128)) .map_err(|_| ContractError::ValueTooLarge {})?; let payment = check_fee_payment(&info, required_fee)?; let start_chain_length = MSG_CHAIN_LENGTH.load(deps.storage)?; let mut attributes = vec![ attr("action", "publish_message"), attr("batch_size", batch_size.to_string()), attr("start_chain_length", start_chain_length.to_string()), attr("fee_paid", format!("{}{}", payment, FEE_DENOM)), ]; let mut msg_chain_length = start_chain_length; for (i, (message, enc_pub_key)) in messages.iter().zip(enc_pub_keys.iter()).enumerate() { if !is_on_babyjubjub_curve(enc_pub_key.x, enc_pub_key.y) { return Err(ContractError::InvalidEncPubKey {}); } let pubkey_storage_key = generate_pubkey_storage_key(enc_pub_key); if USED_ENC_PUB_KEYS.has(deps.storage, pubkey_storage_key.clone()) { return Err(ContractError::EncPubKeyAlreadyUsed {}); } USED_ENC_PUB_KEYS.save(deps.storage, pubkey_storage_key, &true)?; let old_msg_hashes = MSG_HASHES.load(deps.storage, msg_chain_length.to_be_bytes().to_vec())?; let new_hash = hash_message_and_enc_pub_key(message, enc_pub_key, old_msg_hashes); MSG_HASHES.save( deps.storage, (msg_chain_length + Uint256::from_u128(1u128)) .to_be_bytes() .to_vec(), &new_hash, )?; attributes.push(attr( format!("msg_{}_chain_length", i), msg_chain_length.to_string(), )); attributes.push(attr( format!("msg_{}_data", i), to_json_or(&message.data, "[]"), )); attributes.push(attr( format!("msg_{}_enc_pub_key", i), format!( "{:?},{:?}", enc_pub_key.x.to_string(), enc_pub_key.y.to_string() ), )); msg_chain_length += Uint256::from_u128(1u128); } MSG_CHAIN_LENGTH.save(deps.storage, &msg_chain_length)?; attributes.push(attr("end_chain_length", msg_chain_length.to_string())); Ok(Response::new().add_attributes(attributes)) } // in voting pub fn execute_publish_deactivate_message( deps: DepsMut, env: Env, info: MessageInfo, message: MessageData, enc_pub_key: PubKey, ) -> Result { require_deactivate_enabled(deps.as_ref())?; // Check if the period status is Voting let voting_time = VOTINGTIME.load(deps.storage)?; check_voting_time(env.clone(), voting_time)?; // Validate enc_pub_key BEFORE charging fee to prevent fee loss on invalid keys if !is_on_babyjubjub_curve(enc_pub_key.x, enc_pub_key.y) { return Err(ContractError::InvalidEncPubKey {}); } // Check payment: require DEACTIVATE_FEE in FEE_DENOM let deactivate_fee = FEE_CONFIG.load(deps.storage)?.deactivate_fee; let payment = check_fee_payment(&info, deactivate_fee)?; let mut dmsg_chain_length = DMSG_CHAIN_LENGTH.load(deps.storage)?; let maci_parameters: MaciParameters = MACIPARAMETERS.load(deps.storage)?; // Calculate maximum allowed deactivate messages: 5^(state_tree_depth+2)-1 let max_deactivate_messages = Uint256::from_u128(5u128).pow( (maci_parameters.state_tree_depth + Uint256::from_u128(2u128)) .to_string() .parse() .map_err(|e| ContractError::ParseError { value: (maci_parameters.state_tree_depth + Uint256::from_u128(2u128)).to_string(), reason: format!("Failed to parse as u32: {}", e), })?, ) - Uint256::from_u128(1u128); if dmsg_chain_length + Uint256::from_u128(1u128) > max_deactivate_messages { return Err(ContractError::MaxDeactivateMessagesReached { max_deactivate_messages, }); } let processed_dmsg_count = PROCESSED_DMSG_COUNT.load(deps.storage)?; // When the processed_dmsg_count catches up with dmsg_chain_length, it indicates that the previous batch has been processed. // At this point, the new incoming message is the first one of the new batch, and we record the timestamp. if processed_dmsg_count == dmsg_chain_length { FIRST_DMSG_TIMESTAMP.save(deps.storage, &env.block.time)?; } let old_msg_hashes = DMSG_HASHES.load(deps.storage, dmsg_chain_length.to_be_bytes().to_vec())?; let m_n_hash = hash_message_and_enc_pub_key(&message, &enc_pub_key, old_msg_hashes); // Compute the new message hash using the provided message, encrypted public key, and previous hash DMSG_HASHES.save( deps.storage, (dmsg_chain_length + Uint256::from_u128(1u128)) .to_be_bytes() .to_vec(), &m_n_hash, )?; let state_root = state_root(deps.as_ref())?; STATE_ROOT_BY_DMSG.save( deps.storage, (dmsg_chain_length + Uint256::from_u128(1u128)) .to_be_bytes() .to_vec(), &state_root, )?; let old_chain_length = dmsg_chain_length; // Update the message chain length dmsg_chain_length += Uint256::from_u128(1u128); DMSG_CHAIN_LENGTH.save(deps.storage, &dmsg_chain_length)?; let mut deactivate_count = DEACTIVATE_COUNT.load(deps.storage)?; deactivate_count += 1u128; DEACTIVATE_COUNT.save(deps.storage, &deactivate_count)?; let num_sign_ups = NUMSIGNUPS.load(deps.storage)?; Ok(Response::new() .add_attribute("action", "publish_deactivate_message") .add_attribute("dmsg_chain_length", old_chain_length.to_string()) .add_attribute("num_sign_ups", num_sign_ups.to_string()) .add_attribute("message", to_json_or(&message.data, "[]")) .add_attribute( "enc_pub_key", format!( "{:?},{:?}", enc_pub_key.x.to_string(), enc_pub_key.y.to_string() ), ) .add_attribute("fee_paid", format!("{}{}", payment, FEE_DENOM))) } pub fn execute_upload_deactivate_message( deps: DepsMut, env: Env, info: MessageInfo, deactivate_message: Vec>, ) -> Result { require_deactivate_enabled(deps.as_ref())?; if !is_operator(deps.as_ref(), &info.sender.as_ref())? { Err(ContractError::Unauthorized {}) } else { let deactivate_format_data: Vec> = deactivate_message .iter() .map(|input| input.iter().map(|f| f.to_string()).collect()) .collect(); MACI_DEACTIVATE_MESSAGE.save( deps.storage, &env.contract.address, &deactivate_format_data, )?; // MACI_DEACTIVATE_OPERATOR.save(deps.storage, &contract_address, &info.sender)?; Ok(Response::new() .add_attribute("action", "upload_deactivate_message") .add_attribute("contract_address", &env.contract.address.to_string()) .add_attribute("maci_operator", &info.sender.to_string()) .add_attribute( "deactivate_message", to_json_or(&deactivate_format_data, "{}"), )) } } // all time pub fn execute_process_deactivate_message( deps: DepsMut, env: Env, _info: MessageInfo, size: Uint256, new_deactivate_commitment: Uint256, new_deactivate_root: Uint256, groth16_proof: Groth16ProofType, ) -> Result { require_deactivate_enabled(deps.as_ref())?; let processed_dmsg_count = PROCESSED_DMSG_COUNT.load(deps.storage)?; let dmsg_chain_length = DMSG_CHAIN_LENGTH.load(deps.storage)?; if processed_dmsg_count >= dmsg_chain_length { return Err(ContractError::AllDeactivateMessagesProcessed {}); } // Load the MACI parameters let parameters = MACIPARAMETERS.load(deps.storage)?; let batch_size = parameters.message_batch_size; if size > batch_size { return Err(ContractError::BatchSizeOverflow {}); } // --- Checks --- let mut input: [Uint256; 8] = [Uint256::zero(); 8]; input[0] = new_deactivate_root; input[1] = COORDINATORHASH.load(deps.storage)?; let batch_start_index = processed_dmsg_count; let mut batch_end_index = batch_start_index + size; let dmsg_chain_length = DMSG_CHAIN_LENGTH.load(deps.storage)?; if batch_end_index > dmsg_chain_length { batch_end_index = dmsg_chain_length; } input[2] = DMSG_HASHES.load(deps.storage, batch_start_index.to_be_bytes().to_vec())?; input[3] = DMSG_HASHES.load(deps.storage, batch_end_index.to_be_bytes().to_vec())?; input[4] = CURRENT_DEACTIVATE_COMMITMENT.load(deps.storage)?; input[5] = new_deactivate_commitment; input[6] = STATE_ROOT_BY_DMSG.load(deps.storage, batch_end_index.to_be_bytes().to_vec())?; input[7] = Uint256::from(POLL_ID.load(deps.storage)?); // Poll ID for replay attack prevention let input_hash = compute_input_hash(&input); let deactivate_vkeys_str = GROTH16_DEACTIVATE_VKEYS.load(deps.storage)?; run_groth16_verify( deactivate_vkeys_str, &groth16_proof, input_hash, "ProcessDeactivate", )?; // --- Effects (all state mutations after proof verification) --- DNODES.save( deps.storage, Uint256::from_u128(0u128).to_be_bytes().to_vec(), &new_deactivate_root, )?; CURRENT_DEACTIVATE_COMMITMENT.save(deps.storage, &new_deactivate_commitment)?; PROCESSED_DMSG_COUNT.save( deps.storage, &(processed_dmsg_count + batch_end_index - batch_start_index), )?; let mut attributes = vec![ attr("zk_verify", "true"), attr("commitment", new_deactivate_commitment.to_string()), attr("proof", to_json_or(&groth16_proof, "{}")), attr("certification_system", "groth16"), attr("processed_dmsg_count", processed_dmsg_count.to_string()), ]; let first_dmsg_time: Timestamp = FIRST_DMSG_TIMESTAMP.load(deps.storage)?; let current_time = env.block.time; let different_time: u64 = current_time.seconds() - first_dmsg_time.seconds(); if different_time > DELAY_CONFIG.load(deps.storage)?.deactivate_delay { let mut delay_records = DELAY_RECORDS.load(deps.storage)?; let delay_timestamp = first_dmsg_time; let delay_duration = different_time; let delay_reason = format!( "Processing of {} deactivate messages has timed out after {} seconds", size, different_time ); let delay_process_dmsg_count = batch_end_index - batch_start_index; let delay_type = DelayType::DeactivateDelay; let delay_record = DelayRecord { delay_timestamp: delay_timestamp.clone(), delay_duration: delay_duration.clone(), delay_reason: delay_reason.clone(), delay_process_dmsg_count: delay_process_dmsg_count.clone(), delay_type, }; delay_records.records.push(delay_record); DELAY_RECORDS.save(deps.storage, &delay_records)?; attributes.push(attr( "delay_timestamp", delay_timestamp.seconds().to_string(), )); attributes.push(attr("delay_duration", delay_duration.to_string())); attributes.push(attr( "delay_process_dmsg_count", delay_process_dmsg_count.to_string(), )); attributes.push(attr("delay_reason", delay_reason)); attributes.push(attr("delay_type", "deactivate_delay")); } Ok(Response::new() .add_attribute("action", "process_deactivate_message") .add_attributes(attributes)) } /// Shared logic for AddNewKey and PreAddNewKey. /// When `is_pre_populated` is true, inputs and state-leaf hashing follow the PrePopulated path. fn add_key_internal( mut deps: DepsMut, env: Env, pubkey: PubKey, nullifier: Uint256, d: [Uint256; 4], groth16_proof: Groth16ProofType, is_pre_populated: bool, ) -> Result { let voting_time = VOTINGTIME.load(deps.storage)?; check_voting_time(env, voting_time)?; // --- Checks (all validations before any state mutation) --- if NULLIFIERS.has(deps.storage, nullifier.to_be_bytes().to_vec()) { return Err(ContractError::NewKeyExist {}); } let mut num_sign_ups = NUMSIGNUPS.load(deps.storage)?; let max_leaves_count = MAX_LEAVES_COUNT.load(deps.storage)?; if num_sign_ups >= max_leaves_count { return Err(ContractError::StateTreeFull {}); } if !is_on_babyjubjub_curve(pubkey.x, pubkey.y) { return Err(ContractError::InvalidPubKey {}); } if SIGNUPED.has(deps.storage, &pubkey_key(&pubkey)) { return Err(ContractError::AlreadySignedUp {}); } let mut input: [Uint256; 9] = [Uint256::zero(); 9]; if is_pre_populated { input[0] = PRE_DEACTIVATE_ROOT.load(deps.storage)?; // PreAddNewKey MUST use pre_deactivate_coordinator hash input[1] = PRE_DEACTIVATE_COORDINATOR_HASH.load(deps.storage)?; } else { input[0] = DNODES.load( deps.storage, Uint256::from_u128(0u128).to_be_bytes().to_vec(), )?; input[1] = COORDINATORHASH.load(deps.storage)?; } input[2] = nullifier; input[3] = d[0]; input[4] = d[1]; input[5] = d[2]; input[6] = d[3]; input[7] = hash2([pubkey.x, pubkey.y]); // fix: front-running (bind newPubKey to proof) input[8] = Uint256::from(POLL_ID.load(deps.storage)?); // fix: replay attack prevention let input_hash = compute_input_hash(&input); let process_vkeys_str = GROTH16_NEWKEY_VKEYS.load(deps.storage)?; let proof_step = if is_pre_populated { "PreAddNewKey" } else { "AddNewKey" }; // ZK proof verification is the most expensive check — placed last to fail fast on cheaper checks first run_groth16_verify(process_vkeys_str, &groth16_proof, input_hash, proof_step)?; // --- Effects (state mutations only after all checks pass) --- NULLIFIERS.save(deps.storage, nullifier.to_be_bytes().to_vec(), &true)?; let voice_credit_amount = if is_pre_populated { let vc_mode = VOICE_CREDIT_MODE.load(deps.storage)?; match vc_mode { VoiceCreditMode::Unified { amount } => amount, VoiceCreditMode::Dynamic => { // PrePopulated mode is restricted to Unified VC mode during instantiation return Err(ContractError::InvalidRegistrationConfig { reason: "PrePopulated mode requires Unified VoiceCreditMode".to_string(), }); } } } else { VOICE_CREDIT_AMOUNT.load(deps.storage)? }; let state_leaf = if is_pre_populated { StateLeaf { pub_key: pubkey.clone(), voice_credit_balance: voice_credit_amount, vote_option_tree_root: Uint256::from_u128(0), nonce: Uint256::from_u128(0), } .hash_decativate_state_leaf() } else { StateLeaf { pub_key: pubkey.clone(), voice_credit_balance: voice_credit_amount, vote_option_tree_root: Uint256::from_u128(0), nonce: Uint256::from_u128(0), } .hash_new_key_state_leaf(d) }; let state_index = num_sign_ups; state_enqueue(&mut deps, state_leaf)?; num_sign_ups += Uint256::from_u128(1u128); NUMSIGNUPS.save(deps.storage, &num_sign_ups)?; SIGNUPED.save(deps.storage, &pubkey_key(&pubkey), &state_index)?; let action = if is_pre_populated { "pre_add_new_key" } else { "add_new_key" }; let mut resp = Response::new() .add_attribute("action", action) .add_attribute("state_idx", state_index.to_string()) .add_attribute( "pubkey", format!("{:?},{:?}", pubkey.x.to_string(), pubkey.y.to_string()), ) .add_attribute("balance", voice_credit_amount.to_string()); if !is_pre_populated { resp = resp .add_attribute("d0", d[0].to_string()) .add_attribute("d1", d[1].to_string()) .add_attribute("d2", d[2].to_string()) .add_attribute("d3", d[3].to_string()); } Ok(resp) } // in voting pub fn execute_add_new_key( deps: DepsMut, env: Env, info: MessageInfo, pubkey: PubKey, nullifier: Uint256, d: [Uint256; 4], groth16_proof: Groth16ProofType, ) -> Result { // Fee stays in contract balance and is distributed at Claim time. let signup_fee = FEE_CONFIG.load(deps.storage)?.signup_fee; let payment = check_fee_payment(&info, signup_fee)?; let resp = add_key_internal(deps, env, pubkey, nullifier, d, groth16_proof, false)?; Ok(resp.add_attribute("fee_paid", format!("{}{}", payment, FEE_DENOM))) } // in voting — only allowed in PrePopulated registration mode pub fn execute_pre_add_new_key( deps: DepsMut, env: Env, info: MessageInfo, pubkey: PubKey, nullifier: Uint256, d: [Uint256; 4], groth16_proof: Groth16ProofType, ) -> Result { let registration_mode = REGISTRATION_MODE.load(deps.storage)?; if !matches!(registration_mode, RegistrationMode::PrePopulated { .. }) { return Err(ContractError::PreAddNewKeyNotAllowed {}); } // Fee stays in contract balance and is distributed at Claim time. let signup_fee = FEE_CONFIG.load(deps.storage)?.signup_fee; let payment = check_fee_payment(&info, signup_fee)?; let resp = add_key_internal(deps, env, pubkey, nullifier, d, groth16_proof, true)?; Ok(resp.add_attribute("fee_paid", format!("{}{}", payment, FEE_DENOM))) } pub fn execute_start_process_period( deps: DepsMut, env: Env, _info: MessageInfo, ) -> Result { let period = PERIOD.load(deps.storage)?; let voting_time = VOTINGTIME.load(deps.storage)?; if env.block.time <= voting_time.end_time { return Err(ContractError::PeriodError {}); } else { if period.status == PeriodStatus::Ended || period.status == PeriodStatus::Processing || period.status == PeriodStatus::Tallying { return Err(ContractError::PeriodError {}); } } let processed_dmsg_count = PROCESSED_DMSG_COUNT.load(deps.storage)?; let dmsg_chain_length = DMSG_CHAIN_LENGTH.load(deps.storage)?; // Check that all deactivate messages have been processed if processed_dmsg_count != dmsg_chain_length { return Err(ContractError::DmsgLeftProcess {}); } // Update the period status to Processing let period = Period { status: PeriodStatus::Processing, }; PERIOD.save(deps.storage, &period)?; // Compute the state root let state_root = state_root(deps.as_ref())?; // Compute the current state commitment as the hash of the state root and 0 CURRENT_STATE_COMMITMENT.save( deps.storage, &hash2([state_root, Uint256::from_u128(0u128)]), )?; // Return a success response Ok(Response::new().add_attribute("action", "start_process_period")) } pub fn execute_process_message( deps: DepsMut, _env: Env, _info: MessageInfo, new_state_commitment: Uint256, groth16_proof: Groth16ProofType, ) -> Result { require_period_status(deps.as_ref(), PeriodStatus::Processing)?; let mut processed_msg_count = PROCESSED_MSG_COUNT.load(deps.storage)?; let msg_chain_length = MSG_CHAIN_LENGTH.load(deps.storage)?; // Check that all messages have not been processed yet if processed_msg_count >= msg_chain_length { return Err(ContractError::AllMessagesProcessed {}); } // Create an array to store the input values for the SNARK proof let mut input: [Uint256; 8] = [Uint256::zero(); 8]; let num_sign_ups = NUMSIGNUPS.load(deps.storage)?; let max_vote_options = MAX_VOTE_OPTIONS.load(deps.storage)?; let circuit_type = CIRCUITTYPE.load(deps.storage)?; if circuit_type == Uint256::from_u128(0u128) { // 1p1v input[0] = (num_sign_ups << 32) + max_vote_options; // packedVals } else if circuit_type == Uint256::from_u128(1u128) { // qv input[0] = (num_sign_ups << 32) + (circuit_type << 64) + max_vote_options; // packedVals } // input[0] = (num_sign_ups << 32) + max_vote_options; // packedVals // Load the coordinator's public key hash let coordinator_hash = COORDINATORHASH.load(deps.storage)?; input[1] = coordinator_hash; // coordPubKeyHash // Load the MACI parameters let parameters = MACIPARAMETERS.load(deps.storage)?; let batch_size = parameters.message_batch_size; // Compute the start and end indices of the current batch let batch_start_index = (msg_chain_length - processed_msg_count - Uint256::from_u128(1u128)) / batch_size * batch_size; let mut batch_end_index = batch_start_index.clone() + batch_size; if batch_end_index > msg_chain_length { batch_end_index = msg_chain_length; } // Load the hash of the message at the batch start index input[2] = MSG_HASHES.load( deps.storage, batch_start_index.clone().to_be_bytes().to_vec(), )?; // batchStartHash // Load the hash of the message at the batch end index input[3] = MSG_HASHES.load(deps.storage, batch_end_index.to_be_bytes().to_vec())?; // batchEndHash // Load the current state commitment let current_state_commitment = CURRENT_STATE_COMMITMENT.load(deps.storage)?; input[4] = current_state_commitment; // Set the new state commitment input[5] = new_state_commitment; input[6] = CURRENT_DEACTIVATE_COMMITMENT.load(deps.storage)?; input[7] = Uint256::from(POLL_ID.load(deps.storage)?); // Poll ID for replay attack prevention let input_hash = compute_input_hash(&input); let groth16_proof_data = groth16_proof; let process_vkeys_str = GROTH16_PROCESS_VKEYS.load(deps.storage)?; run_groth16_verify( process_vkeys_str, &groth16_proof_data, input_hash, "Process", )?; let attributes = vec![ attr("zk_verify", "true"), attr("commitment", new_state_commitment.to_string()), attr("proof", to_json_or(&groth16_proof_data, "{}")), attr("certification_system", "groth16"), attr("processed_msg_count", processed_msg_count.to_string()), ]; // Proof verify success // Update the current state commitment CURRENT_STATE_COMMITMENT.save(deps.storage, &new_state_commitment)?; // Update the count of processed messages processed_msg_count += batch_end_index - batch_start_index; PROCESSED_MSG_COUNT.save(deps.storage, &processed_msg_count)?; Ok(Response::new() .add_attribute("action", "process_message") .add_attributes(attributes)) } pub fn execute_stop_processing_period( deps: DepsMut, _env: Env, _info: MessageInfo, ) -> Result { require_period_status(deps.as_ref(), PeriodStatus::Processing)?; let num_sign_ups = NUMSIGNUPS.load(deps.storage)?; // If there are registered users, check if all messages have been processed // If num_sign_ups is 0, skip the message processing check as all votes are invalid if num_sign_ups != Uint256::zero() { let processed_msg_count = PROCESSED_MSG_COUNT.load(deps.storage)?; let msg_chain_length = MSG_CHAIN_LENGTH.load(deps.storage)?; if processed_msg_count != msg_chain_length { return Err(ContractError::MsgLeftProcess {}); } } let period = Period { status: PeriodStatus::Tallying, }; PERIOD.save(deps.storage, &period)?; Ok(Response::new() .add_attribute("action", "stop_processing_period") .add_attribute("period", "Tallying")) } pub fn execute_process_tally( deps: DepsMut, _env: Env, _info: MessageInfo, new_tally_commitment: Uint256, groth16_proof: Groth16ProofType, ) -> Result { require_period_status(deps.as_ref(), PeriodStatus::Tallying)?; let mut processed_user_count = PROCESSED_USER_COUNT.load(deps.storage)?; let num_sign_ups = NUMSIGNUPS.load(deps.storage)?; // Check that all users have not been processed yet if processed_user_count >= num_sign_ups { return Err(ContractError::AllUsersProcessed {}); } let parameters = MACIPARAMETERS.load(deps.storage)?; // Calculate the batch size let batch_size = Uint256::from_u128(5u128).pow( parameters .int_state_tree_depth .to_string() .parse() .map_err(|e| ContractError::ParseError { value: parameters.int_state_tree_depth.to_string(), reason: format!("{}", e), })?, ); // Calculate the batch number let batch_num = processed_user_count / batch_size; // Create an array to store the input values for the SNARK proof let mut input: [Uint256; 4] = [Uint256::zero(); 4]; input[0] = (num_sign_ups << 32) + batch_num; // packedVals // Load the current state commitment and current tally commitment let current_state_commitment = CURRENT_STATE_COMMITMENT.load(deps.storage)?; let current_tally_commitment = CURRENT_TALLY_COMMITMENT.load(deps.storage)?; input[1] = current_state_commitment; // stateCommitment input[2] = current_tally_commitment; // tallyCommitment input[3] = new_tally_commitment; // newTallyCommitment let input_hash = compute_input_hash(&input); let groth16_proof_data = groth16_proof; let tally_vkeys_str = GROTH16_TALLY_VKEYS.load(deps.storage)?; run_groth16_verify(tally_vkeys_str, &groth16_proof_data, input_hash, "Tally")?; let attributes = vec![ attr("zk_verify", "true"), attr("commitment", new_tally_commitment.to_string()), attr("proof", to_json_or(&groth16_proof_data, "{}")), attr("certification_system", "groth16"), attr("processed_user_count", processed_user_count.to_string()), ]; // Proof verify success // Update the current tally commitment CURRENT_TALLY_COMMITMENT.save(deps.storage, &new_tally_commitment)?; // Update the count of processed users processed_user_count += batch_size; PROCESSED_USER_COUNT.save(deps.storage, &processed_user_count)?; Ok(Response::new() .add_attribute("action", "process_tally") .add_attributes(attributes)) } fn execute_stop_tallying_period( deps: DepsMut, env: Env, _info: MessageInfo, results: Vec, salt: Uint256, ) -> Result { require_period_status(deps.as_ref(), PeriodStatus::Tallying)?; // Get the final signup count and message count let num_sign_ups = NUMSIGNUPS.load(deps.storage)?; let msg_chain_length = MSG_CHAIN_LENGTH.load(deps.storage)?; // Calculate total workload (signup and message have same weight) let total_work = num_sign_ups + msg_chain_length; let total_work_u128 = total_work .try_into() // Uint256 -> Uint128 .map(|x: Uint128| x.u128()) // Uint128 -> u128 .map_err(|_| ContractError::ValueTooLarge {})?; // Calculate actual delay timeout (linear change between min hours to max hours) let actual_delay: TallyDelayInfo = calculate_tally_delay(deps.as_ref())?; let voting_time = VOTINGTIME.load(deps.storage)?; let current_time = env.block.time; let different_time = current_time.seconds() - voting_time.end_time.seconds(); let mut attributes = vec![ attr("total_work", total_work_u128.to_string()), attr( "actual_delay_seconds", actual_delay.delay_seconds.to_string(), ), ]; if different_time > actual_delay.delay_seconds { let delay_timestamp = voting_time.end_time; let delay_duration = different_time; let delay_reason = format!( "Tallying has timed out after {} seconds (total process: {}, allowed: {} seconds)", different_time, total_work_u128, actual_delay.delay_seconds ); let delay_process_dmsg_count = Uint256::from_u128(0u128); let delay_type = DelayType::TallyDelay; let mut delay_records = DELAY_RECORDS.load(deps.storage)?; let delay_record = DelayRecord { delay_timestamp: delay_timestamp.clone(), delay_duration: delay_duration.clone(), delay_reason: delay_reason.clone(), delay_process_dmsg_count, delay_type, }; delay_records.records.push(delay_record); DELAY_RECORDS.save(deps.storage, &delay_records)?; attributes.extend(vec![ attr("delay_timestamp", delay_timestamp.seconds().to_string()), attr("delay_duration", delay_duration.to_string()), attr("delay_reason", delay_reason), attr("delay_type", "tally_delay"), ]); } let processed_user_count = PROCESSED_USER_COUNT.load(deps.storage)?; let num_sign_ups = NUMSIGNUPS.load(deps.storage)?; let max_vote_options = MAX_VOTE_OPTIONS.load(deps.storage)?; // Check that all users have been processed if processed_user_count < num_sign_ups { return Err(ContractError::NotAllUsersProcessed {}); } // Check that the number of results is not greater than the maximum vote options if Uint256::from_u128(results.len() as u128) > max_vote_options { return Err(ContractError::MaxVoteOptionsExceeded { current: Uint256::from_u128(results.len() as u128), max_allowed: max_vote_options, }); } // Load the QTR library and MACI parameters let qtr_lib = QTR_LIB.load(deps.storage)?; let parameters = MACIPARAMETERS.load(deps.storage)?; // Calculate the results root let results_root = qtr_lib.root_of(parameters.vote_option_tree_depth, results.clone()); // Calculate the tally commitment let tally_commitment = hash2([results_root, salt]); // Load the current tally commitment and verify if needed let current_tally_commitment = CURRENT_TALLY_COMMITMENT.load(deps.storage)?; if current_tally_commitment != Uint256::from_u128(0u128) && tally_commitment != current_tally_commitment { return Err(ContractError::TallyCommitmentMismatch {}); } // Save the results and calculate the sum let mut sum = Uint256::zero(); for i in 0..results.len() { RESULT.save( deps.storage, Uint256::from_u128(i as u128).to_be_bytes().to_vec(), &results[i], )?; sum += results[i]; } // Save the total result TOTAL_RESULT.save(deps.storage, &sum)?; // Update the period status to Ended let period = Period { status: PeriodStatus::Ended, }; PERIOD.save(deps.storage, &period)?; Ok(Response::new() .add_attribute("action", "stop_tallying_period") .add_attribute( "results", serde_json::to_string( &results .iter() .map(|x| x.to_string()) .collect::>(), ) .unwrap_or_else(|_| "[]".to_string()), ) .add_attribute("all_result", sum.to_string()) .add_attributes(attributes)) } fn execute_claim(deps: DepsMut, env: Env, _info: MessageInfo) -> Result { let period = PERIOD.load(deps.storage)?; let voting_time: VotingTime = VOTINGTIME.load(deps.storage)?; let current_time = env.block.time; let admin = ADMIN.load(deps.storage)?.admin; let operator = MACI_OPERATOR.load(deps.storage)?; let fee_recipient = FEE_RECIPIENT.load(deps.storage)?; let denom = "peaka".to_string(); let contract_address = env.contract.address.clone(); let contract_balance = deps.querier.query_balance(contract_address, &denom)?; let contract_balance_amount = contract_balance.amount.u128(); if contract_balance_amount == 0u128 { return Err(ContractError::AllFundsClaimed {}); } // Compute dynamic timeout: delay_allowed + 2 days let actual_delay = calculate_tally_delay(deps.as_ref())?; let tally_timeout_secs = actual_delay.delay_seconds + TALLY_TIMEOUT_EXTRA_SECONDS; // If exceeding the timeout, return all funds to admin if current_time > voting_time.end_time.plus_seconds(tally_timeout_secs) { let message = BankMsg::Send { to_address: admin.to_string(), amount: coins(contract_balance_amount, denom), }; return Ok(Response::new() .add_message(message) .add_attribute("action", "claim") .add_attribute( "is_ended", (period.status == PeriodStatus::Ended).to_string(), ) .add_attribute("operator_reward", "0") .add_attribute("penalty_amount", contract_balance_amount.to_string()) .add_attribute("miss_rate", Uint256::from_u128(0u128).to_string()) .add_attribute("is_tally_timeout", "true")); } // If less than timeout and status is not Ended, return an error if period.status != PeriodStatus::Ended { return Err(ContractError::PeriodError {}); } // First allocate 10% to fee_recipient let fee_rate = Decimal::from_ratio(1u128, 10u128); // 10% let fee_amount = Uint128::from(contract_balance_amount) * fee_rate; let remaining_amount = Uint128::from(contract_balance_amount) - fee_amount; // Calculate distribution between operator and admin let performance = calculate_operator_performance(deps.as_ref())?; let withdraw_amount = Uint256::from_u128(remaining_amount.u128()); // Calculate operator reward based on miss rate let operator_reward = withdraw_amount.multiply_ratio(performance.miss_rate, Uint256::from_u128(100u128)); // Calculate penalty amount let penalty_amount = withdraw_amount - operator_reward; let mut messages: Vec = vec![]; // Send 10% to fee_recipient if !fee_amount.is_zero() { messages.push(CosmosMsg::Bank(BankMsg::Send { to_address: fee_recipient.to_string(), amount: coins(fee_amount.u128(), denom.clone()), })); } // Send penalty amount to admin let penalty_u128_amount = penalty_amount .try_into() // Uint256 -> Uint128 .map(|x: Uint128| x.u128()) // Uint128 -> u128 .map_err(|_| ContractError::ValueTooLarge {})?; if !penalty_amount.is_zero() { messages.push(CosmosMsg::Bank(BankMsg::Send { to_address: admin.to_string(), amount: coins(penalty_u128_amount, denom.clone()), })); } // Send remaining reward to operator let operator_reward_u128_amount = operator_reward .try_into() .map(|x: Uint128| x.u128()) .map_err(|_| ContractError::ValueTooLarge {})?; if !operator_reward.is_zero() { messages.push(CosmosMsg::Bank(BankMsg::Send { to_address: operator.to_string(), amount: coins(operator_reward_u128_amount, denom.clone()), })); } Ok(Response::new() .add_messages(messages) .add_attribute("action", "claim") .add_attribute("is_ended", "true") .add_attribute("fee_to_recipient", fee_amount.to_string()) .add_attribute("operator_reward", operator_reward_u128_amount.to_string()) .add_attribute("penalty_amount", penalty_u128_amount.to_string()) .add_attribute("miss_rate", performance.miss_rate.to_string()) .add_attribute("is_tally_timeout", "false")) } fn balance_of_static_whitelist(deps: Deps, sender: &Addr) -> StdResult { let cfg = WHITELIST.load(deps.storage)?; Ok(cfg .users .iter() .find(|u| u.addr == sender) .map(|u| u.voice_credit_amount.clone()) .unwrap_or_else(Uint256::zero)) } // Load the root node of the state tree fn state_root(deps: Deps) -> Result { let root = NODES.load( deps.storage, Uint256::from_u128(0u128).to_be_bytes().to_vec(), )?; Ok(root) } // Enqueues the state leaf into the tree fn state_enqueue(deps: &mut DepsMut, leaf: Uint256) -> Result { let leaf_idx0 = LEAF_IDX_0.load(deps.storage)?; let num_sign_ups = NUMSIGNUPS.load(deps.storage)?; let leaf_idx = leaf_idx0 + num_sign_ups; NODES.save(deps.storage, leaf_idx.to_be_bytes().to_vec(), &leaf)?; state_update_at(deps, leaf_idx) } // Updates the state at the given index in the tree fn state_update_at(deps: &mut DepsMut, index: Uint256) -> Result { let leaf_idx0 = LEAF_IDX_0.load(deps.storage)?; if index < leaf_idx0 { return Err(ContractError::MustUpdate {}); } let mut idx = index.clone(); let mut height = 0; let zeros = ZEROS_H10.load(deps.storage)?; while idx > Uint256::from_u128(0u128) { let parent_idx = (idx - Uint256::one()) / Uint256::from(5u8); let children_idx0 = parent_idx * Uint256::from(5u8) + Uint256::one(); let zero = zeros[height]; let mut inputs: [Uint256; 5] = [Uint256::zero(); 5]; for i in 0..5 { let node_value = NODES.may_load( deps.storage, (children_idx0 + Uint256::from_u128(i as u128)) .to_be_bytes() .to_vec(), )?; let child = match node_value { Some(value) => value, None => zero, }; inputs[i] = child; } if NODES.has(deps.storage, parent_idx.to_be_bytes().to_vec()) { NODES.update( deps.storage, parent_idx.to_be_bytes().to_vec(), |_c: Option| -> StdResult<_> { Ok(hash5(inputs)) }, )?; } else { NODES.save( deps.storage, parent_idx.to_be_bytes().to_vec(), &hash5(inputs), )?; } height += 1; idx = parent_idx; } Ok(true) } fn check_voting_time(env: Env, voting_time: VotingTime) -> Result<(), ContractError> { let current_time = env.block.time; // Check if the current time is within the voting time range (inclusive of start and end time) if current_time < voting_time.start_time || current_time > voting_time.end_time { return Err(ContractError::PeriodError {}); } Ok(()) } /// Hash a message with its encryption public key and previous hash /// This implements the same logic as the circuit's MessageHasher (Hasher13) /// /// Structure mirrors circuit implementation: /// - Hash first 5 message elements: hash5(m[0..4]) /// - Hash next 5 message elements: hash5(m[5..9]) /// - Hash all together: hash5([m_hash, n_hash, enc_pub_key.x, enc_pub_key.y, prev_hash]) /// /// This ensures message chain integrity and prevents replay attacks pub fn hash_message_and_enc_pub_key( message: &MessageData, enc_pub_key: &PubKey, prev_hash: Uint256, ) -> Uint256 { // Hash first 5 elements of the message let mut m: [Uint256; 5] = [Uint256::zero(); 5]; m[0] = message.data[0]; m[1] = message.data[1]; m[2] = message.data[2]; m[3] = message.data[3]; m[4] = message.data[4]; let m_hash = hash5(m); // Hash next 5 elements of the message let mut n: [Uint256; 5] = [Uint256::zero(); 5]; n[0] = message.data[5]; n[1] = message.data[6]; n[2] = message.data[7]; n[3] = message.data[8]; n[4] = message.data[9]; let n_hash = hash5(n); // Final hash combining message hashes, public key, and previous hash // This matches the circuit's Hasher13 structure: // hasher5([m_hash, n_hash, encPubKey[0], encPubKey[1], prevHash]) let final_hash = hash5([m_hash, n_hash, enc_pub_key.x, enc_pub_key.y, prev_hash]); return final_hash; } // Generate storage key for PubKey fn generate_pubkey_storage_key(pubkey: &PubKey) -> Vec { let mut key = Vec::new(); key.extend_from_slice(&pubkey.x.to_be_bytes()); key.extend_from_slice(&pubkey.y.to_be_bytes()); key } // Only admin can execute fn is_admin(deps: Deps, sender: &str) -> StdResult { let cfg = ADMIN.load(deps.storage)?; let can = cfg.is_admin(&sender); Ok(can) } // Only operator can execute fn is_operator(deps: Deps, sender: &str) -> StdResult { let operator = MACI_OPERATOR.load(deps.storage)?; let can_operator = sender.to_string() == operator.to_string(); Ok(can_operator) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::Admin {} => to_json_binary(&ADMIN.load(deps.storage)?.admin), QueryMsg::Operator {} => to_json_binary(&MACI_OPERATOR.load(deps.storage)?), QueryMsg::GetRoundInfo {} => to_json_binary::(&ROUNDINFO.load(deps.storage)?), QueryMsg::GetVotingTime {} => to_json_binary::(&VOTINGTIME.load(deps.storage)?), QueryMsg::GetPeriod {} => to_json_binary::(&PERIOD.load(deps.storage)?), QueryMsg::GetNumSignUp {} => { to_json_binary::(&NUMSIGNUPS.may_load(deps.storage)?.unwrap_or_default()) } QueryMsg::GetMsgChainLength {} => { to_json_binary::(&MSG_CHAIN_LENGTH.may_load(deps.storage)?.unwrap_or_default()) } QueryMsg::GetDMsgChainLength {} => to_json_binary::( &DMSG_CHAIN_LENGTH .may_load(deps.storage)? .unwrap_or_default(), ), QueryMsg::GetProcessedDMsgCount {} => to_json_binary::( &PROCESSED_DMSG_COUNT .may_load(deps.storage)? .unwrap_or_default(), ), QueryMsg::GetProcessedMsgCount {} => to_json_binary::( &PROCESSED_MSG_COUNT .may_load(deps.storage)? .unwrap_or_default(), ), QueryMsg::GetProcessedUserCount {} => to_json_binary::( &PROCESSED_USER_COUNT .may_load(deps.storage)? .unwrap_or_default(), ), QueryMsg::GetStateTreeRoot {} => to_json_binary::( &state_root(deps).map_err(|e| cosmwasm_std::StdError::generic_err(e.to_string()))?, ), QueryMsg::GetNode { index } => { let node = NODES .may_load(deps.storage, index.to_be_bytes().to_vec())? .unwrap_or_default(); to_json_binary::(&node) } QueryMsg::GetResult { index } => to_json_binary::( &RESULT .may_load(deps.storage, index.to_be_bytes().to_vec())? .unwrap_or_default(), ), QueryMsg::GetAllResult {} => { to_json_binary::(&TOTAL_RESULT.may_load(deps.storage)?.unwrap_or_default()) } QueryMsg::GetAllResults {} => { let max_vote_options = MAX_VOTE_OPTIONS.may_load(deps.storage)?.unwrap_or_default(); let mut results: Vec = Vec::new(); // Convert Uint256 -> Uint128 -> u128 safely let max = max_vote_options .try_into() // Uint256 -> Uint128 .map(|x: Uint128| x.u128()) // Uint128 -> u128 .unwrap_or(0u128); for i in 0..max { let result = RESULT .may_load(deps.storage, Uint256::from_u128(i).to_be_bytes().to_vec())? .unwrap_or_default(); results.push(result); } to_json_binary::>(&results) } QueryMsg::GetStateIdxInc { address } => to_json_binary::( &STATEIDXINC .may_load(deps.storage, &address)? .unwrap_or_default(), ), QueryMsg::GetVoiceCreditBalance { index } => to_json_binary::( &VOICECREDITBALANCE .may_load(deps.storage, index.to_be_bytes().to_vec())? .unwrap_or_default(), ), QueryMsg::GetVoiceCreditAmount {} => to_json_binary::( &VOICE_CREDIT_AMOUNT .may_load(deps.storage)? .unwrap_or_default(), ), QueryMsg::Signuped { pubkey } => { let state_idx = SIGNUPED.may_load(deps.storage, &pubkey_key(&pubkey))?; to_json_binary(&state_idx) } QueryMsg::VoteOptionMap {} => { to_json_binary::>(&VOTEOPTIONMAP.load(deps.storage)?) } QueryMsg::MaxVoteOptions {} => { to_json_binary::(&MAX_VOTE_OPTIONS.may_load(deps.storage)?.unwrap_or_default()) } QueryMsg::QueryCircuitType {} => { to_json_binary::(&CIRCUITTYPE.may_load(deps.storage)?.unwrap_or_default()) } QueryMsg::QueryCertSystem {} => { to_json_binary::(&CERTSYSTEM.may_load(deps.storage)?.unwrap_or_default()) } QueryMsg::QueryPreDeactivateRoot {} => to_json_binary::( &PRE_DEACTIVATE_ROOT .may_load(deps.storage)? .unwrap_or_default(), ), QueryMsg::QueryPreDeactivateCoordinatorHash {} => { let coordinator_hash = PRE_DEACTIVATE_COORDINATOR_HASH.may_load(deps.storage)?; to_json_binary(&coordinator_hash) } QueryMsg::GetDelayRecords {} => { let records = DELAY_RECORDS .may_load(deps.storage)? .unwrap_or(DelayRecords { records: vec![] }); to_json_binary(&records) } QueryMsg::GetTallyDelay {} => { let delay_info = calculate_tally_delay(deps) .map_err(|e| cosmwasm_std::StdError::generic_err(e.to_string()))?; to_json_binary(&delay_info) } QueryMsg::QueryOracleWhitelistConfig {} => { // Compatible: return oracle pubkey from registration mode (same Option as before) let pubkey = get_oracle_pubkey(deps)?; to_json_binary(&pubkey) } QueryMsg::QueryCurrentStateCommitment {} => { let current_state_commitment = CURRENT_STATE_COMMITMENT.may_load(deps.storage)?; to_json_binary(¤t_state_commitment) } QueryMsg::GetCoordinatorHash {} => { let coordinator_hash = COORDINATORHASH.may_load(deps.storage)?; to_json_binary(&coordinator_hash) } QueryMsg::GetMsgHash { index } => { let msg_hash = MSG_HASHES .may_load(deps.storage, index.to_be_bytes().to_vec())? .unwrap_or_default(); to_json_binary(&msg_hash) } QueryMsg::GetCurrentDeactivateCommitment {} => { let current_deactivate_commitment = CURRENT_DEACTIVATE_COMMITMENT.may_load(deps.storage)?; to_json_binary(¤t_deactivate_commitment) } QueryMsg::GetPollId {} => { let poll_id = POLL_ID.load(deps.storage)?; to_json_binary(&poll_id) } QueryMsg::GetDeactivateEnabled {} => { let enabled = DEACTIVATE_ENABLED.load(deps.storage)?; to_json_binary(&enabled) } QueryMsg::GetRegistrationConfig {} => { let deactivate_enabled = DEACTIVATE_ENABLED.load(deps.storage)?; let voice_credit_mode = VOICE_CREDIT_MODE.load(deps.storage)?; let registration_mode = REGISTRATION_MODE.load(deps.storage)?; let config_info = RegistrationConfigInfo { deactivate_enabled, voice_credit_mode, registration_mode, }; to_json_binary(&config_info) } QueryMsg::QueryRegistrationStatus { sender, pubkey, certificate, amount, } => { let registration_mode = REGISTRATION_MODE.load(deps.storage)?; let voice_credit_mode = VOICE_CREDIT_MODE.load(deps.storage)?; let status = match registration_mode { RegistrationMode::SignUpWithStaticWhitelist => { // is_register is tracked per wallet address in WHITELIST let (can_sign_up, is_register, balance) = match sender { Some(s) => { let whitelist = WHITELIST.load(deps.storage)?; let reg = whitelist.is_register(&s); let can = whitelist.is_whitelist(&s) && !reg; let bal = match &voice_credit_mode { VoiceCreditMode::Unified { .. } => { VOICE_CREDIT_AMOUNT.load(deps.storage)? } VoiceCreditMode::Dynamic => balance_of_static_whitelist(deps, &s)?, }; (can, reg, bal) } None => (false, false, Uint256::zero()), }; RegistrationStatus { can_sign_up, is_register, balance, } } RegistrationMode::SignUpWithOracle { .. } => { // is_register is tracked per pubkey in ORACLE_WHITELIST; // oracle_registration_status checks this in a single pass. let (can_sign_up, is_register, balance) = match (&pubkey, &certificate) { (Some(pk), Some(cert)) => oracle_registration_status( deps, &_env, pk, cert, amount, &voice_credit_mode, )?, _ => (false, false, Uint256::zero()), }; RegistrationStatus { can_sign_up, is_register, balance, } } // PrePopulated only supports Unified VoiceCreditMode. // is_register is checked via SIGNUPED (keyed by pubkey). RegistrationMode::PrePopulated { .. } => { let is_register = match &pubkey { Some(pk) => SIGNUPED.has(deps.storage, &pubkey_key(pk)), None => false, }; RegistrationStatus { can_sign_up: false, is_register, balance: VOICE_CREDIT_AMOUNT.load(deps.storage)?, } } }; // Invariant: a registered user can never sign up again. // Each branch above already enforces this, but we guard here centrally // to remain correct if any branch is changed in the future. let status = if status.is_register { RegistrationStatus { can_sign_up: false, ..status } } else { status }; to_json_binary(&status) } QueryMsg::GetFeeConfig {} => { let fee_cfg = FEE_CONFIG.load(deps.storage)?; let config = FeeConfigResponse { message_fee: fee_cfg.message_fee, deactivate_fee: fee_cfg.deactivate_fee, signup_fee: fee_cfg.signup_fee, }; to_json_binary(&config) } QueryMsg::GetDelayConfig {} => { let delay_cfg = DELAY_CONFIG.load(deps.storage)?; let config = DelayConfigResponse { base_delay: delay_cfg.base_delay, message_delay: delay_cfg.message_delay, signup_delay: delay_cfg.signup_delay, deactivate_delay: delay_cfg.deactivate_delay, }; to_json_binary(&config) } } } #[cfg(test)] mod tests {} // Check if the operator has processed all deactivate messages within 15 minutes pub fn check_operator_process_time(deps: Deps, env: Env) -> Result { let current_time = env.block.time; let first_dmsg_time = match FIRST_DMSG_TIMESTAMP.may_load(deps.storage)? { Some(timestamp) => timestamp, None => return Ok(true), // If there is no timestamp for first message, means no deactivate messages need to be processed }; let processed_dmsg_count = PROCESSED_DMSG_COUNT.load(deps.storage)?; let dmsg_chain_length = DMSG_CHAIN_LENGTH.load(deps.storage)?; // If current batch is fully processed, return true if processed_dmsg_count == dmsg_chain_length { return Ok(true); } let time_difference = current_time.seconds() - first_dmsg_time.seconds(); let deactivate_delay = DELAY_CONFIG.load(deps.storage)?.deactivate_delay; if time_difference > deactivate_delay { return Ok(false); } Ok(true) } #[cw_serde] pub struct OperatorPerformance { pub delay_deactivate_count: Uint256, pub delay_tally_count: Uint256, pub miss_rate: Uint256, // Miss rate, range 0-100, represents percentage of operator's deserved reward } pub fn calculate_operator_performance(deps: Deps) -> Result { let delay_records = DELAY_RECORDS.load(deps.storage)?; // Count number of different types of delay records let mut delay_deactivate_count = Uint256::zero(); let mut delay_tally_count = Uint256::zero(); for record in &delay_records.records { match record.delay_type { DelayType::DeactivateDelay => { delay_deactivate_count += record.delay_process_dmsg_count; } DelayType::TallyDelay => { delay_tally_count += Uint256::from_u128(1u128); } } } // Set penalty rate for each type of delay let tally_penalty_rate = PENALTY_RATE.load(deps.storage)?; let deactivate_penalty_rate = Uint256::from_u128(5u128); // 5% penalty for each deactivate delay // Calculate total penalty rate let total_penalty_rate = delay_tally_count * tally_penalty_rate + delay_deactivate_count * deactivate_penalty_rate; // Ensure penalty rate does not exceed 100% let penalty_rate = std::cmp::min(total_penalty_rate, Uint256::from_u128(100u128)); // Calculate miss rate (100% - penalty rate) let miss_rate = Uint256::from_u128(100u128) - penalty_rate; Ok(OperatorPerformance { delay_deactivate_count, delay_tally_count, miss_rate, }) } pub fn calculate_tally_delay(deps: Deps) -> Result { let num_sign_ups = NUMSIGNUPS.load(deps.storage)?; let msg_chain_length = MSG_CHAIN_LENGTH.load(deps.storage)?; let total_work = num_sign_ups + msg_chain_length; let total_work_u128 = total_work .try_into() .map(|x: Uint128| x.u128()) .map_err(|_| ContractError::ValueTooLarge {})?; let num_sign_ups_u64: u64 = num_sign_ups .try_into() .map(|x: Uint128| x.u128() as u64) .map_err(|_| ContractError::ValueTooLarge {})?; let msg_count_u64: u64 = msg_chain_length .try_into() .map(|x: Uint128| x.u128() as u64) .map_err(|_| ContractError::ValueTooLarge {})?; // Load delay configuration from storage (set at instantiation by Registry). let delay_cfg = DELAY_CONFIG.load(deps.storage)?; let base_delay = delay_cfg.base_delay; let message_delay = delay_cfg.message_delay; let signup_delay = delay_cfg.signup_delay; // tally_window = (base_delay + num_signups * signup_delay + msg_count * message_delay) * multiplier let delay_seconds = (base_delay + num_sign_ups_u64 * signup_delay + msg_count_u64 * message_delay) * TALLY_DELAY_MULTIPLIER; let calculated_hours = delay_seconds / 3600; Ok(TallyDelayInfo { delay_seconds, total_work: total_work_u128, num_sign_ups, msg_chain_length, calculated_hours, }) } /// Get oracle (visa/verification) pubkey from registration mode. Compatible with QueryOracleWhitelistConfig. fn get_oracle_pubkey(deps: Deps) -> StdResult> { let mode = REGISTRATION_MODE.may_load(deps.storage)?; Ok(mode.and_then(|m| match m { RegistrationMode::SignUpWithOracle { oracle_pubkey } => Some(oracle_pubkey), _ => None, })) } // ============================================================ // Oracle certificate verification helpers // ============================================================ // Low-level signature verification against an oracle certificate. // The payload format must match what the oracle signs: // { amount, contract_address, pubkey_x, pubkey_y } fn verify_oracle_certificate( deps: Deps, env: &Env, pubkey: &PubKey, certificate: &str, verify_amount: Uint256, oracle_pubkey_str: &str, ) -> StdResult { let contract_address_uint256 = address_to_uint256(&env.contract.address); let payload = VerifyPayload { amount: verify_amount.to_string(), contract_address: contract_address_uint256.to_string(), pubkey_x: pubkey.x.to_string(), pubkey_y: pubkey.y.to_string(), }; let hash = Sha256::digest( serde_json::to_string(&payload) .unwrap_or_default() .as_bytes(), ); let certificate_binary = Binary::from_base64(certificate)?; let oracle_pubkey_binary = Binary::from_base64(oracle_pubkey_str)?; Ok(deps.api.secp256k1_verify( hash.as_ref(), certificate_binary.as_slice(), oracle_pubkey_binary.as_slice(), )?) } // Resolve the amount to use in the oracle verification payload based on VoiceCreditMode. // Returns None when Dynamic mode requires a user-provided amount but none was given. // Convenience key for any Map keyed by (pubkey.x, pubkey.y) – used by both // ORACLE_WHITELIST and SIGNUPED. fn pubkey_key(pubkey: &PubKey) -> (Vec, Vec) { ( pubkey.x.to_be_bytes().to_vec(), pubkey.y.to_be_bytes().to_vec(), ) } // Verify that enough fee was paid and return the actual payment amount. fn check_fee_payment(info: &MessageInfo, required: Uint128) -> Result { let payment = info .funds .iter() .find(|coin| coin.denom == FEE_DENOM) .map(|coin| coin.amount) .unwrap_or(Uint128::zero()); if payment != required { return Err(ContractError::InsufficientFundsSend {}); } Ok(payment) } // Guard: return DeactivateDisabled if the feature is turned off. fn require_deactivate_enabled(deps: Deps) -> Result<(), ContractError> { if !DEACTIVATE_ENABLED.load(deps.storage)? { return Err(ContractError::DeactivateDisabled {}); } Ok(()) } // Guard: return PeriodError if the current period status is not the expected one. fn require_period_status(deps: Deps, expected: PeriodStatus) -> Result<(), ContractError> { let period = PERIOD.load(deps.storage)?; if period.status != expected { return Err(ContractError::PeriodError {}); } Ok(()) } // Compute the SNARK-safe input hash used by all Groth16 proof verifications. fn compute_input_hash(input: &[Uint256]) -> Uint256 { uint256_from_hex_string(&hash_256_uint256_list(input)) % uint256_from_hex_string(SNARK_SCALAR_FIELD_HEX) } // Serialize a value to JSON, returning `fallback` on error. fn to_json_or(value: &T, fallback: &'static str) -> String { serde_json::to_string(value).unwrap_or_else(|_| fallback.to_string()) } // Combined oracle registration status for QueryRegistrationStatus. // Performs a single signature verification and returns (can_sign_up, is_register, balance) // together, avoiding the double verification that would occur when calling the two functions // above separately. fn oracle_registration_status( deps: Deps, env: &Env, pubkey: &PubKey, certificate: &str, amount: Option, voice_credit_mode: &VoiceCreditMode, ) -> StdResult<(bool, bool, Uint256)> { let key = pubkey_key(pubkey); // Already registered: cannot sign up again, but return their actual stored balance if ORACLE_WHITELIST.has(deps.storage, &key) { let stored_balance = ORACLE_WHITELIST.load(deps.storage, &key)?.balance_of(); return Ok((false, true, stored_balance)); } let oracle_pubkey_str = match get_oracle_pubkey(deps)? { Some(p) => p, None => return Ok((false, false, Uint256::zero())), }; // For Unified mode the balance is the contract-wide fixed amount regardless of the certificate. // For Dynamic mode the oracle signs a per-user amount that we must verify. let verify_amount = match voice_credit_mode { VoiceCreditMode::Unified { amount: vc_amount } => *vc_amount, VoiceCreditMode::Dynamic => match amount { Some(a) => a, None => return Ok((false, false, Uint256::zero())), }, }; // Single verification covers can_sign_up, is_register, and balance in one shot if verify_oracle_certificate( deps, env, pubkey, certificate, verify_amount, &oracle_pubkey_str, )? { Ok((true, false, verify_amount)) } else { Ok((false, false, Uint256::zero())) } }