Skip to main content

DelegateCtx

Struct DelegateCtx 

Source
pub struct DelegateCtx { /* private fields */ }
Expand description

Opaque handle to the delegate’s execution environment.

Provides access to:

  • Temporary context: State shared within a single message batch (reset between calls)
  • Persistent secrets: Encrypted storage that survives across all invocations
  • Contract state (V2): Direct synchronous access to local contract state

§Context Methods

§Secret Methods

§Contract Methods (V2)

§Delegate Management Methods (V2)

Implementations§

Source§

impl DelegateCtx

Source

pub fn len(&self) -> usize

Returns the current context length in bytes.

Source

pub fn is_empty(&self) -> bool

Returns true if the context is empty.

Source

pub fn read(&self) -> Vec<u8>

Read the current context bytes.

Returns an empty Vec if no context has been written.

Source

pub fn read_into(&self, buf: &mut [u8]) -> usize

Read context into a provided buffer.

Returns the number of bytes actually read.

Source

pub fn write(&mut self, data: &[u8]) -> bool

Write new context bytes, replacing any existing content.

Returns true on success, false on error.

Source

pub fn clear(&mut self)

Clear the context.

Source

pub fn get_secret_len(&self, key: &[u8]) -> Option<usize>

Get the length of a secret without retrieving its value.

Returns None if the secret does not exist.

Source

pub fn get_secret(&self, key: &[u8]) -> Option<Vec<u8>>

Get a secret by key.

Returns None if the secret does not exist.

Source

pub fn set_secret(&mut self, key: &[u8], value: &[u8]) -> bool

Store a secret.

Returns true on success, false on error.

Source

pub fn has_secret(&self, key: &[u8]) -> bool

Check if a secret exists.

Source

pub fn remove_secret(&mut self, key: &[u8]) -> bool

Remove a secret.

Returns true if the secret was removed, false if it didn’t exist.

Source

pub fn list_secrets(&self, prefix: &[u8]) -> Vec<Vec<u8>>

Enumerate the keys of every secret this delegate has stored whose raw key begins with prefix (pass an empty slice to list all keys).

Returns the matching raw keys (the same byte strings originally passed to set_secret). Order is unspecified. The host caps the number of keys returned; if storage holds more matching keys than the cap, the list is truncated (callers needing exhaustive enumeration should narrow the prefix).

This closes the gap that previously forced apps storing an open-ended key family (e.g. room:<owner_vk>) to maintain their own key registry: after a delegate-WASM rebuild the delegate can now rediscover what it has stored instead of probing a hardcoded key set.

Source

pub fn get_contract_state(&self, instance_id: &[u8; 32]) -> Option<Vec<u8>>

Get contract state by instance ID.

Returns Some(state_bytes) if the contract exists locally, None if not found or on error.

Uses a two-step protocol: first queries the state length, then reads the state bytes into an allocated buffer.

Source

pub fn put_contract_state( &mut self, instance_id: &[u8; 32], state: &[u8], ) -> bool

Store (PUT) contract state by instance ID.

The contract’s code must already be registered in the runtime’s contract store. Returns true on success, false on error.

Source

pub fn update_contract_state( &mut self, instance_id: &[u8; 32], state: &[u8], ) -> bool

Update contract state by instance ID.

Like put_contract_state, but only succeeds if the contract already has stored state. This performs a full state replacement (not a delta-based update through the contract’s update_state logic). Returns true on success, false if no prior state exists or on other errors.

Source

pub fn subscribe_contract(&mut self, instance_id: &[u8; 32]) -> bool

Subscribe to contract updates by instance ID.

Registers interest in receiving ContractNotification when the contract’s state changes, covering state committed locally as well as state arriving from the network.

Delivery is best-effort and lossy. The node drops notifications rather than blocking a state commit when the delivery channel is full, and if that channel is closed it removes the contract’s subscription entry outright — silently, for every delegate subscribed to it. A delegate that needs to be sure should poll contract state as a fallback rather than treat a notification as guaranteed.

§Whether this registers demand is a property of the NODE, not of this library

Subscribing always installs a local notification hook. Whether it also registers demand — keeping the contract in the update mesh and protecting it from eviction — depends on the freenet-core the delegate happens to be running on, and a delegate cannot detect which it has. The call reports success either way.

  • Nodes predating freenet-core#4669 register no demand at all: contract_in_use has no delegate term, so the contract does not enter the renewal set and is not exempt from eviction. Such a delegate sees remote updates only while something else keeps the node subscribed to that contract — typically an open UI client. Close the tab and the notifications stop, with no error reported anywhere.
  • Once #4669 lands, a subscribe registers demand when the node is hosting the contract. If the node can resolve the contract but is not hosting it, the subscribe still succeeds and notifications still work, but no demand is registered — registering demand for a contract the node does not hold would create a pin that can be neither renewed nor reclaimed. Closing that remaining gap needs a subscribe that can bootstrap an unheld contract over the network.

So do not write a delegate that assumes its subscription pins anything. This is documented at the call site rather than left in an issue precisely because nothing in the return value, the logs, or the delegate’s own view distinguishes the cases. Tracked in freenet-core#4669, phase 1 of the freenet-core#5467 epic.

Returns true on success, false if the contract is unknown or on error. Note that the contract must already be in the node’s local store; subscribing does not fetch it.

§The bool cannot express the case above

true means “the node accepted the registration”, and says nothing about whether there is anything for the subscription to fire on. A node that knows the contract’s code but holds no state for it registers the interest and returns true, identically to one that holds the state — which is the ordinary situation at startup, before the node has fetched the contract. The bool also collapses every negative error code into false, so a transient failure is indistinguishable from an unknown contract.

subscribe_contract_checked reports the outcome instead, and is what a delegate should use when its correctness depends on continuing to receive notifications. This method is deliberately left behaviourally unchanged, because altering what it returns would change the behaviour of already-deployed delegate WASM.

Source

pub fn subscribe_contract_checked( &mut self, instance_id: &[u8; 32], ) -> Result<SubscribeOutcome, i64>

Subscribe to contract updates, and learn whether the node actually holds the contract the subscription is against.

This is subscribe_contract with the outcome preserved instead of collapsed into a bool. Use it whenever the delegate’s correctness depends on the subscription being live — a missed notification on a payment address is money, and the failure is otherwise indistinguishable from “nothing has happened yet”.

SubscribeOutcome::NotPinned is a retryable condition, and it is the ordinary one at startup: it clears once the node holds the state. It is not a statement about eviction — see SubscribeOutcome::Pinned, which is deliberately not a durability promise.

§Compatibility

This is a host function, not a wire-format variant, so it costs no bincode variant tag and is additive in both directions:

  • A delegate that does not call it is completely unaffected; host imports resolve by name at module instantiation, so an unimported function costs nothing.
  • A delegate that does call it, on a node too old to provide it, fails to instantiate with a named missing-import error — loudly, at load time, before the delegate has touched any state. A wire variant instead fails mid-protocol at bincode decode, with no way for the delegate to have checked first.

Requires a node providing __frnt__delegate__subscribe_contract_checked in the freenet_delegate_contracts namespace. No released node does yet; the host half is freenet-core#5565.

§Errors

Err(code) carries the negative host error code and means the subscription did not happen at all. It is an i64, matching both the host function’s own return type and list_subscriptions, rather than narrowing to i32: the codes in error_codes all fit in i32, but a narrowing conversion has to decide what to do with one that does not, and every available answer invents a code the host never sent. A call that succeeded but did not pin is Ok(SubscribeOutcome::NotPinned), not an error — collapsing those two is the defect this method exists to fix.

Off-WASM this always returns Err(ERR_NOT_IN_PROCESS) rather than a plausible-looking success, so a host-side test cannot read an outcome out of a stub that never subscribed to anything.

Source

pub fn list_subscriptions(&self) -> Result<Vec<[u8; 32]>, i64>

List the contract instance ids this delegate is currently subscribed to.

A delegate’s subscription set lives in the node, not in the delegate: the WASM is instantiated per invocation and dropped immediately after, so between invocations the delegate has no view of it at all. Without this call it can only keep a parallel record in its own secrets, which drifts from the node’s exactly in the cases that matter, or re-subscribe to everything on every wake.

§What this does not yet solve

Today the node does not survive a restart with its delegate subscriptions intact — it loses them. DELEGATE_SUBSCRIPTIONS is an in-memory map (freenet-core wasm_runtime/native_api.rs), so after a restart this call correctly returns Ok(vec![]), and a delegate should read that as “the node is holding nothing for me”, not as “my subscriptions are gone but recoverable from somewhere else”.

So this call does not, on its own, deliver the restart-replay that freenet-core#5467 asks for. It is the read side of that capability, and it becomes load-bearing when #4669 part 3’s durable delegate-subscription store lands and there is finally something persistent to read back. Until then its value is within a single node lifetime: learning what the node currently holds, without guessing.

Order is unspecified; do not depend on it.

§What this list means

It answers “which contracts will notify me” — not “which contracts am I keeping alive”. Those coincide today, and coincide in the common case once freenet-core#4669 lands, but they are not the same thing by construction.

A delegate subscription is two records on the node: the notification hook, and (after #4669) the demand registration that actually pins the contract. They are written and torn down together on the ordinary paths, but not on all of them — an eviction that sheds a still-in-use contract clears the demand and leaves the hook standing, and the delegate is told nothing. A list sourced from the hook alone would therefore report a contract the delegate is no longer pinning.

That “looks subscribed, is not pinned” state is exactly what freenet-core#5467 exists to make visible, so this call must not reproduce it in the API meant to reveal it. The host is expected to answer from records it can cross-check rather than from the hook alone. This documentation deliberately promises the narrower meaning, so tightening the host’s answer later is a bug fix and not a breaking change.

Both divergences, and why the two obvious fixes are wrong, are tracked in freenet-core#5487. They close together at #4669 part 3’s durable delegate-subscription store, where the two records become one with one owner — so introspection built against the two-record shape will want rewriting when that lands.

§Cost

The node holds delegate subscriptions keyed contract → delegates, so answering this is a scan across every contract carrying any delegate subscription, filtered to the caller — O(all such contracts), not O(this delegate’s subscriptions). Call it on wake or after a restart, which is what it is for; do not call it in a loop or per message.

§Why this returns a Result

An empty list and a failed enumeration mean opposite things to the caller — “you hold no subscriptions, take them out again” versus “I could not tell you” — so they must not be represented by the same value. Collapsing them into an empty Vec is how a delegate ends up concluding its user’s content is unpinned because a host call failed. The error is the raw host code (see error_codes).

Off-WASM this is Err(ERR_NOT_IN_PROCESS) rather than an empty list, for the same reason: a host-side unit test must not be able to read “no subscriptions” out of a stub that never had any.

Host requirement: the node must register these imports in its wasmtime linker. That is a freenet-core change, not a stdlib one — host functions are registered by name and reference no stdlib type, so the stdlib version a node was built against guarantees nothing here. No released node provides them yet. A delegate that calls this against a node that does not fails to instantiate, with a named missing-import error — loud and diagnosable at load time rather than silently mid-protocol, which is the reason for choosing a host function over a message variant.

Source

pub fn schedule_wakeup( &mut self, after: Duration, tag: &[u8], ) -> Result<(), i64>

Ask the host to wake this delegate once after has elapsed.

The host delivers an InboundDelegateMsg::WakeupFired carrying tag verbatim. tag is opaque to the host and is how a delegate tells its own wakeups apart. Re-scheduling with the same tag replaces any prior pending wakeup for this (delegate, tag) pair, which is also how a wakeup is cancelled early — re-arm it far enough out.

Lets an always-on delegate run periodic background work (key rotation, TTL pruning, scheduled publication) with no UI attached, instead of pushing it into a client sync loop that stops when the tab closes. Driving use case: freenet/river#228.

§Why this is a host function and not an outbound message

A delegate’s outbound messages are serialized as one batch (delegate_interface.rs, Result<Vec<OutboundDelegateMsg>, _> in a single bincode::serialize) and decoded whole by the host. An outbound variant the host does not know therefore fails the entire batch, so a delegate built against a newer stdlib, returning [ApplicationMessage(reply), ScheduleWakeup{..}] to a current-release node, would have the reply discarded along with the wakeup — the user’s action silently doing nothing.

That is the direction ordinary rollout produces every time, because stdlib ships before core by policy. A host function fails the other way: an unimported function fails at instantiation, with a named missing import, loudly and once, rather than silently and per message.

WakeupFired remains an inbound wire variant, which has no equivalent hazard: a delegate that cannot schedule never receives one.

§Minimum delay

after is clamped up to MIN_WAKEUP_DELAY by this function, so Duration::ZERO is a one-second delay rather than a tight wake loop. A delegate that re-arms inside its own WakeupFired handler would otherwise spin the node — the same unbounded-work hazard a deadline in the past would have created.

The host is expected to clamp as well, and must, since a delegate can bypass this wrapper entirely (see the note on MAX_WAKEUP_TAG_BYTES). That host-side floor is not implemented yet — freenet-core#3972. The clamp here is what makes the guarantee true of this API today.

Nothing promises precision in the other direction: the guarantee is “not before”.

§Errors

Err(code) carries the negative host error code. error_codes::ERR_INVALID_PARAM is returned without calling the host if tag exceeds MAX_WAKEUP_TAG_BYTES.

Requires a node providing __frnt__delegate__schedule_wakeup in the freenet_delegate_management namespace. No released node does yet; the host half is freenet-core#3972, which must also persist pending wakeups across a restart — see MIN_WAKEUP_DELAY for why that is stated as an obligation rather than a guarantee.

Source

pub fn create_delegate( &mut self, wasm_code: &[u8], params: &[u8], cipher: &[u8; 32], nonce: &[u8; 24], ) -> Result<([u8; 32], [u8; 32]), i32>

Create a new child delegate from WASM bytecode and parameters.

This V2 host function allows a delegate to spawn new delegates at runtime. The child delegate is registered in the node’s delegate store and secret store with the provided cipher and nonce.

Returns Ok((key_hash, code_hash)) where both are 32-byte arrays identifying the newly created delegate. Returns Err(error_code) on failure.

§Resource Limits
  • Maximum creation depth: 4 (prevents fork bombs)
  • Maximum creations per process() call: 8
§Error Codes
  • -1: Called outside process() context
  • -4: Invalid parameter
  • -9: WASM memory bounds violation
  • -20: Depth limit exceeded
  • -21: Per-call creation limit exceeded
  • -23: Invalid WASM module
  • -24: Store registration failed

Trait Implementations§

Source§

impl Debug for DelegateCtx

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for DelegateCtx

Source§

fn default() -> DelegateCtx

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more