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}