Skip to content

Commit 11dacef

Browse files
committed
feat: update round info set time
1 parent d58ad7e commit 11dacef

4 files changed

Lines changed: 321 additions & 2 deletions

File tree

artifacts/checksums.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
cc61ccbba7b73fb75a15468ed45d566b4f858f9f1afa4100f631a16373a26815 cw_amaci-aarch64.wasm
1+
bc0ce3afb95d044fb9e24605f48e00212b09b0d1bfa0a59578cd9211ac5ce30c cw_amaci-aarch64.wasm
22
4607bebf17551c904c451b652ce2e0693fee498c1e7ad45e55e54b9747337dd3 cw_amaci_registry-aarch64.wasm
33
86583bd5db998cff1bd9b45c62e9d207875fa3ae63929d69afeacf4cb3938fab cw_api_saas-aarch64.wasm
44
9b20be65d366f0c05a678448c3cf90a9717ed394e0e77df5aec71cfed7df80e4 cw_maci-aarch64.wasm

contracts/amaci/src/contract.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -829,10 +829,15 @@ pub fn execute(
829829

830830
pub fn execute_set_round_info(
831831
deps: DepsMut,
832-
_env: Env,
832+
env: Env,
833833
info: MessageInfo,
834834
round_info: RoundInfo,
835835
) -> Result<Response, ContractError> {
836+
let voting_time = VOTINGTIME.load(deps.storage)?;
837+
if env.block.time >= voting_time.start_time {
838+
return Err(ContractError::PeriodError {});
839+
}
840+
836841
if !is_admin(deps.as_ref(), info.sender.as_ref())? {
837842
Err(ContractError::Unauthorized {})
838843
} else {

contracts/amaci/src/multitest/tests.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3933,4 +3933,120 @@ mod test {
39333933
InvalidProof, not NewKeyExist — confirms the nullifier was rolled back"
39343934
);
39353935
}
3936+
3937+
// ── set_round_info permission tests ──────────────────────────────────────
3938+
3939+
#[test]
3940+
fn test_set_round_info_success_before_voting() {
3941+
let mut app = create_app();
3942+
3943+
// Block time is before voting start (default start: 1571797424879000000 ns)
3944+
app.update_block(|block| {
3945+
block.time = Timestamp::from_nanos(1571797424879000000 - 60_000_000_000);
3946+
});
3947+
3948+
let maci_contract = MaciContract::instantiate_default(&mut app, false).unwrap();
3949+
3950+
let result = maci_contract.set_round_info(&mut app, owner());
3951+
assert!(result.is_ok(), "Admin should be able to set round info before voting starts");
3952+
}
3953+
3954+
#[test]
3955+
fn test_set_round_info_fails_after_voting_starts() {
3956+
let mut app = create_app();
3957+
3958+
// Block time is before voting start so instantiate succeeds
3959+
app.update_block(|block| {
3960+
block.time = Timestamp::from_nanos(1571797424879000000 - 60_000_000_000);
3961+
});
3962+
3963+
let maci_contract = MaciContract::instantiate_default(&mut app, false).unwrap();
3964+
3965+
// Advance to after voting start
3966+
app.update_block(|block| {
3967+
block.time = Timestamp::from_nanos(1571797424879000000 + 60_000_000_000);
3968+
});
3969+
3970+
let err = maci_contract
3971+
.set_round_info(&mut app, owner())
3972+
.unwrap_err();
3973+
3974+
let contract_err: ContractError = err.downcast().unwrap();
3975+
assert_eq!(
3976+
contract_err,
3977+
ContractError::PeriodError {},
3978+
"Should not be able to set round info after voting starts"
3979+
);
3980+
}
3981+
3982+
#[test]
3983+
fn test_set_round_info_fails_exactly_at_voting_start() {
3984+
let mut app = create_app();
3985+
3986+
app.update_block(|block| {
3987+
block.time = Timestamp::from_nanos(1571797424879000000 - 60_000_000_000);
3988+
});
3989+
3990+
let maci_contract = MaciContract::instantiate_default(&mut app, false).unwrap();
3991+
3992+
// Set block time exactly to voting start_time
3993+
app.update_block(|block| {
3994+
block.time = Timestamp::from_nanos(1571797424879000000);
3995+
});
3996+
3997+
let err = maci_contract
3998+
.set_round_info(&mut app, owner())
3999+
.unwrap_err();
4000+
4001+
let contract_err: ContractError = err.downcast().unwrap();
4002+
assert_eq!(
4003+
contract_err,
4004+
ContractError::PeriodError {},
4005+
"Should not be able to set round info at exact voting start time"
4006+
);
4007+
}
4008+
4009+
#[test]
4010+
fn test_set_round_info_unauthorized() {
4011+
let mut app = create_app();
4012+
4013+
app.update_block(|block| {
4014+
block.time = Timestamp::from_nanos(1571797424879000000 - 60_000_000_000);
4015+
});
4016+
4017+
let maci_contract = MaciContract::instantiate_default(&mut app, false).unwrap();
4018+
4019+
let err = maci_contract
4020+
.set_round_info(&mut app, user1())
4021+
.unwrap_err();
4022+
4023+
let contract_err: ContractError = err.downcast().unwrap();
4024+
assert_eq!(
4025+
contract_err,
4026+
ContractError::Unauthorized {},
4027+
"Non-admin should not be able to set round info"
4028+
);
4029+
}
4030+
4031+
#[test]
4032+
fn test_set_round_info_empty_title_fails() {
4033+
let mut app = create_app();
4034+
4035+
app.update_block(|block| {
4036+
block.time = Timestamp::from_nanos(1571797424879000000 - 60_000_000_000);
4037+
});
4038+
4039+
let maci_contract = MaciContract::instantiate_default(&mut app, false).unwrap();
4040+
4041+
let err = maci_contract
4042+
.set_empty_round_info(&mut app, owner())
4043+
.unwrap_err();
4044+
4045+
let contract_err: ContractError = err.downcast().unwrap();
4046+
assert_eq!(
4047+
contract_err,
4048+
ContractError::TitleIsEmpty {},
4049+
"Empty title should be rejected even before voting starts"
4050+
);
4051+
}
39364052
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/**
2+
* Round Info Change Permission Test
3+
*
4+
* This script demonstrates the round info modification restriction:
5+
* - Round info CAN be updated before voting starts
6+
* - Round info CANNOT be updated once voting has started (contract returns PeriodError)
7+
*
8+
* Steps:
9+
* 1. Create Tenant and API Key
10+
* 2. Create AMACI Round (voting starts ~1 minute from now)
11+
* 3. Update round info BEFORE voting starts → should succeed
12+
* 4. Wait until voting starts
13+
* 5. Update round info AFTER voting starts → should fail with PeriodError
14+
*/
15+
16+
import { MaciClient } from '../src/maci';
17+
import { MaciCircuitType } from '../src/types';
18+
import dotenv from 'dotenv';
19+
dotenv.config();
20+
21+
function generateRandomString(length: number) {
22+
return Math.random()
23+
.toString(36)
24+
.substring(2, 2 + length);
25+
}
26+
27+
function sleep(ms: number): Promise<void> {
28+
return new Promise((resolve) => setTimeout(resolve, ms));
29+
}
30+
31+
async function main() {
32+
// const network = 'mainnet';
33+
// const operator = 'dora16nkezrnvw9fzqqqmmqtrdkw3pqes6qthhse2k4';
34+
35+
const network = 'testnet';
36+
const operator = 'dora149n5yhzgk5gex0eqmnnpnsxh6ys4exg5xyqjzm';
37+
38+
console.log('='.repeat(80));
39+
console.log('Round Info Change Permission Test');
40+
console.log('Verify: round info is locked once voting starts');
41+
console.log('='.repeat(80));
42+
43+
const API_BASE_URL = undefined;
44+
45+
const adminSecret = process.env.ADMIN_SECRET;
46+
if (!adminSecret) {
47+
throw new Error('ADMIN_SECRET environment variable is not set');
48+
}
49+
50+
// ==================== 1. Create Tenant and API Key ====================
51+
const tenantName = `RoundInfo Test ${generateRandomString(8)}`;
52+
console.log(`\n[1/5] Creating Tenant: ${tenantName}`);
53+
54+
const adminMaciClient = new MaciClient({
55+
network,
56+
saasApiEndpoint: API_BASE_URL
57+
});
58+
59+
const tenantData = await adminMaciClient
60+
.getSaasApiClient()
61+
.createTenant({ name: tenantName }, adminSecret);
62+
console.log('✓ Tenant created:', tenantData.id);
63+
64+
const apiKeyData = await adminMaciClient
65+
.getSaasApiClient()
66+
.createApiKey(
67+
{ tenantId: tenantData.id, label: 'RoundInfo Test Key', plan: 'pro' },
68+
adminSecret
69+
);
70+
const apiKey = apiKeyData.apiKey;
71+
console.log('✓ API Key created:', apiKey);
72+
73+
const maciClient = new MaciClient({
74+
network,
75+
saasApiEndpoint: API_BASE_URL,
76+
saasApiKey: apiKey
77+
});
78+
79+
// ==================== 2. Create AMACI Round ====================
80+
// Voting starts in 2 minutes to give enough time to test the pre-start update,
81+
// then wait for start and confirm the post-start update fails.
82+
const VOTING_START_DELAY_MS = 2 * 60 * 1000; // 2 minutes
83+
const startVoting = new Date(Date.now() + VOTING_START_DELAY_MS);
84+
const endVoting = new Date(startVoting.getTime() + 11 * 60 * 1000); // 11 minutes
85+
86+
console.log(`\n[2/5] Creating AMACI Round`);
87+
console.log(` Voting start : ${startVoting.toISOString()}`);
88+
console.log(` Voting end : ${endVoting.toISOString()}`);
89+
90+
const createRoundData = await maciClient.saasCreateAmaciRound({
91+
title: 'Round Info Test Round',
92+
description: 'Testing round info modification restrictions',
93+
link: 'https://test.com',
94+
startVoting: startVoting.toISOString(),
95+
endVoting: endVoting.toISOString(),
96+
operator,
97+
maxVoter: 25,
98+
voteOptionMap: ['Option A', 'Option B', 'Option C'],
99+
circuitType: MaciCircuitType.IP1V,
100+
voiceCreditAmount: 100
101+
});
102+
103+
if (createRoundData.status === 'failed') {
104+
throw new Error(`Round creation failed: ${createRoundData.error ?? 'unknown error'}`);
105+
}
106+
107+
const contractAddress = createRoundData.contractAddress;
108+
if (!contractAddress) {
109+
throw new Error('Contract address not returned');
110+
}
111+
112+
console.log('✓ Round created successfully!');
113+
console.log(' Contract Address:', contractAddress);
114+
console.log(' TX Hash :', createRoundData.txHash);
115+
116+
// ==================== 3. Update Round Info BEFORE Voting Starts ====================
117+
console.log('\n[3/5] Updating round info BEFORE voting starts (should succeed)');
118+
119+
const beforeResult = await maciClient.saasSetRoundInfo({
120+
contractAddress,
121+
title: 'Updated Title (Before Voting)',
122+
description: 'Successfully updated before voting started',
123+
link: 'https://updated-before.test.com'
124+
});
125+
126+
if (beforeResult.status === 'failed') {
127+
throw new Error(
128+
`Round info update failed before voting started — unexpected! Error: ${beforeResult.error ?? 'unknown'}`
129+
);
130+
}
131+
132+
console.log('✓ Round info updated successfully before voting starts');
133+
console.log(' TX Hash:', beforeResult.txHash);
134+
console.log(' Status :', beforeResult.status);
135+
136+
// ==================== 4. Wait for Voting to Start ====================
137+
const msUntilStart = startVoting.getTime() - Date.now();
138+
// Add a small buffer (5 s) to make sure on-chain block time has passed start_time
139+
const waitMs = msUntilStart + 5_000;
140+
141+
console.log(`\n[4/5] Waiting ${Math.ceil(waitMs / 1000)}s for voting to start...`);
142+
143+
const TICK_MS = 10_000;
144+
let remaining = waitMs;
145+
while (remaining > 0) {
146+
const tick = Math.min(TICK_MS, remaining);
147+
await sleep(tick);
148+
remaining -= tick;
149+
if (remaining > 0) {
150+
console.log(` ... ${Math.ceil(remaining / 1000)}s remaining`);
151+
}
152+
}
153+
154+
console.log('✓ Voting has started');
155+
156+
// ==================== 5. Update Round Info AFTER Voting Starts ====================
157+
console.log('\n[5/5] Updating round info AFTER voting starts (should fail with PeriodError)');
158+
159+
const afterResult = await maciClient.saasSetRoundInfo({
160+
contractAddress,
161+
title: 'Updated Title (After Voting — should be rejected)',
162+
description: 'This update should be rejected by the contract',
163+
link: 'https://should-fail.test.com'
164+
});
165+
166+
if (afterResult.status === 'failed') {
167+
console.log('✓ Round info update correctly rejected after voting started');
168+
console.log(' Status:', afterResult.status);
169+
console.log(' Error :', afterResult.error ?? '(PeriodError from contract)');
170+
} else {
171+
// If it succeeded, that means the contract restriction is not working
172+
console.error('✗ ERROR: Round info update succeeded after voting started!');
173+
console.error(' The contract restriction is NOT working as expected.');
174+
console.error(' TX Hash:', afterResult.txHash);
175+
process.exit(1);
176+
}
177+
178+
// ==================== Summary ====================
179+
console.log('\n' + '='.repeat(80));
180+
console.log('Test completed!');
181+
console.log('='.repeat(80));
182+
console.log('\nSummary:');
183+
console.log(' Contract Address :', contractAddress);
184+
console.log(' Voting Start :', startVoting.toISOString());
185+
console.log('\nResults:');
186+
console.log(' [3/5] set_round_info BEFORE voting start → ✓ Succeeded (expected)');
187+
console.log(' [5/5] set_round_info AFTER voting start → ✓ Rejected (expected)');
188+
console.log('\nConclusion: round info is correctly locked once voting starts.');
189+
}
190+
191+
main().catch((error) => {
192+
console.error('\n❌ Test failed:', error);
193+
if (error instanceof Error) {
194+
console.error('Error message:', error.message);
195+
console.error('Stack trace:', error.stack);
196+
}
197+
process.exit(1);
198+
});

0 commit comments

Comments
 (0)