From 33143be0e79273b5ce77a13400bb8ddf08ca0c2e Mon Sep 17 00:00:00 2001 From: Obiajulu-gif Date: Tue, 24 Mar 2026 10:53:22 +0100 Subject: [PATCH 1/5] Implement submit_proof endpoint --- .../contracts/core_contract/src/errors.rs | 11 ++ .../contracts/core_contract/src/events.rs | 6 + .../contracts/core_contract/src/lib.rs | 90 ++++++---- .../contracts/core_contract/src/storage.rs | 41 +++++ .../contracts/core_contract/src/test.rs | 157 ++++++++++++++++++ .../contracts/core_contract/src/types.rs | 18 +- 6 files changed, 289 insertions(+), 34 deletions(-) create mode 100644 gateway-contract/contracts/core_contract/src/test.rs diff --git a/gateway-contract/contracts/core_contract/src/errors.rs b/gateway-contract/contracts/core_contract/src/errors.rs index e69de29..6156ac6 100644 --- a/gateway-contract/contracts/core_contract/src/errors.rs +++ b/gateway-contract/contracts/core_contract/src/errors.rs @@ -0,0 +1,11 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ContractError { + AlreadyInitialized = 1, + NotInitialized = 2, + RootMismatch = 3, + InvalidProof = 4, + DuplicateCommitment = 5, +} diff --git a/gateway-contract/contracts/core_contract/src/events.rs b/gateway-contract/contracts/core_contract/src/events.rs index e69de29..e087fa2 100644 --- a/gateway-contract/contracts/core_contract/src/events.rs +++ b/gateway-contract/contracts/core_contract/src/events.rs @@ -0,0 +1,6 @@ +use soroban_sdk::{contractevent, BytesN}; + +#[contractevent] +pub struct UsernameRegistered { + pub commitment: BytesN<32>, +} diff --git a/gateway-contract/contracts/core_contract/src/lib.rs b/gateway-contract/contracts/core_contract/src/lib.rs index af52c07..bbea07e 100644 --- a/gateway-contract/contracts/core_contract/src/lib.rs +++ b/gateway-contract/contracts/core_contract/src/lib.rs @@ -1,46 +1,76 @@ #![no_std] -use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, panic_with_error, Address, BytesN, Env, -}; +mod errors; +mod events; +mod storage; +mod types; -#[contract] -pub struct Contract; +#[cfg(test)] +mod test; -#[contracttype] -#[derive(Clone)] -pub enum DataKey { - Resolver(BytesN<32>), -} +use soroban_sdk::{contract, contractclient, contractimpl, panic_with_error, Address, BytesN, Env}; -#[contracttype] -#[derive(Clone)] -pub struct ResolveData { - pub wallet: Address, - pub memo: Option, -} +pub use crate::errors::ContractError; +pub use crate::events::UsernameRegistered; +pub use crate::types::{Proof, PublicSignals}; + +#[contract] +pub struct CoreContract; -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum ResolverError { - NotFound = 1, +#[contractclient(name = "VerifierContractClient")] +pub trait VerifierContract { + fn verify_proof(env: Env, proof: Proof, public_signals: PublicSignals) -> bool; } #[contractimpl] -impl Contract { - pub fn register_resolver(env: Env, commitment: BytesN<32>, wallet: Address, memo: Option) { - let key = DataKey::Resolver(commitment); - let data = ResolveData { wallet, memo }; +impl CoreContract { + pub fn init(env: Env, verifier: Address, root: BytesN<32>) { + if storage::is_initialized(&env) { + panic_with_error!(&env, ContractError::AlreadyInitialized); + } - env.storage().persistent().set(&key, &data); + storage::set_verifier(&env, &verifier); + storage::set_root(&env, &root); } - pub fn resolve(env: Env, commitment: BytesN<32>) -> ResolveData { - let key = DataKey::Resolver(commitment); + pub fn submit_proof(env: Env, proof: Proof, public_signals: PublicSignals) { + let current_root = storage::get_root(&env) + .unwrap_or_else(|| panic_with_error!(&env, ContractError::NotInitialized)); + + if current_root != public_signals.old_root.clone() { + panic_with_error!(&env, ContractError::RootMismatch); + } + + if storage::has_commitment(&env, &public_signals.commitment) { + panic_with_error!(&env, ContractError::DuplicateCommitment); + } - match env.storage().persistent().get::(&key) { - Some(data) => data, - None => panic_with_error!(&env, ResolverError::NotFound), + let verifier = storage::get_verifier(&env) + .unwrap_or_else(|| panic_with_error!(&env, ContractError::NotInitialized)); + let verifier_client = VerifierContractClient::new(&env, &verifier); + let is_valid = verifier_client.verify_proof(&proof, &public_signals); + if !is_valid { + panic_with_error!(&env, ContractError::InvalidProof); } + + storage::store_commitment(&env, &public_signals.commitment); + storage::set_root(&env, &public_signals.new_root); + + UsernameRegistered { + commitment: public_signals.commitment, + } + .publish(&env); + } + + pub fn get_root(env: Env) -> Option> { + storage::get_root(&env) + } + + pub fn get_verifier(env: Env) -> Option
{ + storage::get_verifier(&env) + } + + pub fn has_commitment(env: Env, commitment: BytesN<32>) -> bool { + storage::has_commitment(&env, &commitment) } } diff --git a/gateway-contract/contracts/core_contract/src/storage.rs b/gateway-contract/contracts/core_contract/src/storage.rs index e69de29..15dd62c 100644 --- a/gateway-contract/contracts/core_contract/src/storage.rs +++ b/gateway-contract/contracts/core_contract/src/storage.rs @@ -0,0 +1,41 @@ +use soroban_sdk::{contracttype, Address, BytesN, Env}; + +#[contracttype] +#[derive(Clone)] +pub enum DataKey { + Root, + Verifier, + Commitment(BytesN<32>), +} + +pub fn is_initialized(env: &Env) -> bool { + env.storage().instance().has(&DataKey::Root) && env.storage().instance().has(&DataKey::Verifier) +} + +pub fn get_root(env: &Env) -> Option> { + env.storage().instance().get(&DataKey::Root) +} + +pub fn set_root(env: &Env, root: &BytesN<32>) { + env.storage().instance().set(&DataKey::Root, root); +} + +pub fn get_verifier(env: &Env) -> Option
{ + env.storage().instance().get(&DataKey::Verifier) +} + +pub fn set_verifier(env: &Env, verifier: &Address) { + env.storage().instance().set(&DataKey::Verifier, verifier); +} + +pub fn has_commitment(env: &Env, commitment: &BytesN<32>) -> bool { + env.storage() + .persistent() + .has(&DataKey::Commitment(commitment.clone())) +} + +pub fn store_commitment(env: &Env, commitment: &BytesN<32>) { + env.storage() + .persistent() + .set(&DataKey::Commitment(commitment.clone()), &true); +} diff --git a/gateway-contract/contracts/core_contract/src/test.rs b/gateway-contract/contracts/core_contract/src/test.rs new file mode 100644 index 0000000..7adc7f5 --- /dev/null +++ b/gateway-contract/contracts/core_contract/src/test.rs @@ -0,0 +1,157 @@ +#![cfg(test)] + +use crate::errors::ContractError; +use crate::types::{Proof, PublicSignals}; +use crate::{CoreContract, CoreContractClient}; +use soroban_sdk::testutils::Events as _; +use soroban_sdk::{ + contract, contractimpl, contracttype, map, Address, BytesN, Env, Error, IntoVal, Map, Symbol, + Val, +}; + +#[contract] +struct MockVerifierContract; + +#[contracttype] +#[derive(Clone)] +enum MockVerifierDataKey { + ShouldVerify, +} + +#[contractimpl] +impl MockVerifierContract { + pub fn set_should_verify(env: Env, should_verify: bool) { + env.storage() + .instance() + .set(&MockVerifierDataKey::ShouldVerify, &should_verify); + } + + pub fn verify_proof(env: Env, proof: Proof, public_signals: PublicSignals) -> bool { + let should_verify = env + .storage() + .instance() + .get::(&MockVerifierDataKey::ShouldVerify) + .unwrap_or(true); + + should_verify + && proof.a == public_signals.old_root + && proof.b == public_signals.new_root + && proof.c == public_signals.commitment + } +} + +fn bytes(env: &Env, byte: u8) -> BytesN<32> { + BytesN::from_array(env, &[byte; 32]) +} + +fn offchain_register_fixture(env: &Env) -> (Proof, PublicSignals) { + let old_root = bytes(env, 0); + let new_root = bytes(env, 42); + let commitment = bytes(env, 7); + + ( + Proof { + a: old_root.clone(), + b: new_root.clone(), + c: commitment.clone(), + }, + PublicSignals { + old_root, + new_root, + commitment, + }, + ) +} + +fn setup(env: &Env) -> (Address, CoreContractClient<'_>, Address) { + let verifier_id = env.register(MockVerifierContract, ()); + let verifier_client = MockVerifierContractClient::new(env, &verifier_id); + verifier_client.set_should_verify(&true); + + let contract_id = env.register(CoreContract, ()); + let client = CoreContractClient::new(env, &contract_id); + client.init(&verifier_id, &bytes(env, 0)); + + (contract_id, client, verifier_id) +} + +fn assert_submit_error( + result: Result, Result>, + expected: ContractError, +) { + assert_eq!(result, Err(Ok(expected.into()))); +} + +#[test] +fn submit_proof_succeeds_and_updates_state() { + let env = Env::default(); + let (contract_id, client, _) = setup(&env); + let (proof, public_signals) = offchain_register_fixture(&env); + + client.submit_proof(&proof, &public_signals); + + assert_eq!(client.get_root(), Some(public_signals.new_root.clone())); + assert!(client.has_commitment(&public_signals.commitment)); + + let expected_event_data: Map = map![ + &env, + ( + Symbol::new(&env, "commitment"), + public_signals.commitment.clone().into_val(&env) + ) + ]; + assert_eq!( + env.events().all(), + soroban_sdk::vec![ + &env, + ( + contract_id, + (Symbol::new(&env, "username_registered"),).into_val(&env), + expected_event_data.into_val(&env), + ) + ] + ); +} + +#[test] +fn invalid_proof_is_rejected() { + let env = Env::default(); + let (_, client, verifier_id) = setup(&env); + let verifier_client = MockVerifierContractClient::new(&env, &verifier_id); + verifier_client.set_should_verify(&false); + + let (proof, public_signals) = offchain_register_fixture(&env); + let result = client.try_submit_proof(&proof, &public_signals); + + assert_submit_error(result, ContractError::InvalidProof); + assert!(!client.has_commitment(&public_signals.commitment)); + assert_eq!(client.get_root(), Some(public_signals.old_root)); +} + +#[test] +fn stale_root_is_rejected() { + let env = Env::default(); + let (_, client, _) = setup(&env); + let (proof, mut public_signals) = offchain_register_fixture(&env); + public_signals.old_root = bytes(&env, 1); + + let result = client.try_submit_proof(&proof, &public_signals); + + assert_submit_error(result, ContractError::RootMismatch); + assert!(!client.has_commitment(&public_signals.commitment)); + assert_eq!(client.get_root(), Some(bytes(&env, 0))); +} + +#[test] +fn duplicate_commitment_is_rejected() { + let env = Env::default(); + let (_, client, _) = setup(&env); + let (proof, public_signals) = offchain_register_fixture(&env); + + client.submit_proof(&proof, &public_signals); + + let duplicate_result = client.try_submit_proof(&proof, &public_signals); + + assert_submit_error(duplicate_result, ContractError::DuplicateCommitment); + assert_eq!(client.get_root(), Some(public_signals.new_root)); +} diff --git a/gateway-contract/contracts/core_contract/src/types.rs b/gateway-contract/contracts/core_contract/src/types.rs index 31e36bc..0b111c6 100644 --- a/gateway-contract/contracts/core_contract/src/types.rs +++ b/gateway-contract/contracts/core_contract/src/types.rs @@ -1,7 +1,17 @@ -use soroban_sdk::{contracttype, Symbol}; +use soroban_sdk::{contracttype, BytesN}; #[contracttype] -#[derive(Clone)] -pub struct AddressMetadata { - pub label: Symbol, +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Proof { + pub a: BytesN<32>, + pub b: BytesN<32>, + pub c: BytesN<32>, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PublicSignals { + pub old_root: BytesN<32>, + pub new_root: BytesN<32>, + pub commitment: BytesN<32>, } From 790a222dee9a5b0f7814edadc39cd98d280d35cc Mon Sep 17 00:00:00 2001 From: Obiajulu-gif Date: Tue, 24 Mar 2026 15:36:45 +0100 Subject: [PATCH 2/5] Add on-chain merkle root anchoring --- .../contracts/core_contract/src/events.rs | 6 + .../contracts/core_contract/src/lib.rs | 24 ++-- .../contracts/core_contract/src/storage.rs | 15 ++- .../contracts/core_contract/src/test.rs | 107 +++++++++++++++--- 4 files changed, 122 insertions(+), 30 deletions(-) diff --git a/gateway-contract/contracts/core_contract/src/events.rs b/gateway-contract/contracts/core_contract/src/events.rs index e087fa2..9e79fd0 100644 --- a/gateway-contract/contracts/core_contract/src/events.rs +++ b/gateway-contract/contracts/core_contract/src/events.rs @@ -4,3 +4,9 @@ use soroban_sdk::{contractevent, BytesN}; pub struct UsernameRegistered { pub commitment: BytesN<32>, } + +#[contractevent] +pub struct MerkleRootUpdated { + pub old_root: BytesN<32>, + pub new_root: BytesN<32>, +} diff --git a/gateway-contract/contracts/core_contract/src/lib.rs b/gateway-contract/contracts/core_contract/src/lib.rs index bbea07e..7bda19c 100644 --- a/gateway-contract/contracts/core_contract/src/lib.rs +++ b/gateway-contract/contracts/core_contract/src/lib.rs @@ -11,7 +11,7 @@ mod test; use soroban_sdk::{contract, contractclient, contractimpl, panic_with_error, Address, BytesN, Env}; pub use crate::errors::ContractError; -pub use crate::events::UsernameRegistered; +pub use crate::events::{MerkleRootUpdated, UsernameRegistered}; pub use crate::types::{Proof, PublicSignals}; #[contract] @@ -22,6 +22,17 @@ pub trait VerifierContract { fn verify_proof(env: Env, proof: Proof, public_signals: PublicSignals) -> bool; } +fn current_merkle_root(env: &Env) -> BytesN<32> { + storage::get_merkle_root(env) + .unwrap_or_else(|| panic_with_error!(env, ContractError::NotInitialized)) +} + +fn update_merkle_root(env: &Env, old_root: BytesN<32>, new_root: BytesN<32>) { + storage::set_merkle_root(env, &new_root); + + MerkleRootUpdated { old_root, new_root }.publish(env); +} + #[contractimpl] impl CoreContract { pub fn init(env: Env, verifier: Address, root: BytesN<32>) { @@ -30,12 +41,11 @@ impl CoreContract { } storage::set_verifier(&env, &verifier); - storage::set_root(&env, &root); + storage::set_merkle_root(&env, &root); } pub fn submit_proof(env: Env, proof: Proof, public_signals: PublicSignals) { - let current_root = storage::get_root(&env) - .unwrap_or_else(|| panic_with_error!(&env, ContractError::NotInitialized)); + let current_root = current_merkle_root(&env); if current_root != public_signals.old_root.clone() { panic_with_error!(&env, ContractError::RootMismatch); @@ -54,7 +64,7 @@ impl CoreContract { } storage::store_commitment(&env, &public_signals.commitment); - storage::set_root(&env, &public_signals.new_root); + update_merkle_root(&env, current_root, public_signals.new_root.clone()); UsernameRegistered { commitment: public_signals.commitment, @@ -62,8 +72,8 @@ impl CoreContract { .publish(&env); } - pub fn get_root(env: Env) -> Option> { - storage::get_root(&env) + pub fn get_merkle_root(env: Env) -> BytesN<32> { + current_merkle_root(&env) } pub fn get_verifier(env: Env) -> Option
{ diff --git a/gateway-contract/contracts/core_contract/src/storage.rs b/gateway-contract/contracts/core_contract/src/storage.rs index 15dd62c..d721ecb 100644 --- a/gateway-contract/contracts/core_contract/src/storage.rs +++ b/gateway-contract/contracts/core_contract/src/storage.rs @@ -3,21 +3,24 @@ use soroban_sdk::{contracttype, Address, BytesN, Env}; #[contracttype] #[derive(Clone)] pub enum DataKey { - Root, + CurrentMerkleRoot, Verifier, Commitment(BytesN<32>), } pub fn is_initialized(env: &Env) -> bool { - env.storage().instance().has(&DataKey::Root) && env.storage().instance().has(&DataKey::Verifier) + env.storage().persistent().has(&DataKey::CurrentMerkleRoot) + && env.storage().instance().has(&DataKey::Verifier) } -pub fn get_root(env: &Env) -> Option> { - env.storage().instance().get(&DataKey::Root) +pub fn get_merkle_root(env: &Env) -> Option> { + env.storage().persistent().get(&DataKey::CurrentMerkleRoot) } -pub fn set_root(env: &Env, root: &BytesN<32>) { - env.storage().instance().set(&DataKey::Root, root); +pub fn set_merkle_root(env: &Env, root: &BytesN<32>) { + env.storage() + .persistent() + .set(&DataKey::CurrentMerkleRoot, root); } pub fn get_verifier(env: &Env) -> Option
{ diff --git a/gateway-contract/contracts/core_contract/src/test.rs b/gateway-contract/contracts/core_contract/src/test.rs index 7adc7f5..7309abf 100644 --- a/gateway-contract/contracts/core_contract/src/test.rs +++ b/gateway-contract/contracts/core_contract/src/test.rs @@ -5,8 +5,8 @@ use crate::types::{Proof, PublicSignals}; use crate::{CoreContract, CoreContractClient}; use soroban_sdk::testutils::Events as _; use soroban_sdk::{ - contract, contractimpl, contracttype, map, Address, BytesN, Env, Error, IntoVal, Map, Symbol, - Val, + contract, contractimpl, contracttype, map, vec, Address, BytesN, Env, Error, IntoVal, + InvokeError, Map, Symbol, Val, }; #[contract] @@ -44,10 +44,15 @@ fn bytes(env: &Env, byte: u8) -> BytesN<32> { BytesN::from_array(env, &[byte; 32]) } -fn offchain_register_fixture(env: &Env) -> (Proof, PublicSignals) { - let old_root = bytes(env, 0); - let new_root = bytes(env, 42); - let commitment = bytes(env, 7); +fn registration_fixture( + env: &Env, + old_byte: u8, + new_byte: u8, + commitment_byte: u8, +) -> (Proof, PublicSignals) { + let old_root = bytes(env, old_byte); + let new_root = bytes(env, new_byte); + let commitment = bytes(env, commitment_byte); ( Proof { @@ -76,24 +81,46 @@ fn setup(env: &Env) -> (Address, CoreContractClient<'_>, Address) { } fn assert_submit_error( - result: Result, Result>, + result: Result< + Result<(), soroban_sdk::ConversionError>, + Result, + >, expected: ContractError, ) { assert_eq!(result, Err(Ok(expected.into()))); } +#[test] +fn init_sets_the_current_merkle_root() { + let env = Env::default(); + let (_, client, _) = setup(&env); + + assert_eq!(client.get_merkle_root(), bytes(&env, 0)); +} + #[test] fn submit_proof_succeeds_and_updates_state() { let env = Env::default(); let (contract_id, client, _) = setup(&env); - let (proof, public_signals) = offchain_register_fixture(&env); + let (proof, public_signals) = registration_fixture(&env, 0, 42, 7); client.submit_proof(&proof, &public_signals); - assert_eq!(client.get_root(), Some(public_signals.new_root.clone())); + assert_eq!(client.get_merkle_root(), public_signals.new_root.clone()); assert!(client.has_commitment(&public_signals.commitment)); - let expected_event_data: Map = map![ + let expected_root_event_data: Map = map![ + &env, + ( + Symbol::new(&env, "old_root"), + public_signals.old_root.clone().into_val(&env) + ), + ( + Symbol::new(&env, "new_root"), + public_signals.new_root.clone().into_val(&env) + ) + ]; + let expected_registration_event_data: Map = map![ &env, ( Symbol::new(&env, "commitment"), @@ -104,10 +131,15 @@ fn submit_proof_succeeds_and_updates_state() { env.events().all(), soroban_sdk::vec![ &env, + ( + contract_id.clone(), + (Symbol::new(&env, "merkle_root_updated"),).into_val(&env), + expected_root_event_data.into_val(&env), + ), ( contract_id, (Symbol::new(&env, "username_registered"),).into_val(&env), - expected_event_data.into_val(&env), + expected_registration_event_data.into_val(&env), ) ] ); @@ -120,38 +152,79 @@ fn invalid_proof_is_rejected() { let verifier_client = MockVerifierContractClient::new(&env, &verifier_id); verifier_client.set_should_verify(&false); - let (proof, public_signals) = offchain_register_fixture(&env); + let (proof, public_signals) = registration_fixture(&env, 0, 42, 7); let result = client.try_submit_proof(&proof, &public_signals); assert_submit_error(result, ContractError::InvalidProof); assert!(!client.has_commitment(&public_signals.commitment)); - assert_eq!(client.get_root(), Some(public_signals.old_root)); + assert_eq!(client.get_merkle_root(), public_signals.old_root); } #[test] fn stale_root_is_rejected() { let env = Env::default(); let (_, client, _) = setup(&env); - let (proof, mut public_signals) = offchain_register_fixture(&env); + let (proof, mut public_signals) = registration_fixture(&env, 0, 42, 7); public_signals.old_root = bytes(&env, 1); let result = client.try_submit_proof(&proof, &public_signals); assert_submit_error(result, ContractError::RootMismatch); assert!(!client.has_commitment(&public_signals.commitment)); - assert_eq!(client.get_root(), Some(bytes(&env, 0))); + assert_eq!(client.get_merkle_root(), bytes(&env, 0)); } #[test] fn duplicate_commitment_is_rejected() { let env = Env::default(); let (_, client, _) = setup(&env); - let (proof, public_signals) = offchain_register_fixture(&env); + let (proof, public_signals) = registration_fixture(&env, 0, 42, 7); client.submit_proof(&proof, &public_signals); let duplicate_result = client.try_submit_proof(&proof, &public_signals); assert_submit_error(duplicate_result, ContractError::DuplicateCommitment); - assert_eq!(client.get_root(), Some(public_signals.new_root)); + assert_eq!(client.get_merkle_root(), public_signals.new_root); +} + +#[test] +fn root_progresses_across_multiple_registrations() { + let env = Env::default(); + let (_, client, _) = setup(&env); + let (first_proof, first_public_signals) = registration_fixture(&env, 0, 42, 7); + let (second_proof, second_public_signals) = registration_fixture(&env, 42, 99, 8); + + client.submit_proof(&first_proof, &first_public_signals); + client.submit_proof(&second_proof, &second_public_signals); + + assert_eq!(client.get_merkle_root(), second_public_signals.new_root); + assert!(client.has_commitment(&first_public_signals.commitment)); + assert!(client.has_commitment(&second_public_signals.commitment)); +} + +#[test] +fn root_cannot_be_overridden_by_reinitializing() { + let env = Env::default(); + let (_, client, verifier_id) = setup(&env); + + let result = client.try_init(&verifier_id, &bytes(&env, 9)); + + assert_eq!(result, Err(Ok(ContractError::AlreadyInitialized.into()))); + assert_eq!(client.get_merkle_root(), bytes(&env, 0)); +} + +#[test] +fn direct_root_override_entrypoint_is_rejected() { + let env = Env::default(); + let (contract_id, client, _) = setup(&env); + + let result = env.try_invoke_contract::<(), InvokeError>( + &contract_id, + &Symbol::new(&env, "set_merkle_root"), + vec![&env, bytes(&env, 9).into_val(&env)], + ); + + assert_eq!(result, Err(Ok(InvokeError::Abort))); + assert_eq!(client.get_merkle_root(), bytes(&env, 0)); } From 158302185107a85e1e3d86c2c2c959b824d6af4a Mon Sep 17 00:00:00 2001 From: Obiajulu-gif Date: Tue, 24 Mar 2026 17:43:59 +0100 Subject: [PATCH 3/5] feat(contract): add merkle root anchoring --- .../contracts/core_contract/src/errors.rs | 11 + .../contracts/core_contract/src/events.rs | 27 ++ .../contracts/core_contract/src/lib.rs | 94 +++++-- .../contracts/core_contract/src/storage.rs | 44 ++++ .../contracts/core_contract/src/test.rs | 230 ++++++++++++++++++ .../contracts/core_contract/src/types.rs | 18 +- 6 files changed, 393 insertions(+), 31 deletions(-) create mode 100644 gateway-contract/contracts/core_contract/src/test.rs diff --git a/gateway-contract/contracts/core_contract/src/errors.rs b/gateway-contract/contracts/core_contract/src/errors.rs index e69de29..6156ac6 100644 --- a/gateway-contract/contracts/core_contract/src/errors.rs +++ b/gateway-contract/contracts/core_contract/src/errors.rs @@ -0,0 +1,11 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ContractError { + AlreadyInitialized = 1, + NotInitialized = 2, + RootMismatch = 3, + InvalidProof = 4, + DuplicateCommitment = 5, +} diff --git a/gateway-contract/contracts/core_contract/src/events.rs b/gateway-contract/contracts/core_contract/src/events.rs index e69de29..cce5167 100644 --- a/gateway-contract/contracts/core_contract/src/events.rs +++ b/gateway-contract/contracts/core_contract/src/events.rs @@ -0,0 +1,27 @@ +use soroban_sdk::{contractevent, BytesN}; + +#[contractevent] +pub struct UsernameRegistered { + pub commitment: BytesN<32>, +} + +#[contractevent] +pub struct MerkleRootUpdated { + pub old_root: BytesN<32>, + pub new_root: BytesN<32>, +} + +use soroban_sdk::{symbol_short, Symbol}; + +pub const INIT_EVENT: Symbol = symbol_short!("INIT"); +pub const TRANSFER_EVENT: Symbol = symbol_short!("TRANSFER"); +pub const REGISTER_EVENT: Symbol = symbol_short!("REGISTER"); +pub const ROOT_UPDATED: Symbol = symbol_short!("ROOT_UPD"); +pub const MASTER_SET: Symbol = symbol_short!("MSTR_SET"); +pub const ADDR_ADDED: Symbol = symbol_short!("ADDR_ADD"); +pub const CHAIN_ADD: Symbol = symbol_short!("CHAIN_ADD"); +pub const CHAIN_REM: Symbol = symbol_short!("CHAIN_REM"); +pub const VAULT_CREATE: Symbol = symbol_short!("VAULT_CRT"); +pub const DEPOSIT: Symbol = symbol_short!("DEPOSIT"); +pub const WITHDRAW: Symbol = symbol_short!("WITHDRAW"); +pub const SCHED_PAY: Symbol = symbol_short!("SCHED_PAY"); diff --git a/gateway-contract/contracts/core_contract/src/lib.rs b/gateway-contract/contracts/core_contract/src/lib.rs index af52c07..7bda19c 100644 --- a/gateway-contract/contracts/core_contract/src/lib.rs +++ b/gateway-contract/contracts/core_contract/src/lib.rs @@ -1,46 +1,86 @@ #![no_std] -use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, panic_with_error, Address, BytesN, Env, -}; +mod errors; +mod events; +mod storage; +mod types; + +#[cfg(test)] +mod test; + +use soroban_sdk::{contract, contractclient, contractimpl, panic_with_error, Address, BytesN, Env}; + +pub use crate::errors::ContractError; +pub use crate::events::{MerkleRootUpdated, UsernameRegistered}; +pub use crate::types::{Proof, PublicSignals}; #[contract] -pub struct Contract; +pub struct CoreContract; -#[contracttype] -#[derive(Clone)] -pub enum DataKey { - Resolver(BytesN<32>), +#[contractclient(name = "VerifierContractClient")] +pub trait VerifierContract { + fn verify_proof(env: Env, proof: Proof, public_signals: PublicSignals) -> bool; } -#[contracttype] -#[derive(Clone)] -pub struct ResolveData { - pub wallet: Address, - pub memo: Option, +fn current_merkle_root(env: &Env) -> BytesN<32> { + storage::get_merkle_root(env) + .unwrap_or_else(|| panic_with_error!(env, ContractError::NotInitialized)) } -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum ResolverError { - NotFound = 1, +fn update_merkle_root(env: &Env, old_root: BytesN<32>, new_root: BytesN<32>) { + storage::set_merkle_root(env, &new_root); + + MerkleRootUpdated { old_root, new_root }.publish(env); } #[contractimpl] -impl Contract { - pub fn register_resolver(env: Env, commitment: BytesN<32>, wallet: Address, memo: Option) { - let key = DataKey::Resolver(commitment); - let data = ResolveData { wallet, memo }; +impl CoreContract { + pub fn init(env: Env, verifier: Address, root: BytesN<32>) { + if storage::is_initialized(&env) { + panic_with_error!(&env, ContractError::AlreadyInitialized); + } - env.storage().persistent().set(&key, &data); + storage::set_verifier(&env, &verifier); + storage::set_merkle_root(&env, &root); } - pub fn resolve(env: Env, commitment: BytesN<32>) -> ResolveData { - let key = DataKey::Resolver(commitment); + pub fn submit_proof(env: Env, proof: Proof, public_signals: PublicSignals) { + let current_root = current_merkle_root(&env); + + if current_root != public_signals.old_root.clone() { + panic_with_error!(&env, ContractError::RootMismatch); + } + + if storage::has_commitment(&env, &public_signals.commitment) { + panic_with_error!(&env, ContractError::DuplicateCommitment); + } + + let verifier = storage::get_verifier(&env) + .unwrap_or_else(|| panic_with_error!(&env, ContractError::NotInitialized)); + let verifier_client = VerifierContractClient::new(&env, &verifier); + let is_valid = verifier_client.verify_proof(&proof, &public_signals); + if !is_valid { + panic_with_error!(&env, ContractError::InvalidProof); + } + + storage::store_commitment(&env, &public_signals.commitment); + update_merkle_root(&env, current_root, public_signals.new_root.clone()); - match env.storage().persistent().get::(&key) { - Some(data) => data, - None => panic_with_error!(&env, ResolverError::NotFound), + UsernameRegistered { + commitment: public_signals.commitment, } + .publish(&env); + } + + pub fn get_merkle_root(env: Env) -> BytesN<32> { + current_merkle_root(&env) + } + + pub fn get_verifier(env: Env) -> Option
{ + storage::get_verifier(&env) + } + + pub fn has_commitment(env: Env, commitment: BytesN<32>) -> bool { + storage::has_commitment(&env, &commitment) } } diff --git a/gateway-contract/contracts/core_contract/src/storage.rs b/gateway-contract/contracts/core_contract/src/storage.rs index e69de29..d721ecb 100644 --- a/gateway-contract/contracts/core_contract/src/storage.rs +++ b/gateway-contract/contracts/core_contract/src/storage.rs @@ -0,0 +1,44 @@ +use soroban_sdk::{contracttype, Address, BytesN, Env}; + +#[contracttype] +#[derive(Clone)] +pub enum DataKey { + CurrentMerkleRoot, + Verifier, + Commitment(BytesN<32>), +} + +pub fn is_initialized(env: &Env) -> bool { + env.storage().persistent().has(&DataKey::CurrentMerkleRoot) + && env.storage().instance().has(&DataKey::Verifier) +} + +pub fn get_merkle_root(env: &Env) -> Option> { + env.storage().persistent().get(&DataKey::CurrentMerkleRoot) +} + +pub fn set_merkle_root(env: &Env, root: &BytesN<32>) { + env.storage() + .persistent() + .set(&DataKey::CurrentMerkleRoot, root); +} + +pub fn get_verifier(env: &Env) -> Option
{ + env.storage().instance().get(&DataKey::Verifier) +} + +pub fn set_verifier(env: &Env, verifier: &Address) { + env.storage().instance().set(&DataKey::Verifier, verifier); +} + +pub fn has_commitment(env: &Env, commitment: &BytesN<32>) -> bool { + env.storage() + .persistent() + .has(&DataKey::Commitment(commitment.clone())) +} + +pub fn store_commitment(env: &Env, commitment: &BytesN<32>) { + env.storage() + .persistent() + .set(&DataKey::Commitment(commitment.clone()), &true); +} diff --git a/gateway-contract/contracts/core_contract/src/test.rs b/gateway-contract/contracts/core_contract/src/test.rs new file mode 100644 index 0000000..7309abf --- /dev/null +++ b/gateway-contract/contracts/core_contract/src/test.rs @@ -0,0 +1,230 @@ +#![cfg(test)] + +use crate::errors::ContractError; +use crate::types::{Proof, PublicSignals}; +use crate::{CoreContract, CoreContractClient}; +use soroban_sdk::testutils::Events as _; +use soroban_sdk::{ + contract, contractimpl, contracttype, map, vec, Address, BytesN, Env, Error, IntoVal, + InvokeError, Map, Symbol, Val, +}; + +#[contract] +struct MockVerifierContract; + +#[contracttype] +#[derive(Clone)] +enum MockVerifierDataKey { + ShouldVerify, +} + +#[contractimpl] +impl MockVerifierContract { + pub fn set_should_verify(env: Env, should_verify: bool) { + env.storage() + .instance() + .set(&MockVerifierDataKey::ShouldVerify, &should_verify); + } + + pub fn verify_proof(env: Env, proof: Proof, public_signals: PublicSignals) -> bool { + let should_verify = env + .storage() + .instance() + .get::(&MockVerifierDataKey::ShouldVerify) + .unwrap_or(true); + + should_verify + && proof.a == public_signals.old_root + && proof.b == public_signals.new_root + && proof.c == public_signals.commitment + } +} + +fn bytes(env: &Env, byte: u8) -> BytesN<32> { + BytesN::from_array(env, &[byte; 32]) +} + +fn registration_fixture( + env: &Env, + old_byte: u8, + new_byte: u8, + commitment_byte: u8, +) -> (Proof, PublicSignals) { + let old_root = bytes(env, old_byte); + let new_root = bytes(env, new_byte); + let commitment = bytes(env, commitment_byte); + + ( + Proof { + a: old_root.clone(), + b: new_root.clone(), + c: commitment.clone(), + }, + PublicSignals { + old_root, + new_root, + commitment, + }, + ) +} + +fn setup(env: &Env) -> (Address, CoreContractClient<'_>, Address) { + let verifier_id = env.register(MockVerifierContract, ()); + let verifier_client = MockVerifierContractClient::new(env, &verifier_id); + verifier_client.set_should_verify(&true); + + let contract_id = env.register(CoreContract, ()); + let client = CoreContractClient::new(env, &contract_id); + client.init(&verifier_id, &bytes(env, 0)); + + (contract_id, client, verifier_id) +} + +fn assert_submit_error( + result: Result< + Result<(), soroban_sdk::ConversionError>, + Result, + >, + expected: ContractError, +) { + assert_eq!(result, Err(Ok(expected.into()))); +} + +#[test] +fn init_sets_the_current_merkle_root() { + let env = Env::default(); + let (_, client, _) = setup(&env); + + assert_eq!(client.get_merkle_root(), bytes(&env, 0)); +} + +#[test] +fn submit_proof_succeeds_and_updates_state() { + let env = Env::default(); + let (contract_id, client, _) = setup(&env); + let (proof, public_signals) = registration_fixture(&env, 0, 42, 7); + + client.submit_proof(&proof, &public_signals); + + assert_eq!(client.get_merkle_root(), public_signals.new_root.clone()); + assert!(client.has_commitment(&public_signals.commitment)); + + let expected_root_event_data: Map = map![ + &env, + ( + Symbol::new(&env, "old_root"), + public_signals.old_root.clone().into_val(&env) + ), + ( + Symbol::new(&env, "new_root"), + public_signals.new_root.clone().into_val(&env) + ) + ]; + let expected_registration_event_data: Map = map![ + &env, + ( + Symbol::new(&env, "commitment"), + public_signals.commitment.clone().into_val(&env) + ) + ]; + assert_eq!( + env.events().all(), + soroban_sdk::vec![ + &env, + ( + contract_id.clone(), + (Symbol::new(&env, "merkle_root_updated"),).into_val(&env), + expected_root_event_data.into_val(&env), + ), + ( + contract_id, + (Symbol::new(&env, "username_registered"),).into_val(&env), + expected_registration_event_data.into_val(&env), + ) + ] + ); +} + +#[test] +fn invalid_proof_is_rejected() { + let env = Env::default(); + let (_, client, verifier_id) = setup(&env); + let verifier_client = MockVerifierContractClient::new(&env, &verifier_id); + verifier_client.set_should_verify(&false); + + let (proof, public_signals) = registration_fixture(&env, 0, 42, 7); + let result = client.try_submit_proof(&proof, &public_signals); + + assert_submit_error(result, ContractError::InvalidProof); + assert!(!client.has_commitment(&public_signals.commitment)); + assert_eq!(client.get_merkle_root(), public_signals.old_root); +} + +#[test] +fn stale_root_is_rejected() { + let env = Env::default(); + let (_, client, _) = setup(&env); + let (proof, mut public_signals) = registration_fixture(&env, 0, 42, 7); + public_signals.old_root = bytes(&env, 1); + + let result = client.try_submit_proof(&proof, &public_signals); + + assert_submit_error(result, ContractError::RootMismatch); + assert!(!client.has_commitment(&public_signals.commitment)); + assert_eq!(client.get_merkle_root(), bytes(&env, 0)); +} + +#[test] +fn duplicate_commitment_is_rejected() { + let env = Env::default(); + let (_, client, _) = setup(&env); + let (proof, public_signals) = registration_fixture(&env, 0, 42, 7); + + client.submit_proof(&proof, &public_signals); + + let duplicate_result = client.try_submit_proof(&proof, &public_signals); + + assert_submit_error(duplicate_result, ContractError::DuplicateCommitment); + assert_eq!(client.get_merkle_root(), public_signals.new_root); +} + +#[test] +fn root_progresses_across_multiple_registrations() { + let env = Env::default(); + let (_, client, _) = setup(&env); + let (first_proof, first_public_signals) = registration_fixture(&env, 0, 42, 7); + let (second_proof, second_public_signals) = registration_fixture(&env, 42, 99, 8); + + client.submit_proof(&first_proof, &first_public_signals); + client.submit_proof(&second_proof, &second_public_signals); + + assert_eq!(client.get_merkle_root(), second_public_signals.new_root); + assert!(client.has_commitment(&first_public_signals.commitment)); + assert!(client.has_commitment(&second_public_signals.commitment)); +} + +#[test] +fn root_cannot_be_overridden_by_reinitializing() { + let env = Env::default(); + let (_, client, verifier_id) = setup(&env); + + let result = client.try_init(&verifier_id, &bytes(&env, 9)); + + assert_eq!(result, Err(Ok(ContractError::AlreadyInitialized.into()))); + assert_eq!(client.get_merkle_root(), bytes(&env, 0)); +} + +#[test] +fn direct_root_override_entrypoint_is_rejected() { + let env = Env::default(); + let (contract_id, client, _) = setup(&env); + + let result = env.try_invoke_contract::<(), InvokeError>( + &contract_id, + &Symbol::new(&env, "set_merkle_root"), + vec![&env, bytes(&env, 9).into_val(&env)], + ); + + assert_eq!(result, Err(Ok(InvokeError::Abort))); + assert_eq!(client.get_merkle_root(), bytes(&env, 0)); +} diff --git a/gateway-contract/contracts/core_contract/src/types.rs b/gateway-contract/contracts/core_contract/src/types.rs index 31e36bc..0b111c6 100644 --- a/gateway-contract/contracts/core_contract/src/types.rs +++ b/gateway-contract/contracts/core_contract/src/types.rs @@ -1,7 +1,17 @@ -use soroban_sdk::{contracttype, Symbol}; +use soroban_sdk::{contracttype, BytesN}; #[contracttype] -#[derive(Clone)] -pub struct AddressMetadata { - pub label: Symbol, +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Proof { + pub a: BytesN<32>, + pub b: BytesN<32>, + pub c: BytesN<32>, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PublicSignals { + pub old_root: BytesN<32>, + pub new_root: BytesN<32>, + pub commitment: BytesN<32>, } From c76b44ea2c9238ce4b8f71f34c049f15bc9e8f56 Mon Sep 17 00:00:00 2001 From: Obiajulu-gif Date: Tue, 24 Mar 2026 17:57:46 +0100 Subject: [PATCH 4/5] Fix core contract CI compile errors --- .../contracts/core_contract/src/events.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/gateway-contract/contracts/core_contract/src/events.rs b/gateway-contract/contracts/core_contract/src/events.rs index 344912d..0792f17 100644 --- a/gateway-contract/contracts/core_contract/src/events.rs +++ b/gateway-contract/contracts/core_contract/src/events.rs @@ -10,19 +10,30 @@ pub struct MerkleRootUpdated { pub old_root: BytesN<32>, pub new_root: BytesN<32>, } -#![allow(dead_code)] use soroban_sdk::{symbol_short, Symbol}; +#[allow(dead_code)] pub const INIT_EVENT: Symbol = symbol_short!("INIT"); +#[allow(dead_code)] pub const TRANSFER_EVENT: Symbol = symbol_short!("TRANSFER"); +#[allow(dead_code)] pub const REGISTER_EVENT: Symbol = symbol_short!("REGISTER"); +#[allow(dead_code)] pub const ROOT_UPDATED: Symbol = symbol_short!("ROOT_UPD"); +#[allow(dead_code)] pub const MASTER_SET: Symbol = symbol_short!("MSTR_SET"); +#[allow(dead_code)] pub const ADDR_ADDED: Symbol = symbol_short!("ADDR_ADD"); +#[allow(dead_code)] pub const CHAIN_ADD: Symbol = symbol_short!("CHAIN_ADD"); +#[allow(dead_code)] pub const CHAIN_REM: Symbol = symbol_short!("CHAIN_REM"); +#[allow(dead_code)] pub const VAULT_CREATE: Symbol = symbol_short!("VAULT_CRT"); +#[allow(dead_code)] pub const DEPOSIT: Symbol = symbol_short!("DEPOSIT"); +#[allow(dead_code)] pub const WITHDRAW: Symbol = symbol_short!("WITHDRAW"); +#[allow(dead_code)] pub const SCHED_PAY: Symbol = symbol_short!("SCHED_PAY"); From f60b6fa73ea4c9fd7ae2a13f48da639edc2195ac Mon Sep 17 00:00:00 2001 From: Obiajulu-gif Date: Thu, 26 Mar 2026 13:31:41 +0100 Subject: [PATCH 5/5] fix(core-contract): restore coherent core contract module --- .../contracts/core_contract/src/errors.rs | 6 - .../contracts/core_contract/src/events.rs | 26 +-- .../contracts/core_contract/src/lib.rs | 74 +----- .../contracts/core_contract/src/storage.rs | 43 ---- .../contracts/core_contract/src/test.rs | 221 +----------------- .../contracts/core_contract/src/types.rs | 18 +- 6 files changed, 12 insertions(+), 376 deletions(-) diff --git a/gateway-contract/contracts/core_contract/src/errors.rs b/gateway-contract/contracts/core_contract/src/errors.rs index 89a8fb6..710a2b6 100644 --- a/gateway-contract/contracts/core_contract/src/errors.rs +++ b/gateway-contract/contracts/core_contract/src/errors.rs @@ -2,12 +2,6 @@ use soroban_sdk::contracterror; #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum ContractError { - AlreadyInitialized = 1, - NotInitialized = 2, - RootMismatch = 3, - InvalidProof = 4, - DuplicateCommitment = 5, #[repr(u32)] pub enum CoreError { /// The requested resource was not found. diff --git a/gateway-contract/contracts/core_contract/src/events.rs b/gateway-contract/contracts/core_contract/src/events.rs index 0792f17..5321341 100644 --- a/gateway-contract/contracts/core_contract/src/events.rs +++ b/gateway-contract/contracts/core_contract/src/events.rs @@ -1,39 +1,17 @@ -use soroban_sdk::{contractevent, BytesN}; - -#[contractevent] -pub struct UsernameRegistered { - pub commitment: BytesN<32>, -} - -#[contractevent] -pub struct MerkleRootUpdated { - pub old_root: BytesN<32>, - pub new_root: BytesN<32>, -} +#![allow(dead_code)] use soroban_sdk::{symbol_short, Symbol}; -#[allow(dead_code)] pub const INIT_EVENT: Symbol = symbol_short!("INIT"); -#[allow(dead_code)] pub const TRANSFER_EVENT: Symbol = symbol_short!("TRANSFER"); -#[allow(dead_code)] pub const REGISTER_EVENT: Symbol = symbol_short!("REGISTER"); -#[allow(dead_code)] pub const ROOT_UPDATED: Symbol = symbol_short!("ROOT_UPD"); -#[allow(dead_code)] pub const MASTER_SET: Symbol = symbol_short!("MSTR_SET"); -#[allow(dead_code)] pub const ADDR_ADDED: Symbol = symbol_short!("ADDR_ADD"); -#[allow(dead_code)] pub const CHAIN_ADD: Symbol = symbol_short!("CHAIN_ADD"); -#[allow(dead_code)] pub const CHAIN_REM: Symbol = symbol_short!("CHAIN_REM"); -#[allow(dead_code)] +pub const PRIV_SET: Symbol = symbol_short!("PRIV_SET"); pub const VAULT_CREATE: Symbol = symbol_short!("VAULT_CRT"); -#[allow(dead_code)] pub const DEPOSIT: Symbol = symbol_short!("DEPOSIT"); -#[allow(dead_code)] pub const WITHDRAW: Symbol = symbol_short!("WITHDRAW"); -#[allow(dead_code)] pub const SCHED_PAY: Symbol = symbol_short!("SCHED_PAY"); diff --git a/gateway-contract/contracts/core_contract/src/lib.rs b/gateway-contract/contracts/core_contract/src/lib.rs index 9b240ed..a009961 100644 --- a/gateway-contract/contracts/core_contract/src/lib.rs +++ b/gateway-contract/contracts/core_contract/src/lib.rs @@ -1,9 +1,5 @@ #![no_std] -mod errors; -mod events; -mod storage; -mod types; pub mod address_manager; pub mod errors; pub mod events; @@ -16,11 +12,6 @@ pub mod zk_verifier; #[cfg(test)] mod test; -use soroban_sdk::{contract, contractclient, contractimpl, panic_with_error, Address, BytesN, Env}; - -pub use crate::errors::ContractError; -pub use crate::events::{MerkleRootUpdated, UsernameRegistered}; -pub use crate::types::{Proof, PublicSignals}; use address_manager::AddressManager; use errors::CoreError; use events::{REGISTER_EVENT, TRANSFER_EVENT}; @@ -29,52 +20,8 @@ use soroban_sdk::{contract, contractimpl, panic_with_error, Address, Bytes, Byte use types::{ChainType, PrivacyMode, PublicSignals, ResolveData}; #[contract] -pub struct CoreContract; - -#[contractclient(name = "VerifierContractClient")] -pub trait VerifierContract { - fn verify_proof(env: Env, proof: Proof, public_signals: PublicSignals) -> bool; -} - -fn current_merkle_root(env: &Env) -> BytesN<32> { - storage::get_merkle_root(env) - .unwrap_or_else(|| panic_with_error!(env, ContractError::NotInitialized)) -} - -fn update_merkle_root(env: &Env, old_root: BytesN<32>, new_root: BytesN<32>) { - storage::set_merkle_root(env, &new_root); - - MerkleRootUpdated { old_root, new_root }.publish(env); -} +pub struct Contract; -#[contractimpl] -impl CoreContract { - pub fn init(env: Env, verifier: Address, root: BytesN<32>) { - if storage::is_initialized(&env) { - panic_with_error!(&env, ContractError::AlreadyInitialized); - } - - storage::set_verifier(&env, &verifier); - storage::set_merkle_root(&env, &root); - } - - pub fn submit_proof(env: Env, proof: Proof, public_signals: PublicSignals) { - let current_root = current_merkle_root(&env); - - if current_root != public_signals.old_root.clone() { - panic_with_error!(&env, ContractError::RootMismatch); - } - - if storage::has_commitment(&env, &public_signals.commitment) { - panic_with_error!(&env, ContractError::DuplicateCommitment); - } - - let verifier = storage::get_verifier(&env) - .unwrap_or_else(|| panic_with_error!(&env, ContractError::NotInitialized)); - let verifier_client = VerifierContractClient::new(&env, &verifier); - let is_valid = verifier_client.verify_proof(&proof, &public_signals); - if !is_valid { - panic_with_error!(&env, ContractError::InvalidProof); #[contractimpl] impl Contract { pub fn get_smt_root(env: Env) -> BytesN<32> { @@ -156,27 +103,8 @@ impl Contract { } None => panic_with_error!(&env, CoreError::NotFound), } - - storage::store_commitment(&env, &public_signals.commitment); - update_merkle_root(&env, current_root, public_signals.new_root.clone()); - - UsernameRegistered { - commitment: public_signals.commitment, - } - .publish(&env); - } - - pub fn get_merkle_root(env: Env) -> BytesN<32> { - current_merkle_root(&env) - } - - pub fn get_verifier(env: Env) -> Option
{ - storage::get_verifier(&env) } - pub fn has_commitment(env: Env, commitment: BytesN<32>) -> bool { - storage::has_commitment(&env, &commitment) - /// Register a username commitment, mapping it to the caller's address. pub fn register(env: Env, caller: Address, commitment: BytesN<32>) { Registration::register(env, caller, commitment); } diff --git a/gateway-contract/contracts/core_contract/src/storage.rs b/gateway-contract/contracts/core_contract/src/storage.rs index b8d3f82..e7a054d 100644 --- a/gateway-contract/contracts/core_contract/src/storage.rs +++ b/gateway-contract/contracts/core_contract/src/storage.rs @@ -1,46 +1,3 @@ -use soroban_sdk::{contracttype, Address, BytesN, Env}; - -#[contracttype] -#[derive(Clone)] -pub enum DataKey { - CurrentMerkleRoot, - Verifier, - Commitment(BytesN<32>), -} - -pub fn is_initialized(env: &Env) -> bool { - env.storage().persistent().has(&DataKey::CurrentMerkleRoot) - && env.storage().instance().has(&DataKey::Verifier) -} - -pub fn get_merkle_root(env: &Env) -> Option> { - env.storage().persistent().get(&DataKey::CurrentMerkleRoot) -} - -pub fn set_merkle_root(env: &Env, root: &BytesN<32>) { - env.storage() - .persistent() - .set(&DataKey::CurrentMerkleRoot, root); -} - -pub fn get_verifier(env: &Env) -> Option
{ - env.storage().instance().get(&DataKey::Verifier) -} - -pub fn set_verifier(env: &Env, verifier: &Address) { - env.storage().instance().set(&DataKey::Verifier, verifier); -} - -pub fn has_commitment(env: &Env, commitment: &BytesN<32>) -> bool { - env.storage() - .persistent() - .has(&DataKey::Commitment(commitment.clone())) -} - -pub fn store_commitment(env: &Env, commitment: &BytesN<32>) { - env.storage() - .persistent() - .set(&DataKey::Commitment(commitment.clone()), &true); use soroban_sdk::{contracttype, BytesN}; /// Storage keys for the Core contract's persistent and instance storage. diff --git a/gateway-contract/contracts/core_contract/src/test.rs b/gateway-contract/contracts/core_contract/src/test.rs index 983e00c..c9753b6 100644 --- a/gateway-contract/contracts/core_contract/src/test.rs +++ b/gateway-contract/contracts/core_contract/src/test.rs @@ -1,206 +1,5 @@ #![cfg(test)] -use crate::errors::ContractError; -use crate::types::{Proof, PublicSignals}; -use crate::{CoreContract, CoreContractClient}; -use soroban_sdk::testutils::Events as _; -use soroban_sdk::{ - contract, contractimpl, contracttype, map, vec, Address, BytesN, Env, Error, IntoVal, - InvokeError, Map, Symbol, Val, -}; - -#[contract] -struct MockVerifierContract; - -#[contracttype] -#[derive(Clone)] -enum MockVerifierDataKey { - ShouldVerify, -} - -#[contractimpl] -impl MockVerifierContract { - pub fn set_should_verify(env: Env, should_verify: bool) { - env.storage() - .instance() - .set(&MockVerifierDataKey::ShouldVerify, &should_verify); - } - - pub fn verify_proof(env: Env, proof: Proof, public_signals: PublicSignals) -> bool { - let should_verify = env - .storage() - .instance() - .get::(&MockVerifierDataKey::ShouldVerify) - .unwrap_or(true); - - should_verify - && proof.a == public_signals.old_root - && proof.b == public_signals.new_root - && proof.c == public_signals.commitment - } -} - -fn bytes(env: &Env, byte: u8) -> BytesN<32> { - BytesN::from_array(env, &[byte; 32]) -} - -fn registration_fixture( - env: &Env, - old_byte: u8, - new_byte: u8, - commitment_byte: u8, -) -> (Proof, PublicSignals) { - let old_root = bytes(env, old_byte); - let new_root = bytes(env, new_byte); - let commitment = bytes(env, commitment_byte); - - ( - Proof { - a: old_root.clone(), - b: new_root.clone(), - c: commitment.clone(), - }, - PublicSignals { - old_root, - new_root, - commitment, - }, - ) -} - -fn setup(env: &Env) -> (Address, CoreContractClient<'_>, Address) { - let verifier_id = env.register(MockVerifierContract, ()); - let verifier_client = MockVerifierContractClient::new(env, &verifier_id); - verifier_client.set_should_verify(&true); - - let contract_id = env.register(CoreContract, ()); - let client = CoreContractClient::new(env, &contract_id); - client.init(&verifier_id, &bytes(env, 0)); - - (contract_id, client, verifier_id) -} - -fn assert_submit_error( - result: Result< - Result<(), soroban_sdk::ConversionError>, - Result, - >, - expected: ContractError, -) { - assert_eq!(result, Err(Ok(expected.into()))); -} - -#[test] -fn init_sets_the_current_merkle_root() { - let env = Env::default(); - let (_, client, _) = setup(&env); - - assert_eq!(client.get_merkle_root(), bytes(&env, 0)); -} - -#[test] -fn submit_proof_succeeds_and_updates_state() { - let env = Env::default(); - let (contract_id, client, _) = setup(&env); - let (proof, public_signals) = registration_fixture(&env, 0, 42, 7); - - client.submit_proof(&proof, &public_signals); - - assert_eq!(client.get_merkle_root(), public_signals.new_root.clone()); - assert!(client.has_commitment(&public_signals.commitment)); - - let expected_root_event_data: Map = map![ - &env, - ( - Symbol::new(&env, "old_root"), - public_signals.old_root.clone().into_val(&env) - ), - ( - Symbol::new(&env, "new_root"), - public_signals.new_root.clone().into_val(&env) - ) - ]; - let expected_registration_event_data: Map = map![ - &env, - ( - Symbol::new(&env, "commitment"), - public_signals.commitment.clone().into_val(&env) - ) - ]; - assert_eq!( - env.events().all(), - soroban_sdk::vec![ - &env, - ( - contract_id.clone(), - (Symbol::new(&env, "merkle_root_updated"),).into_val(&env), - expected_root_event_data.into_val(&env), - ), - ( - contract_id, - (Symbol::new(&env, "username_registered"),).into_val(&env), - expected_registration_event_data.into_val(&env), - ) - ] - ); -} - -#[test] -fn invalid_proof_is_rejected() { - let env = Env::default(); - let (_, client, verifier_id) = setup(&env); - let verifier_client = MockVerifierContractClient::new(&env, &verifier_id); - verifier_client.set_should_verify(&false); - - let (proof, public_signals) = registration_fixture(&env, 0, 42, 7); - let result = client.try_submit_proof(&proof, &public_signals); - - assert_submit_error(result, ContractError::InvalidProof); - assert!(!client.has_commitment(&public_signals.commitment)); - assert_eq!(client.get_merkle_root(), public_signals.old_root); -} - -#[test] -fn stale_root_is_rejected() { - let env = Env::default(); - let (_, client, _) = setup(&env); - let (proof, mut public_signals) = registration_fixture(&env, 0, 42, 7); - public_signals.old_root = bytes(&env, 1); - - let result = client.try_submit_proof(&proof, &public_signals); - - assert_submit_error(result, ContractError::RootMismatch); - assert!(!client.has_commitment(&public_signals.commitment)); - assert_eq!(client.get_merkle_root(), bytes(&env, 0)); -} - -#[test] -fn duplicate_commitment_is_rejected() { - let env = Env::default(); - let (_, client, _) = setup(&env); - let (proof, public_signals) = registration_fixture(&env, 0, 42, 7); - - client.submit_proof(&proof, &public_signals); - - let duplicate_result = client.try_submit_proof(&proof, &public_signals); - - assert_submit_error(duplicate_result, ContractError::DuplicateCommitment); - assert_eq!(client.get_merkle_root(), public_signals.new_root); -} - -#[test] -fn root_progresses_across_multiple_registrations() { - let env = Env::default(); - let (_, client, _) = setup(&env); - let (first_proof, first_public_signals) = registration_fixture(&env, 0, 42, 7); - let (second_proof, second_public_signals) = registration_fixture(&env, 42, 99, 8); - - client.submit_proof(&first_proof, &first_public_signals); - client.submit_proof(&second_proof, &second_public_signals); - - assert_eq!(client.get_merkle_root(), second_public_signals.new_root); - assert!(client.has_commitment(&first_public_signals.commitment)); - assert!(client.has_commitment(&second_public_signals.commitment)); use crate::smt_root::SmtRoot; use crate::types::{AddressMetadata, ChainType, PrivacyMode, PublicSignals}; use crate::{Contract, ContractClient}; @@ -397,14 +196,8 @@ fn test_auto_pay_roundtrip() { // ── resolver / memo tests ───────────────────────────────────────────────────── #[test] -fn root_cannot_be_overridden_by_reinitializing() { +fn test_resolve_returns_none_when_no_memo() { let env = Env::default(); - let (_, client, verifier_id) = setup(&env); - - let result = client.try_init(&verifier_id, &bytes(&env, 9)); - - assert_eq!(result, Err(Ok(ContractError::AlreadyInitialized.into()))); - assert_eq!(client.get_merkle_root(), bytes(&env, 0)); env.mock_all_auths(); let (_, client, root) = setup_with_root(&env); let caller = Address::generate(&env); @@ -423,18 +216,8 @@ fn root_cannot_be_overridden_by_reinitializing() { } #[test] -fn direct_root_override_entrypoint_is_rejected() { +fn test_set_memo_and_resolve_flow() { let env = Env::default(); - let (contract_id, client, _) = setup(&env); - - let result = env.try_invoke_contract::<(), InvokeError>( - &contract_id, - &Symbol::new(&env, "set_merkle_root"), - vec![&env, bytes(&env, 9).into_val(&env)], - ); - - assert_eq!(result, Err(Ok(InvokeError::Abort))); - assert_eq!(client.get_merkle_root(), bytes(&env, 0)); env.mock_all_auths(); let (_, client, root) = setup_with_root(&env); let caller = Address::generate(&env); diff --git a/gateway-contract/contracts/core_contract/src/types.rs b/gateway-contract/contracts/core_contract/src/types.rs index 6360f04..836766c 100644 --- a/gateway-contract/contracts/core_contract/src/types.rs +++ b/gateway-contract/contracts/core_contract/src/types.rs @@ -1,20 +1,16 @@ -use soroban_sdk::{contracttype, BytesN}; use soroban_sdk::{contracttype, Address, BytesN, Symbol}; #[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Proof { - pub a: BytesN<32>, - pub b: BytesN<32>, - pub c: BytesN<32>, +#[derive(Clone)] +pub struct AddressMetadata { + pub label: Symbol, } #[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PublicSignals { - pub old_root: BytesN<32>, - pub new_root: BytesN<32>, - pub commitment: BytesN<32>, +#[derive(Clone)] +pub struct ResolveData { + pub wallet: Address, + pub memo: Option, } #[contracttype]