Skip to main content

ic_wire/contract/
mod.rs

1#[cfg(test)]
2mod tests;
3
4use crate::{Error, Limits, Wire};
5use alloc::vec::Vec;
6use core::{cell::Cell, marker::PhantomData};
7use postcard::ser_flavors::Flavor;
8
9pub(crate) const HEADER: usize = 11;
10
11/// One consumer-owned endpoint contract. IDs must be unique across incompatible
12/// layouts and methods. A source rename alone must not change its identity.
13pub trait Method {
14    /// Typed request, including any consumer-owned proof/envelope.
15    /// Borrowed fields can use lifetimes on the implementing method marker.
16    type Request: Wire;
17    /// Typed response, typically `Result<Success, ApplicationError>`.
18    type Response: Wire;
19    /// Stable contract identity assigned by the consumer.
20    const ID: u64;
21    /// Exact exported IC method name; used by typed transport adapters.
22    const NAME: &'static str;
23    /// Request admission budget.
24    const REQUEST_LIMITS: Limits;
25    /// Reply admission budget.
26    const RESPONSE_LIMITS: Limits;
27}
28
29macro_rules! retained {
30    ($name:ident) => {
31        /// Validated, immutable bytes bound to a method and direction. Retain
32        /// these for retries; the codec does not decide when retry is safe.
33        pub struct $name<M: Method> {
34            bytes: Vec<u8>,
35            method: PhantomData<fn() -> M>,
36        }
37        impl<M: Method> $name<M> {
38            /// Borrow the exact encoding for transport or hashing.
39            pub fn as_bytes(&self) -> &[u8] {
40                &self.bytes
41            }
42            /// Transfer ownership to a transport or persistence owner.
43            pub fn into_bytes(self) -> Vec<u8> {
44                self.bytes
45            }
46        }
47    };
48}
49retained!(RequestBytes);
50retained!(ResponseBytes);
51
52pub(crate) fn admit<T: Wire>(limits: Limits) -> Result<usize, Error> {
53    let shape = T::SHAPE;
54    if [shape.bytes, shape.heap, shape.nodes, shape.depth].contains(&usize::MAX) {
55        return Err(Error::SchemaLimit);
56    }
57    let size = shape.bytes.checked_add(HEADER).ok_or(Error::SchemaLimit)?;
58    let allocation = shape.heap.checked_add(size).ok_or(Error::SchemaLimit)?;
59    if size > limits.bytes
60        || allocation > limits.allocation
61        || shape.nodes > limits.nodes
62        || shape.depth > limits.depth
63    {
64        return Err(Error::SchemaLimit);
65    }
66    Ok(size)
67}
68
69// Reserve fallibly once, then initialize only emitted bytes. Capacity is not
70// the schema limit: allocators may return more capacity than requested.
71pub(crate) struct Output {
72    pub(crate) bytes: Vec<u8>,
73    pub(crate) limit: usize,
74}
75impl Flavor for Output {
76    type Output = Vec<u8>;
77    fn try_push(&mut self, byte: u8) -> postcard::Result<()> {
78        if self.bytes.len() == self.limit {
79            return Err(postcard::Error::SerializeBufferFull);
80        }
81        self.bytes.push(byte);
82        Ok(())
83    }
84    fn try_extend(&mut self, bytes: &[u8]) -> postcard::Result<()> {
85        if bytes.len() > self.limit - self.bytes.len() {
86            return Err(postcard::Error::SerializeBufferFull);
87        }
88        self.bytes.extend_from_slice(bytes);
89        Ok(())
90    }
91    fn finalize(self) -> postcard::Result<Self::Output> {
92        Ok(self.bytes)
93    }
94}
95
96// Finite encoding visits the value once. Grow only for emitted bytes, keeping
97// allocation requests within the schema ceiling even when Vec has spare capacity.
98struct GrowingOutput<'a> {
99    output: Output,
100    allocation_failed: &'a Cell<bool>,
101}
102impl GrowingOutput<'_> {
103    #[cold]
104    #[inline(never)]
105    fn reserve(&mut self, additional: usize) -> postcard::Result<()> {
106        let bytes = &mut self.output.bytes;
107        if additional > self.output.limit - bytes.len() {
108            return Err(postcard::Error::SerializeBufferFull);
109        }
110        let required = bytes.len() + additional;
111        if required > bytes.capacity() {
112            // Once a message is substantial relative to its ceiling, reserve
113            // the remainder to avoid repeated copies of large batch payloads.
114            let capacity = if required > self.output.limit / 4 {
115                self.output.limit
116            } else {
117                required
118                    .max(bytes.capacity().saturating_mul(2).max(64))
119                    .min(self.output.limit)
120            };
121            bytes
122                .try_reserve_exact(capacity - bytes.len())
123                .map_err(|_| {
124                    self.allocation_failed.set(true);
125                    postcard::Error::SerializeBufferFull
126                })?;
127        }
128        Ok(())
129    }
130}
131impl Flavor for GrowingOutput<'_> {
132    type Output = Vec<u8>;
133    #[inline(always)]
134    fn try_push(&mut self, byte: u8) -> postcard::Result<()> {
135        if self.output.bytes.len() == self.output.limit {
136            return Err(postcard::Error::SerializeBufferFull);
137        }
138        if self.output.bytes.len() == self.output.bytes.capacity() {
139            self.reserve(1)?;
140        }
141        self.output.bytes.push(byte);
142        Ok(())
143    }
144    #[inline(always)]
145    fn try_extend(&mut self, bytes: &[u8]) -> postcard::Result<()> {
146        if bytes.len() > self.output.limit - self.output.bytes.len() {
147            return Err(postcard::Error::SerializeBufferFull);
148        }
149        if bytes.len() > self.output.bytes.capacity() - self.output.bytes.len() {
150            self.reserve(bytes.len())?;
151        }
152        self.output.bytes.extend_from_slice(bytes);
153        Ok(())
154    }
155    fn finalize(self) -> postcard::Result<Self::Output> {
156        Ok(self.output.bytes)
157    }
158}
159
160// Count with the same admitted byte ceiling as encoding, without output storage.
161struct Size {
162    bytes: usize,
163    limit: usize,
164}
165impl Flavor for Size {
166    type Output = usize;
167    fn try_push(&mut self, _: u8) -> postcard::Result<()> {
168        self.try_extend(&[0])
169    }
170    fn try_extend(&mut self, bytes: &[u8]) -> postcard::Result<()> {
171        if bytes.len() > self.limit - self.bytes {
172            return Err(postcard::Error::SerializeBufferFull);
173        }
174        self.bytes += bytes.len();
175        Ok(())
176    }
177    fn finalize(self) -> postcard::Result<usize> {
178        Ok(self.bytes)
179    }
180}
181
182fn encoded_len<T: Wire>(limits: Limits, value: &T) -> Result<usize, Error> {
183    let limit = admit::<T>(limits)?;
184    postcard::serialize_with_flavor(
185        value,
186        Size {
187            bytes: HEADER,
188            limit,
189        },
190    )
191    .map_err(|_| Error::Malformed)
192}
193
194// Compare canonical serialization directly with the admitted payload. Continue
195// after a mismatch so serializer errors still retain their Malformed mapping.
196pub(crate) struct Compare<'a> {
197    pub(crate) remaining: &'a [u8],
198    pub(crate) allowance: usize,
199    pub(crate) equal: bool,
200}
201impl Flavor for Compare<'_> {
202    type Output = bool;
203    #[inline(always)]
204    fn try_push(&mut self, byte: u8) -> postcard::Result<()> {
205        if self.allowance == 0 {
206            return Err(postcard::Error::SerializeBufferFull);
207        }
208        self.allowance -= 1;
209        if let Some((first, rest)) = self.remaining.split_first() {
210            self.equal &= *first == byte;
211            self.remaining = rest;
212        } else {
213            self.equal = false;
214        }
215        Ok(())
216    }
217    fn try_extend(&mut self, bytes: &[u8]) -> postcard::Result<()> {
218        if let [byte] = bytes {
219            return self.try_push(*byte);
220        }
221        if bytes.len() > self.allowance {
222            return Err(postcard::Error::SerializeBufferFull);
223        }
224        self.allowance -= bytes.len();
225        if let Some((prefix, rest)) = self.remaining.split_at_checked(bytes.len()) {
226            // Wasm's bytewise memcmp is expensive for bulk byte/text payloads.
227            // Exact unaligned word loads preserve canonical equality; no hash or
228            // scratch allocation is involved. Small scalar writes stay simple.
229            self.equal &= if core::ptr::eq(prefix.as_ptr(), bytes.as_ptr()) {
230                // Borrowed decoding can serialize the exact input region.
231                // Its length and allowance were checked above; equality needs no scan.
232                // Apply on every host: canonical bytes and rejection rules are unchanged.
233                true
234            } else if cfg!(any(target_arch = "wasm32", test)) && bytes.len() >= 8 {
235                // Process 64 bytes per bulk iteration, then an 8-byte word tail.
236                // Previously bulk blocks were 32 bytes. Exact equality,
237                // frame bytes, admission and serializer-error precedence are unchanged;
238                // no consumer migration is needed. Keep the small-write branch separate
239                // to avoid increasing its Wasm instruction cost.
240                if bytes.len() >= 64 {
241                    equal_bulk(prefix, bytes)
242                } else {
243                    let (left, left_tail) = prefix.as_chunks::<8>();
244                    let (right, right_tail) = bytes.as_chunks::<8>();
245                    left.iter().zip(right).fold(0_u64, |difference, (a, b)| {
246                        difference | (u64::from_ne_bytes(*a) ^ u64::from_ne_bytes(*b))
247                    }) == 0
248                        && left_tail == right_tail
249                }
250            } else {
251                prefix == bytes
252            };
253            self.remaining = rest;
254        } else {
255            self.equal = false;
256            self.remaining = &[];
257        }
258        Ok(())
259    }
260    fn finalize(self) -> postcard::Result<Self::Output> {
261        Ok(self.equal && self.remaining.is_empty())
262    }
263}
264
265// Post-decode verification for finite values. Previously every owned blob was
266// compared byte-for-byte. Controlled Bytes now verifies its length and advances
267// over the unchanged copy; generated records delegate only when ALL children
268// preserve decoding boundaries. Any custom child keeps whole-value comparison.
269// Canonical bytes, typed failures and accepted inputs are unchanged; no migration.
270// This context is created only after successful decoding and trailing checks.
271#[doc(hidden)]
272pub struct Canonical<'a> {
273    compare: Compare<'a>,
274}
275impl Canonical<'_> {
276    /// Compare custom/ordinary Serde output exactly, preserving error precedence.
277    pub fn compare<T: serde::Serialize + ?Sized>(&mut self, value: &T) -> Result<(), Error> {
278        let mut serializer = postcard::Serializer {
279            output: Compare {
280                remaining: self.compare.remaining,
281                allowance: self.compare.allowance,
282                equal: self.compare.equal,
283            },
284        };
285        value
286            .serialize(&mut serializer)
287            .map_err(|_| Error::Malformed)?;
288        self.compare = serializer.output;
289        Ok(())
290    }
291    /// Only for unchanged built-in byte/text values immediately after decoding.
292    pub(crate) fn copied_bytes(&mut self, length: usize) -> Result<(), Error> {
293        self.compare(&length)?;
294        if length > self.compare.allowance {
295            return Err(Error::Malformed);
296        }
297        self.compare.allowance -= length;
298        if let Some(rest) = self.compare.remaining.get(length..) {
299            self.compare.remaining = rest;
300        } else {
301            self.compare.equal = false;
302            self.compare.remaining = &[];
303        }
304        Ok(())
305    }
306}
307
308// Keep the bulk scan out of scalar serializer paths; equality remains exact.
309// The caller has already checked equal slice lengths and the byte allowance.
310#[inline(never)]
311fn equal_bulk(prefix: &[u8], bytes: &[u8]) -> bool {
312    let (left, left_tail) = prefix.as_chunks::<64>();
313    let (right, right_tail) = bytes.as_chunks::<64>();
314    left.iter().zip(right).fold(0_u128, |difference, (a, b)| {
315        let (a, _) = a.as_chunks::<16>();
316        let (b, _) = b.as_chunks::<16>();
317        difference
318            | (u128::from_ne_bytes(a[0]) ^ u128::from_ne_bytes(b[0]))
319            | (u128::from_ne_bytes(a[1]) ^ u128::from_ne_bytes(b[1]))
320            | (u128::from_ne_bytes(a[2]) ^ u128::from_ne_bytes(b[2]))
321            | (u128::from_ne_bytes(a[3]) ^ u128::from_ne_bytes(b[3]))
322    }) == 0
323        && {
324            let (left, left_tail) = left_tail.as_chunks::<8>();
325            let (right, right_tail) = right_tail.as_chunks::<8>();
326            left.iter().zip(right).fold(0_u64, |difference, (a, b)| {
327                difference | (u64::from_ne_bytes(*a) ^ u64::from_ne_bytes(*b))
328            }) == 0
329                && left_tail == right_tail
330        }
331}
332
333// Share allocation/header setup across contract types; byte emission stays inline.
334#[cfg_attr(target_arch = "wasm32", inline(never))]
335fn fresh_output(id: u64, direction: u8, size: usize) -> Result<Output, Error> {
336    // The fixed header always fits after admission. Initialize the same bounded
337    // starting capacity directly, leaving the cold growth path for payloads.
338    let mut bytes = Vec::new();
339    bytes
340        .try_reserve_exact(if size <= 128 { size } else { 64 })
341        .map_err(|_| Error::Allocation)?;
342    bytes.extend_from_slice(&header(id, direction));
343    Ok(Output { bytes, limit: size })
344}
345
346fn encode<T: Wire>(id: u64, direction: u8, limits: Limits, value: &T) -> Result<Vec<u8>, Error> {
347    let size = admit::<T>(limits)?;
348    let output = fresh_output(id, direction, size)?;
349    let allocation_failed = Cell::new(false);
350    let output = GrowingOutput {
351        output,
352        allocation_failed: &allocation_failed,
353    };
354    postcard::serialize_with_flavor(value, output).map_err(|_| {
355        if allocation_failed.get() {
356            Error::Allocation
357        } else {
358            Error::Malformed
359        }
360    })
361}
362
363// Keep fresh encoding separate: routing it through mutable scratch regresses
364// small-message IC instruction cost, including with forced inlining.
365fn encode_into<T: Wire>(
366    id: u64,
367    direction: u8,
368    limits: Limits,
369    value: &T,
370    bytes: &mut Vec<u8>,
371) -> Result<(), Error> {
372    bytes.clear();
373    let size = admit::<T>(limits)?;
374    // The fixed header always fits after admission. Initialize the same bounded
375    // starting capacity directly, leaving the cold growth path for payloads.
376    bytes
377        .try_reserve_exact(size.min(64))
378        .map_err(|_| Error::Allocation)?;
379    bytes.extend_from_slice(&header(id, direction));
380    let allocation_failed = Cell::new(false);
381    let output = GrowingOutput {
382        output: Output {
383            bytes: core::mem::take(bytes),
384            limit: size,
385        },
386        allocation_failed: &allocation_failed,
387    };
388    *bytes = postcard::serialize_with_flavor(value, output).map_err(|_| {
389        if allocation_failed.get() {
390            Error::Allocation
391        } else {
392            Error::Malformed
393        }
394    })?;
395    Ok(())
396}
397
398fn decode<'de, T: Wire + serde::Deserialize<'de>>(
399    id: u64,
400    direction: u8,
401    limits: Limits,
402    bytes: &'de [u8],
403) -> Result<T, Error> {
404    let size = admit::<T>(limits)?;
405    // Admission already proves size <= limits.bytes; the schema ceiling is stricter.
406    if bytes.len() > size {
407        return Err(Error::Limit);
408    }
409    let payload = payload(id, direction, bytes)?;
410    let (value, rest) = postcard::take_from_bytes::<T>(payload).map_err(|_| Error::Malformed)?;
411    if !rest.is_empty() {
412        return Err(Error::Trailing);
413    }
414    let mut canonical = Canonical {
415        compare: Compare {
416            remaining: payload,
417            allowance: size - HEADER,
418            equal: true,
419        },
420    };
421    value.__canonical(&mut canonical)?;
422    let equal = canonical.compare.finalize().map_err(|_| Error::Malformed)?;
423    if !equal {
424        return Err(Error::Noncanonical);
425    }
426    Ok(value)
427}
428
429/// Encode once, retaining exact request bytes bound to `M`.
430pub fn encode_request<M: Method>(value: &M::Request) -> Result<RequestBytes<M>, Error> {
431    Ok(RequestBytes {
432        bytes: encode(M::ID, 0, M::REQUEST_LIMITS, value)?,
433        method: PhantomData,
434    })
435}
436/// Encode a request into reusable scratch storage, replacing its contents.
437///
438/// Successful calls retain capacity, which may exceed this method's allowance;
439/// the encoded length remains bounded. Errors leave the buffer empty and may
440/// discard its allocation. Keep independent immutable bytes for exact retries.
441pub fn encode_request_into<M: Method>(
442    value: &M::Request,
443    bytes: &mut Vec<u8>,
444) -> Result<(), Error> {
445    encode_into(M::ID, 0, M::REQUEST_LIMITS, value, bytes)
446}
447
448/// Encode a response into reusable scratch storage, replacing its contents.
449///
450/// Capacity and error behavior match [`encode_request_into`]. Reusing scratch
451/// storage overwrites previous frames; retain retry bytes independently.
452pub fn encode_response_into<M: Method>(
453    value: &M::Response,
454    bytes: &mut Vec<u8>,
455) -> Result<(), Error> {
456    encode_into(M::ID, 1, M::RESPONSE_LIMITS, value, bytes)
457}
458
459/// Measure the complete request envelope without allocating an output buffer.
460/// Uses the same schema admission as encoding. Custom serializers must be
461/// deterministic; sizing does not retain bytes for retries.
462pub fn encoded_request_len<M: Method>(value: &M::Request) -> Result<usize, Error> {
463    encoded_len(M::REQUEST_LIMITS, value)
464}
465/// Measure the complete response envelope without allocating an output buffer.
466/// Consumers can compare this with a delivery allowance before encoding. Schema
467/// admission still applies to the entire type, not just this value.
468pub fn encoded_response_len<M: Method>(value: &M::Response) -> Result<usize, Error> {
469    encoded_len(M::RESPONSE_LIMITS, value)
470}
471/// Decode a request only after method, direction, canonical and resource checks.
472pub fn decode_request<'de, M: Method>(bytes: &'de [u8]) -> Result<M::Request, Error>
473where
474    M::Request: serde::Deserialize<'de>,
475{
476    decode(M::ID, 0, M::REQUEST_LIMITS, bytes)
477}
478/// Encode a reply. Application errors belong inside `M::Response`.
479pub fn encode_response<M: Method>(value: &M::Response) -> Result<ResponseBytes<M>, Error> {
480    Ok(ResponseBytes {
481        bytes: encode(M::ID, 1, M::RESPONSE_LIMITS, value)?,
482        method: PhantomData,
483    })
484}
485/// Decode a reply under the selected method's response contract.
486pub fn decode_response<'de, M: Method>(bytes: &'de [u8]) -> Result<M::Response, Error>
487where
488    M::Response: serde::Deserialize<'de>,
489{
490    decode(M::ID, 1, M::RESPONSE_LIMITS, bytes)
491}
492
493pub(crate) fn header(id: u64, direction: u8) -> [u8; HEADER] {
494    let mut header = [0; HEADER];
495    header[..2].copy_from_slice(b"IW");
496    header[2..10].copy_from_slice(&id.to_le_bytes());
497    header[10] = direction;
498    header
499}
500// Share fixed-header validation across Wasm codec instantiations.
501#[cfg_attr(target_arch = "wasm32", inline(never))]
502pub(crate) fn payload(id: u64, direction: u8, bytes: &[u8]) -> Result<&[u8], Error> {
503    if bytes.len() < HEADER {
504        return Err(Error::Malformed);
505    }
506    if &bytes[..2] != b"IW" || bytes[2..10] != id.to_le_bytes() || bytes[10] != direction {
507        return Err(Error::Identity);
508    }
509    Ok(&bytes[HEADER..])
510}