-
Notifications
You must be signed in to change notification settings - Fork 69
Implement submit_proof endpoint #132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
33143be
164c91b
5bf6e24
2e40147
b6e12f7
4a03a9a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,6 @@ | ||
| #![allow(dead_code)] | ||
| use soroban_sdk::{contractevent, BytesN}; | ||
|
|
||
| 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"); | ||
| #[contractevent] | ||
| pub struct UsernameRegistered { | ||
| pub commitment: BytesN<32>, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,58 +1,76 @@ | ||
| #![no_std] | ||
|
|
||
| pub mod events; | ||
| pub mod types; | ||
| mod errors; | ||
| mod events; | ||
| mod storage; | ||
| mod types; | ||
|
|
||
| use soroban_sdk::{ | ||
| contract, contracterror, contractimpl, contracttype, panic_with_error, Address, BytesN, Env, | ||
| }; | ||
| use types::ResolveData; | ||
| #[cfg(test)] | ||
| mod test; | ||
|
|
||
| #[contract] | ||
| pub struct Contract; | ||
| use soroban_sdk::{contract, contractclient, contractimpl, panic_with_error, Address, BytesN, Env}; | ||
|
|
||
| #[contracttype] | ||
| #[derive(Clone)] | ||
| pub enum DataKey { | ||
| Resolver(BytesN<32>), | ||
| } | ||
| 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<u64>) { | ||
| 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 set_memo(env: Env, commitment: BytesN<32>, memo_id: u64) { | ||
| let key = DataKey::Resolver(commitment); | ||
| let mut data = env | ||
| .storage() | ||
| .persistent() | ||
| .get::<DataKey, ResolveData>(&key) | ||
| .unwrap_or_else(|| panic_with_error!(&env, ResolverError::NotFound)); | ||
| 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)); | ||
|
|
||
| data.memo = Some(memo_id); | ||
| env.storage().persistent().set(&key, &data); | ||
| } | ||
| 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); | ||
|
Comment on lines
+40
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Once the first submission updates the root, replaying the same proof fails at 🔁 If duplicate should win, swap the checks- 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);
}
+
+ if current_root != public_signals.old_root.clone() {
+ panic_with_error!(&env, ContractError::RootMismatch);
+ }🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| pub fn resolve(env: Env, commitment: BytesN<32>) -> (Address, Option<u64>) { | ||
| let key = DataKey::Resolver(commitment); | ||
| 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); | ||
|
Comment on lines
+48
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: fd -t f "lib.rs" gateway-contractRepository: Alien-Protocol/Alien-Gateway Length of output: 287 🏁 Script executed: wc -l gateway-contract/contracts/core_contract/src/lib.rsRepository: Alien-Protocol/Alien-Gateway Length of output: 126 🏁 Script executed: head -80 gateway-contract/contracts/core_contract/src/lib.rs | cat -nRepository: Alien-Protocol/Alien-Gateway Length of output: 3082 🏁 Script executed: rg "ContractError" gateway-contract/contracts/core_contract/src/lib.rs -A 2 -B 2Repository: Alien-Protocol/Alien-Gateway Length of output: 1402 🏁 Script executed: rg "enum ContractError" gateway-contract -A 20Repository: Alien-Protocol/Alien-Gateway Length of output: 608 🏁 Script executed: rg "verify_proof" gateway-contract -B 5 -A 5Repository: Alien-Protocol/Alien-Gateway Length of output: 2986 🏁 Script executed: rg "VerifierContractClient" gateway-contract -B 2 -A 2Repository: Alien-Protocol/Alien-Gateway Length of output: 2096 🌐 Web query:
💡 Result: In Soroban Rust, generated What
|
||
| } | ||
|
|
||
| storage::store_commitment(&env, &public_signals.commitment); | ||
| storage::set_root(&env, &public_signals.new_root); | ||
|
|
||
| match env.storage().persistent().get::<DataKey, ResolveData>(&key) { | ||
| Some(data) => (data.wallet, data.memo), | ||
| None => panic_with_error!(&env, ResolverError::NotFound), | ||
| UsernameRegistered { | ||
| commitment: public_signals.commitment, | ||
| } | ||
| .publish(&env); | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod test; | ||
| pub fn get_root(env: Env) -> Option<BytesN<32>> { | ||
| storage::get_root(&env) | ||
| } | ||
|
|
||
| pub fn get_verifier(env: Env) -> Option<Address> { | ||
| storage::get_verifier(&env) | ||
| } | ||
|
|
||
| pub fn has_commitment(env: Env, commitment: BytesN<32>) -> bool { | ||
| storage::has_commitment(&env, &commitment) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<BytesN<32>> { | ||
| 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<Address> { | ||
| 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); | ||
|
Comment on lines
+15
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n gateway-contract/contracts/core_contract/src/storage.rsRepository: Alien-Protocol/Alien-Gateway Length of output: 1439 🏁 Script executed: rg "extend_ttl|TTL|ttl" gateway-contract/contracts/core_contract/src/ -iRepository: Alien-Protocol/Alien-Gateway Length of output: 54 🏁 Script executed: find gateway-contract/contracts/core_contract/src -name "*.rs" -type fRepository: Alien-Protocol/Alien-Gateway Length of output: 514 🏁 Script executed: cat -n gateway-contract/contracts/core_contract/src/lib.rsRepository: Alien-Protocol/Alien-Gateway Length of output: 3091 🌐 Web query:
💡 Result: Instance storage TTL extension (contract instance)
Persistent storage TTL extension (per key)
Extending TTL from outside the contract (transaction operation)
Sources: [1] Stellar Docs – Smart contract state archival; [2] Extend TTL for long-lived storage entries to prevent state expiry. Instance storage (Root and Verifier) and persistent storage (Commitments) require explicit TTL extension on Soroban—without it, these entries will expire and be archived. This breaks contract initialization state (Root and Verifier queries will fail) and disables duplicate protection (commitments can be replayed after archival). Add 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,39 +1,160 @@ | ||
| #![cfg(test)] | ||
|
|
||
| use crate::{Contract, ContractClient}; | ||
| use soroban_sdk::testutils::Address as _; | ||
| use soroban_sdk::{Address, BytesN, Env}; | ||
| use crate::errors::ContractError; | ||
| use crate::types::{Proof, PublicSignals}; | ||
| use crate::{CoreContract, CoreContractClient}; | ||
|
Comment on lines
+3
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for PublicSignals constructions with only old_root and new_root (no commitment)
rg -n -A 3 'PublicSignals\s*\{' gateway-contract/contracts/core_contract/src/test.rs | grep -v commitmentRepository: Alien-Protocol/Alien-Gateway Length of output: 1668 🏁 Script executed: # Check the actual PublicSignals definition(s) in types.rs
find gateway-contract -name "types.rs" -type fRepository: Alien-Protocol/Alien-Gateway Length of output: 295 🏁 Script executed: # Look at the PublicSignals definitions and their contexts
rg -n "^pub struct PublicSignals" gateway-contract/contracts/core_contract/src/ -A 5Repository: Alien-Protocol/Alien-Gateway Length of output: 855 🏁 Script executed: # Check the structure and section boundaries in test.rs to understand which lines belong to which tests
rg -n "^#\[test\]|^fn test_" gateway-contract/contracts/core_contract/src/test.rsRepository: Alien-Protocol/Alien-Gateway Length of output: 2513 🏁 Script executed: # Look at the first few lines of new test code (around line 88) to see which PublicSignals variant
sed -n '85,100p' gateway-contract/contracts/core_contract/src/test.rsRepository: Alien-Protocol/Alien-Gateway Length of output: 583 🏁 Script executed: # Look at test code around the old tests section (around line 162) to see which variant
sed -n '160,180p' gateway-contract/contracts/core_contract/src/test.rsRepository: Alien-Protocol/Alien-Gateway Length of output: 613 🏁 Script executed: # Check if the 3-field PublicSignals (with commitment) is used anywhere in test.rs
rg -n "commitment.*:" gateway-contract/contracts/core_contract/src/test.rs | head -20Repository: Alien-Protocol/Alien-Gateway Length of output: 54 🏁 Script executed: # Check if setup_with_root, commitment, and dummy_proof are defined in test.rs
rg -n "^fn setup_with_root|^fn commitment|^fn dummy_proof" gateway-contract/contracts/core_contract/src/test.rsRepository: Alien-Protocol/Alien-Gateway Length of output: 54 🏁 Script executed: # Check if these functions are defined anywhere or if they're truly missing
rg -n "fn setup_with_root|fn commitment|fn dummy_proof" gateway-contract/contracts/core_contract/src/Repository: Alien-Protocol/Alien-Gateway Length of output: 54 Old tests at lines 162+ reference undefined functions and will break when the duplicate The duplicate 2-field 🤖 Prompt for AI Agents |
||
| use soroban_sdk::testutils::Events as _; | ||
| use soroban_sdk::{ | ||
| contract, contractimpl, contracttype, map, Address, BytesN, Env, Error, IntoVal, Map, Symbol, | ||
| Val, | ||
| }; | ||
|
|
||
| fn setup_test(env: &Env) -> (ContractClient<'_>, BytesN<32>, Address) { | ||
| let contract_id = env.register(Contract, ()); | ||
| let client = ContractClient::new(env, &contract_id); | ||
| let commitment = BytesN::from_array(env, &[7u8; 32]); | ||
| let wallet = Address::generate(env); | ||
| #[contract] | ||
| struct MockVerifierContract; | ||
|
|
||
| (client, commitment, wallet) | ||
| #[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, bool>(&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<(), soroban_sdk::ConversionError>, | ||
| Result<Error, soroban_sdk::InvokeError>, | ||
| >, | ||
| expected: ContractError, | ||
| ) { | ||
| assert_eq!(result, Err(Ok(expected.into()))); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_resolve_returns_none_when_no_memo() { | ||
| fn submit_proof_succeeds_and_updates_state() { | ||
| let env = Env::default(); | ||
| let (client, commitment, wallet) = setup_test(&env); | ||
| let (contract_id, client, _) = setup(&env); | ||
| let (proof, public_signals) = offchain_register_fixture(&env); | ||
|
|
||
| client.register_resolver(&commitment, &wallet, &None); | ||
| client.submit_proof(&proof, &public_signals); | ||
|
|
||
| let (resolved_wallet, memo) = client.resolve(&commitment); | ||
| assert_eq!(resolved_wallet, wallet); | ||
| assert_eq!(memo, None); | ||
| assert_eq!(client.get_root(), Some(public_signals.new_root.clone())); | ||
| assert!(client.has_commitment(&public_signals.commitment)); | ||
|
|
||
| let expected_event_data: Map<Symbol, Val> = 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 test_set_memo_and_resolve_flow() { | ||
| fn stale_root_is_rejected() { | ||
| let env = Env::default(); | ||
| let (client, commitment, wallet) = setup_test(&env); | ||
| 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); | ||
|
|
||
| client.register_resolver(&commitment, &wallet, &None); | ||
| client.set_memo(&commitment, &4242u64); | ||
| let duplicate_result = client.try_submit_proof(&proof, &public_signals); | ||
|
|
||
| let (resolved_wallet, memo) = client.resolve(&commitment); | ||
| assert_eq!(resolved_wallet, wallet); | ||
| assert_eq!(memo, Some(4242u64)); | ||
| assert_submit_error(duplicate_result, ContractError::DuplicateCommitment); | ||
| assert_eq!(client.get_root(), Some(public_signals.new_root)); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gate
initbehind authorization.The first caller permanently chooses
verifierandroot. That lets a front-runner initialize the contract with malicious values and lock the contract into bad state forever.🔐 Possible fix
📝 Committable suggestion
🤖 Prompt for AI Agents