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)
get_contract_state,put_contract_state,update_contract_state,subscribe_contract,list_subscriptions
§Delegate Management Methods (V2)
Implementations§
Source§impl DelegateCtx
impl DelegateCtx
Sourcepub fn read(&self) -> Vec<u8> ⓘ
pub fn read(&self) -> Vec<u8> ⓘ
Read the current context bytes.
Returns an empty Vec if no context has been written.
Sourcepub fn read_into(&self, buf: &mut [u8]) -> usize
pub fn read_into(&self, buf: &mut [u8]) -> usize
Read context into a provided buffer.
Returns the number of bytes actually read.
Sourcepub fn write(&mut self, data: &[u8]) -> bool
pub fn write(&mut self, data: &[u8]) -> bool
Write new context bytes, replacing any existing content.
Returns true on success, false on error.
Sourcepub fn get_secret_len(&self, key: &[u8]) -> Option<usize>
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.
Sourcepub fn get_secret(&self, key: &[u8]) -> Option<Vec<u8>>
pub fn get_secret(&self, key: &[u8]) -> Option<Vec<u8>>
Get a secret by key.
Returns None if the secret does not exist.
Sourcepub fn set_secret(&mut self, key: &[u8], value: &[u8]) -> bool
pub fn set_secret(&mut self, key: &[u8], value: &[u8]) -> bool
Store a secret.
Returns true on success, false on error.
Sourcepub fn has_secret(&self, key: &[u8]) -> bool
pub fn has_secret(&self, key: &[u8]) -> bool
Check if a secret exists.
Sourcepub fn remove_secret(&mut self, key: &[u8]) -> bool
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.
Sourcepub fn list_secrets(&self, prefix: &[u8]) -> Vec<Vec<u8>>
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.
Sourcepub fn get_contract_state(&self, instance_id: &[u8; 32]) -> Option<Vec<u8>>
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.
Sourcepub fn put_contract_state(
&mut self,
instance_id: &[u8; 32],
state: &[u8],
) -> bool
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.
Sourcepub fn update_contract_state(
&mut self,
instance_id: &[u8; 32],
state: &[u8],
) -> bool
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.
Sourcepub fn subscribe_contract(&mut self, instance_id: &[u8; 32]) -> bool
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_usehas 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.
Sourcepub fn subscribe_contract_checked(
&mut self,
instance_id: &[u8; 32],
) -> Result<SubscribeOutcome, i64>
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.
Sourcepub fn list_subscriptions(&self) -> Result<Vec<[u8; 32]>, i64>
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.
Sourcepub fn schedule_wakeup(
&mut self,
after: Duration,
tag: &[u8],
) -> Result<(), i64>
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.
Sourcepub fn create_delegate(
&mut self,
wasm_code: &[u8],
params: &[u8],
cipher: &[u8; 32],
nonce: &[u8; 24],
) -> Result<([u8; 32], [u8; 32]), i32>
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