From b5459096704239751add94e2c2556d4b743d694d Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Tue, 3 Feb 2026 17:56:38 +0100 Subject: [PATCH 1/8] feat: add host function API for delegate context and secrets access This adds a new, cleaner API for delegates to access context and secrets via opaque handles (`DelegateCtx` and `SecretsStore`) passed to the `process()` function. This eliminates the need for message round-trips (GetSecretRequest/GetSecretResponse) and allows synchronous access. Breaking change: `DelegateInterface::process` signature now takes `ctx: &mut DelegateCtx` and `secrets: &mut SecretsStore` as the first two parameters. Changes: - Add `delegate_host.rs` with `DelegateCtx` and `SecretsStore` types - Update `DelegateInterface` trait signature - Update `#[delegate]` macro to create and pass the handles - Update example delegate to demonstrate the new API --- examples/delegate.rs | 16 ++ rust-macros/src/delegate_impl.rs | 11 +- rust/src/delegate_host.rs | 301 +++++++++++++++++++++++++++++++ rust/src/delegate_interface.rs | 55 ++++++ rust/src/lib.rs | 4 + 5 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 rust/src/delegate_host.rs diff --git a/examples/delegate.rs b/examples/delegate.rs index 2d56702..67f0deb 100644 --- a/examples/delegate.rs +++ b/examples/delegate.rs @@ -9,10 +9,26 @@ struct Delegate; #[delegate] impl DelegateInterface for Delegate { fn process( + ctx: &mut DelegateCtx, + secrets: &mut SecretsStore, _parameters: Parameters<'static>, _attested: Option<&'static [u8]>, _messages: InboundDelegateMsg, ) -> Result, DelegateError> { + // Example: read context + let _data = ctx.read(); + + // Example: write context + ctx.write(b"some state"); + + // Example: access secrets synchronously + if secrets.has(b"my_key") { + let _secret = secrets.get(b"my_key"); + } + + // Example: store a secret + secrets.set(b"new_key", b"secret_value"); + unimplemented!() } } diff --git a/rust-macros/src/delegate_impl.rs b/rust-macros/src/delegate_impl.rs index 3249cf2..1612a61 100644 --- a/rust-macros/src/delegate_impl.rs +++ b/rust-macros/src/delegate_impl.rs @@ -61,7 +61,16 @@ impl ImplStruct { ).into_raw(), } }; - let result =<#type_name as ::freenet_stdlib::prelude::DelegateInterface>::process( + + // Create opaque handles for context and secrets access. + // SAFETY: The runtime has set up the delegate execution environment + // before calling this function, so the host functions are available. + let mut ctx = unsafe { ::freenet_stdlib::prelude::DelegateCtx::__new() }; + let mut secrets = unsafe { ::freenet_stdlib::prelude::SecretsStore::__new() }; + + let result = <#type_name as ::freenet_stdlib::prelude::DelegateInterface>::process( + &mut ctx, + &mut secrets, parameters, attested, inbound diff --git a/rust/src/delegate_host.rs b/rust/src/delegate_host.rs new file mode 100644 index 0000000..89884c6 --- /dev/null +++ b/rust/src/delegate_host.rs @@ -0,0 +1,301 @@ +//! Host function API for delegates. +//! +//! This module provides synchronous access to delegate context and secrets +//! via host functions, eliminating the need for message round-trips. +//! +//! # Example +//! +//! ```ignore +//! use freenet_stdlib::prelude::*; +//! +//! #[delegate] +//! impl DelegateInterface for MyDelegate { +//! fn process( +//! ctx: &mut DelegateCtx, +//! secrets: &mut SecretsStore, +//! _params: Parameters<'static>, +//! _attested: Option<&'static [u8]>, +//! message: InboundDelegateMsg, +//! ) -> Result, DelegateError> { +//! // Read/write context directly +//! let data = ctx.read(); +//! ctx.write(b"new state"); +//! +//! // Access secrets synchronously +//! if let Some(key) = secrets.get(b"private_key") { +//! // use key... +//! } +//! secrets.set(b"new_secret", b"value"); +//! +//! Ok(vec![]) +//! } +//! } +//! ``` + +// ============================================================================ +// Host function declarations (WASM only) +// ============================================================================ + +#[cfg(target_family = "wasm")] +#[link(wasm_import_module = "freenet_delegate_ctx")] +extern "C" { + /// Returns the current context length in bytes. + fn __frnt__delegate__ctx_len() -> i32; + /// Reads context into the buffer at `ptr` (max `len` bytes). Returns bytes written. + fn __frnt__delegate__ctx_read(ptr: i64, len: i32) -> i32; + /// Writes `len` bytes from `ptr` into the context, replacing existing content. + fn __frnt__delegate__ctx_write(ptr: i64, len: i32); +} + +#[cfg(target_family = "wasm")] +#[link(wasm_import_module = "freenet_delegate_secrets")] +extern "C" { + /// Get a secret. Returns bytes written to `out_ptr`, or -1 if not found. + fn __frnt__delegate__get_secret(key_ptr: i64, key_len: i32, out_ptr: i64, out_len: i32) + -> i32; + /// Store a secret. Returns 0 on success, -1 on error. + fn __frnt__delegate__set_secret(key_ptr: i64, key_len: i32, val_ptr: i64, val_len: i32) -> i32; + /// Check if a secret exists. Returns 1 if yes, 0 if no. + fn __frnt__delegate__has_secret(key_ptr: i64, key_len: i32) -> i32; + /// Remove a secret. Returns 0 on success, -1 if not found. + fn __frnt__delegate__remove_secret(key_ptr: i64, key_len: i32) -> i32; +} + +// ============================================================================ +// DelegateCtx - Opaque handle to mutable context +// ============================================================================ + +/// Opaque handle to the delegate's mutable context. +/// +/// Context persists across messages within a single `inbound_app_message` batch, +/// but is reset between separate runtime calls. Use this for temporary state +/// that needs to be shared across multiple messages in one batch. +/// +/// For persistent state, use [`SecretsStore`] instead. +#[repr(transparent)] +pub struct DelegateCtx { + _private: (), +} + +impl DelegateCtx { + /// Creates the context handle. + /// + /// # Safety + /// + /// This should only be called by macro-generated code when the runtime + /// has set up the delegate execution environment. + #[doc(hidden)] + pub unsafe fn __new() -> Self { + Self { _private: () } + } + + /// Returns the current context length in bytes. + #[inline] + pub fn len(&self) -> usize { + #[cfg(target_family = "wasm")] + { + let len = unsafe { __frnt__delegate__ctx_len() }; + if len < 0 { 0 } else { len as usize } + } + #[cfg(not(target_family = "wasm"))] + { + 0 + } + } + + /// Returns `true` if the context is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Read the current context bytes. + /// + /// Returns an empty `Vec` if no context has been written. + pub fn read(&self) -> Vec { + #[cfg(target_family = "wasm")] + { + let len = unsafe { __frnt__delegate__ctx_len() }; + if len <= 0 { + return Vec::new(); + } + let mut buf = vec![0u8; len as usize]; + let read = unsafe { __frnt__delegate__ctx_read(buf.as_mut_ptr() as i64, len) }; + buf.truncate(read.max(0) as usize); + buf + } + #[cfg(not(target_family = "wasm"))] + { + Vec::new() + } + } + + /// Read context into a provided buffer. + /// + /// Returns the number of bytes actually read. + pub fn read_into(&self, buf: &mut [u8]) -> usize { + #[cfg(target_family = "wasm")] + { + let read = unsafe { + __frnt__delegate__ctx_read(buf.as_mut_ptr() as i64, buf.len() as i32) + }; + read.max(0) as usize + } + #[cfg(not(target_family = "wasm"))] + { + let _ = buf; + 0 + } + } + + /// Write new context bytes, replacing any existing content. + pub fn write(&mut self, data: &[u8]) { + #[cfg(target_family = "wasm")] + { + unsafe { + __frnt__delegate__ctx_write(data.as_ptr() as i64, data.len() as i32); + } + } + #[cfg(not(target_family = "wasm"))] + { + let _ = data; + } + } + + /// Clear the context. + #[inline] + pub fn clear(&mut self) { + self.write(&[]); + } +} + +impl std::fmt::Debug for DelegateCtx { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DelegateCtx") + .field("len", &self.len()) + .finish() + } +} + +// ============================================================================ +// SecretsStore - Opaque handle to secret storage +// ============================================================================ + +/// Maximum buffer size for reading secrets. +const SECRET_MAX_SIZE: usize = 64 * 1024; // 64 KB + +/// Opaque handle to the delegate's secret store. +/// +/// Secrets are persistent across all delegate invocations and are stored +/// securely by the runtime. Use this for sensitive data like private keys, +/// tokens, or other credentials. +/// +/// Each delegate has its own isolated secret namespace - secrets from one +/// delegate cannot be accessed by another. +#[repr(transparent)] +pub struct SecretsStore { + _private: (), +} + +impl SecretsStore { + /// Creates the secrets store handle. + /// + /// # Safety + /// + /// This should only be called by macro-generated code when the runtime + /// has set up the delegate execution environment. + #[doc(hidden)] + pub unsafe fn __new() -> Self { + Self { _private: () } + } + + /// Get a secret by key. + /// + /// Returns `None` if the secret does not exist. + pub fn get(&self, key: &[u8]) -> Option> { + #[cfg(target_family = "wasm")] + { + let mut out = vec![0u8; SECRET_MAX_SIZE]; + let result = unsafe { + __frnt__delegate__get_secret( + key.as_ptr() as i64, + key.len() as i32, + out.as_mut_ptr() as i64, + out.len() as i32, + ) + }; + if result < 0 { + None + } else { + out.truncate(result as usize); + Some(out) + } + } + #[cfg(not(target_family = "wasm"))] + { + let _ = key; + None + } + } + + /// Store a secret. + /// + /// Returns `true` on success, `false` on error. + pub fn set(&mut self, key: &[u8], value: &[u8]) -> bool { + #[cfg(target_family = "wasm")] + { + let result = unsafe { + __frnt__delegate__set_secret( + key.as_ptr() as i64, + key.len() as i32, + value.as_ptr() as i64, + value.len() as i32, + ) + }; + result == 0 + } + #[cfg(not(target_family = "wasm"))] + { + let _ = (key, value); + false + } + } + + /// Check if a secret exists. + pub fn has(&self, key: &[u8]) -> bool { + #[cfg(target_family = "wasm")] + { + let result = + unsafe { __frnt__delegate__has_secret(key.as_ptr() as i64, key.len() as i32) }; + result == 1 + } + #[cfg(not(target_family = "wasm"))] + { + let _ = key; + false + } + } + + /// Remove a secret. + /// + /// Returns `true` if the secret was removed, `false` if it didn't exist. + pub fn remove(&mut self, key: &[u8]) -> bool { + #[cfg(target_family = "wasm")] + { + let result = + unsafe { __frnt__delegate__remove_secret(key.as_ptr() as i64, key.len() as i32) }; + result == 0 + } + #[cfg(not(target_family = "wasm"))] + { + let _ = key; + false + } + } +} + +impl std::fmt::Debug for SecretsStore { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SecretsStore").finish_non_exhaustive() + } +} diff --git a/rust/src/delegate_interface.rs b/rust/src/delegate_interface.rs index 12ddb76..f45ca4d 100644 --- a/rust/src/delegate_interface.rs +++ b/rust/src/delegate_interface.rs @@ -378,6 +378,61 @@ impl<'a> TryFromFbs<&FbsSecretsId<'a>> for SecretsId { /// the delegate to sign messages, it will ask the user for permission /// * A delegate monitors an inbox contract and downloads new messages when /// they arrive +/// +/// # Example +/// +/// ```ignore +/// use freenet_stdlib::prelude::*; +/// +/// struct MyDelegate; +/// +/// #[delegate] +/// impl DelegateInterface for MyDelegate { +/// fn process( +/// ctx: &mut DelegateCtx, +/// secrets: &mut SecretsStore, +/// _params: Parameters<'static>, +/// _attested: Option<&'static [u8]>, +/// message: InboundDelegateMsg, +/// ) -> Result, DelegateError> { +/// // Access secrets synchronously - no round-trip needed! +/// if let Some(key) = secrets.get(b"private_key") { +/// // use key... +/// } +/// +/// // Read/write context for temporary state within a batch +/// ctx.write(b"some state"); +/// +/// Ok(vec![]) +/// } +/// } +/// ``` +#[cfg(feature = "contract")] +pub trait DelegateInterface { + /// Process inbound message, producing zero or more outbound messages in response. + /// + /// # Arguments + /// - `ctx`: Mutable handle to the delegate's context. Context persists across + /// messages within a single batch but is reset between separate runtime calls. + /// - `secrets`: Mutable handle to the delegate's secret store. Secrets persist + /// across all delegate invocations. + /// - `parameters`: The delegate's initialization parameters. + /// - `attested`: An optional identifier for the client of this function. Usually + /// will be a [`ContractInstanceId`]. + /// - `message`: The inbound message to process. + fn process( + ctx: &mut crate::delegate_host::DelegateCtx, + secrets: &mut crate::delegate_host::SecretsStore, + parameters: Parameters<'static>, + attested: Option<&'static [u8]>, + message: InboundDelegateMsg, + ) -> Result, DelegateError>; +} + +/// Legacy delegate interface without host function access. +/// +/// This is used when the `contract` feature is not enabled. +#[cfg(not(feature = "contract"))] pub trait DelegateInterface { /// Process inbound message, producing zero or more outbound messages in response. /// All state for the delegate must be stored using the secret mechanism. diff --git a/rust/src/lib.rs b/rust/src/lib.rs index bf73205..ba72e3d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -3,6 +3,8 @@ mod code_hash; #[cfg(feature = "unstable")] pub mod contract_composition; mod contract_interface; +#[cfg(feature = "contract")] +pub mod delegate_host; mod delegate_interface; pub(crate) mod global; pub mod memory; @@ -37,6 +39,8 @@ pub mod prelude { pub use crate::code_hash::*; pub use crate::contract_interface::wasm_interface::ContractInterfaceResult; pub use crate::contract_interface::*; + #[cfg(feature = "contract")] + pub use crate::delegate_host::{DelegateCtx, SecretsStore}; pub use crate::delegate_interface::wasm_interface::DelegateInterfaceResult; pub use crate::delegate_interface::*; pub use crate::parameters::*; From 28eb14fa0f0479a4bba6b409488be03e604b7de0 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Tue, 3 Feb 2026 18:01:43 +0100 Subject: [PATCH 2/8] style: fmt --- rust/src/delegate_host.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/rust/src/delegate_host.rs b/rust/src/delegate_host.rs index 89884c6..df75a40 100644 --- a/rust/src/delegate_host.rs +++ b/rust/src/delegate_host.rs @@ -51,8 +51,7 @@ extern "C" { #[link(wasm_import_module = "freenet_delegate_secrets")] extern "C" { /// Get a secret. Returns bytes written to `out_ptr`, or -1 if not found. - fn __frnt__delegate__get_secret(key_ptr: i64, key_len: i32, out_ptr: i64, out_len: i32) - -> i32; + fn __frnt__delegate__get_secret(key_ptr: i64, key_len: i32, out_ptr: i64, out_len: i32) -> i32; /// Store a secret. Returns 0 on success, -1 on error. fn __frnt__delegate__set_secret(key_ptr: i64, key_len: i32, val_ptr: i64, val_len: i32) -> i32; /// Check if a secret exists. Returns 1 if yes, 0 if no. @@ -95,7 +94,11 @@ impl DelegateCtx { #[cfg(target_family = "wasm")] { let len = unsafe { __frnt__delegate__ctx_len() }; - if len < 0 { 0 } else { len as usize } + if len < 0 { + 0 + } else { + len as usize + } } #[cfg(not(target_family = "wasm"))] { @@ -136,9 +139,8 @@ impl DelegateCtx { pub fn read_into(&self, buf: &mut [u8]) -> usize { #[cfg(target_family = "wasm")] { - let read = unsafe { - __frnt__delegate__ctx_read(buf.as_mut_ptr() as i64, buf.len() as i32) - }; + let read = + unsafe { __frnt__delegate__ctx_read(buf.as_mut_ptr() as i64, buf.len() as i32) }; read.max(0) as usize } #[cfg(not(target_family = "wasm"))] From ece3b92449ac2ba3ea61618881c75a484cc59828 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Tue, 3 Feb 2026 18:25:35 +0100 Subject: [PATCH 3/8] feat: add error_codes module and update host function signatures - Add error_codes module with named constants for all error values - Update __frnt__delegate__ctx_write to return i32 (error code) - Add __frnt__delegate__get_secret_len host function for querying secret size - Update SecretsStore::get to use get_secret_len for proper buffer allocation - Add SecretsStore::get_len method for querying secret size - Export error_codes module from prelude --- rust/src/delegate_host.rs | 105 ++++++++++++++++++++++++++++++++------ rust/src/lib.rs | 2 +- 2 files changed, 90 insertions(+), 17 deletions(-) diff --git a/rust/src/delegate_host.rs b/rust/src/delegate_host.rs index df75a40..6bad966 100644 --- a/rust/src/delegate_host.rs +++ b/rust/src/delegate_host.rs @@ -31,6 +31,44 @@ //! } //! } //! ``` +//! +//! # Error Codes +//! +//! Host functions return negative values to indicate errors: +//! +//! | Code | Meaning | +//! |------|---------| +//! | 0 | Success | +//! | -1 | Called outside process() context | +//! | -2 | Secret not found | +//! | -3 | Storage operation failed | +//! | -4 | Invalid parameter (e.g., negative length) | +//! | -5 | Context too large (exceeds i32::MAX) | +//! | -6 | Buffer too small | +//! +//! The wrapper methods in [`DelegateCtx`] and [`SecretsStore`] handle these +//! error codes and present a more ergonomic API. + +/// Error codes returned by host functions. +/// +/// Negative values indicate errors, non-negative values indicate success +/// (usually the number of bytes read/written). +pub mod error_codes { + /// Operation succeeded. + pub const SUCCESS: i32 = 0; + /// Called outside of a process() context. + pub const ERR_NOT_IN_PROCESS: i32 = -1; + /// Secret not found. + pub const ERR_SECRET_NOT_FOUND: i32 = -2; + /// Storage operation failed. + pub const ERR_STORAGE_FAILED: i32 = -3; + /// Invalid parameter (e.g., negative length). + pub const ERR_INVALID_PARAM: i32 = -4; + /// Context too large (exceeds i32::MAX). + pub const ERR_CONTEXT_TOO_LARGE: i32 = -5; + /// Buffer too small to hold the data. + pub const ERR_BUFFER_TOO_SMALL: i32 = -6; +} // ============================================================================ // Host function declarations (WASM only) @@ -39,24 +77,26 @@ #[cfg(target_family = "wasm")] #[link(wasm_import_module = "freenet_delegate_ctx")] extern "C" { - /// Returns the current context length in bytes. + /// Returns the current context length in bytes, or negative error code. fn __frnt__delegate__ctx_len() -> i32; - /// Reads context into the buffer at `ptr` (max `len` bytes). Returns bytes written. + /// Reads context into the buffer at `ptr` (max `len` bytes). Returns bytes written, or negative error code. fn __frnt__delegate__ctx_read(ptr: i64, len: i32) -> i32; - /// Writes `len` bytes from `ptr` into the context, replacing existing content. - fn __frnt__delegate__ctx_write(ptr: i64, len: i32); + /// Writes `len` bytes from `ptr` into the context, replacing existing content. Returns 0 on success, or negative error code. + fn __frnt__delegate__ctx_write(ptr: i64, len: i32) -> i32; } #[cfg(target_family = "wasm")] #[link(wasm_import_module = "freenet_delegate_secrets")] extern "C" { - /// Get a secret. Returns bytes written to `out_ptr`, or -1 if not found. + /// Get a secret. Returns bytes written to `out_ptr`, or negative error code. fn __frnt__delegate__get_secret(key_ptr: i64, key_len: i32, out_ptr: i64, out_len: i32) -> i32; - /// Store a secret. Returns 0 on success, -1 on error. + /// Get secret length without fetching value. Returns length, or negative error code. + fn __frnt__delegate__get_secret_len(key_ptr: i64, key_len: i32) -> i32; + /// Store a secret. Returns 0 on success, or negative error code. fn __frnt__delegate__set_secret(key_ptr: i64, key_len: i32, val_ptr: i64, val_len: i32) -> i32; - /// Check if a secret exists. Returns 1 if yes, 0 if no. + /// Check if a secret exists. Returns 1 if yes, 0 if no, or negative error code. fn __frnt__delegate__has_secret(key_ptr: i64, key_len: i32) -> i32; - /// Remove a secret. Returns 0 on success, -1 if not found. + /// Remove a secret. Returns 0 on success, or negative error code. fn __frnt__delegate__remove_secret(key_ptr: i64, key_len: i32) -> i32; } @@ -151,16 +191,20 @@ impl DelegateCtx { } /// Write new context bytes, replacing any existing content. - pub fn write(&mut self, data: &[u8]) { + /// + /// Returns `true` on success, `false` on error. + pub fn write(&mut self, data: &[u8]) -> bool { #[cfg(target_family = "wasm")] { - unsafe { - __frnt__delegate__ctx_write(data.as_ptr() as i64, data.len() as i32); - } + let result = unsafe { + __frnt__delegate__ctx_write(data.as_ptr() as i64, data.len() as i32) + }; + result == 0 } #[cfg(not(target_family = "wasm"))] { let _ = data; + false } } @@ -183,9 +227,6 @@ impl std::fmt::Debug for DelegateCtx { // SecretsStore - Opaque handle to secret storage // ============================================================================ -/// Maximum buffer size for reading secrets. -const SECRET_MAX_SIZE: usize = 64 * 1024; // 64 KB - /// Opaque handle to the delegate's secret store. /// /// Secrets are persistent across all delegate invocations and are stored @@ -211,13 +252,45 @@ impl SecretsStore { Self { _private: () } } + /// Get the length of a secret without retrieving its value. + /// + /// Returns `None` if the secret does not exist. + pub fn get_len(&self, key: &[u8]) -> Option { + #[cfg(target_family = "wasm")] + { + let result = unsafe { + __frnt__delegate__get_secret_len(key.as_ptr() as i64, key.len() as i32) + }; + if result < 0 { + None + } else { + Some(result as usize) + } + } + #[cfg(not(target_family = "wasm"))] + { + let _ = key; + None + } + } + /// Get a secret by key. /// /// Returns `None` if the secret does not exist. pub fn get(&self, key: &[u8]) -> Option> { #[cfg(target_family = "wasm")] { - let mut out = vec![0u8; SECRET_MAX_SIZE]; + // First get the length to allocate the right buffer size + let len = match self.get_len(key) { + Some(len) => len, + None => return None, + }; + + if len == 0 { + return Some(Vec::new()); + } + + let mut out = vec![0u8; len]; let result = unsafe { __frnt__delegate__get_secret( key.as_ptr() as i64, diff --git a/rust/src/lib.rs b/rust/src/lib.rs index ba72e3d..f8d879c 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -40,7 +40,7 @@ pub mod prelude { pub use crate::contract_interface::wasm_interface::ContractInterfaceResult; pub use crate::contract_interface::*; #[cfg(feature = "contract")] - pub use crate::delegate_host::{DelegateCtx, SecretsStore}; + pub use crate::delegate_host::{error_codes, DelegateCtx, SecretsStore}; pub use crate::delegate_interface::wasm_interface::DelegateInterfaceResult; pub use crate::delegate_interface::*; pub use crate::parameters::*; From 8f17a89f0f44121e3595fff3033d253f45ced05a Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Wed, 4 Feb 2026 08:55:13 +0100 Subject: [PATCH 4/8] refactor: merge SecretsStore into DelegateCtx for unified API Simplify the delegate host function API by merging SecretsStore functionality into DelegateCtx. Delegates now use a single context parameter that provides access to both: - Temporary context (read/write/clear) - Persistent secrets (get_secret/set_secret/has_secret/remove_secret) The SecretsStore type is retained as a deprecated type alias for backward compatibility. Changes: - Move secret methods (get/set/has/remove) to DelegateCtx - Update DelegateInterface trait to single ctx parameter - Update #[delegate] macro to remove secrets parameter - Deprecate SecretsStore as type alias to DelegateCtx --- rust-macros/src/delegate_impl.rs | 4 +- rust/src/delegate_host.rs | 133 +++++++++++++++---------------- rust/src/delegate_interface.rs | 12 ++- 3 files changed, 72 insertions(+), 77 deletions(-) diff --git a/rust-macros/src/delegate_impl.rs b/rust-macros/src/delegate_impl.rs index 1612a61..080501e 100644 --- a/rust-macros/src/delegate_impl.rs +++ b/rust-macros/src/delegate_impl.rs @@ -62,15 +62,13 @@ impl ImplStruct { } }; - // Create opaque handles for context and secrets access. + // Create opaque handle for context access (includes secrets). // SAFETY: The runtime has set up the delegate execution environment // before calling this function, so the host functions are available. let mut ctx = unsafe { ::freenet_stdlib::prelude::DelegateCtx::__new() }; - let mut secrets = unsafe { ::freenet_stdlib::prelude::SecretsStore::__new() }; let result = <#type_name as ::freenet_stdlib::prelude::DelegateInterface>::process( &mut ctx, - &mut secrets, parameters, attested, inbound diff --git a/rust/src/delegate_host.rs b/rust/src/delegate_host.rs index 6bad966..3f30b9e 100644 --- a/rust/src/delegate_host.rs +++ b/rust/src/delegate_host.rs @@ -12,26 +12,33 @@ //! impl DelegateInterface for MyDelegate { //! fn process( //! ctx: &mut DelegateCtx, -//! secrets: &mut SecretsStore, //! _params: Parameters<'static>, //! _attested: Option<&'static [u8]>, //! message: InboundDelegateMsg, //! ) -> Result, DelegateError> { -//! // Read/write context directly +//! // Read/write temporary context //! let data = ctx.read(); //! ctx.write(b"new state"); //! -//! // Access secrets synchronously -//! if let Some(key) = secrets.get(b"private_key") { +//! // Access persistent secrets +//! if let Some(key) = ctx.get_secret(b"private_key") { //! // use key... //! } -//! secrets.set(b"new_secret", b"value"); +//! ctx.set_secret(b"new_secret", b"value"); //! //! Ok(vec![]) //! } //! } //! ``` //! +//! # Context vs Secrets +//! +//! - **Context** (`read`/`write`): Temporary state within a single message batch. +//! Reset between separate runtime calls. Use for intermediate processing state. +//! +//! - **Secrets** (`get_secret`/`set_secret`): Persistent encrypted storage. +//! Survives across all delegate invocations. Use for private keys, tokens, etc. +//! //! # Error Codes //! //! Host functions return negative values to indicate errors: @@ -46,8 +53,8 @@ //! | -5 | Context too large (exceeds i32::MAX) | //! | -6 | Buffer too small | //! -//! The wrapper methods in [`DelegateCtx`] and [`SecretsStore`] handle these -//! error codes and present a more ergonomic API. +//! The wrapper methods in [`DelegateCtx`] handle these error codes and present +//! a more ergonomic API. /// Error codes returned by host functions. /// @@ -101,16 +108,21 @@ extern "C" { } // ============================================================================ -// DelegateCtx - Opaque handle to mutable context +// DelegateCtx - Unified handle to context and secrets // ============================================================================ -/// Opaque handle to the delegate's mutable context. +/// Opaque handle to the delegate's execution environment. /// -/// Context persists across messages within a single `inbound_app_message` batch, -/// but is reset between separate runtime calls. Use this for temporary state -/// that needs to be shared across multiple messages in one batch. +/// Provides access to both: +/// - **Temporary context**: State shared within a single message batch (reset between calls) +/// - **Persistent secrets**: Encrypted storage that survives across all invocations /// -/// For persistent state, use [`SecretsStore`] instead. +/// # Context Methods +/// - [`read`](Self::read), [`write`](Self::write), [`len`](Self::len), [`clear`](Self::clear) +/// +/// # Secret Methods +/// - [`get_secret`](Self::get_secret), [`set_secret`](Self::set_secret), +/// [`has_secret`](Self::has_secret), [`remove_secret`](Self::remove_secret) #[repr(transparent)] pub struct DelegateCtx { _private: (), @@ -128,6 +140,10 @@ impl DelegateCtx { Self { _private: () } } + // ======================================================================== + // Context methods (temporary state within a batch) + // ======================================================================== + /// Returns the current context length in bytes. #[inline] pub fn len(&self) -> usize { @@ -196,9 +212,8 @@ impl DelegateCtx { pub fn write(&mut self, data: &[u8]) -> bool { #[cfg(target_family = "wasm")] { - let result = unsafe { - __frnt__delegate__ctx_write(data.as_ptr() as i64, data.len() as i32) - }; + let result = + unsafe { __frnt__delegate__ctx_write(data.as_ptr() as i64, data.len() as i32) }; result == 0 } #[cfg(not(target_family = "wasm"))] @@ -213,54 +228,19 @@ impl DelegateCtx { pub fn clear(&mut self) { self.write(&[]); } -} - -impl std::fmt::Debug for DelegateCtx { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("DelegateCtx") - .field("len", &self.len()) - .finish() - } -} - -// ============================================================================ -// SecretsStore - Opaque handle to secret storage -// ============================================================================ - -/// Opaque handle to the delegate's secret store. -/// -/// Secrets are persistent across all delegate invocations and are stored -/// securely by the runtime. Use this for sensitive data like private keys, -/// tokens, or other credentials. -/// -/// Each delegate has its own isolated secret namespace - secrets from one -/// delegate cannot be accessed by another. -#[repr(transparent)] -pub struct SecretsStore { - _private: (), -} -impl SecretsStore { - /// Creates the secrets store handle. - /// - /// # Safety - /// - /// This should only be called by macro-generated code when the runtime - /// has set up the delegate execution environment. - #[doc(hidden)] - pub unsafe fn __new() -> Self { - Self { _private: () } - } + // ======================================================================== + // Secret methods (persistent encrypted storage) + // ======================================================================== /// Get the length of a secret without retrieving its value. /// /// Returns `None` if the secret does not exist. - pub fn get_len(&self, key: &[u8]) -> Option { + pub fn get_secret_len(&self, key: &[u8]) -> Option { #[cfg(target_family = "wasm")] { - let result = unsafe { - __frnt__delegate__get_secret_len(key.as_ptr() as i64, key.len() as i32) - }; + let result = + unsafe { __frnt__delegate__get_secret_len(key.as_ptr() as i64, key.len() as i32) }; if result < 0 { None } else { @@ -277,14 +257,11 @@ impl SecretsStore { /// Get a secret by key. /// /// Returns `None` if the secret does not exist. - pub fn get(&self, key: &[u8]) -> Option> { + pub fn get_secret(&self, key: &[u8]) -> Option> { #[cfg(target_family = "wasm")] { // First get the length to allocate the right buffer size - let len = match self.get_len(key) { - Some(len) => len, - None => return None, - }; + let len = self.get_secret_len(key)?; if len == 0 { return Some(Vec::new()); @@ -316,7 +293,7 @@ impl SecretsStore { /// Store a secret. /// /// Returns `true` on success, `false` on error. - pub fn set(&mut self, key: &[u8], value: &[u8]) -> bool { + pub fn set_secret(&mut self, key: &[u8], value: &[u8]) -> bool { #[cfg(target_family = "wasm")] { let result = unsafe { @@ -337,7 +314,7 @@ impl SecretsStore { } /// Check if a secret exists. - pub fn has(&self, key: &[u8]) -> bool { + pub fn has_secret(&self, key: &[u8]) -> bool { #[cfg(target_family = "wasm")] { let result = @@ -354,7 +331,7 @@ impl SecretsStore { /// Remove a secret. /// /// Returns `true` if the secret was removed, `false` if it didn't exist. - pub fn remove(&mut self, key: &[u8]) -> bool { + pub fn remove_secret(&mut self, key: &[u8]) -> bool { #[cfg(target_family = "wasm")] { let result = @@ -369,8 +346,30 @@ impl SecretsStore { } } -impl std::fmt::Debug for SecretsStore { +impl Default for DelegateCtx { + fn default() -> Self { + Self { _private: () } + } +} + +impl std::fmt::Debug for DelegateCtx { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SecretsStore").finish_non_exhaustive() + f.debug_struct("DelegateCtx") + .field("context_len", &self.len()) + .finish_non_exhaustive() } } + +// ============================================================================ +// SecretsStore - Deprecated, use DelegateCtx instead +// ============================================================================ + +/// Deprecated: Use [`DelegateCtx`] methods instead. +/// +/// This type alias exists for backward compatibility. New code should use +/// `ctx.get_secret()`, `ctx.set_secret()`, etc. directly on [`DelegateCtx`]. +#[deprecated( + since = "0.2.0", + note = "Use DelegateCtx methods (get_secret, set_secret, etc.) instead" +)] +pub type SecretsStore = DelegateCtx; diff --git a/rust/src/delegate_interface.rs b/rust/src/delegate_interface.rs index f45ca4d..f0ccc90 100644 --- a/rust/src/delegate_interface.rs +++ b/rust/src/delegate_interface.rs @@ -390,15 +390,15 @@ impl<'a> TryFromFbs<&FbsSecretsId<'a>> for SecretsId { /// impl DelegateInterface for MyDelegate { /// fn process( /// ctx: &mut DelegateCtx, -/// secrets: &mut SecretsStore, /// _params: Parameters<'static>, /// _attested: Option<&'static [u8]>, /// message: InboundDelegateMsg, /// ) -> Result, DelegateError> { /// // Access secrets synchronously - no round-trip needed! -/// if let Some(key) = secrets.get(b"private_key") { +/// if let Some(key) = ctx.get_secret(b"private_key") { /// // use key... /// } +/// ctx.set_secret(b"new_key", b"value"); /// /// // Read/write context for temporary state within a batch /// ctx.write(b"some state"); @@ -412,17 +412,15 @@ pub trait DelegateInterface { /// Process inbound message, producing zero or more outbound messages in response. /// /// # Arguments - /// - `ctx`: Mutable handle to the delegate's context. Context persists across - /// messages within a single batch but is reset between separate runtime calls. - /// - `secrets`: Mutable handle to the delegate's secret store. Secrets persist - /// across all delegate invocations. + /// - `ctx`: Mutable handle to the delegate's execution environment. Provides: + /// - **Context** (temporary): `read()`, `write()`, `len()`, `clear()` - state within a batch + /// - **Secrets** (persistent): `get_secret()`, `set_secret()`, `has_secret()`, `remove_secret()` /// - `parameters`: The delegate's initialization parameters. /// - `attested`: An optional identifier for the client of this function. Usually /// will be a [`ContractInstanceId`]. /// - `message`: The inbound message to process. fn process( ctx: &mut crate::delegate_host::DelegateCtx, - secrets: &mut crate::delegate_host::SecretsStore, parameters: Parameters<'static>, attested: Option<&'static [u8]>, message: InboundDelegateMsg, From c8fada34e4212c349af2935479725af1af57f08b Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Wed, 4 Feb 2026 09:09:00 +0100 Subject: [PATCH 5/8] chore: remove deprecated SecretsStore type alias --- rust/src/delegate_host.rs | 14 -------------- rust/src/lib.rs | 2 +- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/rust/src/delegate_host.rs b/rust/src/delegate_host.rs index 3f30b9e..1db62eb 100644 --- a/rust/src/delegate_host.rs +++ b/rust/src/delegate_host.rs @@ -359,17 +359,3 @@ impl std::fmt::Debug for DelegateCtx { .finish_non_exhaustive() } } - -// ============================================================================ -// SecretsStore - Deprecated, use DelegateCtx instead -// ============================================================================ - -/// Deprecated: Use [`DelegateCtx`] methods instead. -/// -/// This type alias exists for backward compatibility. New code should use -/// `ctx.get_secret()`, `ctx.set_secret()`, etc. directly on [`DelegateCtx`]. -#[deprecated( - since = "0.2.0", - note = "Use DelegateCtx methods (get_secret, set_secret, etc.) instead" -)] -pub type SecretsStore = DelegateCtx; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index f8d879c..283b34e 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -40,7 +40,7 @@ pub mod prelude { pub use crate::contract_interface::wasm_interface::ContractInterfaceResult; pub use crate::contract_interface::*; #[cfg(feature = "contract")] - pub use crate::delegate_host::{error_codes, DelegateCtx, SecretsStore}; + pub use crate::delegate_host::{error_codes, DelegateCtx}; pub use crate::delegate_interface::wasm_interface::DelegateInterfaceResult; pub use crate::delegate_interface::*; pub use crate::parameters::*; From 26a081ccb441edb9ad253d60ca66cdba7544f37f Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Wed, 4 Feb 2026 09:10:31 +0100 Subject: [PATCH 6/8] refactor: remove legacy DelegateInterface and cfg guards - Remove legacy DelegateInterface trait (without ctx parameter) - Remove #[cfg(feature = "contract")] guards from delegate_host - DelegateCtx is now always available, not feature-gated --- rust/src/delegate_interface.rs | 19 ------------------- rust/src/lib.rs | 2 -- 2 files changed, 21 deletions(-) diff --git a/rust/src/delegate_interface.rs b/rust/src/delegate_interface.rs index f0ccc90..256ffaa 100644 --- a/rust/src/delegate_interface.rs +++ b/rust/src/delegate_interface.rs @@ -407,7 +407,6 @@ impl<'a> TryFromFbs<&FbsSecretsId<'a>> for SecretsId { /// } /// } /// ``` -#[cfg(feature = "contract")] pub trait DelegateInterface { /// Process inbound message, producing zero or more outbound messages in response. /// @@ -427,24 +426,6 @@ pub trait DelegateInterface { ) -> Result, DelegateError>; } -/// Legacy delegate interface without host function access. -/// -/// This is used when the `contract` feature is not enabled. -#[cfg(not(feature = "contract"))] -pub trait DelegateInterface { - /// Process inbound message, producing zero or more outbound messages in response. - /// All state for the delegate must be stored using the secret mechanism. - /// - /// # Arguments - /// - attested: an optional identifier for the client of this function. Usually will - /// be a [`ContractInstanceId`]. - fn process( - parameters: Parameters<'static>, - attested: Option<&'static [u8]>, - message: InboundDelegateMsg, - ) -> Result, DelegateError>; -} - #[serde_as] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct DelegateContext(#[serde_as(as = "serde_with::Bytes")] Vec); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 283b34e..f5000b5 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -3,7 +3,6 @@ mod code_hash; #[cfg(feature = "unstable")] pub mod contract_composition; mod contract_interface; -#[cfg(feature = "contract")] pub mod delegate_host; mod delegate_interface; pub(crate) mod global; @@ -39,7 +38,6 @@ pub mod prelude { pub use crate::code_hash::*; pub use crate::contract_interface::wasm_interface::ContractInterfaceResult; pub use crate::contract_interface::*; - #[cfg(feature = "contract")] pub use crate::delegate_host::{error_codes, DelegateCtx}; pub use crate::delegate_interface::wasm_interface::DelegateInterfaceResult; pub use crate::delegate_interface::*; From 9f31bdfaca9f16cba0adc5eb9b43cc590fe3bea7 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Wed, 4 Feb 2026 09:21:29 +0100 Subject: [PATCH 7/8] fix: use derive(Default) for DelegateCtx --- rust/src/delegate_host.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/rust/src/delegate_host.rs b/rust/src/delegate_host.rs index 1db62eb..4ec9ca3 100644 --- a/rust/src/delegate_host.rs +++ b/rust/src/delegate_host.rs @@ -123,6 +123,7 @@ extern "C" { /// # Secret Methods /// - [`get_secret`](Self::get_secret), [`set_secret`](Self::set_secret), /// [`has_secret`](Self::has_secret), [`remove_secret`](Self::remove_secret) +#[derive(Default)] #[repr(transparent)] pub struct DelegateCtx { _private: (), @@ -346,12 +347,6 @@ impl DelegateCtx { } } -impl Default for DelegateCtx { - fn default() -> Self { - Self { _private: () } - } -} - impl std::fmt::Debug for DelegateCtx { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DelegateCtx") From 7eb9333ce03c094ca1cc22396de4bb4a7c456ed1 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Wed, 4 Feb 2026 09:22:16 +0100 Subject: [PATCH 8/8] chore: bump versions to 0.1.32 and 0.1.3 --- rust-macros/Cargo.toml | 2 +- rust/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rust-macros/Cargo.toml b/rust-macros/Cargo.toml index c339a3a..bf6c6e3 100644 --- a/rust-macros/Cargo.toml +++ b/rust-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "freenet-macros" -version = "0.1.2" +version = "0.1.3" edition = "2021" rust-version = "1.71.1" publish = true diff --git a/rust/Cargo.toml b/rust/Cargo.toml index b03073f..7887708 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "freenet-stdlib" -version = "0.1.31" +version = "0.1.32" edition = "2021" rust-version = "1.71.1" publish = true