Skip to main content

Limits

Struct Limits 

Source
pub struct Limits { /* private fields */ }
Expand description

Operational limits for work performed on untrusted Contracts, sources, and values.

Limits never participate in Contract identity. Hosts may raise them explicitly for trusted workloads.

§Construction

Fields are private so adding a limit is never a breaking change. Start from a profile and override individual fields with the with_* builders:

use candid_core::{Limits, LimitsProfile};

let limits = LimitsProfile::InteractiveV1
    .limits()
    .with_max_input_bytes(64 * 1024)
    .with_deadline_unix_ms(Some(2_000_000_000_000));
assert_eq!(limits.max_input_bytes(), 64 * 1024);

Limits::default is LimitsProfile::InteractiveV1.

§Zero values

Every limit accepts 0; a zero limit is a defined, fail-closed policy rather than a rejected configuration. A zero byte/count/work limit rejects any input that consumes the resource at all, and with_max_diagnostics(0) retains exactly one out-of-band resource_limit_exceeded sentinel violation so an invalid input never yields an empty error collection. deadline_unix_ms values at or before the current time (including Some(0)) make every bounded operation fail closed with operation_deadline_exceeded.

§Serialization

Limits serializes as the versioned portable configuration described by LimitsConfig, never as a bare field map.

Implementations§

Source§

impl Limits

Source

pub fn max_input_bytes(&self) -> usize

Maximum bytes accepted by a bounded parse entry point before the document is decoded.

Source

pub fn max_source_bytes(&self) -> usize

Maximum bytes of a single resolved DID source.

Source

pub fn max_bundle_bytes(&self) -> usize

Maximum aggregate bytes across every source in a resolved bundle.

Source

pub fn max_sources(&self) -> usize

Maximum number of sources in a resolved bundle.

Source

pub fn max_source_id_bytes(&self) -> usize

Maximum bytes in a single logical source ID (name/path).

Source IDs are otherwise bounded only cumulatively by Limits::max_string_bytes, so one entry could carry a megabyte-long path. This bounds each ID individually on both the resolver and the embedded-sidecar paths.

Source

pub fn max_import_depth(&self) -> usize

Maximum import chain depth during source resolution.

Source

pub fn max_import_edges(&self) -> usize

Maximum import edges across a resolved bundle.

Source

pub fn max_source_nesting(&self) -> usize

Maximum lexical nesting accepted before invoking the upstream parser.

Source

pub fn max_type_depth(&self) -> usize

Maximum semantic type nesting lowered from a checked Candid program.

Source

pub fn max_value_nesting(&self) -> usize

Maximum lexical JSON container nesting accepted before invoking the recursive serde_json decoder on a HostValue document.

This is the HostValue analogue of Limits::max_source_nesting: it bounds lexical nesting ({ and [ in the JSON text) before a recursive decoder runs, whereas Limits::max_value_depth bounds semantic HostValue nesting after decoding. The two units differ — one vec level costs two JSON containers and one record level costs three — so a document rejected here reports value_nesting, never value_depth.

Raising this above 128 has no effect: serde_json applies a fixed 128-frame recursion ceiling that this crate deliberately does not disable, so documents nested deeper than 128 containers are rejected by the decoder as malformed rather than by this limit.

§Choosing a value for a small stack

Rejecting a document costs constant stack, so no input can drive an abort by being deeper than this limit. Accepting one still recurses, at a cost that is build-profile dependent, so this limit is the knob for matching decode to the stack the host actually runs on. Measured on a 64 KiB stack with a nested-opt document:

ProfileCost per containerDeepest safe
release~640 B~103
debug~8 KiB~7

The default of 64 is chosen to keep a release build inside a 64 KiB stack with roughly a third of it to spare, which is the bar tests/deep_nesting.rs sets. A debug build on a stack that small needs this lowered to single digits; a host on an ordinary 8 MiB stack can raise it to 128 without approaching either bound.

Source

pub fn max_type_nodes(&self) -> usize

Maximum type nodes in a Contract arena.

Source

pub fn max_graph_edges(&self) -> usize

Maximum edges in a Contract type graph.

Source

pub fn max_declarations(&self) -> usize

Maximum named declarations in a Contract.

Source

pub fn max_fields(&self) -> usize

Maximum aggregate record/variant fields across a Contract.

This bounds a Contract. It is not the ceiling on how wide a record validate_host_value will accept: that is governed by Limits::max_canonicalization_work, which at its default binds a single record at 2 581 fields — far below the 500 000 permitted here.

Source

pub fn max_methods(&self) -> usize

Maximum aggregate service methods across a Contract.

Source

pub fn max_function_values(&self) -> usize

Maximum aggregate function arguments and results across a Contract.

Source

pub fn max_string_bytes(&self) -> usize

Maximum aggregate string bytes across declaration and method names.

Source

pub fn max_producer_bytes(&self) -> usize

Maximum aggregate bytes across the four producer metadata strings.

Producer metadata is untrusted, caller-supplied provenance that is deliberately kept out of the semantic Contract identities (see crate::ProducerInfo); this bounds the bytes it may contribute to a validated Contract without ever affecting a semantic identity hash.

Source

pub fn max_diagnostics(&self) -> usize

Maximum retained diagnostics per failure.

When more violations are observed than fit under this cap, the final retained item is replaced by a resource_limit_exceeded sentinel carrying the true observed count. A cap of 0 retains exactly that one sentinel, so an invalid input never yields an empty error collection.

Source

pub fn max_canonicalization_work(&self) -> usize

Maximum canonicalization work units per operation.

This counter, not Limits::max_fields or Limits::max_value_elements, is what bounds how wide a record validate_host_value can accept. Record validation is deliberately allocation-free: instead of building a field-ID index it scans pairwise and charges one unit per comparison, checking the deadline before each charge. Three such scans run per record — duplicate detection, field-set agreement, and per-field lookup — so the cost is roughly 1.5n² units for an n-field record.

At this default that puts the ceiling at 2 581 fields: a record with 2 582 fields fails closed with resource_limit_exceeded naming canonicalization_work. That is three orders of magnitude below the 500 000 fields Limits::max_fields permits and the 1 000 000 elements Limits::max_value_elements permits, so those two are not the binding limit for wide records. Raise this counter to validate wider ones, and note the cost grows quadratically. The failure is structured and interruptible, never a hang.

The counter is per operation and shared with Contract canonicalization, so several moderately wide records in one value tree accumulate against the same budget.

Source

pub fn max_provenance_work(&self) -> usize

Maximum work units charged while resolving provenance targets.

Kept separate from Limits::max_canonicalization_work so that rederiving a large graph and then indexing its provenance sidecar cannot jointly exhaust one counter. Bounds building each referenced container’s field-ID / method-name index and every membership test, so adversarial fan-out and duplicate provenance entries cannot drive an unbounded scan.

Source

pub fn max_source_identity_work(&self) -> usize

Maximum work units charged while serializing and hashing source-bundle identity (candid-core:source-bundle:v1).

Each identity computation charges one unit per serialized payload byte during an allocation-free counting pass, then reserves two more units per byte (materializing and hashing the canonical bytes) plus the domain-tag overhead before any allocation occurs. A presented sidecar validation performs two passes on one budget — verifying the presented source_bundle_id and emitting the rederived bundle’s ID — while a plain compilation performs one.

Kept separate from Limits::max_canonicalization_work because the serialized bundle scales with max_bundle_bytes: metering it on the canonicalization counter would either starve graph work or force that default far above what graph canonicalization needs. The default accepts every bundle valid under the default byte/count limits: JSON string escaping expands a byte to at most six, so one pass costs at most 3 * 6 * (max_bundle_bytes + identity strings) + entry overhead, about 213M units for a compile pass and 341M for the two validation passes together; 400M covers both with headroom.

Source

pub fn max_artifact_identity_work(&self) -> usize

Maximum work units charged while computing a detached artifact identity (candid-core:artifact:*; see crate::artifact_id_with_limits).

One unit per artifact byte plus the fixed domain framing cost — the domain tag and its one separator byte. There is no length field and no second kind label in the preimage, so that constant is the whole overhead, and the cost is exactly bytes.len() + domain.len() + 1.

The default is proven against the default byte gate rather than guessed. Limits::max_input_bytes is enforced first, so the largest artifact this ever hashes by default is 4 MiB (4 194 304 bytes); the longest of the frozen domains, candid-core:artifact:contract-envelope-json:v1, is 46 bytes, so the worst case is 4 194 351 units. 10 000 000 covers that with more than twice the headroom, matches Limits::max_canonicalization_work’s default, and cannot overflow: every charge saturates rather than wrapping.

Kept separate from every other work counter on purpose. Artifact identity is an explicit, detached call, so metering it on canonicalization_work would let content-addressing a document starve the Contract canonicalization that follows it on the same budget, and metering it on source_identity_work would do the same to provenance. Raising Limits::max_input_bytes above this value without raising this one makes over-sized artifacts fail on artifact_identity_work instead of on input_bytes; raise both together.

§Wire compatibility

This override key is additive. Limits::default and every configuration that leaves this limit at its profile value still serialize with no max_artifact_identity_work key at all, so existing documents are unchanged in both directions. The implication is one-directional and deliberate: because LimitsConfig rejects unknown override keys, a document that does carry an explicit max_artifact_identity_work override is readable by this build and newer ones but rejected by a build that predates the key. That is the intended failure — silently ignoring an unknown limit override would apply a policy the document did not ask for. LIMITS_CONFIG_VERSION is unchanged, because adding a limit is not a schema break: Limits fields are private precisely so that adding one is never a breaking change, and no existing key, value, or default moved.

Source

pub fn max_type_preflight_work(&self) -> usize

Maximum work units charged while enforcing Limits::max_type_depth.

Two iterative traversals guard type depth before anything recursive expands a parsed bundle: the parse-side preflight that follows declaration references before the upstream checker runs, and the checked-type walk that re-verifies the checker’s environment before lowering. Both walks — and the recursion map each builds first (the declaration reference graph and its cycle members) — charge this counter: one unit per visited syntax node or expansion state, plus one unit per recursive name tracked on the state’s path, which is the real cost of cloning and comparing that path set.

Shared subtrees are deduplicated: each node is expanded once per distinct (node, depth, active recursive names) state rather than once per referencing path, and only names that participate in a reference cycle are ever tracked per path, so ordinary recursive types add a small constant per state. The record DAG that motivated this counter (issue #125: shared aliases re-expanded once per incoming edge, O(2^n) visits from a 932-byte source) costs 2 796 units at n = 24 under deduplication — measured, and pinned together with a minimal program’s exact cost by tests/deep_nesting.rs and the unit tests beside the walks. Shapes that still multiply states — deep stacks of distinct-name diamonds, enormous mutual-recursion groups, long structural alias chains re-walked from every declaration root at shifted depths — exhaust this counter and fail closed with resource_limit_exceeded naming type_preflight_work, instead of hanging.

Kept separate from Limits::max_canonicalization_work because both counters accrue on one budget in a single compilation: metering the depth guard on the graph counter would let a type-heavy bundle starve the canonicalization that follows it on the same budget. The default matches that counter’s and covers realistic contracts by several orders of magnitude: the largest corpus fixture (ledger.did) consumes 806 units end to end, pinned by tests/deep_nesting.rs.

§Choosing a value for a small heap

Deduplication is what makes these walks cheap, and it is also what makes them retain: each distinct state stays in a memo for the duration of the walk, where the previous tree walks held only a stack. So this counter bounds memory as well as time, and the exchange rate is measured rather than assumed: about 19 bytes of peak live heap per unit, near-constant across bundle sizes and pinned by tests/type_preflight_memory.rs.

At this default that authorizes roughly 190 MB of peak heap before the counter refuses — ample headroom on an ordinary host, and more than a small 32-bit wasm32 heap can serve. A host whose allocator fails before the counter does gets an allocation abort in place of the clean, structured resource_limit_exceeded this is designed to return, so a browser or other constrained host should lower this to what its heap can actually hold: 1 000 000 keeps the walks under ~19 MB and still clears every realistic contract by three orders of magnitude. Rejecting costs no memory, so no input can drive an abort by being larger than the configured bound — only by being accepted under a bound the host cannot honor.

The rate holds because a state’s cost does not depend on anything an attacker picks freely: recursive names are interned to dense indices rather than compared or stored as text, and each path’s set is shared with the children that inherit it unchanged rather than copied into every one of a wide record’s fields.

§Wire compatibility

This override key is additive, exactly like Limits::max_artifact_identity_work’s: Limits::default and every configuration that leaves this limit at its profile value still serialize with no max_type_preflight_work key at all, so existing documents are unchanged in both directions, while a document that does carry the key is readable by this build and newer ones but rejected by a build that predates it — the intended failure, since silently ignoring an unknown limit override would apply a policy the document did not ask for. LIMITS_CONFIG_VERSION is unchanged: Limits fields are private precisely so that adding one is never a breaking change, and no existing key, value, or default moved.

Source

pub fn max_value_depth(&self) -> usize

Maximum semantic HostValue nesting depth.

Source

pub fn max_value_elements(&self) -> usize

Maximum aggregate HostValue elements per document.

Reaching this ceiling with a single wide record is not possible at default limits: Limits::max_canonicalization_work binds one record at 2 581 fields, well before the million elements permitted here. This limit is the binding one for wide vectors and for element counts accumulated across a value tree.

Source

pub fn max_value_bytes(&self) -> usize

Maximum aggregate HostValue text/blob bytes per document.

Source

pub fn with_max_input_bytes(self, value: usize) -> Self

Returns self with max_input_bytes replaced. See Limits::max_input_bytes.

Source

pub fn with_max_source_bytes(self, value: usize) -> Self

Returns self with max_source_bytes replaced. See Limits::max_source_bytes.

Source

pub fn with_max_bundle_bytes(self, value: usize) -> Self

Returns self with max_bundle_bytes replaced. See Limits::max_bundle_bytes.

Source

pub fn with_max_sources(self, value: usize) -> Self

Returns self with max_sources replaced. See Limits::max_sources.

Source

pub fn with_max_source_id_bytes(self, value: usize) -> Self

Returns self with max_source_id_bytes replaced. See Limits::max_source_id_bytes.

Source

pub fn with_max_import_depth(self, value: usize) -> Self

Returns self with max_import_depth replaced. See Limits::max_import_depth.

Source

pub fn with_max_import_edges(self, value: usize) -> Self

Returns self with max_import_edges replaced. See Limits::max_import_edges.

Source

pub fn with_max_source_nesting(self, value: usize) -> Self

Returns self with max_source_nesting replaced. See Limits::max_source_nesting.

Source

pub fn with_max_type_depth(self, value: usize) -> Self

Returns self with max_type_depth replaced. See Limits::max_type_depth.

Source

pub fn with_max_value_nesting(self, value: usize) -> Self

Returns self with max_value_nesting replaced. See Limits::max_value_nesting.

Source

pub fn with_max_type_nodes(self, value: usize) -> Self

Returns self with max_type_nodes replaced. See Limits::max_type_nodes.

Source

pub fn with_max_graph_edges(self, value: usize) -> Self

Returns self with max_graph_edges replaced. See Limits::max_graph_edges.

Source

pub fn with_max_declarations(self, value: usize) -> Self

Returns self with max_declarations replaced. See Limits::max_declarations.

Source

pub fn with_max_fields(self, value: usize) -> Self

Returns self with max_fields replaced. See Limits::max_fields.

Source

pub fn with_max_methods(self, value: usize) -> Self

Returns self with max_methods replaced. See Limits::max_methods.

Source

pub fn with_max_function_values(self, value: usize) -> Self

Returns self with max_function_values replaced. See Limits::max_function_values.

Source

pub fn with_max_string_bytes(self, value: usize) -> Self

Returns self with max_string_bytes replaced. See Limits::max_string_bytes.

Source

pub fn with_max_producer_bytes(self, value: usize) -> Self

Returns self with max_producer_bytes replaced. See Limits::max_producer_bytes.

Source

pub fn with_max_diagnostics(self, value: usize) -> Self

Returns self with max_diagnostics replaced. See Limits::max_diagnostics.

Source

pub fn with_max_canonicalization_work(self, value: usize) -> Self

Returns self with max_canonicalization_work replaced. See Limits::max_canonicalization_work.

Source

pub fn with_max_provenance_work(self, value: usize) -> Self

Returns self with max_provenance_work replaced. See Limits::max_provenance_work.

Source

pub fn with_max_source_identity_work(self, value: usize) -> Self

Returns self with max_source_identity_work replaced. See Limits::max_source_identity_work.

Source

pub fn with_max_artifact_identity_work(self, value: usize) -> Self

Returns self with max_artifact_identity_work replaced. See Limits::max_artifact_identity_work.

Source

pub fn with_max_type_preflight_work(self, value: usize) -> Self

Returns self with max_type_preflight_work replaced. See Limits::max_type_preflight_work.

Source

pub fn with_max_value_depth(self, value: usize) -> Self

Returns self with max_value_depth replaced. See Limits::max_value_depth.

Source

pub fn with_max_value_elements(self, value: usize) -> Self

Returns self with max_value_elements replaced. See Limits::max_value_elements.

Source

pub fn with_max_value_bytes(self, value: usize) -> Self

Returns self with max_value_bytes replaced. See Limits::max_value_bytes.

Source§

impl Limits

Source

pub fn profile(&self) -> LimitsProfile

The profile these limits started from. Overridden fields do not change the profile; it names the baseline, not the final values.

Source

pub fn deadline_unix_ms(&self) -> Option<u64>

Optional Unix timestamp in milliseconds after which work must abort.

None means no deadline. A value at or before the current time makes every bounded operation fail closed with operation_deadline_exceeded before performing work.

Bare wasm32-unknown-unknown has no clock, so a deadline cannot be measured there: any explicit deadline fails closed on that target, while None stays unbounded. Cancellation and every quantitative limit are unaffected. See Limits::deadline_exceeded.

Source

pub fn with_deadline_unix_ms(self, deadline_unix_ms: Option<u64>) -> Self

Returns self with deadline_unix_ms replaced. See Limits::deadline_unix_ms.

Source

pub fn deadline_exceeded(&self) -> bool

Whether the configured deadline has already elapsed.

No deadline is never exceeded. On bare wasm32-unknown-unknown there is no clock to compare against — SystemTime::now panics rather than failing — so an explicit deadline is reported as elapsed instead of aborting the process. That is the same fail-closed direction the internal budget takes, and it keeps an explicit deadline from silently becoming unbounded on the one target that cannot honour it.

Trait Implementations§

Source§

impl Clone for Limits

Source§

fn clone(&self) -> Limits

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Limits

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for Limits

Source§

impl<'de> Deserialize<'de> for Limits

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Eq for Limits

Source§

impl From<&Limits> for LimitsConfig

Source§

fn from(limits: &Limits) -> Self

Converts to this type from the input type.
Source§

impl From<Limits> for LimitsConfig

Source§

fn from(limits: Limits) -> Self

Converts to this type from the input type.
Source§

impl PartialEq for Limits

Source§

fn eq(&self, other: &Limits) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl Serialize for Limits

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for Limits

Source§

impl TryFrom<LimitsConfig> for Limits

Source§

type Error = LimitsConfigError

The type returned in the event of a conversion error.
Source§

fn try_from(config: LimitsConfig) -> Result<Self, LimitsConfigError>

Performs the conversion.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Compare self to key and return true if they are equal.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.