From 38e6ba1518b2027c3d07005e1bd72f224e21b0dc Mon Sep 17 00:00:00 2001 From: Collins C Augustine Date: Thu, 23 Apr 2026 12:44:12 +0100 Subject: [PATCH] feat: implemented the SEP-10 challenge verification feature --- contracts/game_contract/src/lib.rs | 91 ++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/contracts/game_contract/src/lib.rs b/contracts/game_contract/src/lib.rs index fb7aa38..4ae0a0a 100644 --- a/contracts/game_contract/src/lib.rs +++ b/contracts/game_contract/src/lib.rs @@ -63,6 +63,10 @@ const FEE_BIPS: Symbol = symbol_short!("FEE_BIPS"); // u32 (0–1000, i.e. 0– const TREASURY_ADDR: Symbol = symbol_short!("TR_ADDR"); // Address const CONTRACT_ADMIN: Symbol = symbol_short!("CT_ADMIN"); // Address +// SEP-10 / Node Identity (#4) +const AUTH_NONCE: Symbol = symbol_short!("AUTH_NONC"); // Map +const NODE_REG: Symbol = symbol_short!("NODE_REG"); // Map, u64> + // ──────────────────────────────────────────────────────────────────────────── // Errors // ──────────────────────────────────────────────────────────────────────────── @@ -86,6 +90,8 @@ pub enum ContractError { /// Invalid or already-used backend signature (#199) Unauthorized = 14, StakeLimitExceeded = 15, + /// Timed out SEP-10 authentication (#4) + AuthTimeout = 16, } #[contract] @@ -748,6 +754,91 @@ impl GameContract { pub fn treasury_balance(env: Env) -> i128 { env.storage().instance().get(&TREASURY).unwrap_or(0) } + + // ── #4 – Verification of SEP-10 challenge logic ────────────────────────── + // + // Verifies a cryptographic challenge signed by a node for authentication. + // This provides on-chain node identity tracking for game settlement and + // off-chain workers based on the SEP-10 challenge-response logic. + // + // Signature payload (SHA-256 pre-image): + // "SEP10" || node_pubkey_bytes || nonce_le_8bytes || timestamp_le_8bytes + pub fn verify_sep10_challenge( + env: Env, + node_pubkey: BytesN<32>, + nonce: u64, + timestamp: u64, + signature: BytesN<64>, + ) -> Result<(), ContractError> { + // 1. Time bounds verification + // Soroban ledger timestamp is in seconds since epoch + let current_time = env.ledger().timestamp(); + // Allow a 5-minute (300 seconds) valid time window + if current_time > timestamp + 300 || current_time < timestamp.saturating_sub(300) { + return Err(ContractError::AuthTimeout); + } + + // 2. Replay protection + let mut nonces: Map = env + .storage() + .instance() + .get(&AUTH_NONCE) + .unwrap_or(Map::new(&env)); + + if nonces.get(nonce).unwrap_or(false) { + return Err(ContractError::Unauthorized); + } + + // 3. Payload construction and Signature verification + let mut payload_bytes = Bytes::new(&env); + + let prefix = Bytes::from_slice(&env, b"SEP10"); + payload_bytes.append(&prefix); + + payload_bytes.append(&node_pubkey.clone().into()); + + let nonce_le: [u8; 8] = nonce.to_le_bytes(); + payload_bytes.append(&Bytes::from_slice(&env, &nonce_le)); + + let timestamp_le: [u8; 8] = timestamp.to_le_bytes(); + payload_bytes.append(&Bytes::from_slice(&env, ×tamp_le)); + + let digest_bytesn: BytesN<32> = env.crypto().sha256(&payload_bytes).into(); + let digest_bytes: Bytes = digest_bytesn.into(); + + // ed25519_verify panics if the signature is invalid + env.crypto() + .ed25519_verify(&node_pubkey, &digest_bytes, &signature); + + // 4. Mark challenge as used + nonces.set(nonce, true); + env.storage().instance().set(&AUTH_NONCE, &nonces); + + // 5. Register node identity for future on-chain tracking + let mut registry: Map, u64> = env + .storage() + .instance() + .get(&NODE_REG) + .unwrap_or(Map::new(&env)); + registry.set(node_pubkey.clone(), current_time); + env.storage().instance().set(&NODE_REG, ®istry); + + // 6. Emit an event for analytics and off-chain indexing + env.events() + .publish((symbol_short!("node_auth"), node_pubkey), timestamp); + + Ok(()) + } + + /// Helper to get a node's last SEP-10 authentication timestamp. + pub fn get_node_auth(env: Env, node_pubkey: BytesN<32>) -> u64 { + let registry: Map, u64> = env + .storage() + .instance() + .get(&NODE_REG) + .unwrap_or(Map::new(&env)); + registry.get(node_pubkey).unwrap_or(0) + } } // ────────────────────────────────────────────────────────────────────────────