From d9c83f19aa0d23038a556e7a64edbcfb293dc649 Mon Sep 17 00:00:00 2001 From: vig-gray Date: Sat, 30 May 2026 21:34:49 +0100 Subject: [PATCH] feat(bridge): add Axelar cross-chain bridge stubs and design doc #217 --- Makefile | 10 ++ contracts/src/bridge.rs | 208 ++++++++++++++++++++++++++++ contracts/src/identity.rs | 180 ++++++++++++++++++++++++ contracts/src/lib.rs | 2 + docs/BRIDGE_DESIGN.md | 181 ++++++++++++++++++++++++ docs/ZK_IDENTITY.md | 282 ++++++++++++++++++++++++++++++++++++++ scripts/sandbox.sh | 262 +++++++++++++++++++++++++++++++++++ 7 files changed, 1125 insertions(+) create mode 100644 Makefile create mode 100644 contracts/src/bridge.rs create mode 100644 contracts/src/identity.rs create mode 100644 docs/BRIDGE_DESIGN.md create mode 100644 docs/ZK_IDENTITY.md create mode 100755 scripts/sandbox.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d65e30e --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +.PHONY: sandbox sandbox-stop sandbox-logs + +sandbox: + @bash scripts/sandbox.sh + +sandbox-stop: + @docker stop skillsphere-sandbox 2>/dev/null || true + +sandbox-logs: + @docker logs -f skillsphere-sandbox \ No newline at end of file diff --git a/contracts/src/bridge.rs b/contracts/src/bridge.rs new file mode 100644 index 0000000..7566f0a --- /dev/null +++ b/contracts/src/bridge.rs @@ -0,0 +1,208 @@ +//! # IBC Cross-Chain Bridge (Issue #217) +//! +//! Enables cross-chain USDC payments from Ethereum/Polygon via Axelar. +//! Users can pay with USDC on EVM chains and receive equivalent tokens on Stellar. + +#![allow(unused_imports)] + +use soroban_sdk::{ + contract, contractimpl, contracterror, contracttype, symbol_short, Address, Env, String, BytesN, +}; + +#[contracterror] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum BridgeError { + Unauthorized = 1, + ReplayDetected = 2, + InvalidChain = 3, + InsufficientBalance = 4, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BridgeMessage { + pub source_chain: String, + pub source_address: String, + pub token: Address, + pub amount: i128, + pub recipient: Address, + pub nonce: u128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RefundRequest { + pub source_chain: String, + pub source_address: String, + pub token: Address, + pub amount: i128, + pub requested_at: u64, +} + +#[contract] +pub struct BridgeContract; + +#[contractimpl] +impl BridgeContract { + /// Validates and processes an incoming bridge message from Axelar. + /// + /// # Arguments + /// * `env` — The Soroban environment + /// * `msg` — The bridge message containing payment details + /// + /// # Errors + /// * `BridgeError::Unauthorized` — If sender is not the authorized relayer + /// * `BridgeError::ReplayDetected` — If the nonce has already been used + /// * `BridgeError::InvalidChain` — If source chain is not supported + /// + /// # Events + /// * Emits `bridge_message_received` with (source_chain, recipient, token, amount, nonce) + pub fn receive_bridge_message(env: Env, msg: BridgeMessage) -> Result<(), BridgeError> { + // TODO: Validate sender is authorized Axelar relayer address + // let relayer = env + // .storage() + // .instance() + // .get(&DataKey::BridgeRelayer) + // .ok_or(BridgeError::Unauthorized)?; + // if env.invoker_contract() != relayer { + // return Err(BridgeError::Unauthorized); + // } + + // TODO: Check nonce for replay protection + // let nonce_key = DataKey::BridgeNonce(msg.source_chain.clone(), msg.source_address.clone(), msg.nonce); + // if env.storage().persistent().has(&nonce_key) { + // return Err(BridgeError::ReplayDetected); + // } + + // TODO: Validate source chain is whitelisted + // let supported_chains: Vec = env.storage().instance().get(&DataKey::SupportedChains).unwrap_or(Vec::new(&env)); + // if !supported_chains.contains(&msg.source_chain) { + // return Err(BridgeError::InvalidChain); + // } + + todo!() + } + + /// Initiates an outbound bridge transfer to another chain. + /// + /// # Arguments + /// * `env` — The Soroban environment + /// * `destination_chain` — Target chain name (e.g., "Ethereum", "Polygon") + /// * `recipient` — Destination address on the target chain + /// * `amount` — Amount of tokens to bridge + /// + /// # Errors + /// * `BridgeError::Unauthorized` — If caller is not authorized + /// * `BridgeError::InsufficientBalance` — If contract lacks sufficient tokens + /// + /// # Events + /// * Emits `bridge_out_initiated` with (destination_chain, recipient, amount) + pub fn initiate_bridge_out( + env: Env, + destination_chain: String, + recipient: String, + amount: i128, + ) -> Result<(), BridgeError> { + // TODO: Validate caller is authorized (could be admin or any user with allowance) + + // TODO: Lock/burn tokens held by the contract + // let token = env.current_contract_address(); // or separate token address + // let token_client = token::Client::new(&env, &token); + // if token_client.balance(&env.current_contract_address()) < amount { + // return Err(BridgeError::InsufficientBalance); + // } + + // TODO: Call Axelar gateway to initiate cross-chain transfer + + todo!() + } + + /// Requests a refund for a failed bridge message. + /// + /// # Arguments + /// * `env` — The Soroban environment + /// * `source_chain` — The original source chain + /// * `source_address` — The original sender address + /// * `nonce` — The nonce from the original bridge message + /// + /// # Errors + /// * `BridgeError::InvalidChain` — If refund request is invalid + /// + /// # Events + /// * Emits `bridge_refund` with (source_chain, source_address, nonce, amount) + pub fn request_refund( + env: Env, + source_chain: String, + source_address: String, + nonce: u128, + ) -> Result { + todo!() + } + + /// Sets the authorized Axelar relayer address (admin only). + /// + /// # Arguments + /// * `env` — The Soroban environment + /// * `relayer` — The new relayer address + /// + /// # Events + /// * Emits `relayer_updated` with the new relayer address + pub fn set_relayer(env: Env, relayer: Address) -> Result<(), BridgeError> { + todo!() + } + + /// Gets the current authorized relayer address. + pub fn get_relayer(env: Env) -> Option
{ + todo!() + } + + /// Checks if an incoming bridge message has already been processed. + pub fn is_nonce_used(env: Env, source_chain: String, source_address: String, nonce: u128) -> bool { + todo!() + } +} + +#[cfg(test)] +mod test { + use super::*; + use soroban_sdk::testutils::Address as _; + use soroban_sdk::{Address, Env, String}; + + #[test] + fn test_bridge_message_structure() { + let env = Env::default(); + let msg = BridgeMessage { + source_chain: String::from_str(&env, "Ethereum"), + source_address: String::from_str(&env, "0x1234...abcd"), + token: Address::generate(&env), + amount: 1000, + recipient: Address::generate(&env), + nonce: 1, + }; + assert_eq!(msg.amount, 1000); + assert_eq!(msg.nonce, 1); + } + + #[test] + fn test_refund_request_structure() { + let env = Env::default(); + let req = RefundRequest { + source_chain: String::from_str(&env, "Ethereum"), + source_address: String::from_str(&env, "0x1234...abcd"), + token: Address::generate(&env), + amount: 1000, + requested_at: 12345, + }; + assert_eq!(req.amount, 1000); + assert_eq!(req.requested_at, 12345); + } + + #[test] + fn test_bridge_error_values() { + assert_eq!(BridgeError::Unauthorized as u32, 1); + assert_eq!(BridgeError::ReplayDetected as u32, 2); + assert_eq!(BridgeError::InvalidChain as u32, 3); + assert_eq!(BridgeError::InsufficientBalance as u32, 4); + } +} \ No newline at end of file diff --git a/contracts/src/identity.rs b/contracts/src/identity.rs new file mode 100644 index 0000000..db6b61c --- /dev/null +++ b/contracts/src/identity.rs @@ -0,0 +1,180 @@ +//! # KYC/KYB Integration Hooks (Issue #215) +//! +//! Optional KYC verification hooks for the identity contract. +//! Allows accounts to require KYC verification before participating in sessions. + +#![allow(unused_imports)] + +use soroban_sdk::{ + contract, contractimpl, contracterror, contracttype, symbol_short, Address, Env, String, +}; + +#[contracterror] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum IdentityError { + NotAdmin = 1, + NotOracle = 2, + KycRequired = 3, + AccountNotFound = 4, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum KycStatus { + NotRequired, + Required, + Verified, + Rejected, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Account { + pub address: Address, + pub kyc_status: KycStatus, +} + +#[contract] +pub struct IdentityContract; + +#[contractimpl] +impl IdentityContract { + /// Sets KYC as required for an account. + /// Only callable by the stored ADMIN address. + /// + /// # Arguments + /// * `env` — The Soroban environment + /// * `account` — The account address + /// + /// # Errors + /// * `IdentityError::NotAdmin` — If caller is not admin + /// + /// # Events + /// * Emits `kyc_required` with (account, KycStatus::Required) + pub fn admin_set_kyc_required(env: Env, account: Address) -> Result<(), IdentityError> { + // TODO: Verify caller is admin + // let admin: Address = env.storage().instance().get(&DataKey::Admin).ok_or(IdentityError::NotAdmin)?; + // admin.require_auth(); + + // TODO: Set account's kyc_status to Required + // let acc_key = DataKey::AccountKyc(account.clone()); + // env.storage().persistent().set(&acc_key, &KycStatus::Required); + + // TODO: Emit kyc_required event + // env.events().publish((symbol_short!("kycReq"),), (account, KycStatus::Required)); + + todo!() + } + + /// Verifies or rejects an account's KYC status. + /// Only callable by the stored KYC_ORACLE address. + /// + /// # Arguments + /// * `env` — The Soroban environment + /// * `account` — The account address to verify + /// * `approved` — true to set Verified, false to set Rejected + /// + /// # Errors + /// * `IdentityError::NotOracle` — If caller is not the KYC oracle + /// + /// # Events + /// * Emits `kyc_status_updated` with (account, new_status) + pub fn oracle_verify_kyc(env: Env, account: Address, approved: bool) -> Result<(), IdentityError> { + // TODO: Verify caller is KYC_ORACLE + // let oracle: Address = env.storage().instance().get(&DataKey::KycOracle).ok_or(IdentityError::NotOracle)?; + // oracle.require_auth(); + + // TODO: Update status based on approval + // let new_status = if approved { KycStatus::Verified } else { KycStatus::Rejected }; + // let acc_key = DataKey::AccountKyc(account.clone()); + // env.storage().persistent().set(&acc_key, &new_status); + + // TODO: Emit kyc_status_updated event + // env.events().publish((symbol_short!("kycUpdt"),), (account, new_status)); + + todo!() + } + + /// Gets the KYC status for an account. + pub fn get_kyc_status(env: Env, account: Address) -> Option { + todo!() + } + + /// Initializes the identity contract with admin and oracle addresses. + /// + /// # Arguments + /// * `env` — The Soroban environment + /// * `admin` — The admin address + /// * `kyc_oracle` — The KYC oracle address + pub fn initialize(env: Env, admin: Address, kyc_oracle: Address) { + todo!() + } +} + +/// Helper macro to check KYC requirement before action execution. +/// If an account has KycStatus::Required and is not yet Verified, returns error. +/// No-ops if status is NotRequired or Verified. +#[macro_export] +macro_rules! require_kyc_if_needed { + ($env:expr, $account:expr) => { + // TODO: Implement KYC check logic + // let status: Option = $env.storage().persistent().get(&DataKey::AccountKyc($account.clone())); + // if let Some(KycStatus::Required) = status { + // return Err(IdentityError::KycRequired); + // } + }; +} + +#[cfg(test)] +mod test { + use super::*; + use soroban_sdk::testutils::Address as _; + use soroban_sdk::{Address, Env, String}; + + #[test] + fn test_kyc_status_variants() { + let env = Env::default(); + + let not_required = KycStatus::NotRequired; + let required = KycStatus::Required; + let verified = KycStatus::Verified; + let rejected = KycStatus::Rejected; + + assert_eq!(not_required, KycStatus::NotRequired); + assert_eq!(required, KycStatus::Required); + assert_eq!(verified, KycStatus::Verified); + assert_eq!(rejected, KycStatus::Rejected); + } + + #[test] + fn test_account_structure() { + let env = Env::default(); + let account = Account { + address: Address::generate(&env), + kyc_status: KycStatus::Required, + }; + assert_eq!(account.kyc_status, KycStatus::Required); + } + + #[test] + fn test_identity_error_values() { + assert_eq!(IdentityError::NotAdmin as u32, 1); + assert_eq!(IdentityError::NotOracle as u32, 2); + assert_eq!(IdentityError::KycRequired as u32, 3); + assert_eq!(IdentityError::AccountNotFound as u32, 4); + } + + #[test] + fn test_kyc_status_transitions() { + let env = Env::default(); + + // Verify status equality for state transitions + let status = KycStatus::Required; + assert_eq!(status, KycStatus::Required); + + // Test Verified status + let verified_status = KycStatus::Verified; + assert!(verified_status != KycStatus::Rejected); + } +} \ No newline at end of file diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 21a8e4f..08707bc 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -4,9 +4,11 @@ mod errors; mod reputation; mod dex; mod governance; +pub mod bridge; pub use errors::Error; pub use reputation::BadgeRecord; pub use dex::SwapPath; +pub use bridge::BridgeError; use soroban_sdk::{ contract, contractclient, contractimpl, contracttype, symbol_short, token, diff --git a/docs/BRIDGE_DESIGN.md b/docs/BRIDGE_DESIGN.md new file mode 100644 index 0000000..5d0de6c --- /dev/null +++ b/docs/BRIDGE_DESIGN.md @@ -0,0 +1,181 @@ +# IBC Cross-Chain Bridge Design (Axelar) + +## Why Axelar? + +Axelar is chosen as the cross-chain messaging solution for SkillSphere due to the following advantages over alternatives like Wormhole and LayerZero: + +### Axelar Advantages + +1. **EVM Compatibility**: Direct support for Ethereum and Polygon with well-established gateway contracts +2. **Soroban Integration**: Axelar's cross-chain messaging is designed for Stellar/Soroban ecosystem with native Rust SDK support +3. **Threshold Signature Security**: Uses a decentralized validator set with threshold signatures, reducing single-point-of-failure risks +4. **Gas Abstraction**: Supports gasless message passing where the relayer pays fees and is reimbursed by the destination contract +5. **Message Authentication**: Built-in payload authentication without requiring additional oracle infrastructure +6. **Production Maturity**: Already deployed and tested across multiple mainnets with established bridge contracts + +### Comparison with Alternatives + +| Feature | Axelar | Wormhole | LayerZero | +|---------|--------|----------|-----------| +| Soroban Support | Native | Limited | None | +| EVM Chains | Ethereum, Polygon, BSC, Avalanche | Ethereum, Solana, BSC, others | Ethereum, Polygon, Arbitrum, Optimism | +| Message Auth | Threshold sigs | Guardian network | Endpoint verification | +| Gas Abstraction | Yes | No | Limited | +| Ecosystem Maturity | High | Medium | Growing | + +## Architecture Diagram + +```mermaid +graph LR + A[EVM Chain
Ethereum/Polygon] --> B[Axelar Gateway
USDC Transfer] + B --> C[Axelar Satellite
Message Payload] + C --> D[Soroban Contract
skillsphere-bridge] + D --> E[Recipient Wallet
Stellar Address] + + subgraph "Source Chain" + A + end + + subgraph "Axelar Network" + B + C + end + + subgraph "Destination" + D + E + end +``` + +### ASCII Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ ETHEREUM / POLYGON │ +│ ┌─────────────┐ ┌──────────────┐ │ +│ │ User Wallet │───►│ Axelar │ │ +│ │ (USDC) │ │ Gateway │ Locks USDC, emits cross-chain msg │ +│ └─────────────┘ └──────────────┘ │ +└──────────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ AXELAR RELAY NETWORK │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ Axelar validators collect, verify, and forward message with payload │ │ +│ │ Payload: {source_chain, source_address, token, amount, recipient, nonce}│ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ STELLAR / SOROBAN │ +│ ┌──────────────────┐ ┌───────────────────────────────────────────────┐ │ +│ │ skillsphere- │───►│ Receives Axelar message │ │ +│ │ bridge.rs │ │ Validates relayer auth + nonce │ │ +│ │ │◄───│ Credits recipient account/wallet │ │ +│ └──────────────────┘ └───────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +## Message Flow for USDC Cross-Chain Payment Settlement + +### Inbound Flow (EVM → Stellar) + +1. **User Initiation** (Ethereum/Polygon): + - User calls `IAxelarGateway.approveToken(address(USDC))` to approve USDC spending + - User calls `IAxelarGateway.payGasForContractCall` with destination = Axelar gateway + - Payload is serialized: `{destination_chain: "Stellar", destination_address: , amount, nonce}` + - `usdc.transferFrom(user, gateway, amount)` locks USDC on EVM + +2. **Axelar Relay**: + - Axelar validators observe the transaction + - Payload is gossiped across the Axelar network + - Threshold signature is assembled for message authenticity + +3. **Soroban Contract Reception**: + - `receive_bridge_message(BridgeMessage)` is called by Axelar's trusted relayer + - Contract validates: + - Sender is authorized Axelar relayer address + - Nonce has not been used before (replay protection) + - Destination chain is supported + - Contract mints equivalent Stellar-based USDC (or uses existing token handler) + - Event `bridge_message_received` is emitted + +### Outbound Flow (Stellar → EVM) + +1. `initiate_bridge_out(destination_chain, recipient, amount)` is called +2. Contract locks/burns Stellar USDC held in contract escrow +3. Cross-contract call to Axelar gateway on Stellar +4. Axelar forwards message to EVM destination +5. USDC is released to recipient on EVM side + +## Security Considerations + +### Replay Attacks + +- **Nonce Tracking**: Each bridge message includes a unique `nonce` that must be stored and checked +- **Storage Key**: `BridgeNonce(source_chain, source_address, nonce)` prevents replay +- **Incremental Nonces**: Recommended to use incrementing nonces per source address + +### Message Validation + +- **Relayer Authentication**: Only Axelar's pre-configured relayer address can call `receive_bridge_message` +- **Signature Verification**: Axelar provides threshold signature proof; contract verifies via Axelar SDK +- **Chain Whitelisting**: Valid `source_chain` values are whitelisted in contract storage + +### Trusted Relayer Model + +- **Axelar Gateway Address**: Stored in `BridgeRelayer` instance key +- **Admin-Only Updates**: Only contract admin can update the relayer address +- **Multi-Signature**: Axelar's threshold model means >50% of validators must agree + +### Additional Security Measures + +1. **Rate Limiting**: Per-account daily bridge limits +2. **Amount Caps**: Maximum bridge amount per transaction +3. **Grace Period**: Optional delay before funds are released +4. **Event Logging**: All bridge actions emit indexed events for monitoring + +## Fallback / Refund Logic + +### Bridge Failure Scenarios + +| Scenario | Handling | +|----------|----------| +| Message never arrives | User can initiate refund after timeout via `request_refund(nonce)` | +| Invalid message | Contract rejects; user must contact support for manual recovery | +| Chain outage | Retry mechanism; events stored for replay once connectivity restored | + +### Refund Process + +1. User detects missing funds after `BRIDGE_TIMEOUT_SECS` (default: 24 hours) +2. User calls `request_refund(nonce)` with original transaction details +3. If nonce is found in pending state and timeout elapsed, funds are returned +4. Event `bridge_refund` is emitted for indexer tracking + +### Recovery Functions + +```rust +pub fn request_refund(env: Env, source_chain: String, source_address: String, nonce: u128) -> Result<(), BridgeError> +pub fn emergency_withdraw(env: Env, token: Address, amount: i128) -> Result<(), BridgeError> // admin only +``` + +## Contract Storage Layout + +``` +Instance Storage: +- BridgeRelayer: Address — Authorized Axelar gateway/relayer address +- BridgePaused: bool — Emergency pause flag + +Persistent Storage: +- BridgeMessage(Key): BridgeMessage — For tracking pending/received messages +- BridgeNonce(source_chain, source_address, nonce): bool — Replay protection +- PendingRefund(nonce): RefundRequest — Pending refund tracking +``` + +## References + +- [Axelar documentation](https://docs.axelar.network/) +- [Axelar Soroban integration](https://github.com/Axelar-Network/axelar-contract-gateway) +- [Stellar USDC token contract](https://stellar.expert/explorer/public/contract/USD...) \ No newline at end of file diff --git a/docs/ZK_IDENTITY.md b/docs/ZK_IDENTITY.md new file mode 100644 index 0000000..b0cbc0d --- /dev/null +++ b/docs/ZK_IDENTITY.md @@ -0,0 +1,282 @@ +# ZK Identity Verification Design + +## Executive Summary + +Zero-knowledge proof (ZK proof) verification for identity on-chain provides significant privacy benefits while maintaining compliance and trust in the SkillSphere ecosystem. This design enables users to prove they possess valid credentials or meet certain criteria without revealing sensitive personal information. + +### Why ZK for Identity? + +1. **Privacy Preservation**: Users can prove eligibility (e.g., "I am a verified expert") without exposing underlying documents or personal data +2. **Regulatory Compliance**: Maintains auditability for regulated jurisdictions while preserving user privacy +3. **Selective Disclosure**: Users choose what to reveal and when, giving them control over their data +4. **Non-Transferable Proofs**: ZK commitments can be bound to specific addresses, preventing credential sharing +5. **Reduced On-Chain Footprint**: Only proof/verification key hashes are stored, minimizing storage costs + +## Current Soroban Limitations + +### Supported Cryptographic Primitives + +Soroban currently supports the following host functions: + +- **Hash Functions**: SHA-256, Keccak-256 +- **Elliptic Curve Operations**: ed25519 signature verification +- **Basic Arithmetic**: Integer operations up to 128-bit +- **Byte Operations**: Concatenation, slicing, comparison + +### Missing for Native ZK Support + +1. **Pairing-Based Cryptography**: Required for Groth16 and PLONK verification (bn254 curve) +2. **Large Field Arithmetic**: Field elements for BN254 (254-bit) or BLS12-381 (381-bit) operations +3. **Elliptic Curve Point Operations**: Addition, multiplication, multiexp for verification equations +4. **Miller Loop Verification**: Core operation for pairing checks +5. **Large Compute Footprint**: ZK verification requires significant CPU cycles + +### Workarounds + +Until native support lands, ZK verification can be achieved via: + +1. **Oracle-Based Verification**: Trusted oracle submits verification results +2. **Pre-compiled Contracts**: WASM libraries bundling elliptic curve math +3. **Hybrid Approach**: On-chain commitment + off-chain oracle attestation + +## ZK Scheme Options + +### Groth16 + +| Aspect | Details | +|--------|---------| +| Proof Size | ~192 bytes (3 G1 points + 1 G2 point) | +| Verification Time | Fast (12 pairings) | +| Setup | Trusted setup required per circuit | +| Quantum Resistance | No | +| Soroban Suitability | Challenging without pairing support | + +**Pros:** +- Smallest proofs among major schemes +- Fast verification +- Widely adopted in production + +**Cons:** +- Requires trusted ceremony per circuit change +- Less flexible for circuit updates + +### PLONK / UltraPLONK + +| Aspect | Details | +|--------|---------| +| Proof Size | ~288 bytes (single proof) | +| Verification Time | Moderate (16+ permutations) | +| Setup | Universal trusted setup (one-time) | +| Quantum Resistance | No | +| Soroban Suitability | Challenging without pairing support | + +**Pros:** +- Universal setup allows multiple circuits +- More flexible circuit updates +- Better for evolving requirements + +**Cons:** +- Larger proofs than Groth16 +- More complex verifier implementation + +### Recommendation + +**Interim**: Use oracle-based verification with Groth16 proofs +**Long-term**: Transition to native PLONK support when Soroban adds pairing host functions + +## Proposed Architecture + +### Phase 1: Oracle-Based Verification (Current) + +``` +┌──────────────┐ ┌─────────────────┐ ┌──────────────────┐ +│ User │───►│ Off-chain │───►│ ZK-Prover │ +│ (Proves: │ │ Prover │ │ (snarkjs) │ +│ "I know secret│ │ (circom) │ │ │ +│ that hashes │ │ │ │ │ +│ to commitment)│ │ │ │ │ +└──────────────┘ └─────────────────┘ └─────────┬────────┘ + │ + ▼ +┌──────────────────┐ ┌─────────────────┐ ┌───────┴────────┐ +│ Trusted Oracle │◄───│ Verification │◄───│ Proof & │ +│ (Authorized │ │ Key (Stored │ │ Public Inputs │ +│ by Contract) │ │ On-Chain) │ │ │ +└────────┬─────────┘ └─────────────────┘ └────────────────┘ + │ + ▼ +┌──────────────────┐ +│ Soroban Identity │ +│ Contract │ +│ (Verifies via │ +│ Oracle) │ +└──────────────────┘ +``` + +### Phase 2: Native Verification (Future) + +When Soroban adds pairing-based cryptography host functions: + +``` +┌──────────────┐ ┌─────────────────┐ ┌──────────────────┐ +│ User │───►│ Off-chain │───►│ ZK-Prover │ +│ │ │ Prover │ │ (snarkjs) │ +└──────────────┘ └─────────────────┘ └─────────┬────────┘ + │ + ┌───────────────────────────────┼──────────────────────┐ + │ ▼ │ + │ ┌────────────────────────────────────────────┐ │ + │ │ On-Chain Verification (Native) │ │ + │ │ - Pairing checks via host functions │ │ + │ │ - Proof verification in WASM │ │ + │ └────────────────────┬─────────────────────────┘ │ + │ │ │ + ▼ ▼ │ +┌──────────────────┐ ┌─────────────────┐ ┌───────┴────────┐ │ +│ Soroban Identity │◄───│ Verification │◄───│ Proof & │ │ +│ Contract │ │ Key (Stored │ │ Public Inputs │ │ +│ │ │ On-Chain) │ │ │ │ +└──────────────────┘ └─────────────────┘ └────────────────┘ │ +``` + +### Phase 3: Full Trustless (Future) + +- Full verifier implemented in Soroban WASM +- No oracle dependency +- On-chain governance for verification key updates + +## Interim Oracle Bridge + +### Trust Minimization Strategies + +1. **Multi-Oracle Consensus**: Require 2-of-3 oracle signatures for verification +2. **Oracle Bonding**: Oracles must stake tokens; slashing for incorrect verifications +3. **Merkle Tree Attestations**: Oracles submit Merkle roots; anyone can challenge with inclusion proofs +4. **Time-Locked Updates**: Oracle signatures for verification key updates have 24h timelock +5. **Public Challenge Period**: 7-day window for anyone to dispute malicious verifications + +### Oracle Interface + +```rust +// Oracle submits verification result +pub fn oracle_verify_identity( + env: Env, + account: Address, + is_valid: bool, + proof_hash: BytesN<32>, + oracle_sig: BytesN<64>, +) -> Result<(), IdentityError> +``` + +## Circuit Design Sketch + +### Identity Commitment Circuit (Pseudocode) + +``` +// Circom circuit: identity_commitment.circom + +template IdentityCommitment() { + // Private inputs (known only to prover) + signal input secret; // User's secret value + + // Public inputs (known to verifier/contract) + signal input commitment; // SHA256 hash stored on-chain + signal input nullifier; // Prevents replay across circuits + + // Constraints + // 1. The secret must hash to the stored commitment + signal computed_hash; + computed_hash <-- HashSHA256(secret); + computed_hash === commitment; + + // 2. Nullifier is derived from secret (prevents double-spend across apps) + signal computed_nullifier; + computed_nullifier <-- HashSHA256(HashSHA256(secret)); + computed_nullifier === nullifier; +} + +component main = IdentityCommitment(); +``` + +### Circuit Application Flow + +1. **Registration**: User submits `sha256(secret)` as their identity commitment +2. **Proof Generation**: User generates ZK proof they know the secret +3. **Verification**: Contract/oracle verifies proof against stored commitment +4. **Nullifier Tracking**: Prevents same secret from being used across multiple protocols + +### Real-World Credential Circuit + +``` +// Extended circuit: kyc_credential.circom + +template KYCCredential() { + // Private inputs + signal input government_id_hash; + signal input expiry_date; + signal input proof_of_age_threshold; // e.g., 18 years + + // Public inputs + signal input min_age_satisfied; // 1 if over threshold, 0 otherwise + + // Constraint: Age must be verified + signal age_valid; + age_valid <-- DeriveAndVerifyAge(government_id_hash, expiry_date); + age_valid === min_age_satisfied; +} +``` + +## Roadmap + +### Phase 1: Oracle-Based (Months 1-3) + +- [ ] Deploy trusted oracle contract +- [ ] Store verification key hashes in Soroban +- [ ] Implement `oracle_verify_kyc` function +- [ ] Off-chain prover using snarkjs +- [ ] Circom circuit for basic identity commitment +- [ ] Multi-oracle consensus (2-of-3) + +### Phase 2: Native Support Wait (Months 3-12) + +- [ ] Monitor Soroban RFC for pairing host functions +- [ ] Contribute to specification discussions +- [ ] Prepare WASM verifier library +- [ ] Integration tests with experimental features + +### Phase 3: Full Trustless (Months 12+) + +- [ ] Replace oracle calls with native verification +- [ ] Remove oracle dependency from contract +- [ ] Enable governance-controlled key updates +- [ ] Add support for circuit upgrades + +## References + +### Soroban Documentation + +- [Soroban Host Functions](https://soroban.stellar.org/docs/reference/host-functions) +- [Soroban Cryptographic Primitives RFC](https://github.com/stellar/rfcs/blob/main/0000-soroban-crypto-primitives.md) +- [Stellar Ecosystem ZK Discussions](https://github.com/stellar/ecosystem-discussions) + +### ZK Libraries and Tools + +- [snarkjs](https://github.com/iden3/snarkjs) - JavaScript ZK toolkit +- [circom](https://docs.circom.io/) - ZK circuit language +- [GROTH16 Paper](https://eprint.iacr.org/2016/260.pdf) - Succinct arguments of knowledge +- [PLONK Paper](https://eprint.iacr.org/2019/1021.pdf) - Polynomial commitments + +### Axelar Integration + +- [Axelar Cross-Chain Messaging](https://docs.axelar.network/developers/axelarscan/overview) +- For oracle-based approach, oracle can run off-chain and submit results via Axelar to Soroban + +### Related Projects + +- [Semaphore](https://semaphore.appliedzkp.org/) - ZK identity on Ethereum +- [Proof of Personhood](https://papers.syntopia.org/popr) - Privacy-preserving identity +- [zkEVM](https://github.com/scroll-tech/zkevm-circuits) - ZK circuits for EVM + +## Implementation Notes + +The initial implementation should use the identity.rs contract with oracle-based verification. The `KycStatus` enum and account structure are designed to accommodate both interim and future phases, with the `Verified` status indicating successful ZK proof verification. \ No newline at end of file diff --git a/scripts/sandbox.sh b/scripts/sandbox.sh new file mode 100755 index 0000000..8cf64c0 --- /dev/null +++ b/scripts/sandbox.sh @@ -0,0 +1,262 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Script to spin up a local Soroban node, deploy contracts, and fund test wallets + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +ENV_FILE="${PROJECT_ROOT}/.env.sandbox" +CONTAINER_NAME="skillsphere-sandbox" +NETWORK_URL="http://localhost:8000" +FAUCET_URL="http://localhost:8000/faucet" +MAX_RETRIES=30 +RETRY_INTERVAL=1 + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +# Cleanup function for graceful shutdown +cleanup() { + log_info "Shutting down sandbox..." + docker stop "${CONTAINER_NAME}" 2>/dev/null || true + docker rm "${CONTAINER_NAME}" 2>/dev/null || true + if [[ -f "${ENV_FILE}" ]]; then + rm -f "${ENV_FILE}" + fi + exit 0 +} + +# Trap SIGINT/SIGTERM +trap cleanup SIGINT SIGTERM + +# Check for required tools +check_requirements() { + local missing_tools=() + + if ! command -v soroban &> /dev/null; then + missing_tools+=("soroban") + fi + + if ! command -v stellar &> /dev/null; then + missing_tools+=("stellar-cli") + fi + + if ! command -v docker &> /dev/null; then + missing_tools+=("docker") + fi + + if [[ ${#missing_tools[@]} -gt 0 ]]; then + log_error "Missing required tools: ${missing_tools[*]}" + log_error "Please install the missing tools and try again." + exit 1 + fi + + log_info "All required tools found." +} + +# Generate deterministic keypairs from fixed seeds +generate_keypairs() { + log_info "Generating deterministic test keypairs..." + + # ALICE key - fixed seed for reproducibility + ALICE_PUB=$(soroban keys generate --seed=27 Alice --show-address 2>/dev/null | grep "Public" | awk '{print $2}') + ALICE_SEC=$(soroban keys generate --seed=27 Alice --show-secret-from-seed 2>/dev/null | grep "Secret" | awk '{print $2}') + + # BOB key - fixed seed for reproducibility + BOB_PUB=$(soroban keys generate --seed=53 Bob --show-address 2>/dev/null | grep "Public" | awk '{print $2}') + BOB_SEC=$(soroban keys generate --seed=53 Bob --show-secret-from-seed 2>/dev/null | grep "Secret" | awk '{print $2}') + + # ADMIN key - fixed seed for reproducibility + ADMIN_PUB=$(soroban keys generate --seed=91 Admin --show-address 2>/dev/null | grep "Public" | awk '{print $2}') + ADMIN_SEC=$(soroban keys generate --seed=91 Admin --show-secret-from-seed 2>/dev/null | grep "Secret" | awk '{print $2}') + + # Alternative deterministic generation using openssl and soroban keys parse + if [[ -z "${ALICE_PUB}" ]]; then + # Use explicit key generation for sandbox + log_info "Using built-in key generation for test accounts..." + ALICE_PUB=$(soroban keys generate --global Alice 2>/dev/null && soroban keys address Alice) + ALICE_SEC="" + BOB_PUB=$(soroban keys generate --global Bob 2>/dev/null && soroban keys address Bob) + BOB_SEC="" + ADMIN_PUB=$(soroban keys generate --global Admin 2>/dev/null && soroban keys address Admin) + ADMIN_SEC="" + fi +} + +# Wait for node to be healthy +wait_for_node() { + log_info "Waiting for Soroban node to be ready..." + local retries=0 + + while [[ $retries -lt $MAX_RETRIES ]]; do + if curl -s "${NETWORK_URL}" > /dev/null 2>&1; then + log_info "Soroban node is healthy!" + return 0 + fi + retries=$((retries + 1)) + sleep "${RETRY_INTERVAL}" + done + + log_error "Node failed to start within $((MAX_RETRIES * RETRY_INTERVAL)) seconds" + exit 1 +} + +# Start local Docker node +start_docker_node() { + log_info "Starting local Soroban/Stellar standalone node..." + + # Stop existing container if running + docker stop "${CONTAINER_NAME}" 2>/dev/null || true + docker rm "${CONTAINER_NAME}" 2>/dev/null || true + + # Start the quickstart container + docker run -d \ + --name "${CONTAINER_NAME}" \ + -p 8000:8000 \ + -p 11626:11626 \ + -e STELLAR_DOCKER_ARGS="--allow-input-expiration --limit-peer=1000" \ + stellar/quickstart:latest --standalone & + + wait_for_node +} + +# Fund test accounts +fund_accounts() { + log_info "Funding test accounts..." + + # Create friendbot funding requests + curl -s "${FAUCET_URL}?${ALICE_PUB}" > /dev/null 2>&1 || true + curl -s "${FAUCET_URL}?${BOB_PUB}" > /dev/null 2>&1 || true + curl -s "${FAUCET_URL}?${ADMIN_PUB}" > /dev/null 2>&1 || true + + log_info "Accounts funded via friendbot." +} + +# Build all contracts +build_contracts() { + log_info "Building contracts..." + cd "${PROJECT_ROOT}/contracts" + cargo build --target wasm32-unknown-unknown --release + cd "${PROJECT_ROOT}" + log_info "Contracts built successfully." +} + +# Deploy contracts and capture IDs +deploy_contracts() { + log_info "Deploying contracts..." + + soroban network add --global sandbox local "${NETWORK_URL}" + + # Find all wasm files in target directory + CONTRACT_IDS=() + CONTRACT_NAMES=() + + while IFS= read -r wasm_file; do + if [[ -n "${wasm_file}" ]]; then + CONTRACT_NAME=$(basename "${wasm_file}" .wasm) + CONTRACT_NAMES+=("${CONTRACT_NAME}") + + DEPLOY_OUTPUT=$(soroban contract deploy \ + --source Alice \ + --wasm "${wasm_file}" \ + --network sandbox 2>&1) + + CONTRACT_ID=$(echo "${DEPLOY_OUTPUT}" | tail -1) + if [[ -n "${CONTRACT_ID}" ]]; then + CONTRACT_IDS+=("${CONTRACT_ID}") + log_info "Deployed ${CONTRACT_NAME}: ${CONTRACT_ID}" + fi + fi + done < <(find "${PROJECT_ROOT}/target/wasm32-unknown-unknown/release" -name "*.wasm" -type f 2>/dev/null) + + log_info "All contracts deployed." +} + +# Write environment file +write_env_file() { + log_info "Writing .env.sandbox file..." + + { + echo "# SkillSphere Sandbox Environment" + echo "# Generated on $(date -Iseconds)" + echo "" + echo "# Network Configuration" + echo "SOROBAN_NETWORK=sandbox" + echo "SOROBAN_RPC_URL=${NETWORK_URL}" + echo "" + echo "# Test Accounts (Public Keys)" + echo "ALICE_PUBLIC_KEY=${ALICE_PUB}" + echo "BOB_PUBLIC_KEY=${BOB_PUB}" + echo "ADMIN_PUBLIC_KEY=${ADMIN_PUB}" + echo "" + echo "# Test Accounts (Secret Keys - use with caution)" + echo "ALICE_SECRET_KEY=${ALICE_SEC}" + echo "BOB_SECRET_KEY=${BOB_SEC}" + echo "ADMIN_SECRET_KEY=${ADMIN_SEC}" + echo "" + echo "# Deployed Contracts" + for i in "${!CONTRACT_NAMES[@]}"; do + echo "${CONTRACT_NAMES[$i]^^}_CONTRACT_ID=${CONTRACT_IDS[$i]}" + done + } > "${ENV_FILE}" + + log_info "Environment file written to ${ENV_FILE}" +} + +# Print summary table +print_summary() { + echo "" + echo "========================================" + echo " SkillSphere Sandbox Summary" + echo "========================================" + echo "" + echo "Network: Sandbox (local)" + echo "RPC URL: ${NETWORK_URL}" + echo "" + echo "--- Funded Accounts ---" + printf " %-12s %s\n" "Alice:" "${ALICE_PUB}" + printf " %-12s %s\n" "Bob:" "${BOB_PUB}" + printf " %-12s %s\n" "Admin:" "${ADMIN_PUB}" + echo "" + echo "--- Deployed Contracts ---" + for i in "${!CONTRACT_NAMES[@]}"; do + printf " %-20s %s\n" "${CONTRACT_NAMES[$i]}:" "${CONTRACT_IDS[$i]}" + done + echo "" + echo "========================================" +} + +# Main execution +main() { + log_info "Starting SkillSphere sandbox setup..." + + check_requirements + generate_keypairs + start_docker_node + fund_accounts + build_contracts + deploy_contracts + write_env_file + print_summary + + log_info "Sandbox is running. Press Ctrl+C to stop." + # Keep script running to maintain container + wait +} + +main "$@" \ No newline at end of file