Borrow bounded batches
Checked variable-length instruction views, reusable wire contracts, and generated clients.
BoundedSlice<'a, T, N> accepts a variable number of checked wire values
without allocating or copying them into a capacity-sized array. It is an
unreleased addition; use matching workspace crates.
Use it for instruction batches, bounded authority lists, or fixed-stride
records. The input carries a little-endian u16 count followed by that many
consecutive T values. N limits the number of elements accepted. The parser
checks the count and available bytes, then validates every element before
returning a borrowed slice. Even an invalid last element refuses the batch.
use hopper::prelude::*;
#[hopper::pod]
#[derive(Clone, Copy)]
#[repr(C)]
pub struct Payment {
pub recipient: Address,
pub amount: WireU64,
}
#[hopper::program(entrypoint = false)]
mod payments {
use super::*;
#[instruction(0)]
pub fn validate(_ctx: &mut Context<'_>, payments: BoundedSlice<'_, Payment, 16>) -> ProgramResult {
for payment in payments.as_slice() {
if payment.amount.get() == 0 {
return Err(ProgramError::InvalidInstructionData);
}
}
Ok(())
}
}
This handler validates amounts. It does not authorize payments or transfer funds. A payment implementation must bind the correct authority, validate recipient accounts, and perform the appropriate System or token CPI.
The wire contract
| Property | Behavior |
|---|---|
| Count | Two bytes, little-endian, at most N |
| Element layout | Nonzero-sized, alignment-1 T: Pod |
| Element validation | Recursive Pod::validate_value checks before admission |
| Storage | A shared slice into the original instruction buffer |
| Empty batch | Accepted for nonzero-sized element types; application may reject it |
| Following arguments | Start immediately after the last element |
| Extra bytes | Refused by generated dispatch unless an explicit final byte tail consumes them |
| Errors | Malformed lengths and representations produce InvalidInstructionData |
BoundedSlice::parse_prefix returns (view, remaining_bytes) for manual
entrypoints. parse_exact additionally requires an empty suffix. Both retain
the input lifetime. Native multi-byte integers cannot be overlaid at arbitrary
offsets; use WireU64, other alignment-1 wire primitives, or #[hopper::pod]
layouts. A custom Pod implementation must uphold its safety contract and
provide validation for any representation restrictions.
The view has no mutable access. A declared capacity does not reserve N
elements on the program's stack. Validation still visits each present element,
so compute use grows with the work performed; this is not an O(1) validation
claim or a universal CU advantage. The instruction must also fit Solana's
transaction and compute limits.
Compatibility and clients
This is the same count prefix as Hopper's BoundedVec, but compatibility
requires that its element codec emits exactly the same fixed wire bytes as
T. It is not a reinterpretation of arbitrary Borsh vectors, Rust Vec
memory, or variable-stride codecs. Existing owned containers stay available.
Generated manifests retain the capacity and element width even when the
handler uses a type alias. Declared maximum wire length must fit the manifest's
u16 size field; oversized declarations fail compilation instead of truncating.
TypeScript clients encode named u8 elements as a Uint8Array; custom wire
elements and aliases use arrays of exact-width Uint8Array values. Generated
Rust clients represent sequence elements as Vec<[u8; WIDTH]>. Neither client
infers nested application encoders from opaque bytes. The program validates
their representations and application rules.
Rust instruction builders containing bounded arguments now return Result.
They check capacities before encoding; their decoders use checked offsets and
refuse trailing bytes, excessive counts, truncated elements, and malformed
UTF-8 strings. Regenerate these clients and handle the result when upgrading.
Their *_DATA_LEN constants are maximum lengths for dynamic instructions.
Fixed-only instruction builders retain their existing signatures.
Python builders accept a list of exact-width byte elements and encode bounded strings as UTF-8, checking byte capacity. C, Go and Kotlin builders explicitly refuse bounded encodings before producing instruction data. See the 0.6 migration guide for the client support matrix and custom owned element codec requirements.
Verify the boundary
The batch fixture runs manual and generated dispatch with no allocator. It verifies pointer identity on Solana, validates all elements, and writes its test state only after the complete batch passes. Compiled VM tests check malformed second elements, every tag byte, batch limits, truncations, arithmetic overflow, and complete account preservation on refusal. The client check compiles generated Rust and executes generated TypeScript against independently assembled bytes.
