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
27 changes: 19 additions & 8 deletions contracts/agent-passport-validator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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);
Expand All @@ -189,6 +199,7 @@ impl AgentPassportValidator {
new: admin,
},
);
Ok(())
}

/// Internal logic for verifying a single passport proof.
Expand Down Expand Up @@ -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<Address> = env.storage().instance().get(&DataKey::Admin);
Expand Down
121 changes: 118 additions & 3 deletions contracts/agent-passport-validator/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,22 @@
"bool": true
}
},
{
"key": {
"vec": [
{
"symbol": "RegistryRoots"
}
]
},
"val": {
"vec": [
{
"u256": "3068829097279014190258251168411223843893512111345273305725860278806736175051"
}
]
}
},
{
"key": {
"vec": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,22 @@
"bool": true
}
},
{
"key": {
"vec": [
{
"symbol": "RegistryRoots"
}
]
},
"val": {
"vec": [
{
"u256": "3068829097279014190258251168411223843893512111345273305725860278806736175050"
}
]
}
},
{
"key": {
"vec": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,22 @@
"bool": true
}
},
{
"key": {
"vec": [
{
"symbol": "RegistryRoots"
}
]
},
"val": {
"vec": [
{
"u256": "3068829097279014190258251168411223843893512111345273305725860278806736175050"
}
]
}
},
{
"key": {
"vec": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,22 @@
"bool": true
}
},
{
"key": {
"vec": [
{
"symbol": "RegistryRoots"
}
]
},
"val": {
"vec": [
{
"u256": "3068829097279014190258251168411223843893512111345273305725860278806736175050"
}
]
}
},
{
"key": {
"vec": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,22 @@
"bool": true
}
},
{
"key": {
"vec": [
{
"symbol": "RegistryRoots"
}
]
},
"val": {
"vec": [
{
"u256": "3068829097279014190258251168411223843893512111345273305725860278806736175050"
}
]
}
},
{
"key": {
"vec": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,22 @@
"bool": true
}
},
{
"key": {
"vec": [
{
"symbol": "RegistryRoots"
}
]
},
"val": {
"vec": [
{
"u256": "3068829097279014190258251168411223843893512111345273305725860278806736175050"
}
]
}
},
{
"key": {
"vec": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,22 @@
"bool": true
}
},
{
"key": {
"vec": [
{
"symbol": "RegistryRoots"
}
]
},
"val": {
"vec": [
{
"u256": "3068829097279014190258251168411223843893512111345273305725860278806736175050"
}
]
}
},
{
"key": {
"vec": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,22 @@
"bool": true
}
},
{
"key": {
"vec": [
{
"symbol": "RegistryRoots"
}
]
},
"val": {
"vec": [
{
"u256": "3068829097279014190258251168411223843893512111345273305725860278806736175051"
}
]
}
},
{
"key": {
"vec": [
Expand Down
Loading
Loading