diff --git a/contracts/agent-passport-validator/src/lib.rs b/contracts/agent-passport-validator/src/lib.rs index 4d64699..1b678ff 100644 --- a/contracts/agent-passport-validator/src/lib.rs +++ b/contracts/agent-passport-validator/src/lib.rs @@ -22,7 +22,7 @@ //! [0] registryRoot [1] nullifierHash [2] agentId [3] spendCap use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, panic_with_error, Address, BytesN, Env, + contract, contracterror, contractimpl, contracttype, Address, BytesN, Env, Symbol, Vec, U256, }; @@ -58,18 +58,28 @@ const TTL_THRESHOLD: u32 = 17_280; #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(u32)] pub enum Error { + /// Contract has not been initialised yet; call `init` first. NotInitialized = 1, + /// `init` was already called; it may only be called once. AlreadyInitialized = 2, /// Wrong number of public inputs for the agent_passport circuit. + /// Expected exactly 4: [registryRoot, nullifierHash, agentId, spendCap]. BadPublicInputs = 3, /// This nullifier was already spent — replay / Sybil attempt. + /// The nullifier is stored permanently in persistent storage after first use. NullifierUsed = 4, - /// The Groth16 proof did not verify against the embedded key. + /// The Groth16 proof did not verify against the embedded verification key. + /// Either the proof is malformed, the inputs were tampered with, or the + /// wrong verifier contract is configured. InvalidProof = 5, - /// Batch size exceeds the limit of 8. + /// Batch size exceeds the hard limit of 8 proofs per call. BatchTooLarge = 6, - /// The registry root is not in the approved allow-list. + /// The registry root supplied in public_inputs[0] is not in the + /// admin-maintained allow-list. The holder's identity provider is not + /// currently attested. UnknownRegistryRoot = 7, + /// No pending admin has been proposed via `transfer_admin`. + NoPendingAdmin = 8, } #[contracttype] @@ -167,11 +177,11 @@ pub struct AgentPassportValidator; #[contractimpl] impl AgentPassportValidator { /// One-time wiring: who can re-point the verifier, and the verifier's - /// contract address. Panics on a second call. - pub fn init(env: Env, admin: Address, verifier: Address, initial_root: U256) { + /// contract address. Returns [`Error::AlreadyInitialized`] on a second call. + pub fn init(env: Env, admin: Address, verifier: Address, initial_root: U256) -> Result<(), Error> { let storage = env.storage().instance(); if storage.has(&DataKey::Initialized) { - panic_with_error!(&env, Error::AlreadyInitialized); + return Err(Error::AlreadyInitialized); } storage.set(&DataKey::Initialized, &true); storage.set(&DataKey::Admin, &admin); @@ -189,6 +199,7 @@ impl AgentPassportValidator { new: admin, }, ); + Ok(()) } /// Internal logic for verifying a single passport proof. @@ -453,7 +464,7 @@ impl AgentPassportValidator { .storage() .instance() .get(&DataKey::PendingAdmin) - .ok_or(Error::NotInitialized)?; + .ok_or(Error::NoPendingAdmin)?; pending_admin.require_auth(); let old_admin: Option
= env.storage().instance().get(&DataKey::Admin); diff --git a/contracts/agent-passport-validator/src/test.rs b/contracts/agent-passport-validator/src/test.rs index fa9f07a..cb2c5e5 100644 --- a/contracts/agent-passport-validator/src/test.rs +++ b/contracts/agent-passport-validator/src/test.rs @@ -287,15 +287,15 @@ fn public_heartbeat_keeps_instance_storage_alive() { } #[test] -#[should_panic] fn init_is_one_shot() { let env = Env::default(); let client = setup(&env, u256(&env, PI_ROOT)); let admin = Address::generate(&env); let verifier_addr = Address::generate(&env); let root = u256(&env, PI_ROOT); - // Second init must panic with AlreadyInitialized. - client.init(&admin, &verifier_addr, &root); + // Second init must return the typed AlreadyInitialized error. + let res = client.try_init(&admin, &verifier_addr, &root); + assert_eq!(res, Err(Ok(Error::AlreadyInitialized))); } #[test] @@ -498,3 +498,118 @@ fn test_audit_logging() { assert_eq!(entry3.root, root); assert_eq!(entry3.success, true); } + +// --------------------------------------------------------------------------- +// NotInitialized error path — every admin/read entrypoint on an uninitialised +// contract must return Error::NotInitialized rather than panicking. +// --------------------------------------------------------------------------- + +/// Helper that registers the validator WASM but does NOT call `init`. +fn uninitialised_client(env: &Env) -> AgentPassportValidatorClient<'static> { + let validator_addr = env.register(AgentPassportValidator, ()); + AgentPassportValidatorClient::new(env, &validator_addr) +} + +#[test] +fn not_initialized_verifier_read() { + let env = Env::default(); + let client = uninitialised_client(&env); + let res = client.try_verifier(); + assert_eq!(res, Err(Ok(Error::NotInitialized))); +} + +#[test] +fn not_initialized_set_verifier() { + let env = Env::default(); + env.mock_all_auths(); + let client = uninitialised_client(&env); + let new_verifier = Address::generate(&env); + let res = client.try_set_verifier(&new_verifier); + assert_eq!(res, Err(Ok(Error::NotInitialized))); +} + +#[test] +fn not_initialized_add_registry_root() { + let env = Env::default(); + env.mock_all_auths(); + let client = uninitialised_client(&env); + let res = client.try_add_registry_root(&U256::from_u32(&env, 1)); + assert_eq!(res, Err(Ok(Error::NotInitialized))); +} + +#[test] +fn not_initialized_remove_registry_root() { + let env = Env::default(); + env.mock_all_auths(); + let client = uninitialised_client(&env); + let res = client.try_remove_registry_root(&U256::from_u32(&env, 1)); + assert_eq!(res, Err(Ok(Error::NotInitialized))); +} + +#[test] +fn not_initialized_transfer_admin() { + let env = Env::default(); + env.mock_all_auths(); + let client = uninitialised_client(&env); + let new_admin = Address::generate(&env); + let res = client.try_transfer_admin(&new_admin); + assert_eq!(res, Err(Ok(Error::NotInitialized))); +} + +#[test] +fn not_initialized_renounce_admin() { + let env = Env::default(); + env.mock_all_auths(); + let client = uninitialised_client(&env); + let res = client.try_renounce_admin(); + assert_eq!(res, Err(Ok(Error::NotInitialized))); +} + +#[test] +fn not_initialized_verify_and_register() { + let env = Env::default(); + let client = uninitialised_client(&env); + let res = + client.try_verify_and_register(&real_proof(&env), &real_public_inputs(&env)); + // The verifier address is absent from storage → NotInitialized. + // (The unknown-root check fires first, but both are typed errors.) + assert!(res.is_err()); +} + +// --------------------------------------------------------------------------- +// NoPendingAdmin — accept_admin when no transfer has been started. +// --------------------------------------------------------------------------- + +#[test] +fn accept_admin_with_no_pending_admin_returns_typed_error() { + let env = Env::default(); + env.mock_all_auths(); + let client = setup(&env, u256(&env, PI_ROOT)); + // No transfer_admin was called, so PendingAdmin is absent. + let res = client.try_accept_admin(); + assert_eq!(res, Err(Ok(Error::NoPendingAdmin))); +} + +// --------------------------------------------------------------------------- +// verify_and_register on an uninitialised contract surfaces NotInitialized +// from the verifier-address lookup (after the root check). +// --------------------------------------------------------------------------- + +#[test] +fn verify_internal_not_initialized_when_verifier_missing() { + let env = Env::default(); + // Register validator but skip init entirely. + let client = uninitialised_client(&env); + // Provide valid-looking public inputs (root check will fire first since + // there's no root either — that's still a typed error, not a panic). + let res = + client.try_verify_and_register(&real_proof(&env), &real_public_inputs(&env)); + assert!( + matches!( + res, + Err(Ok(Error::NotInitialized)) | Err(Ok(Error::UnknownRegistryRoot)) + ), + "expected NotInitialized or UnknownRegistryRoot, got {:?}", + res + ); +} diff --git a/contracts/agent-passport-validator/test_snapshots/test/can_manage_registry_roots.1.json b/contracts/agent-passport-validator/test_snapshots/test/can_manage_registry_roots.1.json index 85244c3..8cd0aee 100644 --- a/contracts/agent-passport-validator/test_snapshots/test/can_manage_registry_roots.1.json +++ b/contracts/agent-passport-validator/test_snapshots/test/can_manage_registry_roots.1.json @@ -292,6 +292,22 @@ "bool": true } }, + { + "key": { + "vec": [ + { + "symbol": "RegistryRoots" + } + ] + }, + "val": { + "vec": [ + { + "u256": "3068829097279014190258251168411223843893512111345273305725860278806736175051" + } + ] + } + }, { "key": { "vec": [ diff --git a/contracts/agent-passport-validator/test_snapshots/test/init_is_one_shot.1.json b/contracts/agent-passport-validator/test_snapshots/test/init_is_one_shot.1.json index 0725e62..2a47ff0 100644 --- a/contracts/agent-passport-validator/test_snapshots/test/init_is_one_shot.1.json +++ b/contracts/agent-passport-validator/test_snapshots/test/init_is_one_shot.1.json @@ -115,6 +115,22 @@ "bool": true } }, + { + "key": { + "vec": [ + { + "symbol": "RegistryRoots" + } + ] + }, + "val": { + "vec": [ + { + "u256": "3068829097279014190258251168411223843893512111345273305725860278806736175050" + } + ] + } + }, { "key": { "vec": [ diff --git a/contracts/agent-passport-validator/test_snapshots/test/mixed_batch_results.1.json b/contracts/agent-passport-validator/test_snapshots/test/mixed_batch_results.1.json index 0aac681..4c729a1 100644 --- a/contracts/agent-passport-validator/test_snapshots/test/mixed_batch_results.1.json +++ b/contracts/agent-passport-validator/test_snapshots/test/mixed_batch_results.1.json @@ -210,6 +210,22 @@ "bool": true } }, + { + "key": { + "vec": [ + { + "symbol": "RegistryRoots" + } + ] + }, + "val": { + "vec": [ + { + "u256": "3068829097279014190258251168411223843893512111345273305725860278806736175050" + } + ] + } + }, { "key": { "vec": [ diff --git a/contracts/agent-passport-validator/test_snapshots/test/registers_a_valid_passport.1.json b/contracts/agent-passport-validator/test_snapshots/test/registers_a_valid_passport.1.json index fda90d2..36bf4da 100644 --- a/contracts/agent-passport-validator/test_snapshots/test/registers_a_valid_passport.1.json +++ b/contracts/agent-passport-validator/test_snapshots/test/registers_a_valid_passport.1.json @@ -214,6 +214,22 @@ "bool": true } }, + { + "key": { + "vec": [ + { + "symbol": "RegistryRoots" + } + ] + }, + "val": { + "vec": [ + { + "u256": "3068829097279014190258251168411223843893512111345273305725860278806736175050" + } + ] + } + }, { "key": { "vec": [ diff --git a/contracts/agent-passport-validator/test_snapshots/test/rejects_batch_too_large.1.json b/contracts/agent-passport-validator/test_snapshots/test/rejects_batch_too_large.1.json index 8154a8d..0c4ce04 100644 --- a/contracts/agent-passport-validator/test_snapshots/test/rejects_batch_too_large.1.json +++ b/contracts/agent-passport-validator/test_snapshots/test/rejects_batch_too_large.1.json @@ -115,6 +115,22 @@ "bool": true } }, + { + "key": { + "vec": [ + { + "symbol": "RegistryRoots" + } + ] + }, + "val": { + "vec": [ + { + "u256": "3068829097279014190258251168411223843893512111345273305725860278806736175050" + } + ] + } + }, { "key": { "vec": [ diff --git a/contracts/agent-passport-validator/test_snapshots/test/rejects_nullifier_replay.1.json b/contracts/agent-passport-validator/test_snapshots/test/rejects_nullifier_replay.1.json index 6ec9701..4613280 100644 --- a/contracts/agent-passport-validator/test_snapshots/test/rejects_nullifier_replay.1.json +++ b/contracts/agent-passport-validator/test_snapshots/test/rejects_nullifier_replay.1.json @@ -211,6 +211,22 @@ "bool": true } }, + { + "key": { + "vec": [ + { + "symbol": "RegistryRoots" + } + ] + }, + "val": { + "vec": [ + { + "u256": "3068829097279014190258251168411223843893512111345273305725860278806736175050" + } + ] + } + }, { "key": { "vec": [ diff --git a/contracts/agent-passport-validator/test_snapshots/test/rejects_tampered_public_input.1.json b/contracts/agent-passport-validator/test_snapshots/test/rejects_tampered_public_input.1.json index 9659a53..9fe10db 100644 --- a/contracts/agent-passport-validator/test_snapshots/test/rejects_tampered_public_input.1.json +++ b/contracts/agent-passport-validator/test_snapshots/test/rejects_tampered_public_input.1.json @@ -116,6 +116,22 @@ "bool": true } }, + { + "key": { + "vec": [ + { + "symbol": "RegistryRoots" + } + ] + }, + "val": { + "vec": [ + { + "u256": "3068829097279014190258251168411223843893512111345273305725860278806736175050" + } + ] + } + }, { "key": { "vec": [ diff --git a/contracts/agent-passport-validator/test_snapshots/test/rejects_unknown_registry_root.1.json b/contracts/agent-passport-validator/test_snapshots/test/rejects_unknown_registry_root.1.json index 193d204..92113d7 100644 --- a/contracts/agent-passport-validator/test_snapshots/test/rejects_unknown_registry_root.1.json +++ b/contracts/agent-passport-validator/test_snapshots/test/rejects_unknown_registry_root.1.json @@ -115,6 +115,22 @@ "bool": true } }, + { + "key": { + "vec": [ + { + "symbol": "RegistryRoots" + } + ] + }, + "val": { + "vec": [ + { + "u256": "3068829097279014190258251168411223843893512111345273305725860278806736175051" + } + ] + } + }, { "key": { "vec": [ diff --git a/contracts/agent-passport-validator/test_snapshots/test/rejects_wrong_input_count.1.json b/contracts/agent-passport-validator/test_snapshots/test/rejects_wrong_input_count.1.json index 8154a8d..0c4ce04 100644 --- a/contracts/agent-passport-validator/test_snapshots/test/rejects_wrong_input_count.1.json +++ b/contracts/agent-passport-validator/test_snapshots/test/rejects_wrong_input_count.1.json @@ -115,6 +115,22 @@ "bool": true } }, + { + "key": { + "vec": [ + { + "symbol": "RegistryRoots" + } + ] + }, + "val": { + "vec": [ + { + "u256": "3068829097279014190258251168411223843893512111345273305725860278806736175050" + } + ] + } + }, { "key": { "vec": [ diff --git a/contracts/agent-passport-validator/test_snapshots/test/verifies_a_batch_of_passports.1.json b/contracts/agent-passport-validator/test_snapshots/test/verifies_a_batch_of_passports.1.json index 0c929e8..cd6b99a 100644 --- a/contracts/agent-passport-validator/test_snapshots/test/verifies_a_batch_of_passports.1.json +++ b/contracts/agent-passport-validator/test_snapshots/test/verifies_a_batch_of_passports.1.json @@ -115,6 +115,22 @@ "bool": true } }, + { + "key": { + "vec": [ + { + "symbol": "RegistryRoots" + } + ] + }, + "val": { + "vec": [ + { + "u256": "3068829097279014190258251168411223843893512111345273305725860278806736175050" + } + ] + } + }, { "key": { "vec": [