diff --git a/Cargo.toml b/Cargo.toml index 73d3819c..9e8f6d86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ river = { path = "cli", package = "river" } # Freenet dependencies freenet-scaffold = "0.2.1" freenet-scaffold-macro = "0.2.1" -freenet-stdlib = { version = "0.1.30", features = ["contract"] } +freenet-stdlib = { version = "0.1.32", features = ["contract"] } [workspace.package] version = "0.1.2" diff --git a/delegates/chat-delegate/src/context.rs b/delegates/chat-delegate/src/context.rs index ab52e2ef..de3a9d0b 100644 --- a/delegates/chat-delegate/src/context.rs +++ b/delegates/chat-delegate/src/context.rs @@ -1,46 +1,7 @@ use super::*; -/// Context for the chat delegate, storing pending operations. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub(super) struct ChatDelegateContext { - /// Map of secret IDs to pending operations - pub(super) pending_ops: HashMap, -} - -/// Structure to store the index of keys for an app +/// Structure to store the index of keys for an origin #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub(super) struct KeyIndex { pub(super) keys: Vec, } - -impl TryFrom for ChatDelegateContext { - type Error = DelegateError; - - fn try_from(value: DelegateContext) -> Result { - if value == DelegateContext::default() { - return Ok(Self::default()); - } - ciborium::from_reader(value.as_ref()) - .map_err(|err| DelegateError::Deser(format!("Failed to deserialize context: {err}"))) - } -} - -impl TryFrom<&ChatDelegateContext> for DelegateContext { - type Error = DelegateError; - - fn try_from(value: &ChatDelegateContext) -> Result { - let mut buffer = Vec::new(); - ciborium::ser::into_writer(value, &mut buffer) - .map_err(|err| DelegateError::Deser(format!("Failed to serialize context: {err}")))?; - Ok(DelegateContext::new(buffer)) - } -} - -impl TryFrom<&mut ChatDelegateContext> for DelegateContext { - type Error = DelegateError; - - fn try_from(value: &mut ChatDelegateContext) -> Result { - // Delegate to the immutable reference implementation - Self::try_from(&*value) - } -} diff --git a/delegates/chat-delegate/src/handlers.rs b/delegates/chat-delegate/src/handlers.rs index 0539a0d0..ca4e87dc 100644 --- a/delegates/chat-delegate/src/handlers.rs +++ b/delegates/chat-delegate/src/handlers.rs @@ -1,15 +1,14 @@ use super::*; use ed25519_dalek::{Signer, SigningKey}; -use freenet_stdlib::prelude::ContractInstanceId; +use freenet_stdlib::prelude::{ContractInstanceId, DelegateCtx}; use river_core::chat_delegate::{RequestId, RoomKey}; -/// Handle an application message +/// Handle an application message using the host function API for direct secret access. pub(crate) fn handle_application_message( + ctx: &mut DelegateCtx, app_msg: ApplicationMessage, origin: &Origin, ) -> Result, DelegateError> { - let mut context = ChatDelegateContext::try_from(app_msg.context)?; - // Deserialize the request message let request: ChatDelegateRequestMsg = ciborium::from_reader(app_msg.payload.as_slice()) .map_err(|e| DelegateError::Deser(format!("Failed to deserialize request: {e}")))?; @@ -24,19 +23,19 @@ pub(crate) fn handle_application_message( ) .as_str(), ); - handle_store_request(&mut context, origin, key, value, app_msg.app) + handle_store_request(ctx, origin, key, value, app_msg.app) } ChatDelegateRequestMsg::GetRequest { key } => { logging::info(format!("Delegate received GetRequest key: {key:?}").as_str()); - handle_get_request(&mut context, origin, key, app_msg.app) + handle_get_request(ctx, origin, key, app_msg.app) } ChatDelegateRequestMsg::DeleteRequest { key } => { logging::info(format!("Delegate received DeleteRequest key: {key:?}").as_str()); - handle_delete_request(&mut context, origin, key, app_msg.app) + handle_delete_request(ctx, origin, key, app_msg.app) } ChatDelegateRequestMsg::ListRequest => { logging::info("Delegate received ListRequest"); - handle_list_request(&mut context, origin, app_msg.app) + handle_list_request(ctx, origin, app_msg.app) } // Signing key management @@ -47,19 +46,13 @@ pub(crate) fn handle_application_message( logging::info( format!("Delegate received StoreSigningKey for room: {room_key:?}").as_str(), ); - handle_store_signing_key( - &mut context, - origin, - room_key, - signing_key_bytes, - app_msg.app, - ) + handle_store_signing_key(ctx, origin, room_key, signing_key_bytes, app_msg.app) } ChatDelegateRequestMsg::GetPublicKey { room_key } => { logging::info( format!("Delegate received GetPublicKey for room: {room_key:?}").as_str(), ); - handle_get_public_key(&mut context, origin, room_key, app_msg.app) + handle_get_public_key(ctx, origin, room_key, app_msg.app) } // Signing operations - all include request_id for correlation @@ -70,7 +63,7 @@ pub(crate) fn handle_application_message( } => { logging::info(format!("Delegate received SignMessage for room: {room_key:?}").as_str()); handle_sign_request( - &mut context, + ctx, origin, room_key, request_id, @@ -84,14 +77,7 @@ pub(crate) fn handle_application_message( member_bytes, } => { logging::info(format!("Delegate received SignMember for room: {room_key:?}").as_str()); - handle_sign_request( - &mut context, - origin, - room_key, - request_id, - member_bytes, - app_msg.app, - ) + handle_sign_request(ctx, origin, room_key, request_id, member_bytes, app_msg.app) } ChatDelegateRequestMsg::SignBan { room_key, @@ -99,14 +85,7 @@ pub(crate) fn handle_application_message( ban_bytes, } => { logging::info(format!("Delegate received SignBan for room: {room_key:?}").as_str()); - handle_sign_request( - &mut context, - origin, - room_key, - request_id, - ban_bytes, - app_msg.app, - ) + handle_sign_request(ctx, origin, room_key, request_id, ban_bytes, app_msg.app) } ChatDelegateRequestMsg::SignConfig { room_key, @@ -114,14 +93,7 @@ pub(crate) fn handle_application_message( config_bytes, } => { logging::info(format!("Delegate received SignConfig for room: {room_key:?}").as_str()); - handle_sign_request( - &mut context, - origin, - room_key, - request_id, - config_bytes, - app_msg.app, - ) + handle_sign_request(ctx, origin, room_key, request_id, config_bytes, app_msg.app) } ChatDelegateRequestMsg::SignMemberInfo { room_key, @@ -132,7 +104,7 @@ pub(crate) fn handle_application_message( format!("Delegate received SignMemberInfo for room: {room_key:?}").as_str(), ); handle_sign_request( - &mut context, + ctx, origin, room_key, request_id, @@ -148,14 +120,7 @@ pub(crate) fn handle_application_message( logging::info( format!("Delegate received SignSecretVersion for room: {room_key:?}").as_str(), ); - handle_sign_request( - &mut context, - origin, - room_key, - request_id, - record_bytes, - app_msg.app, - ) + handle_sign_request(ctx, origin, room_key, request_id, record_bytes, app_msg.app) } ChatDelegateRequestMsg::SignEncryptedSecret { room_key, @@ -165,14 +130,7 @@ pub(crate) fn handle_application_message( logging::info( format!("Delegate received SignEncryptedSecret for room: {room_key:?}").as_str(), ); - handle_sign_request( - &mut context, - origin, - room_key, - request_id, - secret_bytes, - app_msg.app, - ) + handle_sign_request(ctx, origin, room_key, request_id, secret_bytes, app_msg.app) } ChatDelegateRequestMsg::SignUpgrade { room_key, @@ -181,7 +139,7 @@ pub(crate) fn handle_application_message( } => { logging::info(format!("Delegate received SignUpgrade for room: {room_key:?}").as_str()); handle_sign_request( - &mut context, + ctx, origin, room_key, request_id, @@ -192,358 +150,136 @@ pub(crate) fn handle_application_message( } } -/// Handle a store request -pub(crate) fn handle_store_request( - context: &mut ChatDelegateContext, +// ============================================================================ +// Key-Value Storage Handlers +// ============================================================================ + +/// Handle a store request - stores value and updates the index +fn handle_store_request( + ctx: &mut DelegateCtx, origin: &Origin, key: ChatDelegateKey, value: Vec, app: ContractInstanceId, ) -> Result, DelegateError> { - // Create a unique key for this app's data - let secret_id = create_origin_key(origin, &key); - - // Create the index key + // Create a unique key for this origin's data + let secret_key = create_origin_key(origin, &key); let index_key = create_index_key(origin); - // Store the original request in context for later processing after we get the index - context.pending_ops.insert( - SecretIdKey::from(&index_key), - PendingOperation::Store { - origin: origin.clone(), - client_key: key.clone(), - }, - ); + // Store the value directly via host function + // Note: In WASM, set_secret returns true on success. In non-WASM tests, it always returns false. + #[cfg(target_family = "wasm")] + if !ctx.set_secret(&secret_key, &value) { + return Err(DelegateError::Other( + "Failed to store secret via host function".into(), + )); + } + #[cfg(not(target_family = "wasm"))] + let _ = ctx.set_secret(&secret_key, &value); + + logging::info(&format!( + "Stored secret with key length {}", + secret_key.len() + )); + + // Update the key index + let mut key_index = get_key_index(ctx, &index_key); + if !key_index.keys.contains(&key) { + key_index.keys.push(key.clone()); + set_key_index(ctx, &index_key, &key_index)?; + logging::info(&format!( + "Added key to index, now has {} keys", + key_index.keys.len() + )); + } // Create response for the client let response = ChatDelegateResponseMsg::StoreResponse { - key: key.clone(), + key, result: Ok(()), value_size: value.len(), }; - // Serialize context - let context_bytes = DelegateContext::try_from(&*context)?; - - // Create the three messages we need to send: - // 1. Response to the client - let app_response = create_app_response(&response, &context_bytes, app)?; - - // 2. Store the actual value - let set_secret = OutboundDelegateMsg::SetSecretRequest(SetSecretRequest { - key: secret_id, - value: Some(value), - }); - - // 3. Request the current index to update it - let get_index = create_get_index_request(index_key, &context_bytes)?; - - // Return all messages - Ok(vec![app_response, set_secret, get_index]) + Ok(vec![create_app_response(&response, app)?]) } -/// Handle a get request -pub(crate) fn handle_get_request( - context: &mut ChatDelegateContext, +/// Handle a get request - retrieves value directly +fn handle_get_request( + ctx: &mut DelegateCtx, origin: &Origin, key: ChatDelegateKey, - app: ContractInstanceId, // Add app parameter + app: ContractInstanceId, ) -> Result, DelegateError> { - // Create a unique key for this origin contract's data - let secret_id = create_origin_key(origin, &key); - - // Store the original request in context for later processing - context.pending_ops.insert( - SecretIdKey::from(&secret_id), - PendingOperation::Get { - origin: origin.clone(), - client_key: key.clone(), - app, // Store the passed app identifier - }, - ); - - // Serialize context - let context_bytes = DelegateContext::try_from(&*context)?; - - // Create and return the get request - let get_secret = create_get_request(secret_id, &context_bytes)?; - - Ok(vec![get_secret]) + // Create a unique key for this origin's data + let secret_key = create_origin_key(origin, &key); + + // Get the value directly via host function + let value = ctx.get_secret(&secret_key); + logging::info(&format!( + "Retrieved secret, value present: {}", + value.is_some() + )); + + // Create response for the client + let response = ChatDelegateResponseMsg::GetResponse { key, value }; + + Ok(vec![create_app_response(&response, app)?]) } -/// Handle a delete request -pub(crate) fn handle_delete_request( - context: &mut ChatDelegateContext, +/// Handle a delete request - removes value and updates the index +fn handle_delete_request( + ctx: &mut DelegateCtx, origin: &Origin, key: ChatDelegateKey, app: ContractInstanceId, ) -> Result, DelegateError> { - // Create a unique key for this app's data - let secret_id = create_origin_key(origin, &key); - - // Create the index key + // Create keys + let secret_key = create_origin_key(origin, &key); let index_key = create_index_key(origin); - // Store the original request in context for later processing after we get the index - context.pending_ops.insert( - SecretIdKey::from(&index_key), - PendingOperation::Delete { - origin: origin.clone(), - client_key: key.clone(), - }, - ); + // Remove the secret via host function + ctx.remove_secret(&secret_key); + logging::info("Removed secret"); + + // Update the key index + let mut key_index = get_key_index(ctx, &index_key); + key_index.keys.retain(|k| k != &key); + set_key_index(ctx, &index_key, &key_index)?; + logging::info(&format!( + "Removed key from index, now has {} keys", + key_index.keys.len() + )); // Create response for the client let response = ChatDelegateResponseMsg::DeleteResponse { - key: key.clone(), + key, result: Ok(()), }; - // Serialize context - let context_bytes = DelegateContext::try_from(&*context)?; - - // Create the three messages we need to send: - // 1. Response to the client - let app_response = create_app_response(&response, &context_bytes, app)?; - - // 2. Delete the actual value - let set_secret = OutboundDelegateMsg::SetSecretRequest(SetSecretRequest { - key: secret_id, - value: None, // Setting to None deletes the secret - }); - - // 3. Request the current index to update it - let get_index = create_get_index_request(index_key, &context_bytes)?; - - // Return all messages - Ok(vec![app_response, set_secret, get_index]) + Ok(vec![create_app_response(&response, app)?]) } -/// Handle a list request -pub(crate) fn handle_list_request( - context: &mut ChatDelegateContext, +/// Handle a list request - returns all keys for this origin +fn handle_list_request( + ctx: &mut DelegateCtx, origin: &Origin, - id: ContractInstanceId, + app: ContractInstanceId, ) -> Result, DelegateError> { - // Create the index key let index_key = create_index_key(origin); - // Store a special marker in the context to indicate this is a list request - context.pending_ops.insert( - SecretIdKey::from(&index_key), - PendingOperation::List { - origin: origin.clone(), - app: id, // Store the app identifier (parameter name is 'id' here) - }, - ); - - // Serialize context - let context_bytes = DelegateContext::try_from(&*context)?; - - // Create and return the get index request - let get_index = create_get_index_request(index_key, &context_bytes)?; - - Ok(vec![get_index]) -} - -/// Handle a get secret response -pub(crate) fn handle_get_secret_response( - get_secret_response: freenet_stdlib::prelude::GetSecretResponse, -) -> Result, DelegateError> { - logging::info("Received GetSecretResponse"); - - // Deserialize context - let mut context = match ChatDelegateContext::try_from(get_secret_response.context.clone()) { - Ok(ctx) => ctx, - Err(e) => { - logging::info(&format!("Failed to deserialize context: {e}")); - return Err(e); - } - }; + // Get the key index directly + let key_index = get_key_index(ctx, &index_key); + logging::info(&format!( + "Returning list with {} keys", + key_index.keys.len() + )); - // Get the key as a string to check its type - let key_str = String::from_utf8_lossy(get_secret_response.key.key()).to_string(); - let key_clone = get_secret_response.key.clone(); - - logging::info(&format!("Processing response for key: {key_str}")); - - // Route based on key type - let result = if key_str.ends_with(KEY_INDEX_SUFFIX) { - logging::info("This is a key index response"); - handle_key_index_response(&key_clone, &mut context, get_secret_response) - } else if key_str.starts_with("signing_key:") { - logging::info("This is a signing key response"); - handle_signing_get_response(&key_clone, &mut context, get_secret_response) - } else { - logging::info("This is a regular get response"); - handle_regular_get_response(&key_clone, &mut context, get_secret_response) + // Create response for the client + let response = ChatDelegateResponseMsg::ListResponse { + keys: key_index.keys, }; - match &result { - Ok(msgs) => logging::info(&format!("Returning {} messages", msgs.len())), - Err(e) => logging::info(&format!("Error handling response: {e}")), - } - - result -} - -/// Handle a key index response -pub(crate) fn handle_key_index_response( - secret_id: &SecretsId, - context: &mut ChatDelegateContext, - get_secret_response: freenet_stdlib::prelude::GetSecretResponse, -) -> Result, DelegateError> { - logging::info("Handling key index response"); - - // This is a response to a key index request - let secret_id_key = SecretIdKey::from(secret_id); - if let Some(pending_op) = context.pending_ops.get(&secret_id_key).cloned() { - let mut outbound_msgs = Vec::new(); - - // Parse the key index or create a new one if it doesn't exist - let mut key_index = if let Some(index_data) = &get_secret_response.value { - ciborium::from_reader::(index_data.as_slice()).unwrap_or_else(|e| { - logging::info(&format!( - "Failed to deserialize key index, creating new one: {e}" - )); - KeyIndex::default() - }) - } else { - logging::info("No index data found, creating new index"); - KeyIndex::default() - }; - - match &pending_op { - PendingOperation::List { app, .. } => { - // Extract app here - // Create list response - let response = ChatDelegateResponseMsg::ListResponse { - keys: key_index.keys.clone(), - }; - - // Remove the pending operation *before* creating the response - context.pending_ops.remove(&secret_id_key); - - // Create response message using the *updated* context - let context_bytes = DelegateContext::try_from(&*context)?; - // Pass the retrieved app identifier - let app_response = create_app_response(&response, &context_bytes, *app)?; - outbound_msgs.push(app_response); - logging::info(&format!( - "Created list response with {} keys", - key_index.keys.len() - )); - } - PendingOperation::Store { client_key, .. } - | PendingOperation::Delete { client_key, .. } => { - // This is a store or delete operation that needs to update the index - let is_delete = pending_op.is_delete_operation(); - - if is_delete { - // For delete operations, remove the key - key_index.keys.retain(|k| k != client_key); - logging::info(&format!( - "Removed key from index, now has {} keys", - key_index.keys.len() - )); - } else { - // For store operations, add the key if it doesn't exist - if !key_index.keys.contains(client_key) { - key_index.keys.push(client_key.clone()); - logging::info(&format!( - "Added key to index, now has {} keys", - key_index.keys.len() - )); - } else { - logging::info("Key already exists in index, not adding"); - } - } - - // Serialize the updated index - let mut index_bytes = Vec::new(); - ciborium::ser::into_writer(&key_index, &mut index_bytes).map_err(|e| { - DelegateError::Deser(format!("Failed to serialize key index: {e}")) - })?; - - // Create set secret request to update the index - let set_index = OutboundDelegateMsg::SetSecretRequest(SetSecretRequest { - key: secret_id.clone(), - value: Some(index_bytes), - }); - - outbound_msgs.push(set_index); - } - PendingOperation::Get { .. } => { - return Err(DelegateError::Other( - "Unexpected Get operation for key index response".to_string(), - )); - } - PendingOperation::GetPublicKey { .. } | PendingOperation::Sign { .. } => { - return Err(DelegateError::Other( - "Unexpected signing operation for key index response".to_string(), - )); - } - } - - // Remove the pending operation (moved inside List case, Store/Delete need further refactoring for Bug #1) - context.pending_ops.remove(&secret_id_key); - - logging::info(&format!( - "Returning {} outbound messages", - outbound_msgs.len() - )); - Ok(outbound_msgs) - } else { - // No pending operation for this key index - logging::info(&format!("No pending key index request for: {secret_id:?}")); - Err(DelegateError::Other(format!( - "No pending key index request for: {secret_id:?}" - ))) - } -} - -/// Handle a regular get response -pub(crate) fn handle_regular_get_response( - secret_id: &SecretsId, - context: &mut ChatDelegateContext, - get_secret_response: freenet_stdlib::prelude::GetSecretResponse, -) -> Result, DelegateError> { - logging::info("Handling regular get response"); - - let secret_id_key = SecretIdKey::from(secret_id); - // Extract app along with client_key - if let Some(PendingOperation::Get { - client_key, app, .. - }) = context.pending_ops.get(&secret_id_key).cloned() - { - // Create response - let response = ChatDelegateResponseMsg::GetResponse { - key: client_key.clone(), - value: get_secret_response.value.clone(), - }; - - // Remove the pending get request *before* creating the response - context.pending_ops.remove(&secret_id_key); - - // Create response message using the *updated* context - let context_bytes = DelegateContext::try_from(&*context)?; - // Pass the retrieved app identifier - let app_response = create_app_response(&response, &context_bytes, app)?; - - logging::info(&format!( - "Returning get response for key: {:?}, value present: {}, to app: {:?}", - client_key, - get_secret_response.value.is_some(), - app // Log the target app - )); - Ok(vec![app_response]) - } else { - let key_str = String::from_utf8_lossy(secret_id.key()).to_string(); - logging::info(&format!("No pending get request for key: {key_str}")); - Err(DelegateError::Other(format!( - "No pending get request for key: {key_str}" - ))) - } + Ok(vec![create_app_response(&response, app)?]) } // ============================================================================ @@ -572,25 +308,36 @@ pub(crate) fn handle_regular_get_response( // // ============================================================================ -/// Create a secret ID for storing a signing key for a room. +/// Create a secret key for storing a signing key for a room. /// Format: "signing_key:{origin_base58}:{room_key_base58}" -fn create_signing_key_secret_id(origin: &Origin, room_key: &RoomKey) -> SecretsId { +fn create_signing_key_secret_key(origin: &Origin, room_key: &RoomKey) -> Vec { let origin_b58 = bs58::encode(&origin.0).into_string(); let room_key_b58 = bs58::encode(room_key).into_string(); - let key = format!("signing_key:{origin_b58}:{room_key_b58}"); - SecretsId::new(key.into_bytes()) + format!("signing_key:{origin_b58}:{room_key_b58}").into_bytes() } /// Handle a store signing key request -pub(crate) fn handle_store_signing_key( - context: &mut ChatDelegateContext, +fn handle_store_signing_key( + ctx: &mut DelegateCtx, origin: &Origin, room_key: RoomKey, signing_key_bytes: [u8; 32], app: ContractInstanceId, ) -> Result, DelegateError> { - // Create the secret ID for storing the signing key - let secret_id = create_signing_key_secret_id(origin, &room_key); + let secret_key = create_signing_key_secret_key(origin, &room_key); + + // Store the signing key directly via host function + // Note: In WASM, set_secret returns true on success. In non-WASM tests, it always returns false. + #[cfg(target_family = "wasm")] + if !ctx.set_secret(&secret_key, &signing_key_bytes) { + return Err(DelegateError::Other( + "Failed to store signing key via host function".into(), + )); + } + #[cfg(not(target_family = "wasm"))] + let _ = ctx.set_secret(&secret_key, &signing_key_bytes); + + logging::info("Stored signing key for room"); // Create response for the client let response = ChatDelegateResponseMsg::StoreSigningKeyResponse { @@ -598,210 +345,130 @@ pub(crate) fn handle_store_signing_key( result: Ok(()), }; - // Serialize context - let context_bytes = DelegateContext::try_from(&*context)?; - - // Create the response message - let app_response = create_app_response(&response, &context_bytes, app)?; - - // Store the signing key in the secret storage - let set_secret = OutboundDelegateMsg::SetSecretRequest(SetSecretRequest { - key: secret_id, - value: Some(signing_key_bytes.to_vec()), - }); - - logging::info(&format!( - "Storing signing key for room, secret storage requested" - )); - - Ok(vec![app_response, set_secret]) + Ok(vec![create_app_response(&response, app)?]) } /// Handle a get public key request -pub(crate) fn handle_get_public_key( - context: &mut ChatDelegateContext, +fn handle_get_public_key( + ctx: &mut DelegateCtx, origin: &Origin, room_key: RoomKey, app: ContractInstanceId, ) -> Result, DelegateError> { - // Create the secret ID for the signing key - let secret_id = create_signing_key_secret_id(origin, &room_key); - - // Store the pending operation in context - context.pending_ops.insert( - SecretIdKey::from(&secret_id), - PendingOperation::GetPublicKey { - origin: origin.clone(), - room_key, - app, - }, - ); - - // Serialize context - let context_bytes = DelegateContext::try_from(&*context)?; + let secret_key = create_signing_key_secret_key(origin, &room_key); + + // Get the signing key directly via host function + let public_key = ctx.get_secret(&secret_key).and_then(|sk_bytes| { + if sk_bytes.len() == 32 { + let sk_array: [u8; 32] = sk_bytes.try_into().ok()?; + let signing_key = SigningKey::from_bytes(&sk_array); + Some(signing_key.verifying_key().to_bytes()) + } else { + None + } + }); - // Create request to get the signing key - let get_secret = create_get_request(secret_id, &context_bytes)?; + logging::info(&format!( + "Retrieved public key for room, key present: {}", + public_key.is_some() + )); - logging::info("Requesting signing key from secret storage"); + // Create response for the client + let response = ChatDelegateResponseMsg::GetPublicKeyResponse { + room_key, + public_key, + }; - Ok(vec![get_secret]) + Ok(vec![create_app_response(&response, app)?]) } /// Handle a sign request (for any signable type) -pub(crate) fn handle_sign_request( - context: &mut ChatDelegateContext, +fn handle_sign_request( + ctx: &mut DelegateCtx, origin: &Origin, room_key: RoomKey, request_id: RequestId, data_to_sign: Vec, app: ContractInstanceId, ) -> Result, DelegateError> { - // Create the secret ID for the signing key - let secret_id = create_signing_key_secret_id(origin, &room_key); - - // Store the pending operation in context with the data to sign and request_id - context.pending_ops.insert( - SecretIdKey::from(&secret_id), - PendingOperation::Sign { - origin: origin.clone(), - room_key, - request_id, - data_to_sign, - app, - }, - ); + let secret_key = create_signing_key_secret_key(origin, &room_key); + + // Get the signing key and sign directly + let signature: Result, String> = match ctx.get_secret(&secret_key) { + Some(sk_bytes) => { + if sk_bytes.len() == 32 { + // Safe: we just checked length is 32 + let sk_array: [u8; 32] = sk_bytes.try_into().expect("length verified"); + let signing_key = SigningKey::from_bytes(&sk_array); + let sig = signing_key.sign(&data_to_sign); + Ok(sig.to_bytes().to_vec()) + } else { + Err(format!( + "Invalid signing key length: expected 32, got {}", + sk_bytes.len() + )) + } + } + None => Err("Signing key not found for this room".to_string()), + }; - // Serialize context - let context_bytes = DelegateContext::try_from(&*context)?; + logging::info(&format!( + "Sign request for room, signature created: {}", + signature.is_ok() + )); + + // Create response for the client + let response = ChatDelegateResponseMsg::SignResponse { + room_key, + request_id, + signature, + }; - // Create request to get the signing key - let get_secret = create_get_request(secret_id, &context_bytes)?; + Ok(vec![create_app_response(&response, app)?]) +} - logging::info("Requesting signing key from secret storage for signing"); +// ============================================================================ +// Helper Functions +// ============================================================================ - Ok(vec![get_secret]) +/// Get the key index from secrets, or return empty if not found +fn get_key_index(ctx: &mut DelegateCtx, index_key: &[u8]) -> KeyIndex { + ctx.get_secret(index_key) + .and_then(|data| ciborium::from_reader::(data.as_slice()).ok()) + .unwrap_or_default() } -/// Handle a get secret response for signing-related operations -pub(crate) fn handle_signing_get_response( - secret_id: &SecretsId, - context: &mut ChatDelegateContext, - get_secret_response: freenet_stdlib::prelude::GetSecretResponse, -) -> Result, DelegateError> { - let secret_id_key = SecretIdKey::from(secret_id); - - // Check if this is a GetPublicKey or Sign operation - if let Some(pending_op) = context.pending_ops.get(&secret_id_key).cloned() { - match pending_op { - PendingOperation::GetPublicKey { room_key, app, .. } => { - // Get the public key from the stored signing key - let public_key = if let Some(sk_bytes) = get_secret_response.value { - if sk_bytes.len() == 32 { - let sk_array: [u8; 32] = sk_bytes.try_into().map_err(|_| { - DelegateError::Other("Invalid signing key length".to_string()) - })?; - let signing_key = SigningKey::from_bytes(&sk_array); - Some(signing_key.verifying_key().to_bytes()) - } else { - None - } - } else { - None - }; - - // Create response - let response = ChatDelegateResponseMsg::GetPublicKeyResponse { - room_key, - public_key, - }; - - // Remove the pending operation - context.pending_ops.remove(&secret_id_key); - - // Serialize context - let context_bytes = DelegateContext::try_from(&*context)?; - let app_response = create_app_response(&response, &context_bytes, app)?; - - logging::info(&format!( - "Returning public key for room: {:?}, key present: {}", - room_key, - public_key.is_some() - )); - - Ok(vec![app_response]) - } - PendingOperation::Sign { - room_key, - request_id, - data_to_sign, - app, - .. - } => { - // Sign the data using the stored signing key - let signature = if let Some(sk_bytes) = get_secret_response.value { - if sk_bytes.len() == 32 { - let sk_array: [u8; 32] = sk_bytes.try_into().map_err(|_| { - DelegateError::Other("Invalid signing key length".to_string()) - })?; - let signing_key = SigningKey::from_bytes(&sk_array); - let sig = signing_key.sign(&data_to_sign); - Ok(sig.to_bytes().to_vec()) - } else { - Err(format!( - "Invalid signing key length: {} bytes (expected 32)", - sk_bytes.len() - )) - } - } else { - Err("Signing key not found for room".to_string()) - }; - - // Create response with request_id for correlation - let response = ChatDelegateResponseMsg::SignResponse { - room_key, - request_id, - signature, - }; - - // Remove the pending operation - context.pending_ops.remove(&secret_id_key); - - // Serialize context - let context_bytes = DelegateContext::try_from(&*context)?; - let app_response = create_app_response(&response, &context_bytes, app)?; - - logging::info(&format!("Returning signature for room: {:?}", room_key)); - - Ok(vec![app_response]) - } - _ => { - // Not a signing operation, return error - logging::info(&format!( - "Unexpected pending operation type for signing response: {:?}", - secret_id - )); - Err(DelegateError::Other(format!( - "Unexpected pending operation type for: {:?}", - secret_id - ))) - } - } - } else { - let key_str = String::from_utf8_lossy(secret_id.key()).to_string(); - logging::info(&format!("No pending signing operation for key: {key_str}")); - Err(DelegateError::Other(format!( - "No pending signing operation for key: {key_str}" - ))) +/// Set the key index in secrets +fn set_key_index( + ctx: &mut DelegateCtx, + index_key: &[u8], + key_index: &KeyIndex, +) -> Result<(), DelegateError> { + let mut index_bytes = Vec::new(); + ciborium::ser::into_writer(key_index, &mut index_bytes) + .map_err(|e| DelegateError::Deser(format!("Failed to serialize key index: {e}")))?; + + // Note: In WASM, set_secret returns true on success. In non-WASM tests, it always returns false. + #[cfg(target_family = "wasm")] + if !ctx.set_secret(index_key, &index_bytes) { + return Err(DelegateError::Other( + "Failed to store key index via host function".into(), + )); } + #[cfg(not(target_family = "wasm"))] + let _ = ctx.set_secret(index_key, &index_bytes); + + Ok(()) } +// ============================================================================ +// Tests +// ============================================================================ + #[cfg(test)] mod tests { use super::*; - use crate::models::*; - use crate::utils::*; - use freenet_stdlib::prelude::DelegateContext; + use freenet_stdlib::prelude::DelegateCtx; /// Helper function to create empty parameters for testing fn create_test_parameters() -> Parameters<'static> { @@ -817,7 +484,7 @@ mod tests { ciborium::ser::into_writer(&request, &mut payload) .map_err(|e| panic!("Failed to serialize request: {e}")) .unwrap(); - ApplicationMessage::new(app_id, payload) // Pass app_id here + ApplicationMessage::new(app_id, payload) } /// Helper function to extract response from outbound messages @@ -832,6 +499,12 @@ mod tests { None } + // Test origin bytes - using a fixed ContractInstanceId for testing + fn get_test_origin_bytes() -> &'static [u8] { + static ORIGIN: [u8; 32] = [42u8; 32]; + &ORIGIN + } + #[test] fn test_store_request() { let key = b"test_key".to_vec(); @@ -841,19 +514,20 @@ mod tests { key: river_core::chat_delegate::ChatDelegateKey(key.clone()), value: value.clone(), }; - let dummy_app_id = ContractInstanceId::new([1u8; 32]); // Dummy ID for test + let dummy_app_id = ContractInstanceId::new([1u8; 32]); let app_msg = create_app_message(request, dummy_app_id); let inbound_msg = InboundDelegateMsg::ApplicationMessage(app_msg); let result = crate::ChatDelegate::process( + &mut DelegateCtx::default(), create_test_parameters(), Some(get_test_origin_bytes()), inbound_msg, ) .unwrap(); - // Should have 3 messages: app response, set secret, get index - assert_eq!(result.len(), 3); + // Should have 1 message: app response (secrets are stored via host function) + assert_eq!(result.len(), 1); // Check app response let response = extract_response(result).unwrap(); @@ -881,33 +555,35 @@ mod tests { let request = ChatDelegateRequestMsg::GetRequest { key: river_core::chat_delegate::ChatDelegateKey(key.clone()), }; - let dummy_app_id = ContractInstanceId::new([2u8; 32]); // Dummy ID for test + let dummy_app_id = ContractInstanceId::new([2u8; 32]); let app_msg = create_app_message(request, dummy_app_id); let inbound_msg = InboundDelegateMsg::ApplicationMessage(app_msg); let result = crate::ChatDelegate::process( + &mut DelegateCtx::default(), create_test_parameters(), Some(get_test_origin_bytes()), inbound_msg, ) .unwrap(); - // Should have 1 message: get secret request + // Should have 1 message: app response with the value (or None if not found) assert_eq!(result.len(), 1); - // Check it's a get secret request - match &result[0] { - OutboundDelegateMsg::GetSecretRequest(req) => { - // Verify the key contains our test origin and key - let key_str = String::from_utf8(req.key.key().to_vec()) - .map_err(|e| panic!("Invalid UTF-8 in key: {e}")) - .unwrap(); - - // The key format is "origin:key" where origin is base58 encoded - // Just check that it contains some part of our test key - assert!(key_str.contains("test_key")); + // Check it's a GetResponse + let response = extract_response(result).unwrap(); + match response { + ChatDelegateResponseMsg::GetResponse { + key: resp_key, + value: _, + } => { + assert_eq!( + resp_key, + river_core::chat_delegate::ChatDelegateKey(key.clone()) + ); + // Value will be None in test since we didn't store it first } - _ => panic!("Expected GetSecretRequest, got {:?}", result[0]), + _ => panic!("Expected GetResponse, got {:?}", response), } } @@ -918,22 +594,23 @@ mod tests { let request = ChatDelegateRequestMsg::DeleteRequest { key: river_core::chat_delegate::ChatDelegateKey(key.clone()), }; - let dummy_app_id = ContractInstanceId::new([3u8; 32]); // Dummy ID for test + let dummy_app_id = ContractInstanceId::new([3u8; 32]); let app_msg = create_app_message(request, dummy_app_id); let inbound_msg = InboundDelegateMsg::ApplicationMessage(app_msg); let result = crate::ChatDelegate::process( + &mut DelegateCtx::default(), create_test_parameters(), Some(get_test_origin_bytes()), inbound_msg, ) .unwrap(); - // Should have 3 messages: app response, set secret (with None value), get index - assert_eq!(result.len(), 3); + // Should have 1 message: app response + assert_eq!(result.len(), 1); - // Check app response - let response = extract_response(result.clone()).unwrap(); + // Check response + let response = extract_response(result).unwrap(); match response { ChatDelegateResponseMsg::DeleteResponse { key: resp_key, @@ -947,165 +624,100 @@ mod tests { } _ => panic!("Expected DeleteResponse, got {:?}", response), } - - // Check set secret request has None value (deletion) - let mut found_set_request = false; - for msg in result { - if let OutboundDelegateMsg::SetSecretRequest(req) = msg { - if req.value.is_none() { - found_set_request = true; - break; - } - } - } - assert!( - found_set_request, - "No SetSecretRequest with None value found" - ); } #[test] fn test_list_request() { let request = ChatDelegateRequestMsg::ListRequest; - let dummy_app_id = ContractInstanceId::new([4u8; 32]); // Dummy ID for test + let dummy_app_id = ContractInstanceId::new([4u8; 32]); let app_msg = create_app_message(request, dummy_app_id); let inbound_msg = InboundDelegateMsg::ApplicationMessage(app_msg); let result = crate::ChatDelegate::process( + &mut DelegateCtx::default(), create_test_parameters(), Some(get_test_origin_bytes()), inbound_msg, ) .unwrap(); - // Should have 1 message: get index request + // Should have 1 message: app response with list assert_eq!(result.len(), 1); - // Check it's a get secret request for the index - match &result[0] { - OutboundDelegateMsg::GetSecretRequest(req) => { - // Verify the key contains our app ID and key_index suffix - let key_str = String::from_utf8(req.key.key().to_vec()) - .map_err(|e| panic!("Invalid UTF-8 in key: {e}")) - .unwrap(); - assert!(key_str.contains(KEY_INDEX_SUFFIX)); + // Check response + let response = extract_response(result).unwrap(); + match response { + ChatDelegateResponseMsg::ListResponse { keys } => { + // Empty list since we haven't stored anything + assert!(keys.is_empty()); } - _ => panic!("Expected GetSecretRequest, got {:?}", result[0]), + _ => panic!("Expected ListResponse, got {:?}", response), } } #[test] - fn test_get_secret_response_for_regular_get() { - let key = b"test_key".to_vec(); - let value = b"test_value".to_vec(); - - // Create a context with a pending get - let mut context = ChatDelegateContext::default(); - - let test_origin = create_test_origin(); - // Need a dummy app id for the test context - let dummy_app_id = ContractInstanceId::new([0u8; 32]); - - let key_delegate = river_core::chat_delegate::ChatDelegateKey(key.clone()); - let app_key = create_origin_key(&test_origin, &key_delegate); - context.pending_ops.insert( - SecretIdKey::from(&app_key), - PendingOperation::Get { - origin: test_origin.clone(), - client_key: river_core::chat_delegate::ChatDelegateKey(key.clone()), - app: dummy_app_id, // Add dummy app id - }, - ); - - // Serialize the context - let context_bytes = DelegateContext::try_from(&context) - .map_err(|e| panic!("Failed to serialize context: {e}")) - .unwrap(); - - // Create a get secret response - let get_response = freenet_stdlib::prelude::GetSecretResponse { - key: app_key.clone(), - value: Some(value.clone()), - context: context_bytes, - }; + fn test_error_on_processed_message() { + let request = ChatDelegateRequestMsg::ListRequest; + let mut payload = Vec::new(); + ciborium::ser::into_writer(&request, &mut payload).unwrap(); - let inbound_msg = InboundDelegateMsg::GetSecretResponse(get_response); + let dummy_app_id = ContractInstanceId::new([5u8; 32]); + let app_msg = ApplicationMessage::new(dummy_app_id, payload).processed(true); + let inbound_msg = InboundDelegateMsg::ApplicationMessage(app_msg); - // Pass the attested origin parameter let result = crate::ChatDelegate::process( + &mut DelegateCtx::default(), create_test_parameters(), Some(get_test_origin_bytes()), inbound_msg, - ) - .unwrap(); - - // Should have 1 message: app response - assert_eq!(result.len(), 1); + ); - // Check app response - let response = extract_response(result).unwrap(); - match response { - ChatDelegateResponseMsg::GetResponse { - key: resp_key, - value: resp_value, - } => { - assert_eq!( - resp_key, - river_core::chat_delegate::ChatDelegateKey(key.clone()) - ); - assert_eq!(resp_value, Some(value)); - } - _ => panic!("Expected GetResponse, got {:?}", response), + assert!(result.is_err()); + if let Err(DelegateError::Other(msg)) = result { + assert!(msg.contains("already processed")); + } else { + panic!("Expected DelegateError::Other, got {:?}", result); } } #[test] - fn test_get_secret_response_for_list_request() { - let keys = vec![b"key1".to_vec(), b"key2".to_vec(), b"key3".to_vec()]; - - // Create a key index with some keys - let wrapped_keys: Vec = keys - .clone() - .into_iter() - .map(river_core::chat_delegate::ChatDelegateKey) - .collect(); - let key_index = KeyIndex { keys: wrapped_keys }; - let mut index_bytes = Vec::new(); - ciborium::ser::into_writer(&key_index, &mut index_bytes) - .map_err(|e| panic!("Failed to serialize key index: {e}")) - .unwrap(); - - let test_origin = create_test_origin(); - // Need a dummy app id for the test context - let dummy_app_id = ContractInstanceId::new([1u8; 32]); + fn test_error_on_missing_attested() { + let request = ChatDelegateRequestMsg::ListRequest; + let dummy_app_id = ContractInstanceId::new([6u8; 32]); + let app_msg = create_app_message(request, dummy_app_id); + let inbound_msg = InboundDelegateMsg::ApplicationMessage(app_msg); - // Create a context with a pending list request - let mut context = ChatDelegateContext::default(); - let index_key = create_index_key(&test_origin); - context.pending_ops.insert( - SecretIdKey::from(&index_key), - PendingOperation::List { - origin: test_origin.clone(), - app: dummy_app_id, // Add dummy app id - }, + // Pass None for attested + let result = crate::ChatDelegate::process( + &mut DelegateCtx::default(), + create_test_parameters(), + None, + inbound_msg, ); + assert!(result.is_err()); - // Serialize the context - let context_bytes = DelegateContext::try_from(&context) - .map_err(|e| panic!("Failed to serialize context: {e}")) - .unwrap(); + if let Err(DelegateError::Other(msg)) = result { + assert!(msg.contains("missing attested origin")); + } else { + panic!("Expected DelegateError::Other, got {:?}", result); + } + } - // Create a get secret response for the index - let get_response = freenet_stdlib::prelude::GetSecretResponse { - key: index_key.clone(), - value: Some(index_bytes), - context: context_bytes, - }; + #[test] + fn test_store_signing_key() { + let room_key: river_core::chat_delegate::RoomKey = [7u8; 32]; + let signing_key_bytes: [u8; 32] = [8u8; 32]; - let inbound_msg = InboundDelegateMsg::GetSecretResponse(get_response); + let request = ChatDelegateRequestMsg::StoreSigningKey { + room_key, + signing_key_bytes, + }; + let dummy_app_id = ContractInstanceId::new([7u8; 32]); + let app_msg = create_app_message(request, dummy_app_id); + let inbound_msg = InboundDelegateMsg::ApplicationMessage(app_msg); - // Pass the attested origin parameter let result = crate::ChatDelegate::process( + &mut DelegateCtx::default(), create_test_parameters(), Some(get_test_origin_bytes()), inbound_msg, @@ -1115,185 +727,101 @@ mod tests { // Should have 1 message: app response assert_eq!(result.len(), 1); - // Check app response + // Check response let response = extract_response(result).unwrap(); match response { - ChatDelegateResponseMsg::ListResponse { keys: resp_keys } => { - let wrapped_keys: Vec = keys - .clone() - .into_iter() - .map(river_core::chat_delegate::ChatDelegateKey) - .collect(); - assert_eq!(resp_keys, wrapped_keys); + ChatDelegateResponseMsg::StoreSigningKeyResponse { + room_key: resp_room_key, + result, + } => { + assert_eq!(resp_room_key, room_key); + assert!(result.is_ok()); } - _ => panic!("Expected ListResponse, got {:?}", response), + _ => panic!("Expected StoreSigningKeyResponse, got {:?}", response), } } #[test] - fn test_get_secret_response_for_store_request() { - let key = b"test_key".to_vec(); - - // Create a key index with some existing keys - let existing_keys = vec![b"existing_key".to_vec()]; - let key_index = KeyIndex { - keys: existing_keys - .into_iter() - .map(river_core::chat_delegate::ChatDelegateKey) - .collect(), - }; - let mut index_bytes = Vec::new(); - ciborium::ser::into_writer(&key_index, &mut index_bytes) - .map_err(|e| panic!("Failed to serialize key index: {e}")) - .unwrap(); - - let test_origin = create_test_origin(); - - // Create a context with a pending store request - let mut context = ChatDelegateContext::default(); - let index_key = create_index_key(&test_origin); - context.pending_ops.insert( - SecretIdKey::from(&index_key), - PendingOperation::Store { - origin: test_origin.clone(), - client_key: river_core::chat_delegate::ChatDelegateKey(key.clone()), - }, - ); - - // Serialize the context - let context_bytes = DelegateContext::try_from(&context) - .map_err(|e| panic!("Failed to serialize context: {e}")) - .unwrap(); - - // Create a get secret response for the index - let get_response = freenet_stdlib::prelude::GetSecretResponse { - key: index_key.clone(), - value: Some(index_bytes), - context: context_bytes, - }; + fn test_get_public_key_not_found() { + let room_key: river_core::chat_delegate::RoomKey = [9u8; 32]; - let inbound_msg = InboundDelegateMsg::GetSecretResponse(get_response); + let request = ChatDelegateRequestMsg::GetPublicKey { room_key }; + let dummy_app_id = ContractInstanceId::new([8u8; 32]); + let app_msg = create_app_message(request, dummy_app_id); + let inbound_msg = InboundDelegateMsg::ApplicationMessage(app_msg); - // Pass the attested origin parameter let result = crate::ChatDelegate::process( + &mut DelegateCtx::default(), create_test_parameters(), Some(get_test_origin_bytes()), inbound_msg, ) .unwrap(); - // Should have 1 message: set secret request to update the index + // Should have 1 message: app response assert_eq!(result.len(), 1); - // Check it's a set secret request - match &result[0] { - OutboundDelegateMsg::SetSecretRequest(req) => { - // Deserialize the value to check the updated index - let updated_index: KeyIndex = - ciborium::from_reader(req.value.as_ref().unwrap().as_slice()) - .map_err(|e| panic!("Failed to deserialize updated index: {e}")) - .unwrap(); - - // Should contain both the existing key and our new key - assert_eq!(updated_index.keys.len(), 2); - let key_wrapped = river_core::chat_delegate::ChatDelegateKey(key.clone()); - let existing_key_wrapped = - river_core::chat_delegate::ChatDelegateKey(b"existing_key".to_vec()); - assert!(updated_index.keys.contains(&key_wrapped)); - assert!(updated_index.keys.contains(&existing_key_wrapped)); + // Check response - public key should be None since no key is stored + let response = extract_response(result).unwrap(); + match response { + ChatDelegateResponseMsg::GetPublicKeyResponse { + room_key: resp_room_key, + public_key, + } => { + assert_eq!(resp_room_key, room_key); + // In non-WASM test environment, get_secret returns None + assert!(public_key.is_none()); } - _ => panic!("Expected SetSecretRequest, got {:?}", result[0]), + _ => panic!("Expected GetPublicKeyResponse, got {:?}", response), } } #[test] - fn test_error_on_processed_message() { - let key = b"test_key".to_vec(); - let value = b"test_value".to_vec(); + fn test_sign_message_without_key_returns_error() { + let room_key: river_core::chat_delegate::RoomKey = [10u8; 32]; + let request_id: river_core::chat_delegate::RequestId = 12345; + let message_bytes = b"test message to sign".to_vec(); - let request = ChatDelegateRequestMsg::StoreRequest { - key: river_core::chat_delegate::ChatDelegateKey(key), - value, + let request = ChatDelegateRequestMsg::SignMessage { + room_key, + request_id, + message_bytes, }; - let dummy_app_id = ContractInstanceId::new([5u8; 32]); // Dummy ID for test - let mut app_msg = create_app_message(request, dummy_app_id); - app_msg = app_msg.processed(true); // Mark as already processed + let dummy_app_id = ContractInstanceId::new([9u8; 32]); + let app_msg = create_app_message(request, dummy_app_id); let inbound_msg = InboundDelegateMsg::ApplicationMessage(app_msg); - let _origin = create_test_origin(); - let result = crate::ChatDelegate::process( + &mut DelegateCtx::default(), create_test_parameters(), Some(get_test_origin_bytes()), inbound_msg, - ); - assert!(result.is_err()); - - if let Err(DelegateError::Other(msg)) = result { - assert!(msg.contains("cannot process an already processed message")); - } else { - panic!("Expected DelegateError::Other, got {:?}", result); - } - } - - #[test] - fn test_error_on_unexpected_message_type() { - let get_secret_request = GetSecretRequest { - key: SecretsId::new(vec![1, 2, 3]), - context: DelegateContext::default(), - processed: false, - }; - - let inbound_msg = InboundDelegateMsg::GetSecretRequest(get_secret_request); - - let _origin = create_test_origin(); - - let result = crate::ChatDelegate::process( - create_test_parameters(), - Some(get_test_origin_bytes()), - inbound_msg, - ); - assert!(result.is_err()); - - if let Err(DelegateError::Other(msg)) = result { - assert!(msg.contains("unexpected message type")); - } else { - panic!("Expected DelegateError::Other, got {:?}", result); - } - } - - #[test] - fn test_error_on_missing_attested() { - let key = b"test_key".to_vec(); - - let request = ChatDelegateRequestMsg::GetRequest { - key: river_core::chat_delegate::ChatDelegateKey(key), - }; - let dummy_app_id = ContractInstanceId::new([6u8; 32]); // Dummy ID for test - let app_msg = create_app_message(request, dummy_app_id); - let inbound_msg = InboundDelegateMsg::ApplicationMessage(app_msg); + ) + .unwrap(); - // Pass None for attested - let result = crate::ChatDelegate::process(create_test_parameters(), None, inbound_msg); - assert!(result.is_err()); + // Should have 1 message: app response + assert_eq!(result.len(), 1); - if let Err(DelegateError::Other(msg)) = result { - assert!(msg.contains("missing attested origin")); - } else { - panic!("Expected DelegateError::Other, got {:?}", result); + // Check response - signature should be an error since no key is stored + let response = extract_response(result).unwrap(); + match response { + ChatDelegateResponseMsg::SignResponse { + room_key: resp_room_key, + request_id: resp_request_id, + signature, + } => { + assert_eq!(resp_room_key, room_key); + assert_eq!(resp_request_id, request_id); + // Should be an error because no signing key is stored + assert!(signature.is_err()); + let err_msg = signature.unwrap_err(); + assert!( + err_msg.contains("not found"), + "Expected 'not found' error, got: {}", + err_msg + ); + } + _ => panic!("Expected SignResponse, got {:?}", response), } } - - // Helper function to create a test origin - fn create_test_origin() -> Origin { - Origin(vec![0u8, 0u8, 0u8, 0u8]) - } - - // Get the bytes from the test origin for process calls - fn get_test_origin_bytes() -> &'static [u8] { - // Using lazy_static would be better in a real app - // but for tests this is simpler - static TEST_ORIGIN: [u8; 4] = [0u8, 0u8, 0u8, 0u8]; - &TEST_ORIGIN - } } diff --git a/delegates/chat-delegate/src/lib.rs b/delegates/chat-delegate/src/lib.rs index b8013091..cafa83fd 100644 --- a/delegates/chat-delegate/src/lib.rs +++ b/delegates/chat-delegate/src/lib.rs @@ -7,9 +7,8 @@ mod utils; use context::*; use freenet_stdlib::prelude::{ - delegate, ApplicationMessage, DelegateContext, DelegateError, DelegateInterface, - GetSecretRequest, InboundDelegateMsg, OutboundDelegateMsg, Parameters, SecretsId, - SetSecretRequest, + delegate, ApplicationMessage, DelegateContext, DelegateCtx, DelegateError, DelegateInterface, + InboundDelegateMsg, OutboundDelegateMsg, Parameters, }; use handlers::*; use models::*; @@ -19,27 +18,25 @@ use utils::*; mod logging; use river_core::chat_delegate::*; -use serde::ser::SerializeTuple; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; /// Chat delegate for storing and retrieving data in the Freenet secret storage. /// /// This delegate provides a key-value store interface for chat applications, -/// maintaining an index of keys for each application and handling storage, -/// retrieval, deletion, and listing operations. +/// using the host function API for direct secret access (no message round-trips). pub struct ChatDelegate; #[delegate] impl DelegateInterface for ChatDelegate { fn process( + ctx: &mut DelegateCtx, _parameters: Parameters<'static>, attested: Option<&'static [u8]>, message: InboundDelegateMsg, ) -> Result, DelegateError> { let message_type = match message { InboundDelegateMsg::ApplicationMessage(_) => "application message", - InboundDelegateMsg::GetSecretResponse(_) => "get secret response", + InboundDelegateMsg::GetSecretResponse(_) => "get secret response (deprecated)", InboundDelegateMsg::UserResponse(_) => "user response", InboundDelegateMsg::GetSecretRequest(_) => "get secret request", InboundDelegateMsg::GetContractResponse(_) => "get contract response", @@ -67,11 +64,18 @@ impl DelegateInterface for ChatDelegate { "cannot process an already processed message".into(), )) } else { - handle_application_message(app_msg, &origin) + handle_application_message(ctx, app_msg, &origin) } } - InboundDelegateMsg::GetSecretResponse(response) => handle_get_secret_response(response), + // These message types are no longer used with the host function API + InboundDelegateMsg::GetSecretResponse(_) => { + logging::info("Received deprecated GetSecretResponse - use host function API"); + Err(DelegateError::Other( + "GetSecretResponse is deprecated - secrets are accessed via host functions" + .into(), + )) + } InboundDelegateMsg::UserResponse(_) => { logging::info("Received unexpected UserResponse"); diff --git a/delegates/chat-delegate/src/models.rs b/delegates/chat-delegate/src/models.rs index ceab6815..7cf671fe 100644 --- a/delegates/chat-delegate/src/models.rs +++ b/delegates/chat-delegate/src/models.rs @@ -1,283 +1,10 @@ use super::*; -use freenet_stdlib::prelude::ContractInstanceId; -use river_core::chat_delegate::{RequestId, RoomKey}; // Constants pub(crate) const KEY_INDEX_SUFFIX: &str = "::key_index"; pub(crate) const ORIGIN_KEY_SEPARATOR: &str = ":"; -/// Different types of pending operations -#[derive(Debug, Clone)] -pub(crate) enum PendingOperation { - /// Regular get operation for a specific key - Get { - origin: Origin, - client_key: ChatDelegateKey, - app: ContractInstanceId, - }, - /// Store operation that needs to update the index - Store { - origin: Origin, - client_key: ChatDelegateKey, - }, - /// Delete operation that needs to update the index - Delete { - origin: Origin, - client_key: ChatDelegateKey, - }, - /// List operation to retrieve all keys - List { - origin: Origin, - app: ContractInstanceId, - }, - /// Get public key operation (waiting for signing key from secret storage) - GetPublicKey { - origin: Origin, - room_key: RoomKey, - app: ContractInstanceId, - }, - /// Sign operation (waiting for signing key from secret storage) - Sign { - origin: Origin, - room_key: RoomKey, - request_id: RequestId, - data_to_sign: Vec, - app: ContractInstanceId, - }, -} - -impl PendingOperation { - pub(crate) fn is_delete_operation(&self) -> bool { - matches!(self, Self::Delete { .. }) - } -} - -/// Parameters for the chat delegate. -/// Currently empty, but could be extended with configuration options. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[warn(dead_code)] -pub(crate) struct ChatDelegateParameters; - -impl TryFrom> for ChatDelegateParameters { - type Error = DelegateError; - - fn try_from(_params: Parameters<'_>) -> Result { - // Currently no parameters are used, but this could be extended - Ok(Self {}) - } -} - -/// A wrapper around SecretsId that implements Hash and Eq -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub(crate) struct SecretIdKey(pub(crate) String); - -impl From<&SecretsId> for SecretIdKey { - fn from(id: &SecretsId) -> Self { - // Convert the SecretsId to a string representation for hashing - Self(String::from_utf8_lossy(id.key()).to_string()) - } -} - -// Add Serialize/Deserialize for PendingOperation -impl Serialize for PendingOperation { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - Self::Get { - origin, - client_key, - app, - } => { - let mut seq = serializer.serialize_tuple(4)?; - seq.serialize_element(&0u8)?; // Type tag for Get - seq.serialize_element(origin)?; - seq.serialize_element(client_key)?; - seq.serialize_element(app)?; - seq.end() - } - Self::Store { origin, client_key } => { - let mut seq = serializer.serialize_tuple(3)?; - seq.serialize_element(&1u8)?; // Type tag for Store - seq.serialize_element(origin)?; - seq.serialize_element(client_key)?; - seq.end() - } - Self::Delete { origin, client_key } => { - let mut seq = serializer.serialize_tuple(3)?; - seq.serialize_element(&2u8)?; // Type tag for Delete - seq.serialize_element(origin)?; - seq.serialize_element(client_key)?; - seq.end() - } - Self::List { origin, app } => { - let mut seq = serializer.serialize_tuple(3)?; - seq.serialize_element(&3u8)?; // Type tag for List - seq.serialize_element(origin)?; - seq.serialize_element(app)?; - seq.end() - } - Self::GetPublicKey { - origin, - room_key, - app, - } => { - let mut seq = serializer.serialize_tuple(4)?; - seq.serialize_element(&4u8)?; // Type tag for GetPublicKey - seq.serialize_element(origin)?; - seq.serialize_element(room_key)?; - seq.serialize_element(app)?; - seq.end() - } - Self::Sign { - origin, - room_key, - request_id, - data_to_sign, - app, - } => { - let mut seq = serializer.serialize_tuple(6)?; - seq.serialize_element(&5u8)?; // Type tag for Sign - seq.serialize_element(origin)?; - seq.serialize_element(room_key)?; - seq.serialize_element(request_id)?; - seq.serialize_element(data_to_sign)?; - seq.serialize_element(app)?; - seq.end() - } - } - } -} - -impl<'de> Deserialize<'de> for PendingOperation { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - use serde::de::{Error, SeqAccess, Visitor}; - use std::fmt; - - struct PendingOpVisitor; - - impl<'de> Visitor<'de> for PendingOpVisitor { - type Value = PendingOperation; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a tuple with a type tag and operation data") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: SeqAccess<'de>, - { - let tag: u8 = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(0, &self))?; - - match tag { - 0 => { - // Get - let origin: Origin = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(1, &self))?; - let client_key: ChatDelegateKey = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(2, &self))?; - let app: ContractInstanceId = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(3, &self))?; - Ok(PendingOperation::Get { - origin, - client_key, - app, - }) - } - 1 => { - // Store - let origin: Origin = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(1, &self))?; - let client_key: ChatDelegateKey = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(2, &self))?; - Ok(PendingOperation::Store { origin, client_key }) - } - 2 => { - // Delete - let origin: Origin = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(1, &self))?; - let client_key: ChatDelegateKey = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(2, &self))?; - Ok(PendingOperation::Delete { origin, client_key }) - } - 3 => { - // List - let origin: Origin = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(1, &self))?; - let app: ContractInstanceId = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(2, &self))?; - Ok(PendingOperation::List { origin, app }) - } - 4 => { - // GetPublicKey - let origin: Origin = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(1, &self))?; - let room_key: RoomKey = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(2, &self))?; - let app: ContractInstanceId = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(3, &self))?; - Ok(PendingOperation::GetPublicKey { - origin, - room_key, - app, - }) - } - 5 => { - // Sign - let origin: Origin = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(1, &self))?; - let room_key: RoomKey = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(2, &self))?; - let request_id: RequestId = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(3, &self))?; - let data_to_sign: Vec = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(4, &self))?; - let app: ContractInstanceId = seq - .next_element()? - .ok_or_else(|| Error::invalid_length(5, &self))?; - Ok(PendingOperation::Sign { - origin, - room_key, - request_id, - data_to_sign, - app, - }) - } - _ => Err(Error::custom(format!( - "Unknown operation type tag: {}", - tag - ))), - } - } - } - // Use max length of 6 (Sign has the most elements) - deserializer.deserialize_tuple(6, PendingOpVisitor) - } -} - -/// Origin contract ID +/// Origin contract ID - represents the attested identity of the caller #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct Origin(pub(crate) Vec); diff --git a/delegates/chat-delegate/src/utils.rs b/delegates/chat-delegate/src/utils.rs index 7d926105..f5bd1999 100644 --- a/delegates/chat-delegate/src/utils.rs +++ b/delegates/chat-delegate/src/utils.rs @@ -1,64 +1,31 @@ use super::*; use freenet_stdlib::prelude::ContractInstanceId; -/// Helper function to create a unique app key -pub(crate) fn create_origin_key(origin: &Origin, key: &ChatDelegateKey) -> SecretsId { - SecretsId::new( - format!( - "{}{}{}", - origin.to_b58(), - ORIGIN_KEY_SEPARATOR, - String::from_utf8_lossy(key.as_bytes()) - ) - .into_bytes(), +/// Helper function to create a unique secret key for an origin's data +pub(crate) fn create_origin_key(origin: &Origin, key: &ChatDelegateKey) -> Vec { + format!( + "{}{}{}", + origin.to_b58(), + ORIGIN_KEY_SEPARATOR, + String::from_utf8_lossy(key.as_bytes()) ) + .into_bytes() } -/// Helper function to create an index key -pub(crate) fn create_index_key(origin: &Origin) -> SecretsId { - SecretsId::new( - format!( - "{}{}{}", - origin.to_b58(), - ORIGIN_KEY_SEPARATOR, - KEY_INDEX_SUFFIX - ) - .into_bytes(), +/// Helper function to create an index key for an origin +pub(crate) fn create_index_key(origin: &Origin) -> Vec { + format!( + "{}{}{}", + origin.to_b58(), + ORIGIN_KEY_SEPARATOR, + KEY_INDEX_SUFFIX ) -} - -/// Helper function to create a get request -pub(crate) fn create_get_request( - secret_id: SecretsId, - context: &DelegateContext, -) -> Result { - let get_secret = OutboundDelegateMsg::GetSecretRequest(GetSecretRequest { - key: secret_id, - context: context.clone(), - processed: false, - }); - - Ok(get_secret) -} - -/// Helper function to create a get index request -pub(crate) fn create_get_index_request( - index_secret_id: SecretsId, - context: &DelegateContext, -) -> Result { - let get_index = OutboundDelegateMsg::GetSecretRequest(GetSecretRequest { - key: index_secret_id, - context: context.clone(), - processed: false, - }); - - Ok(get_index) + .into_bytes() } /// Helper function to create an app response pub(crate) fn create_app_response( response: &T, - context: &DelegateContext, app: ContractInstanceId, ) -> Result { // Serialize response @@ -73,8 +40,8 @@ pub(crate) fn create_app_response( // Create response message let app_msg = ApplicationMessage::new(app, response_bytes) - .with_context(context.clone()) - .processed(false); // + .with_context(DelegateContext::default()) + .processed(true); Ok(OutboundDelegateMsg::ApplicationMessage(app_msg)) }