Skip to main content

freenet_stdlib/
delegate_host.rs

1//! Host function API for delegates.
2//!
3//! This module provides synchronous access to delegate context, secrets, and
4//! contract state via host functions, eliminating the need for message round-trips.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use freenet_stdlib::prelude::*;
10//!
11//! #[delegate]
12//! impl DelegateInterface for MyDelegate {
13//!     fn process(
14//!         ctx: &mut DelegateCtx,
15//!         _params: Parameters<'static>,
16//!         _attested: Option<&'static [u8]>,
17//!         message: InboundDelegateMsg,
18//!     ) -> Result<Vec<OutboundDelegateMsg>, DelegateError> {
19//!         // Read/write temporary context
20//!         let data = ctx.read();
21//!         ctx.write(b"new state");
22//!
23//!         // Access persistent secrets
24//!         if let Some(key) = ctx.get_secret(b"private_key") {
25//!             // use key...
26//!         }
27//!         ctx.set_secret(b"new_secret", b"value");
28//!
29//!         // Read contract state the node already holds (no round-trip)
30//!         let contract_id = [0u8; 32]; // your contract instance ID
31//!         if let Some(state) = ctx.get_contract_state(&contract_id) {
32//!             // process state...
33//!         }
34//!
35//!         Ok(vec![])
36//!     }
37//! }
38//! ```
39//!
40//! # Context vs Secrets vs Contracts
41//!
42//! - **Context** (`read`/`write`): Temporary state within a single message batch.
43//!   Reset between separate runtime calls. Use for intermediate processing state.
44//!
45//! - **Secrets** (`get_secret`/`set_secret`): Persistent encrypted storage.
46//!   Survives across all delegate invocations. Use for private keys, tokens, etc.
47//!
48//! - **Contracts** (`get_contract_state`): a synchronous read of contract state
49//!   the node already holds locally, with no request/response round-trip.
50//!
51//!   There is no host function for *writing* or *subscribing*. A delegate does
52//!   both by emitting the corresponding `OutboundDelegateMsg` —
53//!   `PutContractRequest`, `UpdateContractRequest`, `SubscribeContractRequest` —
54//!   which go through the node's normal contract path.
55//!
56//! # Adding a host function is the additive way to extend this API
57//!
58//! Host functions are resolved **by name at module instantiation**. A delegate
59//! that imports one an older node does not provide fails to load, with a named
60//! missing-import error; a delegate that does not import it is unaffected. So
61//! adding a host function is additive for every existing delegate, and its
62//! failure mode for a too-old node is loud and diagnosable at load time.
63//!
64//! Contrast the message API (`OutboundDelegateMsg`): a new variant sent to an
65//! older host fails mid-protocol at bincode decode, with no way for the
66//! delegate to have detected the host's version first. Where a capability can
67//! be expressed either way, prefer the host function.
68//!
69//! # Error Codes
70//!
71//! Host functions return negative values to indicate errors:
72//!
73//! | Code | Meaning |
74//! |------|---------|
75//! | 0    | Success |
76//! | -1   | Called outside process() context |
77//! | -2   | Secret not found |
78//! | -3   | Storage operation failed |
79//! | -4   | Invalid parameter (e.g., negative length) |
80//! | -5   | Context too large (exceeds i32::MAX) |
81//! | -6   | Buffer too small |
82//! | -7   | Contract not found in local store |
83//! | -8   | Internal state store error |
84//! | -9   | WASM memory bounds violation |
85//! | -10  | Contract code not registered |
86//!
87//! The wrapper methods in [`DelegateCtx`] handle these error codes and present
88//! a more ergonomic API.
89
90/// Error codes returned by host functions.
91///
92/// Negative values indicate errors, non-negative values indicate success
93/// (usually the number of bytes read/written).
94pub mod error_codes {
95    /// Operation succeeded.
96    pub const SUCCESS: i32 = 0;
97    /// Called outside of a process() context.
98    pub const ERR_NOT_IN_PROCESS: i32 = -1;
99    /// Secret not found.
100    pub const ERR_SECRET_NOT_FOUND: i32 = -2;
101    /// Storage operation failed.
102    pub const ERR_STORAGE_FAILED: i32 = -3;
103    /// Invalid parameter (e.g., negative length).
104    pub const ERR_INVALID_PARAM: i32 = -4;
105    /// Context too large (exceeds i32::MAX).
106    pub const ERR_CONTEXT_TOO_LARGE: i32 = -5;
107    /// Buffer too small to hold the data.
108    pub const ERR_BUFFER_TOO_SMALL: i32 = -6;
109    /// Contract not found in local store.
110    pub const ERR_CONTRACT_NOT_FOUND: i32 = -7;
111    /// Internal state store error.
112    pub const ERR_STORE_ERROR: i32 = -8;
113    /// WASM memory bounds violation (pointer/length out of range).
114    pub const ERR_MEMORY_BOUNDS: i32 = -9;
115    /// Contract code not registered in the index.
116    pub const ERR_CONTRACT_CODE_NOT_REGISTERED: i32 = -10;
117    /// Delegate creation depth limit exceeded.
118    pub const ERR_DEPTH_EXCEEDED: i32 = -20;
119    /// Per-call delegate creation limit exceeded.
120    pub const ERR_CREATIONS_EXCEEDED: i32 = -21;
121    /// Invalid WASM module (failed to construct DelegateContainer).
122    pub const ERR_INVALID_WASM: i32 = -23;
123    /// Failed to register delegate in secret/delegate store.
124    pub const ERR_STORE_FAILED: i32 = -24;
125}
126
127// ============================================================================
128// Host function declarations (WASM only)
129// ============================================================================
130
131#[cfg(target_family = "wasm")]
132#[link(wasm_import_module = "freenet_delegate_ctx")]
133extern "C" {
134    /// Returns the current context length in bytes, or negative error code.
135    fn __frnt__delegate__ctx_len() -> i32;
136    /// Reads context into the buffer at `ptr` (max `len` bytes). Returns bytes written, or negative error code.
137    fn __frnt__delegate__ctx_read(ptr: i64, len: i32) -> i32;
138    /// Writes `len` bytes from `ptr` into the context, replacing existing content. Returns 0 on success, or negative error code.
139    fn __frnt__delegate__ctx_write(ptr: i64, len: i32) -> i32;
140}
141
142#[cfg(target_family = "wasm")]
143#[link(wasm_import_module = "freenet_delegate_secrets")]
144extern "C" {
145    /// Get a secret. Returns bytes written to `out_ptr`, or negative error code.
146    fn __frnt__delegate__get_secret(key_ptr: i64, key_len: i32, out_ptr: i64, out_len: i32) -> i32;
147    /// Get secret length without fetching value. Returns length, or negative error code.
148    fn __frnt__delegate__get_secret_len(key_ptr: i64, key_len: i32) -> i32;
149    /// Store a secret. Returns 0 on success, or negative error code.
150    fn __frnt__delegate__set_secret(key_ptr: i64, key_len: i32, val_ptr: i64, val_len: i32) -> i32;
151    /// Check if a secret exists. Returns 1 if yes, 0 if no, or negative error code.
152    fn __frnt__delegate__has_secret(key_ptr: i64, key_len: i32) -> i32;
153    /// Remove a secret. Returns 0 on success, or negative error code.
154    fn __frnt__delegate__remove_secret(key_ptr: i64, key_len: i32) -> i32;
155    /// Length (in bytes) of the serialized key list for all stored secret keys
156    /// whose raw key starts with the `prefix_len`-byte prefix at `prefix_ptr`
157    /// (an empty prefix matches every key). Returns the byte count to allocate
158    /// before calling `__frnt__delegate__list_secrets`, or a negative error code.
159    fn __frnt__delegate__list_secrets_len(prefix_ptr: i64, prefix_len: i32) -> i32;
160    /// Enumerate stored secret keys matching the prefix. Writes a length-prefixed
161    /// list to `out_ptr` (max `out_len` bytes): each record is a 4-byte
162    /// little-endian length followed by that many key bytes. Returns the number
163    /// of bytes written, or a negative error code.
164    fn __frnt__delegate__list_secrets(
165        prefix_ptr: i64,
166        prefix_len: i32,
167        out_ptr: i64,
168        out_len: i32,
169    ) -> i32;
170}
171
172#[cfg(target_family = "wasm")]
173#[link(wasm_import_module = "freenet_delegate_contracts")]
174extern "C" {
175    /// Get contract state length. Returns byte count, or negative error code (i64).
176    fn __frnt__delegate__get_contract_state_len(id_ptr: i64, id_len: i32) -> i64;
177    /// Get contract state. Returns byte count written, or negative error code (i64).
178    fn __frnt__delegate__get_contract_state(
179        id_ptr: i64,
180        id_len: i32,
181        out_ptr: i64,
182        out_len: i64,
183    ) -> i64;
184}
185
186#[cfg(target_family = "wasm")]
187#[link(wasm_import_module = "freenet_delegate_management")]
188extern "C" {
189    /// Create a new delegate from WASM code + parameters.
190    /// Returns 0 on success, negative error code on failure.
191    /// On success, writes 32 bytes to out_key_ptr and 32 bytes to out_hash_ptr.
192    fn __frnt__delegate__create_delegate(
193        wasm_ptr: i64,
194        wasm_len: i64,
195        params_ptr: i64,
196        params_len: i64,
197        cipher_ptr: i64,
198        nonce_ptr: i64,
199        out_key_ptr: i64,
200        out_hash_ptr: i64,
201    ) -> i32;
202}
203
204// ============================================================================
205// DelegateCtx - Unified handle to context, secrets, and contracts
206// ============================================================================
207
208/// Opaque handle to the delegate's execution environment.
209///
210/// Provides access to:
211/// - **Temporary context**: State shared within a single message batch (reset between calls)
212/// - **Persistent secrets**: Encrypted storage that survives across all invocations
213/// - **Contract state**: Direct synchronous read of local contract state
214///
215/// # Context Methods
216/// - [`read`](Self::read), [`write`](Self::write), [`len`](Self::len), [`clear`](Self::clear)
217///
218/// # Secret Methods
219/// - [`get_secret`](Self::get_secret), [`set_secret`](Self::set_secret),
220///   [`has_secret`](Self::has_secret), [`remove_secret`](Self::remove_secret)
221///
222/// # Contract Methods
223/// - [`get_contract_state`](Self::get_contract_state)
224///
225/// # Delegate Management Methods
226/// - [`create_delegate`](Self::create_delegate)
227#[derive(Default)]
228#[repr(transparent)]
229pub struct DelegateCtx {
230    _private: (),
231}
232
233impl DelegateCtx {
234    /// Creates the context handle.
235    ///
236    /// # Safety
237    ///
238    /// This should only be called by macro-generated code when the runtime
239    /// has set up the delegate execution environment.
240    #[doc(hidden)]
241    pub unsafe fn __new() -> Self {
242        Self { _private: () }
243    }
244
245    // ========================================================================
246    // Context methods (temporary state within a batch)
247    // ========================================================================
248
249    /// Returns the current context length in bytes.
250    #[inline]
251    pub fn len(&self) -> usize {
252        #[cfg(target_family = "wasm")]
253        {
254            let len = unsafe { __frnt__delegate__ctx_len() };
255            if len < 0 {
256                0
257            } else {
258                len as usize
259            }
260        }
261        #[cfg(not(target_family = "wasm"))]
262        {
263            0
264        }
265    }
266
267    /// Returns `true` if the context is empty.
268    #[inline]
269    pub fn is_empty(&self) -> bool {
270        self.len() == 0
271    }
272
273    /// Read the current context bytes.
274    ///
275    /// Returns an empty `Vec` if no context has been written.
276    pub fn read(&self) -> Vec<u8> {
277        #[cfg(target_family = "wasm")]
278        {
279            let len = unsafe { __frnt__delegate__ctx_len() };
280            if len <= 0 {
281                return Vec::new();
282            }
283            let mut buf = vec![0u8; len as usize];
284            let read = unsafe { __frnt__delegate__ctx_read(buf.as_mut_ptr() as i64, len) };
285            buf.truncate(read.max(0) as usize);
286            buf
287        }
288        #[cfg(not(target_family = "wasm"))]
289        {
290            Vec::new()
291        }
292    }
293
294    /// Read context into a provided buffer.
295    ///
296    /// Returns the number of bytes actually read.
297    pub fn read_into(&self, buf: &mut [u8]) -> usize {
298        #[cfg(target_family = "wasm")]
299        {
300            let read =
301                unsafe { __frnt__delegate__ctx_read(buf.as_mut_ptr() as i64, buf.len() as i32) };
302            read.max(0) as usize
303        }
304        #[cfg(not(target_family = "wasm"))]
305        {
306            let _ = buf;
307            0
308        }
309    }
310
311    /// Write new context bytes, replacing any existing content.
312    ///
313    /// Returns `true` on success, `false` on error.
314    pub fn write(&mut self, data: &[u8]) -> bool {
315        #[cfg(target_family = "wasm")]
316        {
317            let result =
318                unsafe { __frnt__delegate__ctx_write(data.as_ptr() as i64, data.len() as i32) };
319            result == 0
320        }
321        #[cfg(not(target_family = "wasm"))]
322        {
323            let _ = data;
324            false
325        }
326    }
327
328    /// Clear the context.
329    #[inline]
330    pub fn clear(&mut self) {
331        self.write(&[]);
332    }
333
334    // ========================================================================
335    // Secret methods (persistent encrypted storage)
336    // ========================================================================
337
338    /// Get the length of a secret without retrieving its value.
339    ///
340    /// Returns `None` if the secret does not exist.
341    pub fn get_secret_len(&self, key: &[u8]) -> Option<usize> {
342        #[cfg(target_family = "wasm")]
343        {
344            let result =
345                unsafe { __frnt__delegate__get_secret_len(key.as_ptr() as i64, key.len() as i32) };
346            if result < 0 {
347                None
348            } else {
349                Some(result as usize)
350            }
351        }
352        #[cfg(not(target_family = "wasm"))]
353        {
354            let _ = key;
355            None
356        }
357    }
358
359    /// Get a secret by key.
360    ///
361    /// Returns `None` if the secret does not exist.
362    pub fn get_secret(&self, key: &[u8]) -> Option<Vec<u8>> {
363        #[cfg(target_family = "wasm")]
364        {
365            // First get the length to allocate the right buffer size
366            let len = self.get_secret_len(key)?;
367
368            if len == 0 {
369                return Some(Vec::new());
370            }
371
372            let mut out = vec![0u8; len];
373            let result = unsafe {
374                __frnt__delegate__get_secret(
375                    key.as_ptr() as i64,
376                    key.len() as i32,
377                    out.as_mut_ptr() as i64,
378                    out.len() as i32,
379                )
380            };
381            if result < 0 {
382                None
383            } else {
384                out.truncate(result as usize);
385                Some(out)
386            }
387        }
388        #[cfg(not(target_family = "wasm"))]
389        {
390            let _ = key;
391            None
392        }
393    }
394
395    /// Store a secret.
396    ///
397    /// Returns `true` on success, `false` on error.
398    pub fn set_secret(&mut self, key: &[u8], value: &[u8]) -> bool {
399        #[cfg(target_family = "wasm")]
400        {
401            let result = unsafe {
402                __frnt__delegate__set_secret(
403                    key.as_ptr() as i64,
404                    key.len() as i32,
405                    value.as_ptr() as i64,
406                    value.len() as i32,
407                )
408            };
409            result == 0
410        }
411        #[cfg(not(target_family = "wasm"))]
412        {
413            let _ = (key, value);
414            false
415        }
416    }
417
418    /// Check if a secret exists.
419    pub fn has_secret(&self, key: &[u8]) -> bool {
420        #[cfg(target_family = "wasm")]
421        {
422            let result =
423                unsafe { __frnt__delegate__has_secret(key.as_ptr() as i64, key.len() as i32) };
424            result == 1
425        }
426        #[cfg(not(target_family = "wasm"))]
427        {
428            let _ = key;
429            false
430        }
431    }
432
433    /// Remove a secret.
434    ///
435    /// Returns `true` if the secret was removed, `false` if it didn't exist.
436    pub fn remove_secret(&mut self, key: &[u8]) -> bool {
437        #[cfg(target_family = "wasm")]
438        {
439            let result =
440                unsafe { __frnt__delegate__remove_secret(key.as_ptr() as i64, key.len() as i32) };
441            result == 0
442        }
443        #[cfg(not(target_family = "wasm"))]
444        {
445            let _ = key;
446            false
447        }
448    }
449
450    /// Enumerate the keys of every secret this delegate has stored whose raw
451    /// key begins with `prefix` (pass an empty slice to list all keys).
452    ///
453    /// Returns the matching raw keys (the same byte strings originally passed
454    /// to [`set_secret`](Self::set_secret)). Order is unspecified. The host
455    /// caps the number of keys returned; if storage holds more matching keys
456    /// than the cap, the list is truncated (callers needing exhaustive
457    /// enumeration should narrow the prefix).
458    ///
459    /// This closes the gap that previously forced apps storing an open-ended
460    /// key family (e.g. `room:<owner_vk>`) to maintain their own key registry:
461    /// after a delegate-WASM rebuild the delegate can now rediscover what it
462    /// has stored instead of probing a hardcoded key set.
463    pub fn list_secrets(&self, prefix: &[u8]) -> Vec<Vec<u8>> {
464        #[cfg(target_family = "wasm")]
465        {
466            let len = unsafe {
467                __frnt__delegate__list_secrets_len(prefix.as_ptr() as i64, prefix.len() as i32)
468            };
469            if len <= 0 {
470                // Negative => error; zero => no matching keys. Either way the
471                // caller gets an empty list (errors are non-fatal: enumeration
472                // is advisory).
473                return Vec::new();
474            }
475            let mut out = vec![0u8; len as usize];
476            let written = unsafe {
477                __frnt__delegate__list_secrets(
478                    prefix.as_ptr() as i64,
479                    prefix.len() as i32,
480                    out.as_mut_ptr() as i64,
481                    out.len() as i32,
482                )
483            };
484            if written < 0 {
485                return Vec::new();
486            }
487            out.truncate(written as usize);
488            decode_secret_key_list(&out)
489        }
490        #[cfg(not(target_family = "wasm"))]
491        {
492            let _ = prefix;
493            Vec::new()
494        }
495    }
496
497    // ========================================================================
498    // Contract methods (direct synchronous read of local state)
499    // ========================================================================
500
501    /// Get contract state by instance ID.
502    ///
503    /// Returns `Some(state_bytes)` if the contract exists locally,
504    /// `None` if not found or on error.
505    ///
506    /// Uses a two-step protocol: first queries the state length, then reads
507    /// the state bytes into an allocated buffer.
508    pub fn get_contract_state(&self, instance_id: &[u8; 32]) -> Option<Vec<u8>> {
509        #[cfg(target_family = "wasm")]
510        {
511            // Step 1: Get the state length
512            let len = unsafe {
513                __frnt__delegate__get_contract_state_len(instance_id.as_ptr() as i64, 32)
514            };
515            if len < 0 {
516                return None;
517            }
518            let len = len as usize;
519            if len == 0 {
520                return Some(Vec::new());
521            }
522
523            // Step 2: Read the state bytes
524            let mut buf = vec![0u8; len];
525            let read = unsafe {
526                __frnt__delegate__get_contract_state(
527                    instance_id.as_ptr() as i64,
528                    32,
529                    buf.as_mut_ptr() as i64,
530                    buf.len() as i64,
531                )
532            };
533            if read < 0 {
534                None
535            } else {
536                buf.truncate(read as usize);
537                Some(buf)
538            }
539        }
540        #[cfg(not(target_family = "wasm"))]
541        {
542            let _ = instance_id;
543            None
544        }
545    }
546
547    /// Create a new child delegate from WASM bytecode and parameters.
548    ///
549    /// This V2 host function allows a delegate to spawn new delegates at runtime.
550    /// The child delegate is registered in the node's delegate store and secret store
551    /// with the provided cipher and nonce.
552    ///
553    /// Returns `Ok((key_hash, code_hash))` where both are 32-byte arrays identifying
554    /// the newly created delegate. Returns `Err(error_code)` on failure.
555    ///
556    /// # Resource Limits
557    /// - Maximum creation depth: 4 (prevents fork bombs)
558    /// - Maximum creations per process() call: 8
559    ///
560    /// # Error Codes
561    /// - `-1`: Called outside process() context
562    /// - `-4`: Invalid parameter
563    /// - `-9`: WASM memory bounds violation
564    /// - `-20`: Depth limit exceeded
565    /// - `-21`: Per-call creation limit exceeded
566    /// - `-23`: Invalid WASM module
567    /// - `-24`: Store registration failed
568    pub fn create_delegate(
569        &mut self,
570        wasm_code: &[u8],
571        params: &[u8],
572        cipher: &[u8; 32],
573        nonce: &[u8; 24],
574    ) -> Result<([u8; 32], [u8; 32]), i32> {
575        #[cfg(target_family = "wasm")]
576        {
577            let mut key_buf = [0u8; 32];
578            let mut hash_buf = [0u8; 32];
579            let result = unsafe {
580                __frnt__delegate__create_delegate(
581                    wasm_code.as_ptr() as i64,
582                    wasm_code.len() as i64,
583                    params.as_ptr() as i64,
584                    params.len() as i64,
585                    cipher.as_ptr() as i64,
586                    nonce.as_ptr() as i64,
587                    key_buf.as_mut_ptr() as i64,
588                    hash_buf.as_mut_ptr() as i64,
589                )
590            };
591            if result == 0 {
592                Ok((key_buf, hash_buf))
593            } else {
594                Err(result)
595            }
596        }
597        #[cfg(not(target_family = "wasm"))]
598        {
599            let _ = (wasm_code, params, cipher, nonce);
600            Err(error_codes::ERR_NOT_IN_PROCESS)
601        }
602    }
603}
604
605impl std::fmt::Debug for DelegateCtx {
606    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
607        f.debug_struct("DelegateCtx")
608            .field("context_len", &self.len())
609            .finish_non_exhaustive()
610    }
611}
612
613// ============================================================================
614// Secret-key-list wire codec (shared host↔delegate contract for list_secrets)
615// ============================================================================
616
617/// Serialize a list of raw secret keys into the wire format read back by
618/// [`decode_secret_key_list`]: for each key, a 4-byte little-endian length
619/// followed by that many key bytes. This is the encoding the host
620/// (`__frnt__delegate__list_secrets`) writes into the delegate's output buffer.
621///
622/// Kept in stdlib (rather than only in the host) so the format has exactly one
623/// authoritative definition that both sides — and the round-trip tests — share.
624pub fn encode_secret_key_list<'a, I>(keys: I) -> Vec<u8>
625where
626    I: IntoIterator<Item = &'a [u8]>,
627{
628    let mut buf = Vec::new();
629    for key in keys {
630        buf.extend_from_slice(&(key.len() as u32).to_le_bytes());
631        buf.extend_from_slice(key);
632    }
633    buf
634}
635
636/// Inverse of [`encode_secret_key_list`]. A truncated trailing record (which can
637/// only happen if the buffer was clipped mid-record) is dropped rather than
638/// panicking, so a short read degrades to "fewer keys" instead of a trap.
639pub fn decode_secret_key_list(buf: &[u8]) -> Vec<Vec<u8>> {
640    let mut keys = Vec::new();
641    let mut pos = 0usize;
642    while pos + 4 <= buf.len() {
643        let len = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]) as usize;
644        pos += 4;
645        if pos + len > buf.len() {
646            // Truncated record: stop here rather than over-read.
647            break;
648        }
649        keys.push(buf[pos..pos + len].to_vec());
650        pos += len;
651    }
652    keys
653}
654
655#[cfg(test)]
656mod secret_key_list_codec_tests {
657    use super::{decode_secret_key_list, encode_secret_key_list};
658
659    #[test]
660    fn round_trip_multiple_keys() {
661        let keys: Vec<&[u8]> = vec![b"room:alice", b"room:bob", b"private_key"];
662        let encoded = encode_secret_key_list(keys.iter().copied());
663        let decoded = decode_secret_key_list(&encoded);
664        assert_eq!(
665            decoded,
666            vec![
667                b"room:alice".to_vec(),
668                b"room:bob".to_vec(),
669                b"private_key".to_vec()
670            ]
671        );
672    }
673
674    #[test]
675    fn round_trip_empty_list() {
676        let encoded = encode_secret_key_list(std::iter::empty::<&[u8]>());
677        assert!(encoded.is_empty());
678        assert!(decode_secret_key_list(&encoded).is_empty());
679    }
680
681    #[test]
682    fn round_trip_empty_key() {
683        // A zero-length key is a legal (if unusual) record.
684        let encoded = encode_secret_key_list([b"".as_slice()]);
685        assert_eq!(encoded, vec![0, 0, 0, 0]);
686        assert_eq!(decode_secret_key_list(&encoded), vec![Vec::<u8>::new()]);
687    }
688
689    #[test]
690    fn truncated_trailing_record_is_dropped() {
691        let mut encoded = encode_secret_key_list([b"abc".as_slice(), b"defgh".as_slice()]);
692        // Clip mid-way through the second record's payload.
693        encoded.truncate(encoded.len() - 2);
694        assert_eq!(decode_secret_key_list(&encoded), vec![b"abc".to_vec()]);
695    }
696}