Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion contracts/agent-passport-validator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ pub enum Error {
UnknownRegistryRoot = 6,
/// Batch size exceeds the limit of 8.
BatchTooLarge = 7,
/// An agent passport already exists for this agent ID.
AlreadyRegistered = 8,
}

#[contracttype]
Expand Down Expand Up @@ -224,6 +226,12 @@ impl AgentPassportValidator {
return Err(Error::NullifierUsed);
}

// (1.5) reject duplicate agent registration — prevent silent overwrites.
let pass_key = DataKey::Passport(agent_id.clone());
if persistent.has(&pass_key) {
return Err(Error::AlreadyRegistered);
}

// (2) cross-contract soundness check. `try_verify` so an invalid proof
// surfaces as our typed error instead of trapping the whole tx.
let verifier_addr: Address = env
Expand Down Expand Up @@ -311,7 +319,10 @@ impl AgentPassportValidator {
Error::NullifierUsed => Some(Symbol::new(&env, "NullifierUsed")),
Error::InvalidProof => Some(Symbol::new(&env, "InvalidProof")),
Error::NotInitialized => Some(Symbol::new(&env, "NotInitialized")),
Error::UnknownRegistryRoot => Some(Symbol::new(&env, "UnknownRegistryRoot")),
Error::UnknownRegistryRoot => {
Some(Symbol::new(&env, "UnknownRegistryRoot"))
}
Error::AlreadyRegistered => Some(Symbol::new(&env, "AlreadyRegistered")),
_ => Some(Symbol::new(&env, "Error")),
};
results.push_back(VerifyResult {
Expand Down
31 changes: 30 additions & 1 deletion contracts/agent-passport-validator/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,11 @@ fn mixed_batch_results() {
public_inputs: real_public_inputs(&env),
});

// 2. Invalid proof (tampered public input, but unique nullifier)
// 2. Invalid proof (tampered public input, unique agent ID and nullifier)
let mut tampered_pi = real_public_inputs(&env);
tampered_pi.set(IDX_SPEND_CAP, U256::from_u32(&env, 999));
tampered_pi.set(IDX_NULLIFIER, U256::from_u32(&env, 9999));
tampered_pi.set(IDX_AGENT_ID, U256::from_u32(&env, 8888));
inputs.push_back(VerifyInput {
proof: real_proof(&env),
public_inputs: tampered_pi,
Expand Down Expand Up @@ -258,6 +259,34 @@ fn rejects_nullifier_replay() {
assert_eq!(res, Err(Ok(Error::NullifierUsed)));
}

#[test]
fn rejects_duplicate_agent_registration() {
let env = Env::default();
let client = setup(&env, u256(&env, PI_ROOT));

// First registration succeeds.
let att = client.verify_and_register(&real_proof(&env), &real_public_inputs(&env));
let agent_id = u256(&env, PI_AGENT);
assert_eq!(att.agent_id, agent_id);
assert!(client.is_registered(&agent_id));

// Second registration with the same agentId but different nullifier should fail.
// We can't generate a valid proof with different nullifier from fixtures, so we
// test that the duplicate agent guard triggers before proof verification.
let mut second_inputs = real_public_inputs(&env);
second_inputs.set(IDX_NULLIFIER, U256::from_u32(&env, 99999));

let res = client.try_verify_and_register(&real_proof(&env), &second_inputs);
// The duplicate agent check happens before proof verification, so we get
// AlreadyRegistered instead of InvalidProof.
assert_eq!(res, Err(Ok(Error::AlreadyRegistered)));

// Verify the original passport remains unchanged.
let stored = client.get_passport(&agent_id).unwrap();
assert_eq!(stored.nullifier, u256(&env, PI_NULLIFIER));
assert_eq!(stored.spend_cap, u256(&env, PI_CAP));
}

#[test]
fn rejects_tampered_public_input() {
let env = Env::default();
Expand Down
Loading