The BigFile Interface Specification
Introduction
Welcome to the BigFile! We speak of "the" BigFile, because although under the hood a large number of physical computers are working together in a blockchain protocol, in the end we have the appearance of a single, shared, secure and world-wide accessible computer. Developers who want to build decentralized applications (or dapps for short) that run on the BigFile blockchain and end-users who want to use those dapps need to know very little, if anything, about the underlying protocol. However, knowing some details about the interfaces that the BigFile exposes can allow interested developers and architects to take fuller advantages of the unique features that the BigFile provides.
Target audience
This document describes this external view of the BigFile, i.e. the low-level interfaces it provides to dapp developers and users, and what will happen when they use these interfaces.
While this document describes the external interface and behavior of the BigFile, it is not intended as end-user or end-developer documentation. Most developers will interact with the BigFile through additional tooling like the SDK, Cube Development Kits and Motoko. Please see the developer docs for suitable documentation.
The target audience of this document are
those who use these low-level interfaces (e.g. implement agents, canister developments kits, emulators, other tooling).
those who implement these low-level interfaces (e.g. developers of the BigFile implementation)
those who want to understand the intricacies of the BigFile's behavior in great detail (e.g. to do a security analysis)
This document is a rigorous, technically dense reference. It is not an introduction to the BigFile, and as such most useful to those who understand the high-level concepts. Please see more high-level documentation first.
Scope of this document
If you think of the BigFile as a distributed engine that executes WebAssembly-based dapps, then this document describes exclusively the aspect of executing those dapps. To the extent possible, this document will not talk about consensus protocols, nodes, subnets, orthogonal persistence or governance.
This document tries to be implementation agnostic: It would apply just as well to a (hypothetical) compatible reimplementation of the BigFile. This implies that this document does not cover interfaces towards those running the BigFile (e.g. data center operators, protocol developers, governance users), as topics like node update, monitoring, logging are inherently tied to the actual implementation and its architecture.
Overview of the BigFile
Dapps on the BigFile, or BIG for short, are implemented as canister smart contracts, or canisters for short. If you want to build on the BigFile as a dapp developer, you first create a canister module that contains the WebAssembly code and configuration for your dapp, and deploy it using the HTTPS interface. You can create canister modules using the Motoko language and the SDK, which is more convenient. If you want to use your own tooling, however, then this document describes what a canister module looks like and how the WebAssembly code can interact with the BIG.
Once your dapp is running on the BigFile, it is a canister smart contract, and users can interact with it. They can use the HTTPS interface to interact with the canister according to the System API.
The user can also use the HTTPS interface to issue read-only queries, which are faster, but cannot change the state of a canister.
A typical use of the BigFile. (This is a simplified view; some of the arrows represent multiple interaction steps or polling.)
Sections "HTTPS Interface" and "Cube interface (System API)" describe these interfaces, together with a brief description of what they do. Afterwards, you will find a more formal description of the BigFile that describes its abstract behavior with more rigor.
Nomenclature
To get some consistency in this document, we try to use the following terms with precision:
We avoid the term "client", as it could be the client of the BigFile or the client inside the distributed network that makes up the BigFile. Instead, we use the term user to denote the external entity interacting with the BigFile, even if in most cases it will be some code (sometimes called "agent") acting on behalf of a (human) user.
The public entry points of canisters are called methods. Methods can be declared to be either update methods (state mutation is preserved, can call update and query methods of arbitrary canisters), query methods (state mutation is discarded, no further calls can be made), or composite query methods (state mutation is discarded, can call query and composite query methods of canisters on the same subnet).
Methods can be called, from caller to callee, and will eventually incur a response which is either a reply or a reject. A method may have parameters, which are provided with concrete arguments in a method call.
External calls can be update calls, which can only call update and query methods, and query calls, which can only call query and composite query methods. Inter-canister calls issued while evaluating an update call can call update and query methods (just like update calls). Inter-canister calls issued while evaluating a query call (to a composite query method) can call query and composite query methods (just like query calls). Note that calls from a canister to itself also count as "inter-canister". Update and query call offer a security/efficiency trade-off. Update calls are executed in replicated mode, i.e. execution takes place in parallel on multiple replicas who need to arrive at a consensus on what the result of the call is. Query calls are fast but offer less guarantees since they are executed in non-replicated mode, by a single replica.
Internally, a call or a response is transmitted as a message from a sender to a receiver. Messages do not have a response.
WebAssembly functions are exported by the WebAssembly module or provided by the System API. These are invoked and can either trap or return, possibly with a return value. Functions, too, have parameters and take arguments.
External users interact with the BigFile by issuing requests on the HTTPS interface. Requests have responses which can either be replies or rejects. Some requests cause internal messages to be created.
Canisters and users are identified by a principal, sometimes also called an id.
Pervasive concepts
Before going into the details of the four public interfaces described in this document (namely the agent-facing HTTPS interface, the canister-facing System API, the virtual Management canister and the System State Tree), this section introduces some concepts that transcend multiple interfaces.
Unspecified constants and limits
This specification may refer to certain constants and limits without specifying their concrete value (yet), i.e. they are implementation defined. Many are resource limits which are relevant only to specify the error-handling behavior of the BIG (which, as mentioned above, is also not yet precisely described in this document). This list is not complete.
MAX_CYCLES_PER_MESSAGE
Amount of cycles that a canister has to have before a message is attempted to be executed, which is deducted from the canister balance before message execution. See Message execution.
MAX_CYCLES_PER_RESPONSE
Amount of cycles that the BIG sets aside when a canister performs a call. This is used to pay for processing the response message, and unused cycles after the execution of the response are refunded. See Message execution.
MAX_CYCLES_PER_QUERY
Maximum amount of cycles that can be used in total (across all calls to query and composite query methods and their callbacks) during evaluation of a query call.
CHUNK_STORE_SIZE
Maximum number of chunks that can be stored within the chunk store of a canister.
MAX_CHUNKS_IN_LARGE_WASM
Maximum number of chunks that can comprise a large Wasm module.
DEFAULT_PROVISIONAL_CYCLES_BALANCE
Amount of cycles allocated to a new canister by default, if not explicitly specified. See BIG method.
MAX_CALL_DEPTH_COMPOSITE_QUERY
Maximum nesting level of calls during evaluation of a query call to a composite query method.
MAX_WALL_CLOCK_TIME_COMPOSITE_QUERY
Maximum wall clock time spent on evaluation of a query call.
Principals
Principals are generic identifiers for canisters, users and possibly other concepts in the future. As far as most uses of the BIG are concerned they are opaque binary blobs with a length between 0 and 29 bytes, and there is intentionally no mechanism to tell canister ids and user ids apart.
There is, however, some structure to them to encode specific authentication and authorization behavior.
Special forms of Principals
In this section, H
denotes SHA-224, ·
denotes blob concatenation and |p|
denotes the length of p
in bytes, encoded as a single byte.
There are several classes of ids:
Opaque ids.
These are always generated by the BIG and have no structure of interest outside of it.
Typically, these end with the byte 0x01
, but users of the BIG should not need to care about that.
Self-authenticating ids.
These have the form
H(public_key) · 0x02
(29 bytes).An external user can use these ids as the
sender
of a request if they own the corresponding private key. The public key uses one of the encodings described in Signatures.Derived ids
These have the form
H(|registering_principal| · registering_principal · derivation_nonce) · 0x03
(29 bytes).These ids are treated specially when an id needs to be registered. In such a request, whoever requests an id can provide a
derivation_nonce
. By hashing that together with the principal of the caller, every principal has a space of ids that only they can register ids from.
Derived IDs are currently not explicitly used in this document, but they may be used internally or in the future.
Anonymous id
This has the form
0x04
, and is used for the anonymous caller. It can be used in call and query requests without a signature.Reserved ids
These have the form of
blob · 0x7f
,0 ≤ |blob| < 29
.These ids can be useful for applications that want to re-use the Textual representation of principals but want to indicate explicitly that the blob does not address any canisters or a user.
When the BIG creates a fresh id, it never creates a self-authenticating id, reserved id, an anonymous id or an id derived from what could be a canister or user.
Textual representation of principals
We specify a canonical textual format that is recommended whenever principals need to be printed or read in textual format, e.g. in log messages, transactions browser, command line tools, source code.
The textual representation of a blob b
is Grouped(Base32(CRC32(b) · b))
where
CRC32
is a four byte check sequence, calculated as defined by ISO 3309, ITU-T V.42, and elsewhere, and stored as big-endian, i.e., the most significant byte comes first and then the less significant bytes come in descending order of significance (MSB B2 B1 LSB).Base32
is the Base32 encoding as defined in RFC 4648, with no padding character added.The middle dot denotes concatenation.
Grouped
takes an ASCII string and inserts the separator-
(dash) every 5 characters. The last group may contain less than 5 characters. A separator never appears at the beginning or end.
The textual representation is conventionally printed with lower case letters, but parsed case-insensitively.
Because the maximum size of a principal is 29 bytes, the textual representation will be no longer than 63 characters (10 times 5 plus 3 characters with 10 separators in between them).
The canister with id 0xABCD01
has check sequence 0x233FF206
(online calculator); the final id is thus em77e-bvlzu-aq
.
Example encoding from hex, and decoding to hex, in bash (the following can be pasted into a terminal as is):
function textual_encode() {
( echo "$1" | xxd -r -p | /usr/bin/crc32 /dev/stdin; echo -n "$1" ) |
xxd -r -p | base32 | tr A-Z a-z |
tr -d = | fold -w5 | paste -sd'-' -
}
function textual_decode() {
echo -n "$1" | tr -d - | tr a-z A-Z |
fold -w 8 | xargs -n1 printf '%-8s' | tr ' ' = |
base32 -d | xxd -p | tr -d '\n' | cut -b9- | tr a-z A-Z
}
Cube lifecycle
Dapps on the BigFile are called cubes. Conceptually, they consist of the following pieces of state:
A cube id (a principal)
Their controllers (a possibly empty list of principal)
A cycle balance
A reserved cycles balance, which are cycles set aside from the main cycle balance for resource payments.
The canister status, which is one of
running
,stopping
orstopped
.Resource reservations
A canister can be empty (e.g. directly after creation) or non-empty. A non-empty canister also has
code, in the form of a canister module
state (memories, globals etc.)
possibly further data that is specific to the implementation of the BIG (e.g. queues)
Canisters are empty after creation and uninstallation, and become non-empty through code installation.
If an empty canister receives a response, that response is dropped, as if the canister trapped when processing the response. The cycles set aside for its processing and the cycles carried on the responses are added to the canister's cycles balance.
Cube cycles
The BIG relies on cycles, a utility token, to manage its resources. A canister pays for the resources it uses from its cycle balances. A cycle_balance is stored as 128-bit unsigned integers and operations on them are saturating. In particular, if cycles are added to a canister that would bring its main cycle balance beyond 2128-1, then the balance will be capped at 2128-1 and any additional cycles will be lost.
When both the main and the reserved cycles balances of a canister fall to zero, the canister is deallocated. This has the same effect as
uninstalling the canister (as described in BIG method)
setting all resource reservations to zero
Afterwards the canister is empty. It can be reinstalled after topping up its main balance.
Once the BIG frees the resources of a canister, its id, cycle balances, controllers, canister version, and the total number of canister changes are preserved on the BIG for a minimum of 10 years. What happens to the canister after this period is currently unspecified.
Cube status
The canister status can be used to control whether the canister is processing calls:
In status
running
, calls to the canister are processed as normal.In status
stopping
, calls to the canister are rejected by the BIG with reject codeCANISTER_ERROR
(5), but responses to the canister are processed as normal.In status
stopped
, calls to the canister are rejected by the BIG with reject codeCANISTER_ERROR
(5), and there are no outstanding responses.
In all cases, calls to the management canister are processed, regardless of the state of the managed canister.
The controllers of the canister can initiate transitions between these states using stop_canister
and start_canister
, and query the state using canister_status
(NB: this call returns additional information, such as the cycle balance of the canister). The canister itself can also query its state using ic0.canister_status
.
This status is orthogonal to whether a canister is empty or not: an empty canister can be in status running
. Calls to such a canister are still rejected by the BIG, but because the canister is empty.
This status is orthogonal to whether a canister is frozen or not: a frozen canister can be in status running
. Calls to such a canister are still rejected by the BIG, but because the canister is frozen, the returned reject code is SYS_TRANSIENT
.
Signatures
Digital signature schemes are used for authenticating messages in various parts of the BIG infrastructure. Signatures are domain separated, which means that every message is prefixed with a byte string that is unique to the purpose of the signature.
The BIG supports multiple signature schemes, with details given in the following subsections. For each scheme, we specify the data encoded in the public key (which is always DER-encoded, and indicates the scheme to use) as well as the form of the signatures (which are opaque blobs for the purposes of the rest of this specification).
In all cases, the signed payload is the concatenation of the domain separator and the message. All uses of signatures in this specification indicate a domain separator, to uniquely identify the purpose of the signature. The domain separators are prefix-free by construction, as their first byte indicates their length.
Ed25519 and ECDSA signatures
Plain signatures are supported for the schemes
Ed25519 or
ECDSA on curve P-256 (also known as
secp256r1
), using SHA-256 as hash function, as well as on the Koblitz curvesecp256k1
.Public keys must be valid for signature schemes Ed25519 or ECDSA and are encoded as DER.
See RFC 8410 for DER encoding of Ed25519 public keys.
See RFC 5480 for DER encoding of ECDSA public keys; the DER encoding must not specify a hash function. For curve
secp256k1
, the OID 1.3.132.0.10 is used. The points must be specified in uncompressed form (i.e.0x04
followed by the big-endian 32-byte encodings ofx
andy
).
The signatures are encoded as the concatenation of the 32-byte big endian encodings of the two values r and s.
Web Authentication
The allowed signature schemes for web authentication are
ECDSA on curve P-256 (also known as
secp256r1
), using SHA-256 as hash function.RSA PKCS#1v1.5 (RSASSA-PKCS1-v1_5), using SHA-256 as hash function.
The signature is calculated by using the payload as the challenge in the web authentication assertion.
The signature is checked by verifying that the challenge
field contains the base64url encoding of the payload, and that signature
verifies on authenticatorData · SHA-256(utf8(clientDataJSON))
, as specified in the WebAuthn w3c recommendation.
The public key is encoded as a DER-wrapped COSE key.
It uses the
SubjectPublicKeyInfo
type used for other types of public keys (see, e.g., RFC 8410, Section 4), with OID 1.3.6.1.4.1.56387.1.1 (iso.org.dod.internet.private.enterprise.dfinity.mechanisms.der-wrapped-cose). TheBIT STRING
fieldsubjectPublicKey
contains the COSE encoding. See WebAuthn w3c recommendation or RFC 8152 for details on the COSE encoding.
A DER wrapping of a COSE key is shown below. It can be parsed via the command sed "s/#.*//" | xxd -r -p | openssl asn1parse -inform der
.
30 5E # SEQUENCE of length 94 bytes
30 0C # SEQUENCE of length 12 bytes
06 0A 2B 06 01 04 01 83 B8 43 01 01 # OID 1.3.6.1.4.1.56387.1.1
03 4E 00 # BIT STRING encoding of length 78,
A501 0203 2620 0121 5820 7FFD 8363 2072 # length is at byte boundary
FD1B FEAF 3FBA A431 46E0 EF95 C3F5 5E39 # contents is a valid COSE key
94A4 1BBF 2B51 74D7 71DA 2258 2032 497E # with ECDSA on curve P-256
ED0A 7F6F 0009 2876 5B83 1816 2CFD 80A9
4E52 5A6A 368C 2363 063D 04E6 ED
You can also view the wrapping in an online ASN.1 JavaScript decoder.
The signature is a CBOR (see CBOR) value consisting of a data item with major type 6 ("Semantic tag") and tag value
55799
, followed by a map with three mandatory fields:authenticator_data
(blob
): WebAuthn authenticator data.client_data_json
(text
): WebAuthn client data in JSON representation.signature
(blob
): Signature as specified in the WebAuthn w3c recommendation, which means DER encoding in the case of an ECDSA signature.
Cube signatures
The BIG also supports a scheme where a canister can sign a payload by declaring a special "certified variable".
This section makes forward references to other concepts in this document, in particular the section Certification.
The public key is a DER-wrapped structure that indicates the signing canister, and includes a freely choosable seed. Each choice of seed yields a distinct public key for the canister, and the canister can choose to encode information, such as a user id, in the seed.
More concretely, it uses the
SubjectPublicKeyInfo
type used for other types of public keys (see, e.g., RFC 8410, Section 4), with OID 1.3.6.1.4.1.56387.1.2 (iso.org.dod.internet.private.enterprise.dfinity.mechanisms.canister-signature).The
BIT STRING
fieldsubjectPublicKey
is the blob|signing_canister_id| · signing_canister_id · seed
, where|signing_canister_id|
is the one-byte encoding of the the length of thesigning_canister_id
and·
denotes blob concatenation.The signature is a CBOR (see CBOR) value consisting of a data item with major type 6 ("Semantic tag") and tag value
55799
, followed by a map with two mandatory fields:certificate
(blob
): A CBOR-encoded certificate as per Encoding of certificates.tree
(hash-tree
): A hash tree as per Encoding of certificates.
Given a payload together with public key and signature in the format described above the signature can be verified by checking the following two conditions:
The
certificate
must be a valid certificate as described in Certification, withlookup_path(["canister", <signing_canister_id>, "certified_data"], certificate.tree) = Found (reconstruct(tree))
where
signing_canister_id
is the id of the signing canister andreconstruct
is a function that computes a root-hash for the tree.If the
certificate
includes a subnet delegation, then thesigning_canister_id
must be included in the delegation's canister id range (see Delegation).The
tree
must be awell_formed
tree withlookup_path(["sig", <s>, <m>], tree) = Found ""
where
s
is the SHA-256 hash of theseed
used in the public key andm
is the SHA-256 hash of the payload.
Supplementary Technologies
CBOR
Concise Binary Object Representation (CBOR) is a data format with a small code footprint, small message size and an extensible interface. CBOR is used extensively throughout the BigFile as the primary format for data exchange between components within the system.
cbor.io and wikipedia.org contain a lot of helpful background information and relevant tools. cbor.me in particular, is very helpful for converting between CBOR hex and diagnostic information.
For example, the following CBOR hex:
82 61 61 a1 61 62 61 63
Can be converted into the following CBOR diagnostic format:
["a", {"b": "c"}]
Particular concepts to note from the spec are:
CDDL
The Concise Data Definition Language (CDDL) is a data description language for CBOR. It is used at various points throughout this document to describe how certain data structures are encoded with CBOR.
The system state tree
Parts of the BIG state are publicly exposed (e.g. via Request: Read state or Certified data) in a verified way (see Certification for the machinery for certifying). This section describes the content of this system state abstractly.
Conceptually, the system state is a tree with labeled children, and values in the leaves. Equivalently, the system state is a mapping from paths (sequences of labels) to values, where the domain is prefix-free.
Labels are always blobs (but often with a human readable representation). In this document, paths are written suggestively with slashes as separators; the actual encoding is not actually using slashes as delimiters, and labels may contain the 0x2F byte (ASCII /
) just fine. Values are either natural numbers, text values or blob values.
This section specifies the publicly relevant paths in the tree.
Time
/time
(natural):All partial state trees include a timestamp, indicating the time at which the state is current.
Api boundary nodes information
The state tree contains information about all API boundary nodes (the source of truth for these API boundary node records is stored in the NNS registry canister).
/api_boundary_nodes/<node_id>/domain
(text)Domain name associated with a node. All domains are unique across nodes. Example:
api-bn1.example.com
./api_boundary_nodes/<node_id>/ipv4_address
(text)Public IPv4 address of a node in the dotted-decimal notation. If no
ipv4_address
is available for the corresponding node, then this path does not exist.
Example:192.168.10.150
./api_boundary_nodes/<node_id>/ipv6_address
(text)Public IPv6 address of a node in the hexadecimal notation with colons. Example:
3002:0bd6:0000:0000:0000:ee00:0033:6778
.
Subnet information
The state tree contains information about the topology of the BigFile.
/subnet/<subnet_id>/public_key
(blob)The public key of the subnet (a DER-encoded BLS key, see Certification)
/subnet/<subnet_id>/canister_ranges
(blob)The set of canister ids assigned to this subnet, represented as a list of closed intervals of canister ids, ordered lexicographically, and encoded as CBOR (see CBOR) according to this CDDL (see CDDL):
canister_ranges = tagged<[*canister_range]>
canister_range = [principal principal]
principal = bytes .size (0..29)
tagged<t> = #6.55799(t) ; the CBOR tag/subnet/<subnet_id>/metrics
(blob)A collection of subnet-wide metrics related to this subnet's current resource usage and/or performance. The metrics are a CBOR map with the following fields:
num_canisters
(nat
): The number of canisters on this subnet.canister_state_bytes
(nat
): The total size of the state in bytes taken by canisters on this subnet since this subnet was created.consumed_cycles_total
(map
): The total number of cycles consumed by all current and deleted canisters on this subnet. It's a map of two values, a low part of typenat
and a high part of typeopt nat
.update_transactions_total
(nat
): The total number of transactions processed on this subnet since this subnet was created.
Because this uses the lexicographic ordering of princpials, and the byte distinguishing the various classes of ids is at the end, this range by construction conceptually includes principals of various classes. This specification needs to take care that the fact that principals that are not canisters may appear in these ranges does not cause confusion.
/subnet/<subnet_id>/node/<node_id>/public_key
(blob)The public key of a node (a DER-encoded Ed25519 signing key, see RFC 8410 for reference) with principal
<node_id>
belonging to the subnet with principal<subnet_id>
.
Request status
For each asynchronous request known to the BigFile, its status is in a subtree at /request_status/<request_id>
. Please see Overview of canister calling for more details on how asynchronous requests work.
/request_status/<request_id>/status
(text)One of
received
,processing
,replied
,rejected
ordone
, see Overview of canister calling for more details on what each status means./request_status/<request_id>/reply
(blob)If the status is
replied
, then this path contains the reply blob, else it is not present./request_status/<request_id>/reject_code
(natural)If the status is
rejected
, then this path contains the reject code (see Reject codes), else it is not present./request_status/<request_id>/reject_message
(text)If the status is
rejected
, then this path contains a textual diagnostic message, else it is not present./request_status/<request_id>/error_code
(text)If the status is
rejected
, then this path might be present and contain an implementation-specific error code (see Error codes), else it is not present.
Immediately after submitting a request, the request may not show up yet as the BigFile is still working on accepting the request as pending.
Request statuses will not actually be kept around indefinitely, and eventually the BigFile forgets about the request. This will happen no sooner than the request's expiry time, so that replay attacks are prevented.
Certified data
/canister/<canister_id>/certified_data
(blob):The certified data of the canister with the given id, see Certified data.
Cube information
Users have the ability to learn about the hash of the canister's module, its current controllers, and metadata in a certified way.
/canister/<canister_id>/module_hash
(blob):If the canister is empty, this path does not exist. If the canister is not empty, it exists and contains the SHA256 hash of the currently installed canister module. Cf. BIG method.
/canister/<canister_id>/controllers
(blob):The current controllers of the canister. The value consists of a CBOR (see CBOR) data item with major type 6 ("Semantic tag") and tag value
55799
, followed by an array of principals in their binary form (CDDL#6.55799([* bytes .size (0..29)])
, see CDDL)./canister/<canister_id>/metadata/<name>
(blob):If the canister has a custom section called
icp:public <name>
oricp:private <name>
, this path contains the content of the custom section. Otherwise, this path does not exist.It is recommended for the canister to have a custom section called "icp:public candid:service", which contains the UTF-8 encoding of the Candid interface for the canister.
HTTPS Interface
The concrete mechanism that users use to send requests to the BigFile is via an HTTPS API, which exposes three endpoints to handle interactions, plus one for diagnostics:
At
/api/v2/canister/<effective_canister_id>/call
the user can submit (asynchronous, potentially state-changing) calls.At
/api/v2/canister/<effective_canister_id>/read_state
or/api/v2/subnet/<subnet_id>/read_state
the user can read various information about the state of the BigFile. In particular, they can poll for the status of a call here.At
/api/v2/canister/<effective_canister_id>/query
the user can perform (synchronous, non-state-changing) query calls.At
/api/v2/status
the user can retrieve status information about the BigFile.
In these paths, the <effective_canister_id>
is the textual representation of the effective canister id.
Requests to /api/v2/canister/<effective_canister_id>/call
, /api/v2/canister/<effective_canister_id>/read_state
, /api/v2/subnet/<subnet_id>/read_state
, and /api/v2/canister/<effective_canister_id>/query
are POST requests with a CBOR-encoded request body, which consists of a authentication envelope (as per Authentication) and request-specific content as described below.
This document does not yet explain how to find the location and port of the BigFile.
Overview of canister calling
Users interact with the BigFile by calling canisters. By the very nature of a blockchain protocol, they cannot be acted upon immediately, but only with a delay. Moreover, the actual node that the user talks to may not be honest or, for other reasons, may fail to get the request on the way. This implies the following high-level workflow:
A user submits a call via the HTTPS Interface. No useful information is returned in the immediate response (as such information cannot be trustworthy anyways).
For a certain amount of time, the BIG behaves as if it does not know about the call.
The BIG asks the targeted canister if it is willing to accept this message and be charged for the expense of processing it. This uses the Ingress message inspection API for normal calls. For calls to the management canister, the rules in The BIG management canister apply.
At some point, the BIG may accept the call for processing and set its status to
received
. This indicates that the BIG as a whole has received the call and plans on processing it (although it may still not get processed if the BIG is under high load). Furthermore, the user should also be able to ask any endpoint about the status of the pending call.Once it is clear that the call will be acted upon (sufficient resources, call not yet expired), the status changes to
processing
. Now the user has the guarantee that the request will have an effect, e.g. it will reach the target canister.The BIG is processing the call. For some calls this may be atomic, for others this involves multiple internal steps.
Eventually, a response will be produced, and can be retrieved for a certain amount of time. The response is either a
reply
, indicating success, or areject
, indicating some form of error.In the case that the call has been retained for long enough, but the request has not expired yet, the BIG can forget the response data and only remember the call as
done
, to prevent a replay attack.Once the expiry time is past, the BIG can prune the call and its response, and completely forget about it.
This yields the following interaction diagram:
State transitions may be instantaneous and not always externally visible. For example, the state of a request may move from received
via processing
to replied
in one go. Similarly, the BIG may not implement the done
state at all, and keep calls in state replied
/rejected
until they are pruned.
All gray states are not explicitly represented in the state of the BIG, and are indistinguishable from "call does not exist".
The characteristic property of the received
state is that the call has made it past the (potentially malicious) endpoint into the state of the BIG. It is now pointless (but harmless) to submit the (identical) call again. Before reaching that state, submitting the identical call to further nodes might be a useful safeguard against a malicious or misbehaving node.
The characteristic property of the processing
state is that the initial effect of the call has happened or will happen. This is best explained by an example: Consider a counter canister. It exports a method inc
that increases the counter. Assume that the canister is bug free, and is not going to be forcibly removed. A user submits a call to call inc
. If the user sees request status processing
, the state change is guaranteed to happen. The user can stop monitoring the status and does not have to retry submitting.
A call may be rejected by the BIG or the canister. In either case, there is no guarantee about how much processing of the call has happened.
To avoid replay attacks, the transition from done
or received
to pruned
must happen no earlier than the call's ingress_expiry
field.
Calls must stay in replied
or rejected
long enough for polling users to catch the response.
When asking the BIG about the state or call of a request, the user uses the request id (see Request ids) to read the request status (see Request status) from the state tree (see Request: Read state).
Request: Call
In order to call a canister, the user makes a POST request to /api/v2/canister/<effective_canister_id>/call
. The request body consists of an authentication envelope with a content
map with the following fields:
request_type
(text
): Alwayscall
sender
,nonce
,ingress_expiry
: See Authenticationcanister_id
(blob
): The principal of the canister to call.method_name
(text
): Name of the canister method to callarg
(blob
): Argument to pass to the canister method
The HTTP response to this request can have the following responses:
202 HTTP status with empty body. Implying the request was accepted by the BIG for further processing. Users should use
read_state
to determine the status of the call.200 HTTP status with non-empty body. Implying an execution pre-processing error occurred. The body of the response contains more information about the BIG specific error encountered. The body is a CBOR map with the following fields:
reject_code
(nat
): The reject code (see Reject codes).reject_message
(text
): a textual diagnostic message.error_code
(text
): an optional implementation-specific textual error code (see Error codes).
4xx HTTP status for client errors (e.g. malformed request). Except for 429 HTTP status, retrying the request will likely have the same outcome.
5xx HTTP status when the server has encountered an error or is otherwise incapable of performing the request. The request might succeed if retried at a later time.
This request type can also be used to call a query method (but not a composite query method). A user may choose to go this way, instead of via the faster and cheaper Request: Query call below, if they want to get a certified response. Note that the canister state will not be changed by sending a call request type for a query method (except for cycle balance change due to message execution).
The functionality exposed via the The BIG management canister can be used this way.
Request: Read state
Requesting paths with the prefix /subnet
at /api/v2/canister/<effective_canister_id>/read_state
might be deprecated in the future. Hence, users might want to point their requests for paths with the prefix /subnet
to /api/v2/subnet/<subnet_id>/read_state
.
On the BIG mainnet, the root subnet ID tdb26-jop6k-aogll-7ltgs-eruif-6kk7m-qpktf-gdiqx-mxtrf-vb5e6-eqe
can be used to retrieve the list of all BIG mainnet's subnets by requesting the prefix /subnet
at /api/v2/subnet/tdb26-jop6k-aogll-7ltgs-eruif-6kk7m-qpktf-gdiqx-mxtrf-vb5e6-eqe/read_state
.
In order to read parts of the The system state tree, the user makes a POST request to /api/v2/canister/<effective_canister_id>/read_state
or /api/v2/subnet/<subnet_id>/read_state
. The subnet form should be used when the information to be retrieved is subnet specific, i.e., when requesting paths with the prefix /time
or /subnet
, and the subnet form must be used when requesting paths of the form /subnet/<subnet_id>/metrics
. The request body consists of an authentication envelope with a content
map with the following fields:
request_type
(text
): Alwaysread_state
sender
,nonce
,ingress_expiry
: See Authenticationpaths
(sequence of paths): A list of at most 1000 paths, where a path is itself a sequence of at most 127 blobs.
The HTTP response to this request consists of a CBOR (see CBOR) map with the following fields:
certificate
(blob
): A certificate (see Certification).If this
certificate
includes a subnet delegation (see Delegation), thenfor requests to
/api/v2/canister/<effective_canister_id>/read_state
, the<effective_canister_id>
must be included in the delegation's canister id range,for requests to
/api/v2/subnet/<subnet_id>/read_state
, the<subnet_id>
must match the delegation's subnet id.
The returned certificate reveals all values whose path has a requested path as a prefix except for
- paths with prefix
/subnet/<subnet_id>/node
which are only contained in the returned certificate if<effective_canister_id>
belongs to the canister ranges of the subnet<subnet_id>
, i.e., if<effective_canister_id>
belongs to the value at the path/subnet/<subnet_id>/canister_ranges
in the state tree.
The returned certificate also always reveals /time
, even if not explicitly requested.
The returned certificate might also reveal the SHA-256 hashes of values whose paths have not been requested and whose paths might not even be allowed to be requested by the sender of the HTTP request. This means that unauthorized users might obtain the SHA-256 hashes of ingress message responses and private custom sections of the canister's module. Hence, users are advised to use cryptographically strong nonces in their HTTP requests and canister developers that aim at keeping data confidential are advised to add a secret cryptographic salt to their canister's responses and private custom sections.
All requested paths must have the following form:
/time
. Can always be requested./api_boundary_nodes
,/api_boundary_nodes/<node_id>
,/api_boundary_nodes/<node_id>/domain
,/api_boundary_nodes/<node_id>/ipv4_address
,/api_boundary_nodes/<node_id>/ipv6_address
. Can always be requested./subnet
,/subnet/<subnet_id>
,/subnet/<subnet_id>/public_key
,/subnet/<subnet_id>/canister_ranges
,/subnet/<subnet_id>/metrics
,/subnet/<subnet_id>/node
,/subnet/<subnet_id>/node/<node_id>
,/subnet/<subnet_id>/node/<node_id>/public_key
. Can always be requested./request_status/<request_id>
,/request_status/<request_id>/status
,/request_status/<request_id>/reply
,/request_status/<request_id>/reject_code
,/request_status/<request_id>/reject_message
,/request_status/<request_id>/error_code
. Can be requested if no path with such a prefix exists in the state tree orthe sender of the original request referenced by
<request_id>
is the same as the sender of the read state request andthe effective canister id of the original request referenced by
<request_id>
matches<effective_canister_id>
.
/canisters/<canister_id>/module_hash
. Can be requested if<canister_id>
matches<effective_canister_id>
./canisters/<canister_id>/controllers
. Can be requested if<canister_id>
matches<effective_canister_id>
. The order of controllers in the value at this path may vary depending on the implementation./canisters/<canister_id>/metadata/<name>
. Can be requested if<canister_id>
matches<effective_canister_id>
,<name>
is encoded in UTF-8, andcanister with canister id
<canister_id>
does not exist orcanister with canister id
<canister_id>
is empty orcanister with canister id
<canister_id>
does not have<name>
as its custom section or<name>
is a public custom section or<name>
is a private custom section and the sender of the read state request is a controller of the canister.
Moreover, all paths with prefix /request_status/<request_id>
must refer to the same request ID <request_id>
.
If a path cannot be requested, then the HTTP response to the read state request is undefined.
Note that the paths /canisters/<canister_id>/certified_data
are not accessible with this method; these paths are only exposed to the canisters themselves via the System API (see Certified data).
See The system state tree for details on the state tree.
Request: Query call
A query call is a fast, but less secure way to call a canister. Only methods that are explicitly marked as "query methods" and "composite query methods" by the canister can be called this way. In contrast to a query method, a composite query method can make further calls to query and composite query methods of canisters on the same subnet.
The following limits apply to the evaluation of a query call:
The amount of cycles that are used in total (across all calls to query and composite query methods and their callbacks) during evaluation of a query call is at most
MAX_CYCLES_PER_QUERY
.The maximum nesting level of calls during evaluation of a query call is at most
MAX_CALL_DEPTH_COMPOSITE_QUERY
.The wall clock time spent on evaluation of a query call is at most
MAX_WALL_CLOCK_TIME_COMPOSITE_QUERY
.
Composite query methods are EXPERIMENTAL and there might be breaking changes of their behavior in the future. Use at your own risk!
In order to make a query call to a canister, the user makes a POST request to /api/v2/canister/<effective_canister_id>/query
. The request body consists of an authentication envelope with a content
map with the following fields:
request_type
(text
): Always"query"
.sender
,nonce
,ingress_expiry
: See Authentication.canister_id
(blob
): The principal of the canister to call.method_name
(text
): Name of the canister method to call.arg
(blob
): Argument to pass to the canister method.
Cube methods that do not change the canister state (except for cycle balance changes due to message execution) can be executed more efficiently. This method provides that ability, and returns the canister's response directly within the HTTP response.
If the query call resulted in a reply, the response is a CBOR (see CBOR) map with the following fields:
status
(text
):"replied"
reply
: a CBOR map with the fieldarg
(blob
) which contains the reply data.signatures
([+ node-signature]
): a list containing one node signature for the returned query response.
If the call resulted in a reject, the response is a CBOR map with the following fields:
status
(text
):"rejected"
reject_code
(nat
): The reject code (see Reject codes).reject_message
(text
): a textual diagnostic message.error_code
(text
): an optional implementation-specific textual error code (see Error codes).signatures
([+ node-signature]
): a list containing one node signature for the returned query response.
Although signatures
only contains one node signature, we still declare its type to be a list to prevent future breaking changes
if we include more signatures in a future version of the protocol specification.
The response to a query call contains a list with one signature for the returned response produced by the BIG node that evaluated the query call. The signature (whose type is denoted as node-signature
) is a CBOR (see CBOR) map with the following fields:
timestamp
(nat
): the timestamp of the signature.signature
(blob
): the actual signature.identity
(principal
): the principal of the node producing the signature.
Given a query (the content
map from the request body) Q
, a response R
, and a certificate Cert
that is obtained by requesting the path /subnet
in a separate read state request to /api/v2/canister/<effective_canister_id>/read_state
, the following predicate describes when the returned response R
is correctly signed:
verify_response(Q, R, Cert)
= verify_cert(Cert) ∧
((Cert.delegation = NoDelegation ∧ SubnetId = RootSubnetId ∧ lookup(["subnet",SubnetId,"canister_ranges"], Cert) = Found Ranges) ∨
(SubnetId = Cert.delegation.subnet_id ∧ lookup(["subnet",SubnetId,"canister_ranges"], Cert.delegation.certificate) = Found Ranges)) ∧
effective_canister_id ∈ Ranges ∧
∀ {timestamp: T, signature: Sig, identity: NodeId} ∈ R.signatures.
lookup(["subnet",SubnetId,"node",NodeId,"public_key"], Cert) = Found PK ∧
if R.status = "replied" then
verify_signature PK Sig ("\x0Bic-response" · hash_of_map({
status: "replied",
reply: R.reply,
timestamp: T,
request_id: hash_of_map(Q)}))
else
verify_signature PK Sig ("\x0Bic-response" · hash_of_map({
status: "rejected",
reject_code: R.reject_code,
reject_message: R.reject_message,
error_code: R.error_code,
timestamp: T,
request_id: hash_of_map(Q)}))
where RootSubnetId
is the a priori known principal of the root subnet. Moreover, all timestamps in R.signatures
, the certificate Cert
, and its optional delegation must be "recent enough".
This specification leaves it up to the client to define expiry times for the timestamps in R.signatures
, the certificate Cert
, and its optional delegation. A reasonable expiry time for timestamps in R.signatures
and the certificate Cert
is 5 minutes (analogously to the maximum allowed ingress expiry enforced by the BIG mainnet). Delegations require expiry times of at least a week since the BIG mainnet refreshes the delegations only after replica upgrades which typically happen once a week.
Effective canister id
The <effective_canister_id>
in the URL paths of requests is the effective destination of the request.
It must be contained in the canister ranges of a subnet, otherwise the corresponding HTTP request is rejected.
If the request is an update call to the Management Cube (
aaaaa-aa
), then:If the call is to the
provisional_create_canister_with_cycles
method, then any principal can be used as the effective canister id for this call.Otherwise, if the
arg
is a Candid-encoded record with acanister_id
field of typeprincipal
, then the effective canister id must be that principal.Otherwise, the call is rejected by the system independently of the effective canister id.
If the request is an update call to a canister that is not the Management Cube (
aaaaa-aa
) or if the request is a query call, then the effective canister id must be thecanister_id
in the request.
The expectation is that user-side agent code shields users and developers from the notion of effective canister id, in analogy to how the System API interface shields canister developers from worrying about routing.
The BigFile blockchain mainnet does not support provisional_create_canister_with_cycles
and thus all calls to this method are rejected independently of the effective canister id.
In development instances of the BigFile Protocol (e.g. testnets), the effective canister id of a request submitted to a node must be a canister id from the canister ranges of the subnet to which the node belongs.
Authentication
All requests coming in via the HTTPS interface need to be either anonymous or authenticated using a cryptographic signature. To that end, the following fields are present in the content
map in all cases:
nonce
(blob
, optional): Arbitrary user-provided data of length at most 32 bytes, typically randomly generated. This can be used to create distinct requests with otherwise identical fields.ingress_expiry
(nat
, required): An upper limit on the validity of the request, expressed in nanoseconds since 1970-01-01 (like ic0.time()). This avoids replay attacks: The BIG will not accept requests, or transition requests from statusreceived
to statusprocessing
, if their expiry date is in the past. The BIG may refuse to accept requests with an ingress expiry date too far in the future. This applies to synchronous and asynchronous requests alike (and could have been calledrequest_expiry
).sender
(Principal
, required): The user who issued the request.
The envelope, i.e. the overall request, has the following keys:
content
(record
): the actual request contentsender_pubkey
(blob
, optional): Public key used to authenticate this request. Since a user may in the future have more than one key, this field tells the BIG which key is used.sender_delegation
(array
of maps, optional): a chain of delegations, starting with the one signed bysender_pubkey
and ending with the one delegating to the key relating tosender_sig
. Every public key in the chain of delegations should appear exactly once: cycles (a public key delegates to another public key that already previously appeared in the chain) or self-signed delegations (a public key delegates to itself) are not allowed and such requests will be refused by the BIG.sender_sig
(blob
, optional): Signature to authenticate this request.
The public key must authenticate the sender
principal:
A public key can authenticate a principal if the latter is a self-authenticating id derived from that public key (see Special forms of Principals).
The fields
sender_pubkey
,sender_sig
, andsender_delegation
must be omitted if thesender
field is the anonymous principal. The fieldssender_pubkey
andsender_sig
must be set if thesender
field is not the anonymous principal.
The request id (see Request ids) is calculated from the content record. This allows the signature to be based on the request id, and implies that signature and public key are not semantically relevant.
The field sender_pubkey
contains a public key supported by one of the schemes described in Signatures.
Signing transactions can be delegated from one key to another one. If delegation is used, then the sender_delegation
field contains an array of delegations, each of which is a map with the following fields:
delegation
(map
): Map with fields:pubkey
(blob
): Public key as described in Signatures.expiration
(nat
): Expiration of the delegation, in nanoseconds since 1970-01-01, analogously to theingress_expiry
field above.targets
(array
ofCanisterId
, optional): If this field is set, the delegation only applies for requests sent to the canisters in the list. The list must contain no more than 1000 elements; otherwise, the request will not be accepted by the BIG.
signature
(blob
): Signature on the 32-byte representation-independent hash of the map contained in thedelegation
field as described in Signatures, using the 27 bytes\x1Aic-request-auth-delegation
as the domain separator.For the first delegation in the array, this signature is created with the key corresponding to the public key from the
sender_pubkey
field, all subsequent delegations are signed with the key corresponding to the public key contained in the preceding delegation.
The sender_sig
field is calculated by signing the concatenation of the 11 bytes \x0Aic-request
(the domain separator) and the 32 byte request id with the secret key that belongs to the key specified in the last delegation or, if no delegations are present, the public key specified in sender_pubkey
.
The delegation field, if present, must not contain more than 20 delegations.
Representation-independent hashing of structured data
Structured data, such as (recursive) maps, are authenticated by signing a representation-independent hash of the data. This hash is computed as follows (using SHA256 in the steps below):
For each field that is present in the map (i.e. omitted optional fields are indeed omitted):
- concatenate the hash of the field's name (in ascii-encoding, without terminal
\x00
) and the hash of the value (as specified below).
- concatenate the hash of the field's name (in ascii-encoding, without terminal
Sort these concatenations from low to high.
Concatenate the sorted elements, and hash the result.
The resulting hash of length 256 bits (32 bytes) is the representation-independent hash.
Field values are hashed as follows:
Binary blobs (
canister_id
,arg
,nonce
,module
) are hashed as-is.Strings (
request_type
,method_name
) are hashed by hashing their binary encoding in UTF-8, without a terminal\x00
.Natural numbers (
compute_allocation
,memory_allocation
,ingress_expiry
) are hashed by hashing their binary encoding using the shortest form Unsigned LEB128 encoding. For example,0
should be encoded as a single zero byte[0x00]
and624485
should be encoded as byte sequence[0xE5, 0x8E, 0x26]
.Integers are hashed by hashing their encoding using the shortest form Signed LEB128 encoding. For example,
0
should be encoded as a single zero byte[0x00]
and-123456
should be encoded as byte sequence[0xC0, 0xBB, 0x78]
.Arrays (
paths
) are hashed by hashing the concatenation of the hashes of the array elements.Maps (
sender_delegation
) are hashed by recursively computing their representation-independent hash.
Example calculation (where H
denotes SHA-256 and ·
denotes blob concatenation) of a representation independent hash
for a map with a nested map in a field value:
hash_of_map({ "reply": { "arg": "DIDL\x00\x00" } })
= H(concat (sort [ H("reply") · hash_of_map({ "arg": "DIDL\x00\x00" }) ]))
= H(concat (sort [ H("reply") · H(concat (sort [ H("arg") · H("DIDL\x00\x00") ])) ]))
Request ids
When signing requests or querying the status of a request (see Request status) in the state tree, the user identifies the request using a request id, which is the representation-independent hash of the content
map of the original request. A request id must have length of 32 bytes.
The request id is independent of the representation of the request (currently only CBOR, see CBOR), and does not change if the specification adds further optional fields to a request type.
The recommended textual representation of a request id is a hexadecimal string with lower-case letters prefixed with '0x'. E.g., request id consisting of bytes [00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 0A, 0B, 0C, 0D, 0E, 0F, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 1A, 1B, 1C, 1D, 1E, 1F]
should be displayed as 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
.
Example calculation (where H
denotes SHA-256 and ·
denotes blob concatenation) in which we assume that the optional nonce is not provided and thus omitted:
hash_of_map({ request_type: "call", sender: 0x04, ingress_expiry: 1685570400000000000, canister_id: 0x00000000000004D2, method_name: "hello", arg: "DIDL\x00\xFD*"})
= H(concat (sort
[ H("request_type") · H("call")
, H("sender") · H("0x04")
, H("ingress_expiry") · H(1685570400000000000)
, H("canister_id") · H("\x00\x00\x00\x00\x00\x00\x04\xD2")
, H("method_name") · H("hello")
, H("arg") · H("DIDL\x00\xFD*")
]))
= H(concat (sort
[ 769e6f87bdda39c859642b74ce9763cdd37cb1cd672733e8c54efaa33ab78af9 · 7edb360f06acaef2cc80dba16cf563f199d347db4443da04da0c8173e3f9e4ed
, 0a367b92cf0b037dfd89960ee832d56f7fc151681bb41e53690e776f5786998a · e52d9c508c502347344d8c07ad91cbd6068afc75ff6292f062a09ca381c89e71
, 26cec6b6a9248a96ab24305b61b9d27e203af14a580a5b1ff2f67575cab4a868 · db8e57abc8cda1525d45fdd2637af091bc1f28b35819a40df71517d1501f2c76
, 0a3eb2ba16702a387e6321066dd952db7a31f9b5cc92981e0a92dd56802d3df9 · 4d8c47c3c1c837964011441882d745f7e92d10a40cef0520447c63029eafe396
, 293536232cf9231c86002f4ee293176a0179c002daa9fc24be9bb51acdd642b6 · 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
, b25f03dedd69be07f356a06fe35c1b0ddc0de77dcd9066c4be0c6bbde14b23ff · 6c0b2ae49718f6995c02ac5700c9c789d7b7862a0d53e6d40a73f1fcd2f70189
]))
= H(concat
[ 0a367b92cf0b037dfd89960ee832d56f7fc151681bb41e53690e776f5786998a · e52d9c508c502347344d8c07ad91cbd6068afc75ff6292f062a09ca381c89e71
, 0a3eb2ba16702a387e6321066dd952db7a31f9b5cc92981e0a92dd56802d3df9 · 4d8c47c3c1c837964011441882d745f7e92d10a40cef0520447c63029eafe396
, 26cec6b6a9248a96ab24305b61b9d27e203af14a580a5b1ff2f67575cab4a868 · db8e57abc8cda1525d45fdd2637af091bc1f28b35819a40df71517d1501f2c76
, 293536232cf9231c86002f4ee293176a0179c002daa9fc24be9bb51acdd642b6 · 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
, 769e6f87bdda39c859642b74ce9763cdd37cb1cd672733e8c54efaa33ab78af9 · 7edb360f06acaef2cc80dba16cf563f199d347db4443da04da0c8173e3f9e4ed
, b25f03dedd69be07f356a06fe35c1b0ddc0de77dcd9066c4be0c6bbde14b23ff · 6c0b2ae49718f6995c02ac5700c9c789d7b7862a0d53e6d40a73f1fcd2f70189
])
= 1d1091364d6bb8a6c16b203ee75467d59ead468f523eb058880ae8ec80e2b101
Reject codes
An API request or inter-canister call that is pending in the BIG will eventually result in either a reply (indicating success, and carrying data) or a reject (indicating an error of some sorts). A reject contains a rejection code that classifies the error and a hopefully helpful reject message string.
Rejection codes are member of the following enumeration:
SYS_FATAL
(1): Fatal system error, retry unlikely to be useful.SYS_TRANSIENT
(2): Transient system error, retry might be possible.DESTINATION_INVALID
(3): Invalid destination (e.g. canister/account does not exist)CANISTER_REJECT
(4): Explicit reject by the canister.CANISTER_ERROR
(5): Cube error (e.g., trap, no response)
The symbolic names of this enumeration are used throughout this specification, but on all interfaces (HTTPS API, System API), they are represented as positive numbers as given in the list above.
The error message is guaranteed to be a string, i.e. not arbitrary binary data.
When canisters explicitly reject a message (see Public methods), they can specify the reject message, but not the reject code; it is always CANISTER_REJECT
. In this sense, the reject code is trustworthy: If the BIG responds with a SYS_FATAL
reject, then it really was the BIG issuing this reject.
Error codes
Implementations of the API can provide additional details for rejected messages in the form of a textual label identifying the error condition. API clients can use these labels to handle errors programmatically or suggest recovery paths to the user. The specification reserves error codes matching the regular expression BIG[0-9]+
(e.g., IC502
) for the BigFile implementation of the API.
Status endpoint
Additionally, the BigFile provides an API endpoint to obtain various status fields at
/api/v2/status
For this endpoint, the user performs a GET request, and receives a CBOR (see CBOR) value with the following fields. The BIG may include additional implementation-specific fields.
root_key
(blob, optional): The public key (a DER-encoded BLS key) of the root key of this instance of the BigFile Protocol. This must be present in short-lived development instances, to allow the agent to fetch the public key. For the BigFile, agents must have an independent trustworthy source for this data, and must not be tempted to fetch it from this insecure location.
See CBOR encoding of requests and responses for details on the precise CBOR encoding of this object.
Future additions may include local time, geographic location, and other useful implementation-specific information such as blockheight. This data may possibly be signed by the node.
CBOR encoding of requests and responses
Requests and responses are specified here as records with named fields and using suggestive human readable syntax. The actual format in the body of the HTTP request or response, however, is CBOR (see CBOR).
Concretely, it consists of a data item with major type 6 ("Semantic tag") and tag value 55799
, followed by a record.
Requests consist of an envelope record with keys sender_sig
(a blob), sender_pubkey
(a blob) and content
(a record). The first two are metadata that are used for request authentication, while the last one is the actual content of the request.
The following encodings are used:
Strings: Major type 3 ("Text string").
Blobs: Major type 2 ("Byte string").
Nats: Major type 0 ("Unsigned integer") if small enough to fit that type, else the Bignum format is used.
Records: Major type 5 ("Map of pairs of data items"), followed by the fields, where keys are encoded with major type 3 ("Text string").
Arrays: Major type 4 ("Array of data items").
As advised by section "Creating CBOR-Based Protocols" of the CBOR spec, we clarify that:
Floating-point numbers may not be used to encode integers.
Duplicate keys are prohibited in CBOR maps.
A typical request would be (written in CBOR diagnostic notation, which can be checked and converted on cbor.me):
55799({
"content": {
"request_type": "call",
"canister_id": h'ABCD01',
"method_name": "say_hello",
"arg": h'0061736d01000000'
},
"sender_sig": h'DEADBEEF',
"sender_pubkey": h'b7a3c12dc0c8c748ab07525b701122b88bd78f600c76342d27f25e5f92444cde'
})
CDDL description of requests and responses
This section summarizes the format of the CBOR data passed to and from the entry points described above. You can also download the file and see CDDL for more information.
Ordering guarantees
The order in which the various messages between canisters are delivered and executed is not fully specified. The guarantee provided by the BIG is that if a canister sends two messages to a canister and they both start being executed by the receiving canister, then they do so in the order in which the messages were sent.
More precisely:
Messages between any two canisters, if delivered to the canister, start executing in order. Note that message delivery can fail for arbitrary reasons (e.g., high system load).
If a WebAssembly function, within a single invocation, makes multiple calls to the same canister, they are queued in the order of invocations to
ic0.call_perform
.Responses (including replies with
ic0.msg_reply
, explicit rejects withic0.msg_reject
and system-generated error responses) do not have any ordering guarantee relative to each other or to method calls.There is no particular order guarantee for ingress messages submitted via the HTTPS interface.
Synchronicity across nodes
This document describes the BigFile as having a single global state that can be modified and queried. In reality, it consists of many nodes, which may not be perfectly in sync.
As long as you talk to one (honest) node only, the observed behavior is nicely sequential. If you issue an update (i.e. state-mutating) call to a canister (e.g. bump a counter), and node A indicates that the call has been executed, and you then issue a query call to node A, then A's response is guaranteed to include the effect of the update call (and you will receive the updated counter value).
If you then (quickly) issue a read request to node B, it may be that B responds to your read query based on the old state of the canister (and you might receive the old counter value).
A related problem is that query calls are not certified, and nodes may be dishonest in their response. In that case, the user might want to get more assurance by querying multiple nodes and comparing the result. However, it is (currently) not possible to query a specific state.
Applications can work around these problems. For the first problem, the query result could be such that the user can tell if the update has been received or not. For the second problem, even if using certified data is not possible, if replies are monotonic in some sense the user can get assurance in their intersection (e.g. if the query returns a list of events that grows over time, then even if different nodes return different lists, the user can get assurance in those events that are reported by many nodes).
Cube module format
A canister module is a WebAssembly module that is either in binary format (typically .wasm
) or gzip-compressed (typically .wasm.gz
). If the module starts with byte sequence [0x1f, 0x8b, 0x08]
, then the system decompresses the contents as a gzip stream according to RFC-1952 and then parses the output as a WebAssembly binary.
Cube interface (System API)
The System API is the interface between the running canister and the BigFile. It allows the WebAssembly module of a canister to expose functionality to the users (method entry points) and the BIG (e.g. initialization), and exposes functionality of the BIG to the canister (e.g. calling other canisters). Because WebAssembly is rather low-level, it also explains how to express higher level concepts (e.g. binary blobs).
We want to leverage advanced WebAssembly features, such as WebAssembly host references. But as they are not yet supported by all tools involved, this section describes an initial System API that does not rely on host references. In section Outlook: Using Host References, we outline some of the proposed uses of WebAssembly host references.
WebAssembly module requirements
In order for a WebAssembly module to be usable as the code for the canister, it needs to conform to the following requirements:
It may only import a function if it is listed in Overview of imports.
It may have a
(start)
function.If it exports a function called
canister_init
, the function must have type() -> ()
.If it exports a function called
canister_inspect_message
, the function must have type() -> ()
.If it exports a function called
canister_pre_upgrade
, the function must have type() -> ()
.If it exports a function called
canister_post_upgrade
, the function must have type() -> ()
.If it exports a function called
canister_heartbeat
, the function must have type() -> ()
.If it exports a function called
canister_global_timer
, the function must have type() -> ()
.If it exports any functions called
canister_update <name>
,canister_query <name>
, orcanister_composite_query <name>
for somename
, the functions must have type() -> ()
.It may not export more than one function called
canister_update <name>
,canister_query <name>
, orcanister_composite_query <name>
with the samename
.It may not export other methods the names of which start with the prefix
canister_
besides the methods allowed above.It may not have both
icp:public <name>
andicp:private <name>
with the samename
as the custom section name.It may not have other custom sections the names of which start with the prefix
icp:
besides the `icp:public ` and `icp:private `.The BIG may reject WebAssembly modules that
declare more than 50,000 functions, or
declare more than 1,000 globals, or
declare more than 16 exported custom sections (the custom section names with prefix
icp:
), orthe number of all exported functions called
canister_update <name>
,canister_query <name>
, orcanister_composite_query <name>
exceeds 1,000, orthe sum of
<name>
lengths in all exported functions calledcanister_update <name>
,canister_query <name>
, orcanister_composite_query <name>
exceeds 20,000, orthe total size of the custom sections (the sum of
<name>
lengths in their namesicp:public <name>
andicp:private <name>
plus the sum of their content lengths) exceeds 1MiB.
Interpretation of numbers
WebAssembly number types (i32
, i64
) do not indicate if the numbers are to be interpreted as signed or unsigned. Unless noted otherwise, whenever the System API interprets them as numbers (e.g. memory pointers, buffer offsets, array sizes), they are to be interpreted as unsigned.
Entry points
The canister provides entry points which are invoked by the BIG under various circumstances:
The canister may export a function with name
canister_init
and type() -> ()
.The canister may export a function with name
canister_pre_upgrade
and type() -> ()
.The canister may export a function with name
canister_post_upgrade
and type() -> ()
.The canister may export functions with name
canister_inspect_message
with type() -> ()
.The canister may export a function with name
canister_heartbeat
with type() -> ()
.The canister may export a function with name
canister_global_timer
with type() -> ()
.The canister may export functions with name
canister_update <name>
and type() -> ()
.The canister may export functions with name
canister_query <name>
and type() -> ()
.The canister may export functions with name
canister_composite_query <name>
and type() -> ()
.The canister table may contain functions of type
(env : i32) -> ()
which may be used as callbacks for inter-canister calls and composite query methods.
If the execution of any of these entry points traps for any reason, then all changes to the WebAssembly state, as well as the effect of any externally visible system call (like ic0.msg_reply
, ic0.msg_reject
, ic0.call_perform
), are discarded. For upgrades, this transactional behavior applies to the canister_pre_upgrade
/canister_post_upgrade
sequence as a whole.
Cube initialization
If canister_init
is present, then this is the first exported WebAssembly function invoked by the BIG. The argument that was passed along with the canister initialization call (see BIG method) is available to the canister via ic0.msg_arg_data_size/copy
.
The BIG assumes the canister to be fully instantiated if the canister_init
method entry point returns. If the canister_init
method entry point traps, then canister installation has failed, and the canister is reverted to its previous state (i.e. empty with install
, or whatever it was for a reinstall
).
Cube upgrades
When a canister is upgraded to a new WebAssembly module, the BIG:
Invokes
canister_pre_upgrade
(if exported by the current canister code andskip_pre_upgrade
is notopt true
) on the old instance, to give the canister a chance to clean up (e.g. move data to stable memory).Instantiates the new module, including the execution of
(start)
, with a fresh WebAssembly state.Invokes
canister_post_upgrade
(if present) on the new instance, passing thearg
provided in theinstall_code
call (BIG method).
The stable memory is preserved throughout the process; any other WebAssembly state is discarded.
During these steps, no other entry point of the old or new canister is invoked. The canister_init
function of the new canister is not invoked.
These steps are atomic: If canister_pre_upgrade
or canister_post_upgrade
trap, the upgrade has failed, and the canister is reverted to the previous state. Otherwise, the upgrade has succeeded, and the old instance is discarded.
The skip_pre_upgrade
flag can be enabled to skip the execution of the canister_pre_upgrade
method on the old canister instance.
The main purpose of this mode is recovery from cases when the canister_pre_upgrade
hook traps unconditionally preventing the normal upgrade path.
Skipping the pre-upgrade can lead to data loss. Use it only as the last resort and only if the stable memory already contains the entire canister state.
Public methods
To define a public method of name name
, a WebAssembly module exports a function with name canister_update <name>
, canister_query <name>
, or canister_composite_query <name>
and type () -> ()
. We call this the method entry point. The name of the exported function distinguishes update, query, and composite query methods.
The space in canister_update <name>
, canister_query <name>
, and canister_composite_query <name>
, resp., is intentional. There is exactly one space between canister_update/canister_query/canister_composite_query
and the <name>
.
The argument of the call (e.g. the content of the arg
field in the API request to call a canister method) is copied into the canister on demand using the System API functions shown below.
Eventually, a method will want to send a response, using ic0.reply
or ic0.reject
Heartbeat
For periodic or time-based execution, the WebAssembly module can export a function with name canister_heartbeat
. The heartbeats scheduling algorithm is implementation-defined.
canister_heartbeat
is triggered by the BIG, and therefore has no arguments and cannot reply or reject. Still, the function canister_heartbeat
can initiate new calls.
While an implementation will likely try to keep the interval between canister_heartbeat
invocations to within a few seconds, this is not formally part of this specification.
Global timer
For time-based execution, the WebAssembly module can export a function with name canister_global_timer
. This function is called if the canister has set its global timer (using the System API function ic0.global_timer_set
) and the current time (as returned by the System API function ic0.time
) has passed the value of the global timer.
Once the function canister_global_timer
is scheduled, the canister's global timer is deactivated. The global timer is also deactivated upon changes to the canister's Wasm module (calling install_code
, install_chunked_code
, uninstall_code
methods of the management canister or if the canister runs out of cycles). In particular, the function canister_global_timer
won't be scheduled again unless the canister sets the global timer again (using the System API function ic0.global_timer_set
). The global timer scheduling algorithm is implementation-defined.
canister_global_timer
is triggered by the BIG, and therefore has no arguments and cannot reply or reject. Still, the function canister_global_timer
can initiate new calls.
While an implementation will likely try to keep the interval between the value of the global timer and the time-stamp of the canister_global_timer
invocation within a few seconds, this is not formally part of this specification.
Callbacks
Callbacks are addressed by their table index (as a proxy for a Wasm funcref
).
In the reply callback of a inter-canister method call, the argument refers to the response to that call. In reject callbacks, no argument is available.
Overview of imports
The following sections describe various System API functions, also referred to as system calls, which we summarize here.
ic0.msg_arg_data_size : () -> i32; // I U Q CQ Ry CRy F
ic0.msg_arg_data_copy : (dst : i32, offset : i32, size : i32) -> (); // I U Q CQ Ry CRy F
ic0.msg_caller_size : () -> i32; // *
ic0.msg_caller_copy : (dst : i32, offset: i32, size : i32) -> (); // *
ic0.msg_reject_code : () -> i32; // Ry Rt CRy CRt
ic0.msg_reject_msg_size : () -> i32; // Rt CRt
ic0.msg_reject_msg_copy : (dst : i32, offset : i32, size : i32) -> (); // Rt CRt
ic0.msg_reply_data_append : (src : i32, size : i32) -> (); // U Q CQ Ry Rt CRy CRt
ic0.msg_reply : () -> (); // U Q CQ Ry Rt CRy CRt
ic0.msg_reject : (src : i32, size : i32) -> (); // U Q CQ Ry Rt CRy CRt
ic0.msg_cycles_available : () -> i64; // U Rt Ry
ic0.msg_cycles_available128 : (dst : i32) -> (); // U Rt Ry
ic0.msg_cycles_refunded : () -> i64; // Rt Ry
ic0.msg_cycles_refunded128 : (dst : i32) -> (); // Rt Ry
ic0.msg_cycles_accept : (max_amount : i64) -> (amount : i64); // U Rt Ry
ic0.msg_cycles_accept128 : (max_amount_high : i64, max_amount_low: i64, dst : i32)
-> (); // U Rt Ry
ic0.cycles_burn128 : (amount_high : i64, amount_low : i64, dst : i32) -> (); // I G U Ry Rt C T
ic0.canister_self_size : () -> i32; // *
ic0.canister_self_copy : (dst : i32, offset : i32, size : i32) -> (); // *
ic0.canister_cycle_balance : () -> i64; // *
ic0.canister_cycle_balance128 : (dst : i32) -> (); // *
ic0.canister_status : () -> i32; // *
ic0.canister_version : () -> i64; // *
ic0.msg_method_name_size : () -> i32; // F
ic0.msg_method_name_copy : (dst : i32, offset : i32, size : i32) -> (); // F
ic0.accept_message : () -> (); // F
ic0.call_new : // U CQ Ry Rt CRy CRt T
( callee_src : i32,
callee_size : i32,
name_src : i32,
name_size : i32,
reply_fun : i32,
reply_env : i32,
reject_fun : i32,
reject_env : i32
) -> ();
ic0.call_on_cleanup : (fun : i32, env : i32) -> (); // U CQ Ry Rt CRy CRt T
ic0.call_data_append : (src : i32, size : i32) -> (); // U CQ Ry Rt CRy CRt T
ic0.call_cycles_add : (amount : i64) -> (); // U Ry Rt T
ic0.call_cycles_add128 : (amount_high : i64, amount_low: i64) -> (); // U Ry Rt T
ic0.call_perform : () -> ( err_code : i32 ); // U CQ Ry Rt CRy CRt T
ic0.stable_size : () -> (page_count : i32); // * s
ic0.stable_grow : (new_pages : i32) -> (old_page_count : i32); // * s
ic0.stable_write : (offset : i32, src : i32, size : i32) -> (); // * s
ic0.stable_read : (dst : i32, offset : i32, size : i32) -> (); // * s
ic0.stable64_size : () -> (page_count : i64); // * s
ic0.stable64_grow : (new_pages : i64) -> (old_page_count : i64); // * s
ic0.stable64_write : (offset : i64, src : i64, size : i64) -> (); // * s
ic0.stable64_read : (dst : i64, offset : i64, size : i64) -> (); // * s
ic0.certified_data_set : (src: i32, size: i32) -> (); // I G U Ry Rt T
ic0.data_certificate_present : () -> i32; // *
ic0.data_certificate_size : () -> i32; // Q CQ
ic0.data_certificate_copy : (dst: i32, offset: i32, size: i32) -> (); // Q CQ
ic0.time : () -> (timestamp : i64); // *
ic0.global_timer_set : (timestamp : i64) -> i64; // I G U Ry Rt C T
ic0.performance_counter : (counter_type : i32) -> (counter : i64); // * s
ic0.is_controller: (src: i32, size: i32) -> ( result: i32); // * s
ic0.in_replicated_execution: () -> (result: i32); // * s
ic0.debug_print : (src : i32, size : i32) -> (); // * s
ic0.trap : (src : i32, size : i32) -> (); // * s
The comment after each function lists from where these functions may be invoked:
I
: fromcanister_init
orcanister_post_upgrade
G
: fromcanister_pre_upgrade
U
: fromcanister_update …
Q
: fromcanister_query …
CQ
: fromcanister_composite_query …
Ry
: from a reply callbackRt
: from a reject callbackCRy
: from a reply callback in composite queryCRt
: from a reject callback in composite queryC
: from a cleanup callbackCC
: from a cleanup callback in composite querys
: the(start)
module initialization functionF
: fromcanister_inspect_message
T
: from system task (canister_heartbeat
orcanister_global_timer
)*
=I G U Q CQ Ry Rt CRy CRt C CC F T
(NB: Not(start)
)
If the canister invokes a system call from somewhere else, it will trap.
Since Wasm doesn't have a 128-bit number type, calls requiring 128-bit arguments (e.g., the 128-bit versions of cycle operations) encode such arguments as a pair of 64-bit numbers containing the high and low bits.
Blob-typed arguments and results
WebAssembly functions parameter and result types can only be primitive number types. To model functions that accept or return blobs or text values, the following idiom is used:
To provide access to a string or blob foo
, the System API provides two functions:
ic0.foo_size : () -> i32
ic0.foo_copy : (dst : i32, offset: i32, size : i32) -> ()
The *_size
function indicates the size, in bytes, of foo
. The *_copy
function copies size
bytes from foo[offset..offset+size]
to memory[dst..dst+size]
. This traps if offset+size
is greater than the size of foo
, or if dst+size
exceeds the size of the Wasm memory.
Dually, a System API function that conceptually takes a blob or string as a parameter foo
has two parameters:
ic0.set_foo : (src : i32, size: i32) -> …
which copies, at the time of function invocation, the data referred to by src
/size
out of the canister. Unless otherwise noted, this traps if src+size
exceeds the size of the WebAssembly memory.
Method arguments
The canister can access an argument. For canister_init
, canister_post_upgrade
and method entry points, the argument is the argument of the call; in a reply callback, it refers to the received reply. So the lifetime of the argument data is a single WebAssembly function execution, not the whole method call tree.
ic0.msg_arg_data_size : () → i32
andic0.msg_arg_data_copy : (dst : i32, offset : i32, size : i32) → ()
The message argument data.
ic0.msg_caller_size : () → i32
andic0.msg_caller_copy : (dst : i32, offset: i32, size : i32) → ()
The identity of the caller, which may be a canister id or a user id. During canister installation or upgrade, this is the id of the user or canister requesting the installation or upgrade. During a system task (heartbeat or global timer), this is the id of the management canister.
ic0.msg_reject_code : () → i32
Returns the reject code, if the current function is invoked as a reject callback.
It returns the special "no error" code
0
if the callback is not invoked as a reject callback; this allows canisters to use a single entry point for both the reply and reject callback, if they choose to do so.ic0.msg_reject_msg_size : () → i32
andic0.msg_reject_msg_copy : (dst : i32, offset : i32, size : i32) → ()
The reject message. Traps if there is no reject message (i.e. if
reject_code
is0
).
Responding
Eventually, the canister will want to respond to the original call, either by replying (indicating success) or rejecting (signalling an error):
ic0.msg_reply_data_append : (src : i32, size : i32) → ()
Appends data it to the (initially empty) data reply. Traps if the total appended data exceeds the maximum response size.
This traps if the current call already has been or does not need to be responded to.
Any data assembled, but not replied using
ic0.msg_reply
, gets discarded at the end of the current message execution. In particular, the reply buffer gets reset when the canister yields control without callingic0.msg_reply
.
This can be invoked multiple times within the same message execution to build up the argument with data from various places on the Wasm heap. This way, the canister does not have to first copy all the pieces from various places into one location.
ic0.msg_reply : () → ()
Replies to the sender with the data assembled using
ic0.msg_reply_data_append
.This function can be called at most once (a second call will trap), and must be called exactly once to indicate success.
See Cycles for how this interacts with cycles available on this call.
ic0.msg_reject : (src : i32, size : i32) → ()
Rejects the call. The data referred to by
src
/size
is used for the diagnostic message.This system call traps if
src+size
exceeds the size of the WebAssembly memory, or if the current call already has been or does not need to be responded to, or if the data referred to bysrc
/size
is not valid UTF8.The other end will receive this reject with reject code
CANISTER_REJECT
, see Reject codes.Possible reply data assembled using
ic0.msg_reply_data_append
is discarded.See Cycles for how this interacts with cycles available on this call.
Ingress message inspection
A canister can inspect ingress messages before executing them. When the BIG receives an update call from a user, the BIG will use the canister method canister_inspect_message
to determine whether the message shall be accepted. If the canister is empty (i.e. does not have a Wasm module), then the ingress message will be rejected. If the canister is not empty and does not implement canister_inspect_message
, then the ingress message will be accepted.
In canister_inspect_message
, the canister can accept the message by invoking ic0.accept_message : () → ()
. This function traps if invoked twice. If the canister traps in canister_inspect_message
or does not call ic0.accept_message
, then the access is denied.
The canister_inspect_message
is executed by a single node and thus its outcome depends on the state of this node.
In particular, the canister_inspect_message
might be executed on a state that does not reflect the changes
made by a previously successfully completed update call if the canister_inspect_message
is executed by a node
that is not up-to-date in terms of its state.
The canister_inspect_message
is not invoked for query calls, inter-canister calls or calls to the management canister.
Self-identification
A canister can learn about its own identity:
ic0.canister_self_size : () → i32
andic0.canister_self_copy: (dst : i32, offset : i32, size : i32) → ()
These functions allow the canister to query its own canister id (as a blob).
Cube status
This function allows a canister to find out if it is running, stopping or stopped (see BIG method and BIG method for context).
ic0.canister_status : () → i32
returns the current status of the canister:
Status
1
indicates running,2
indicates stopping, and3
indicates stopped.Status
3
(stopped) can be observed, for example, incanister_pre_upgrade
and can be used to prevent accidentally upgrading a canister that is not fully stopped.
Cube version
For each canister, the system maintains a canister version. Upon canister creation, it is set to 0, and it is guaranteed to be incremented upon every change of the canister's code, settings, running status (Running, Stopping, Stopped), and memory (WASM and stable memory), i.e., upon every successful management canister call of methods update_settings
, install_code
, install_chunked_code
, uninstall_code
, start_canister
, and stop_canister
on that canister, code uninstallation due to that canister running out of cycles, canister's running status transitioning from Stopping to Stopped, and successful execution of update methods, response callbacks, heartbeats, and global timers. The system can arbitrarily increment the canister version also if the canister's code, settings, running status, and memory do not change.
ic0.canister_version : () → i64
returns the current canister version.
During the canister upgrade process, canister_pre_upgrade
sees the old counter value, and canister_post_upgrade
sees the new counter value.
Inter-canister method calls
When handling an update call (or a callback), a canister can do further calls to another canister. Calls are assembled in a builder-like fashion, starting with ic0.call_new
, adding more attributes using the ic0.call_*
functions, and eventually performing the call with ic0.call_perform
.
ic0.call_new : ( callee_src : i32, callee_size : i32, name_src : i32, name_size : i32, reply_fun : i32, reply_env : i32, reject_fun : i32, reject_env : i32, ) → ()
Begins assembling a call to the canister specified by callee_src/_size
at method name_src/_size
.
The BIG records two mandatory callback functions, represented by a table entry index *_fun
and some additional value *_env
. When the response comes back, the table is read at the corresponding index, expected to be a function of type (env : i32) -> ()
, and passed the corresponding *_env
value.
The reply callback is executed upon successful completion of the method call, which can query the reply using ic0.msg_arg_data_*
.
The reject callback is executed if the method call fails asynchronously or the other canister explicitly rejects the call. The reject code and message can be queried using ic0.msg_reject_code
and ic0.msg_reject_msg_*
.
This deducts MAX_CYCLES_PER_RESPONSE
cycles from the canister balance and sets them aside for response processing. This will trap if not sufficient cycles are available.
Subsequent calls to the following functions set further attributes of that call, until the call is concluded (with ic0.call_perform
) or discarded (by returning without calling ic0.call_perform
or by starting a new call with ic0.call_new
.)
ic0.call_on_cleanup : (fun : i32, env : i32) → ()
If a cleanup callback (of type (env : i32) -> ()
) is specified for this call, it is executed if and only if the reply
or the reject
callback was executed and trapped (for any reason).
During the execution of the cleanup
function, only a subset of the System API is available (namely ic0.debug_print
, ic0.trap
and the ic0.stable_*
functions). The cleanup function is expected to run swiftly (within a fixed, yet to be specified cycle limit) and serves to free resources associated with the callback.
If this traps (e.g. runs out of cycles), the state changes from the cleanup
function are discarded, as usual, and no further actions are taken related to that call. Canisters likely want to avoid this from happening.
There must be at most one call to ic0.call_on_cleanup
between ic0.call_new
and ic0.call_perform
.
ic0.call_data_append : (src : i32, size : i32) -> ()
Appends the specified bytes to the argument of the call. Initially, the argument is empty. Traps if the total appended data exceeds the maximum inter-canister call payload.
This may be called multiple times between
ic0.call_new
andic0.call_perform
.ic0.call_cycles_add : (amount : i64) -> ()
This adds cycles onto a call. See Cycles.
This may be called multiple times between
ic0.call_new
andic0.call_perform
.ic0.call_cycles_add128 : (amount_high : i64, amount_low : i64) -> ()
This adds cycles onto a call. See Cycles.
This may be called multiple times between
ic0.call_new
andic0.call_perform
.ic0.call_perform : () -> ( err_code : i32 )
This concludes assembling the call. It queues the call message to the given destination, but does not actually act on it until the current WebAssembly function returns without trapping.
If the function returns
0
as theerr_code
, the BIG was able to enqueue the call. In this case, the call will either be delivered, returned because the destination canister does not exist or returned because of an out of cycles condition. This also means that exactly one of the reply or reject callbacks will be executed.If the function returns a non-zero value, the call cannot (and will not be) performed. This can happen due to a lack of resources within the BIG, but also if it would reduce the current cycle balance to a level below where the canister would be frozen.
After
ic0.call_perform
and before the next call toic0.call_new
, all otheric0.call_*
function calls trap.
Cycles
Each canister maintains a balance of cycles, which are used to pay for platform usage. Cycles are represented by 128-bit values.
This specification currently does not go into details about which actions cost how many cycles and/or when. In general, you must assume that the canister's cycle balance can change arbitrarily between method executions, and during each System API function call, unless explicitly mentioned otherwise.
ic0.canister_cycle_balance : () → i64
Indicates the current cycle balance of the canister. It is the canister balance before the execution of the current message, minus a reserve to pay for the execution of the current message, minus any cycles queued up to be sent via
ic0.call_cycles_add
. After execution of the message, the BIG may add unused cycles from the reserve back to the balance.
This call traps if the current balance does not fit into a 64-bit value. Canisters that need to deal with larger cycles balances should use ic0.canister_cycles_balance128
instead.
ic0.canister_cycle_balance128 : (dst : i32) → ()
Indicates the current cycle balance of the canister by copying the value at the location
dst
in the canister memory. It is the canister balance before the execution of the current message, minus a reserve to pay for the execution of the current message, minus any cycles queued up to be sent viaic0.call_cycles_add128
. After execution of the message, the BIG may add unused cycles from the reserve back to the balance.ic0.msg_cycles_available : () → i64
Returns the amount of cycles that were transferred by the caller of the current call, and is still available in this message.
Initially, in the update method entry point, this is the amount that the caller passed to the canister. When cycles are accepted (
ic0.msg_cycles_accept
), this reports fewer cycles accordingly. When the call is responded to (reply or reject), all available cycles are refunded to the caller, and this will return 0.
This call traps if the amount of cycles available does not fit into a 64-bit value. Please use ic0.msg_cycles_available128
instead.
ic0.msg_cycles_available128 : (dst : i32) → ()
Indicates the number of cycles transferred by the caller of the current call, still available in this message. The amount of cycles is represented by a 128-bit value. This call copies this value starting at the location
dst
in the canister memory.Initially, in the update method entry point, this is the amount that the caller passed to the canister. When cycles are accepted (
ic0.msg_cycles_accept128
), this reports fewer cycles accordingly. When the call is responded to (reply or reject), all available cycles are refunded to the caller, and this will report 0 cycles.ic0.msg_cycles_accept : (max_amount : i64) → (amount : i64)
This moves cycles from the call to the canister balance. It moves as many cycles as possible, up to these constraints:
It moves no more cycles than
max_amount
.It moves no more cycles than available according to
ic0.msg_cycles_available
, andIt can be called multiple times, each time possibly adding more cycles to the balance.
The return value indicates how many cycles were actually moved.
This system call does not trap.
Example: To accept all cycles provided in a call, invoke ic0.msg_cycles_accept(ic0.msg_cycles_available())
in the method handler or a callback handler, before calling reply or reject.
ic0.msg_cycles_accept128 : (max_amount_high : i64, max_amount_low : i64, dst : i32) → ()
This moves cycles from the call to the canister balance. It moves as many cycles as possible, up to these constraints:
It moves no more cycles than the amount obtained by combining
max_amount_high
andmax_amount_low
. Cycles are represented by 128-bit values.It moves no more cycles than available according to
ic0.msg_cycles_available128
, andIt can be called multiple times, each time possibly adding more cycles to the balance.
This call also copies the amount of cycles that were actually moved starting at the location
dst
in the canister memory.This does not trap.
ic0.cycles_burn128 : (amount_high : i64, amount_low : i64, dst : i32) -> ()
This burns cycles from the canister. It burns as many cycles as possible, up to these constraints:
It burns no more cycles than the amount obtained by combining
amount_high
andamount_low
. Cycles are represented by 128-bit values.It burns no more cycles than the amount of cycles available for spending
liquid_balance(balance, reserved_balance, freezing_limit)
, wherereserved_balance
are cycles reserved for resource payments andfreezing_limit
is the amount of idle cycles burned by the canister during itsfreezing_threshold
.It can be called multiple times, each time possibly burning more cycles from the balance.
This call also copies the amount of cycles that were actually burned starting at the location
dst
in the canister memory.This system call does not trap.
ic0.call_cycles_add : (amount : i64) → ()
This function moves cycles from the canister balance onto the call under construction, to be transferred with that call.
The cycles are deducted from the balance as shown by
ic0.canister_cycle_balance
immediately, and moved back if the call cannot be performed (e.g. ific0.call_perform
signals an error, or if the canister invokesic0.call_new
or returns without callingic0.call_perform
).This system call traps if trying to transfer more cycles than are in the current balance of the canister.
This system call traps if the cycle balance of the canister after transferring cycles decreases below the canister's freezing limit.
ic0.call_cycles_add128 : (amount_high : i64, amount_low : i64) → ()
This function moves cycles from the canister balance onto the call under construction, to be transferred with that call.
The amount of cycles it moves is represented by a 128-bit value which can be obtained by combining the
amount_high
andamount_low
parameters.The cycles are deducted from the balance as shown by
ic0.canister_cycles_balance128
immediately, and moved back if the call cannot be performed (e.g. ific0.call_perform
signals an error, or if the canister invokesic0.call_new
or returns without callingic0.call_perform
).This traps if trying to transfer more cycles than are in the current balance of the canister.
This system call traps if the cycle balance of the canister after transferring cycles decreases below the canister's freezing limit.
ic0.msg_cycles_refunded : () → i64
This function can only be used in a callback handler (reply or reject), and indicates the amount of cycles that came back with the response as a refund. The refund has already been added to the canister balance automatically.
This call traps if the amount of cycles refunded does not fit into a 64-bit value. In general, it is recommended to use ic0.msg_cycles_refunded128
instead.
ic0.msg_cycles_refunded128 : (dst : i32) → ()
This function can only be used in a callback handler (reply or reject), and indicates the amount of cycles that came back with the response as a refund. The refund has already been added to the canister balance automatically.
Stable memory
Canisters have the ability to store and retrieve data from a secondary memory. The purpose of this stable memory is to provide space to store data beyond upgrades. The interface mirrors roughly the memory-related instructions of WebAssembly, and tries to be forward compatible with exposing this feature as an additional memory.
The stable memory is initially empty and can be grown up to the Wasm stable memory limit (provided the subnet has capacity).
ic0.stable_size : () → (page_count : i32)
returns the current size of the stable memory in WebAssembly pages. (One WebAssembly page is 64KiB)
This system call traps if the size of the stable memory exceeds 232 bytes.
ic0.stable_grow : (new_pages : i32) → (old_page_count : i32)
tries to grow the memory by
new_pages
many pages containing zeroes.This system call traps if the previous size of the memory exceeds 232 bytes.
If the new size of the memory exceeds 232 bytes or growing is unsuccessful, then it returns
-1
.Otherwise, it grows the memory and returns the previous size of the memory in pages.
ic0.stable_write : (offset : i32, src : i32, size : i32) → ()
copies the data referred to by
src
/size
out of the canister and replaces the corresponding segment starting atoffset
in the stable memory.This system call traps if the size of the stable memory exceeds 232 bytes.
It also traps if
src+size
exceeds the size of the WebAssembly memory oroffset+size
exceeds the size of the stable memory.ic0.stable_read : (dst : i32, offset : i32, size : i32) → ()
copies the data referred to by
offset
/size
out of the stable memory and replaces the corresponding bytes starting atdest
in the canister memory.This system call traps if the size of the stable memory exceeds 232 bytes.
It also traps if
dst+size
exceeds the size of the WebAssembly memory oroffset+size
exceeds the size of the stable memoryic0.stable64_size : () → (page_count : i64)
returns the current size of the stable memory in WebAssembly pages. (One WebAssembly page is 64KiB)
ic0.stable64_grow : (new_pages : i64) → (old_page_count : i64)
tries to grow the memory by
new_pages
many pages containing zeroes.If successful, returns the previous size of the memory (in pages). Otherwise, returns
-1
.ic0.stable64_write : (offset : i64, src : i64, size : i64) → ()
Copies the data from location [src, src+size) of the canister memory to location [offset, offset+size) in the stable memory.
This system call traps if
src+size
exceeds the size of the WebAssembly memory oroffset+size
exceeds the size of the stable memory.ic0.stable64_read : (dst : i64, offset : i64, size : i64) → ()
Copies the data from location [offset, offset+size) of the stable memory to the location [dst, dst+size) in the canister memory.
This system call traps if
dst+size
exceeds the size of the WebAssembly memory oroffset+size
exceeds the size of the stable memory.
System time
The canister can query the BIG for the current time.
ic0.time : () -> i64
The time is given as nanoseconds since 1970-01-01. The BIG guarantees that
the time, as observed by the canister, is monotonically increasing, even across canister upgrades.
within an invocation of one entry point, the time is constant.
The times observed by different canisters are unrelated, and calls from one canister to another may appear to travel "backwards in time".
While an implementation will likely try to keep the time returned by ic0.time
close to the real time, this is not formally part of this specification.
Global timer
The canister can set a global timer to make the system schedule a call to the exported canister_global_timer
Wasm method after the specified time. The time must be provided as nanoseconds since 1970-01-01.
ic0.global_timer_set : (timestamp : i64) -> i64
The function returns the previous value of the timer. If no timer is set before invoking the function, then the function returns zero.
Passing zero as an argument to the function deactivates the timer and thus prevents the system from scheduling calls to the canister's canister_global_timer
Wasm method.
Performance counter
The canister can query one of the "performance counters", which is a deterministic monotonically increasing integer approximating the amount of work the canister has done. Developers might use this data to profile and optimize the canister performance.
ic0.performance_counter : (counter_type : i32) -> i64
The argument type
decides which performance counter to return:
0 : current execution instruction counter. The number of WebAssembly instructions the canister has executed since the beginning of the current Message execution.
1 : call context instruction counter.
For replicated message execution, it is the number of WebAssembly instructions the canister has executed within the call context of the current Message execution since Call context creation. The counter monotonically increases across all message executions in the call context until the corresponding call context is removed.
For non-replicated message execution, it is the number of WebAssembly instructions the canister has executed within the corresponding
composite_query_helper
in Query call. The counter monotonically increases across the executions of the composite query method and the composite query callbacks until the correspondingcomposite_query_helper
returns (ignoring WebAssembly instructions executed within any further downstream calls ofcomposite_query_helper
).
In the future, the BIG might expose more performance counters.
Replicated execution check
The canister can check whether it is currently running in replicated or non replicated execution.
ic0.in_replicated_execution : () -> (result: i32)
Returns 1 if the canister is being run in replicated mode and 0 otherwise.
Controller check
The canister can check whether a given principal is one of its controllers.
ic0.is_controller : (src : i32, size: i32) -> (result: i32)
Checks whether the principal identified by src
/size
is one of the controllers of the canister. If yes, then a value of 1 is returned, otherwise a 0 is returned. It can be called multiple times.
This system call traps if src+size
exceeds the size of the WebAssembly memory or the principal identified by src
/size
is not a valid binary encoding of a principal.
Certified data
For each canister, the BIG keeps track of "certified data", a canister-defined blob. For fresh canisters (upon install or reinstall), this blob is the empty blob (""
).
ic0.certified_data_set : (src: i32, size : i32) -> ()
The canister can update the certified data with this call. The passed data must be no larger than 32 bytes. This can be used any number of times.
When executing a query or composite query method via a query call (i.e. in non-replicated mode), the canister can fetch a certificate that authenticates to third parties the value last set via ic0.certified_data_set
. The certificate is not available in composite query method callbacks and in query and composite query methods evaluated on canisters other than the target canister of the query call.
ic0.data_certificate_present : () -> i32
returns
1
if a certificate is present, and0
otherwise.This will return
1
when called from a query or composite query method on the target canister of a query call.This will return
0
for update methods, if a query or composite query method is executed in replicated mode (e.g. when invoked via an update call or inter-canister call), and in composite query method callbacks and in query and composite query methods evaluated on canisters other than the target canister of a query call.ic0.data_certificate_size : () → i32
andic0.data_certificate_copy : (dst: i32, offset: i32, size: i32) → ()
Copies the certificate for the current value of the certified data to the canister.
The certificate is a blob as described in Certification that contains the values at path
/canister/<canister_id>/certified_data
and at path/time
of The system state tree.If this
certificate
includes a subnet delegation, then the id of the current canister will be included in the delegation's canister id range.This traps if
ic0.data_certificate_present()
returns0
.
Debugging aids
In a local canister execution environment, the canister needs a way to emit textual trace messages. On the "real" network, these do not do anything.
ic0.debug_print : (src : i32, size : i32) -> ()
When executing in an environment that supports debugging, this copies out the data specified by
src
andsize
, and logs, prints or stores it in an environment-appropriate way. The copied data may likely be a valid string in UTF8-encoding, but the environment should be prepared to handle binary data (e.g. by printing it in escaped form). The data does typically not include a terminating\0
or\n
.Semantically, this function is always a no-op, and never traps, even if the
src+size
exceeds the size of the memory, or if this function is executed from(start)
. If the environment cannot perform the print, it just skips it.
Similarly, the System API allows the canister to effectively trap, but give some indication about why it trapped:
ic0.trap : (src : i32, size : i32) -> ()
This function always traps.
The environment may copy out the data specified by
src
andsize
, and log, print or store it in an environment-appropriate way, or include it in system-generated reject messages where appropriate. The copied data may likely be a valid string in UTF8-encoding, but the environment should be prepared to handle binary data (e.g. by printing it in escaped form or substituting invalid characters).
Outlook: Using Host References
The BigFile aims to make the most of the WebAssembly platform, and embraces WebAssembly features. With WebAssembly host references, we can make the platform more secure, the interfaces more abstract and more compositional. The above ic0
System API does not yet use WebAssembly host references. Once they become available on our platform, a new version of the System API using host references will be available via the ic
module. The changes will be, at least
The introduction of a
api_nonce
reference, which models the capability to use the System API. It is passed as an argument tocanister_init
,canister_update <name>
etc., and expected as an argument by almost all System API function calls. (The debugging aids remain unconstrained.)The use of references, instead of binary blobs, to address principals (user ids, canister ids), e.g. in
ic0.msg_caller
or inic0.call_new
. Additional functions will be provided to convert between the transparent binary representation of principals and references.Making the builder interface to create calls build calls identified by a reference, rather than having an implicit partial call in the background.
A canister may only use the old or the new interface; the BIG detects which interface the canister intends to use based on the names and types of its function imports and exports.
The BIG management canister
The interfaces above provide the fundamental ability for external users and canisters to contact other canisters. But the BigFile provides additional functionality, such as canister and user management. This functionality is exposed to external users and canisters via the BIG management canister.
The BIG management canister is just a facade; it does not actually exist as a canister (with isolated state, Wasm code, etc.).
The BIG management canister address is aaaaa-aa
(i.e. the empty blob).
It is possible to use the management canister via external requests (a.k.a. ingress messages). The cost of processing that request is charged to the canister that is being managed. Most methods only permit the controllers to call them. Calls to raw_rand
and deposit_cycles
are never accepted as ingress messages.
Interface overview
The interface description below, in Candid syntax, describes the available functionality.
type canister_id = principal;
type wasm_module = blob;
type canister_settings = record {
controllers : opt vec principal;
compute_allocation : opt nat;
memory_allocation : opt nat;
freezing_threshold : opt nat;
reserved_cycles_limit : opt nat;
};
type definite_canister_settings = record {
controllers : vec principal;
compute_allocation : nat;
memory_allocation : nat;
freezing_threshold : nat;
reserved_cycles_limit : nat;
};
type change_origin = variant {
from_user : record {
user_id : principal;
};
from_canister : record {
canister_id : principal;
canister_version : opt nat64;
};
};
type change_details = variant {
creation : record {
controllers : vec principal;
};
code_uninstall;
code_deployment : record {
mode : variant { install; reinstall; upgrade };
module_hash : blob;
};
controllers_change : record {
controllers : vec principal;
};
};
type change = record {
timestamp_nanos : nat64;
canister_version : nat64;
origin : change_origin;
details : change_details;
};
type chunk_hash = blob;
type http_header = record {
name : text;
value : text;
};
type http_request_result = record {
status : nat;
headers : vec http_header;
body : blob;
};
type ecdsa_curve = variant {
secp256k1;
};
type satoshi = nat64;
type bitcoin_network = variant {
mainnet;
testnet;
};
type bitcoin_address = text;
type block_hash = blob;
type outpoint = record {
txid : blob;
vout : nat32;
};
type utxo = record {
outpoint : outpoint;
value : satoshi;
height : nat32;
};
type bitcoin_get_utxos_args = record {
address : bitcoin_address;
network : bitcoin_network;
filter : opt variant {
min_confirmations : nat32;
page : blob;
};
};
type bitcoin_get_utxos_query_args = record {
address : bitcoin_address;
network : bitcoin_network;
filter : opt variant {
min_confirmations : nat32;
page : blob;
};
};
type bitcoin_get_current_fee_percentiles_args = record {
network : bitcoin_network;
};
type bitcoin_get_utxos_result = record {
utxos : vec utxo;
tip_block_hash : block_hash;
tip_height : nat32;
next_page : opt blob;
};
type bitcoin_get_utxos_query_result = record {
utxos : vec utxo;
tip_block_hash : block_hash;
tip_height : nat32;
next_page : opt blob;
};
type bitcoin_get_balance_args = record {
address : bitcoin_address;
network : bitcoin_network;
min_confirmations : opt nat32;
};
type bitcoin_get_balance_query_args = record {
address : bitcoin_address;
network : bitcoin_network;
min_confirmations : opt nat32;
};
type bitcoin_send_transaction_args = record {
transaction : blob;
network : bitcoin_network;
};
type millisatoshi_per_byte = nat64;
type node_metrics = record {
node_id : principal;
num_blocks_total : nat64;
num_block_failures_total : nat64;
};
type create_canister_args = record {
settings : opt canister_settings;
sender_canister_version : opt nat64;
};
type create_canister_result = record {
canister_id : canister_id;
};
type update_settings_args = record {
canister_id : principal;
settings : canister_settings;
sender_canister_version : opt nat64;
};
type upload_chunk_args = record {
canister_id : principal;
chunk : blob;
};
type clear_chunk_store_args = record {
canister_id : canister_id;
};
type stored_chunks_args = record {
canister_id : canister_id;
};
type install_code_args = record {
mode : variant {
install;
reinstall;
upgrade : opt record {
skip_pre_upgrade : opt bool;
};
};
canister_id : canister_id;
wasm_module : wasm_module;
arg : blob;
sender_canister_version : opt nat64;
};
type install_chunked_code_args = record {
mode : variant {
install;
reinstall;
upgrade : opt record {
skip_pre_upgrade : opt bool;
};
};
target_canister : canister_id;
storage_canister : opt canister_id;
chunk_hashes_list : vec chunk_hash;
wasm_module_hash : blob;
arg : blob;
sender_canister_version : opt nat64;
};
type uninstall_code_args = record {
canister_id : canister_id;
sender_canister_version : opt nat64;
};
type start_canister_args = record {
canister_id : canister_id;
};
type stop_canister_args = record {
canister_id : canister_id;
};
type canister_status_args = record {
canister_id : canister_id;
};
type canister_status_result = record {
status : variant { running; stopping; stopped };
settings : definite_canister_settings;
module_hash : opt blob;
memory_size : nat;
cycles : nat;
reserved_cycles : nat;
idle_cycles_burned_per_day : nat;
};
type canister_info_args = record {
canister_id : canister_id;
num_requested_changes : opt nat64;
};
type canister_info_result = record {
total_num_changes : nat64;
recent_changes : vec change;
module_hash : opt blob;
controllers : vec principal;
};
type delete_canister_args = record {
canister_id : canister_id;
};
type deposit_cycles_args = record {
canister_id : canister_id;
};
type http_request_args = record {
url : text;
max_response_bytes : opt nat64;
method : variant { get; head; post };
headers : vec http_header;
body : opt blob;
transform : opt record {
function : func(record { response : http_request_result; context : blob }) -> (http_request_result) query;
context : blob;
};
};
type ecdsa_public_key_args = record {
canister_id : opt canister_id;
derivation_path : vec blob;
key_id : record { curve : ecdsa_curve; name : text };
};
type ecdsa_public_key_result = record {
public_key : blob;
chain_code : blob;
};
type sign_with_ecdsa_args = record {
message_hash : blob;
derivation_path : vec blob;
key_id : record { curve : ecdsa_curve; name : text };
};
type sign_with_ecdsa_result = record {
signature : blob;
};
type node_metrics_history_args = record {
subnet_id : principal;
start_at_timestamp_nanos : nat64;
};
type node_metrics_history_result = vec record {
timestamp_nanos : nat64;
node_metrics : vec node_metrics;
};
type provisional_create_canister_with_cycles_args = record {
amount : opt nat;
settings : opt canister_settings;
specified_id : opt canister_id;
sender_canister_version : opt nat64;
};
type provisional_create_canister_with_cycles_result = record {
canister_id : canister_id;
};
type provisional_top_up_canister_args = record {
canister_id : canister_id;
amount : nat;
};
type raw_rand_result = blob;
type stored_chunks_result = vec chunk_hash;
type upload_chunk_result = chunk_hash;
type bitcoin_get_balance_result = satoshi;
type bitcoin_get_balance_query_result = satoshi;
type bitcoin_get_current_fee_percentiles_result = vec millisatoshi_per_byte;
service ic : {
create_canister : (create_canister_args) -> (create_canister_result);
update_settings : (update_settings_args) -> ();
upload_chunk : (upload_chunk_args) -> (upload_chunk_result);
clear_chunk_store : (clear_chunk_store_args) -> ();
stored_chunks : (stored_chunks_args) -> (stored_chunks_result);
install_code : (install_code_args) -> ();
install_chunked_code : (install_chunked_code_args) -> ();
uninstall_code : (uninstall_code_args) -> ();
start_canister : (start_canister_args) -> ();
stop_canister : (stop_canister_args) -> ();
canister_status : (canister_status_args) -> (canister_status_result);
canister_info : (canister_info_args) -> (canister_info_result);
delete_canister : (delete_canister_args) -> ();
deposit_cycles : (deposit_cycles_args) -> ();
raw_rand : () -> (raw_rand_result);
http_request : (http_request_args) -> (http_request_result);
// Threshold ECDSA signature
ecdsa_public_key : (ecdsa_public_key_args) -> (ecdsa_public_key_result);
sign_with_ecdsa : (sign_with_ecdsa_args) -> (sign_with_ecdsa_result);
// bitcoin interface
bitcoin_get_balance : (bitcoin_get_balance_args) -> (bitcoin_get_balance_result);
bitcoin_get_balance_query : (bitcoin_get_balance_query_args) -> (bitcoin_get_balance_query_result) query;
bitcoin_get_utxos : (bitcoin_get_utxos_args) -> (bitcoin_get_utxos_result);
bitcoin_get_utxos_query : (bitcoin_get_utxos_query_args) -> (bitcoin_get_utxos_query_result) query;
bitcoin_send_transaction : (bitcoin_send_transaction_args) -> ();
bitcoin_get_current_fee_percentiles : (bitcoin_get_current_fee_percentiles_args) -> (bitcoin_get_current_fee_percentiles_result);
// metrics interface
node_metrics_history : (node_metrics_history_args) -> (node_metrics_history_result);
// provisional interfaces for the pre-ledger world
provisional_create_canister_with_cycles : (provisional_create_canister_with_cycles_args) -> (provisional_create_canister_with_cycles_result);
provisional_top_up_canister : (provisional_top_up_canister_args) -> ();
};
The binary encoding of arguments and results are as per Candid specification.
BIG method create_canister
Before deploying a canister, the administrator of the canister first has to register it with the BIG, to get a canister id (with an empty canister behind it), and then separately install the code.
The optional settings
parameter can be used to set the following settings:
controllers
(vec principal
)A list of at most 10 principals. The principals in this list become the controllers of the canister. Note that the caller of the
create_canister
call is not a controller of the canister unless it is a member of thecontrollers
list.Default value: A list containing only the caller of the
create_canister
call.compute_allocation
(nat
)Must be a number between 0 and 100, inclusively. It indicates how much compute power should be guaranteed to this canister, expressed as a percentage of the maximum compute power that a single canister can allocate. If the BIG cannot provide the requested allocation, for example because it is oversubscribed, the call will be rejected.
Default value: 0
memory_allocation
(nat
)Must be a number between 0 and 248 (i.e 256TB), inclusively. It indicates how much memory the canister is allowed to use in total. Any attempt to grow memory usage beyond this allocation will fail. If the BIG cannot provide the requested allocation, for example because it is oversubscribed, the call will be rejected. If set to 0, then memory growth of the canister will be best-effort and subject to the available memory on the BIG.
Default value: 0
freezing_threshold
(nat
)Must be a number between 0 and 264-1, inclusively, and indicates a length of time in seconds.
A canister is considered frozen whenever the BIG estimates that the canister would be depleted of cycles before
freezing_threshold
seconds pass, given the canister's current size and the BIG's current cost for storage.Calls to a frozen canister will be rejected with
SYS_TRANSIENT
reject code. Additionally, a canister cannot perform calls if that would, due the cost of the call and transferred cycles, would push the balance into frozen territory; these calls fail withic0.call_perform
returning a non-zero error code.Default value: 2592000 (approximately 30 days).
reserved_cycles_limit
(nat
)Must be a number between 0 and 2128-1, inclusively, and indicates the upper limit on
reserved_cycles
of the canister.An operation that allocates resources such as compute and memory will fail if the new value of
reserved_cycles
exceeds this limit.Default value: 5_000_000_000_000 (5 trillion cycles).
The optional sender_canister_version
parameter can contain the caller's canister version. If provided, its value must be equal to ic0.canister_version
.
Until code is installed, the canister is Empty
and behaves like a canister that has no public methods.
BIG method update_settings
Only controllers of the canister can update settings. See BIG method for a description of settings.
Not including a setting in the settings
record means not changing that field. The defaults described above are only relevant during canister creation.
The optional sender_canister_version
parameter can contain the caller's canister version. If provided, its value must be equal to ic0.canister_version
.
BIG method upload_chunk
Canisters have associated some storage space (hence forth chunk storage) where they can hold chunks of Wasm modules that are too lage to fit in a single message. This method allows the controllers of a canister (and the canister itself) to upload such chunks. The method returns the hash of the chunk that was stored. The size of each chunk must be at most 1MiB. The maximum number of chunks in the chunk store is CHUNK_STORE_SIZE
chunks. The storage cost of each chunk is fixed and corresponds to storing 1MiB of data.
BIG method clear_chunk_store
Cube controllers (and the canister itself) can clear the entire chunk storage of a canister.
BIG method stored_chunks
Cube controllers (and the canister itself) can list the hashes of chunks in the chunk storage of a canister.
BIG method install_code
This method installs code into a canister.
Only controllers of the canister can install code.
If
mode = variant { install }
, the canister must be empty before. This will instantiate the canister module and invoke itscanister_init
method (if present), as explained in Section "Cube initialization", passing thearg
to the canister.If
mode = variant { reinstall }
, if the canister was not empty, its existing code and state (including stable memory) is removed before proceeding as formode = install
.Note that this is different from
uninstall_code
followed byinstall_code
, asuninstall_code
generates a synthetic reject response to all callers of the uninstalled canister that the uninstalled canister did not yet reply to and ensures that callbacks to outstanding calls made by the uninstalled canister won't be executed (i.e., upon receiving a response from a downstream call made by the uninstalled canister, the cycles attached to the response are refunded, but no callbacks are executed).If
mode = variant { upgrade }
,mode = variant { upgrade = opt record { skip_pre_upgrade = null } }
, ormode = variant { upgrade = opt record { skip_pre_upgrade = opt false} }
, this will perform an upgrade of a non-empty canister as described in Cube upgrades, passingarg
to thecanister_post_upgrade
method of the new instance.If
mode = variant { upgrade = opt record { skip_pre_upgrade = opt true} }
, the system handles this method similarly to themode = variant { upgrade }
case, except that it does not execute thecanister_pre_upgrade
method on the old instance.
This is atomic: If the response to this request is a reject
, then this call had no effect.
Some canisters may not be able to make sense of callbacks after upgrades; these should be stopped first, to wait for all outstanding callbacks, or be uninstalled first, to prevent outstanding callbacks from being invoked (by marking the corresponding call contexts as deleted). It is expected that the canister admin (or their tooling) does that separately.
The wasm_module
field specifies the canister module to be installed. The system supports multiple encodings of the wasm_module
field, as described in Cube module format:
If the
wasm_module
starts with byte sequence[0x00, 'a', 's', 'm']
, the system parseswasm_module
as a raw WebAssembly binary.If the
wasm_module
starts with byte sequence[0x1f, 0x8b, 0x08]
, the system parseswasm_module
as a gzip-compressed WebAssembly binary.
The optional sender_canister_version
parameter can contain the caller's canister version. If provided, its value must be equal to ic0.canister_version
.
This method traps if the canister's cycle balance decreases below the canister's freezing limit after executing the method.
BIG method install_chunked_code
This method installs code that had previously been uploaded in chunks.
Only controllers of the target canister can call this method.
The mode
, arg
, and sender_canister_version
parameters are as for install_code
.
The target_canister
specifies the canister where the code should be installed.
The optional storage_canister
specifies the canister in whose chunk storage the chunks are stored (this parameter defaults to target_canister
if not specified).
For the call to succeed, the caller must be a controller of the storage_canister
or the caller must be the storage_canister
. The storage_canister
must be on the same subnet as the target canister.
The chunk_hashes_list
specifies a list of hash values [h1,...,hk]
with k <= MAX_CHUNKS_IN_LARGE_WASM
. The system looks up in the chunk store of storage_canister
(or that of the target canister if storage_canister
is not specified) blobs corresponding to h1,...,hk
and concatenates them to obtain a blob of bytes referred to as wasm_module
in install_code
. It then checks that the SHA-256 hash of wasm_module
is equal to the wasm_module_hash
parameter and calls install_code
with parameters (record {mode; target_canister; wasm_module; arg; sender_canister_version})
.
BIG method uninstall_code
This method removes a canister's code and state, making the canister empty again.
Only controllers of the canister can uninstall code.
Uninstalling a canister's code will reject all calls that the canister has not yet responded to, and drop the canister's code and state. Outstanding responses to the canister will not be processed, even if they arrive after code has been installed again. Cycles attached to such responses will still be refunded though.
The cube is now empty. In particular, any incoming or queued calls will be rejected.
A cube after uninstalling retains its cycle balances, controllers, history, status, and allocations.
The optional sender_cube_version
parameter can contain the caller's cube version. If provided, its value must be equal to ic0.cube_version
.
BIG method cube_status
Indicates various information about the cube. It contains:
The status of the cube. It could be one of
running
,stopping
orstopped
.The "settings" of the cube containing:
The controllers of the cube. The order of returned controllers may vary depending on the implementation.
The compute and memory allocation of the cube.
The freezing threshold of the cube in seconds.
The reserved cycles limit of the cube, i.e., the maximum number of cycles that can be in the cube's reserved balance after increasing the cube's memory allocation and/or actual memory usage.
A SHA256 hash of the module installed on the cube. This is
null
if the cube is empty.The actual memory usage of the cube.
The cycle balance of the cube.
The reserved cycles balance of the cube, i.e., the number of cycles reserved when increasing the cube's memory allocation and/or actual memory usage.
The idle cycle consumption of the cube, i.e., the number of cycles burned by the cube per day due to its compute and memory allocation and actual memory usage.
Only the controllers of the cube or the cube itself can request its status.
BIG method canister_info
Provides the history of the canister, its current module SHA-256 hash, and its current controllers. Every canister can call this method on every other canister (including itself). Users cannot call this method.
The canister history consists of a list of canister changes (canister creation, code uninstallation, code deployment, or controllers change). Every canister change consists of the system timestamp at which the change was performed, the canister version after performing the change, the change's origin (a user or a canister), and its details. The change origin includes the principal (called originator in the following) that initiated the change and, if the originator is a canister, the originator's canister version when the originator initiated the change (if available). Code deployments are described by their mode (code install, code reinstall, code upgrade) and the SHA-256 hash of the newly deployed canister module. Cube creations and controllers changes are described by the full new set of the canister controllers after the change. The order of controllers stored in the canister history may vary depending on the implementation.
The system can drop the oldest canister changes from the list to keep its length bounded (at least 20
changes are guaranteed to remain in the list). The system also drops all canister changes if the canister runs out of cycles.
The following parameters should be supplied for the call:
canister_id
: the canister ID of the canister to retrieve information about.num_requested_changes
: optional, specifies the number of requested canister changes. If not provided, the default value of0
will be used.
The returned response contains the following fields:
total_num_changes
: the total number of canister changes that have been ever recorded in the history. This value does not change if the system drops the oldest canister changes from the list of changes.recent_changes
: the list containing the most recent canister changes. Ifnum_requested_changes
is provided, then this list contains that number of changes or, if more changes are requested than available in the history, then this list contains all changes available in the history. Ifnum_requested_changes
is not specified, then this list is empty.module_hash
: the SHA-256 hash of the currently installed canister module (ornull
if the canister is empty).controllers
: the current set of canister controllers. The order of returned controllers may vary depending on the implementation.
BIG method stop_canister
The controllers of a canister may stop a canister (e.g., to prepare for a canister upgrade).
Stopping a canister is not an atomic action. The immediate effect is that the status of the canister is changed to stopping
(unless the canister is already stopped). The BIG will reject all calls to a stopping canister, indicating that the canister is stopping. Responses to a stopping canister are processed as usual. When all outstanding responses have been processed (so there are no open call contexts), the canister status is changed to stopped
and the management canister responds to the caller of the stop_canister
request.
BIG method start_canister
A canister may be started by its controllers.
If the canister status was stopped
or stopping
then the canister status is simply set to running
. In the latter case all stop_canister
calls which are processing fail (and are rejected).
If the canister was already running
then the status stays unchanged.
BIG method delete_canister
This method deletes a canister from the BIG.
Only controllers of the canister can delete it and the canister must already be stopped. Deleting a canister cannot be undone, any state stored on the canister is permanently deleted and its cycles are discarded. Once a canister is deleted, its ID cannot be reused.
BIG method deposit_cycles
This method deposits the cycles included in this call into the specified canister.
There is no restriction on who can invoke this method.
BIG method raw_rand
This method takes no input and returns 32 pseudo-random bytes to the caller. The return value is unknown to any part of the BIG at time of the submission of this call. A new return value is generated for each call to this method.
BIG method ecdsa_public_key
This method returns a SEC1 encoded ECDSA public key for the given canister using the given derivation path. If the canister_id
is unspecified, it will default to the canister id of the caller. The derivation_path
is a vector of variable length byte strings. Each byte string may be of arbitrary length, including empty. The total number of byte strings in the derivation_path
must be at most 255. The key_id
is a struct specifying both a curve and a name. The availability of a particular key_id
depends on implementation.
For curve secp256k1
, the public key is derived using a generalization of BIP32 (see ia.cr/2021/1330, Appendix D). To derive (non-hardened) BIP32-compatible public keys, each byte string (blob
) in the derivation_path
must be a 4-byte big-endian encoding of an unsigned integer less than 231. If the derivation_path
contains a byte string that is not a 4-byte big-endian encoding of an unsigned integer less than 231, then a derived public key will be returned, but that key derivation process will not be compatible with the BIP32 standard.
The return result is an extended public key consisting of an ECDSA public_key
, encoded in SEC1 compressed form, and a chain_code
, which can be used to deterministically derive child keys of the public_key
.
BIG method sign_with_ecdsa
This method returns a new ECDSA signature of the given message_hash
that can be separately verified against a derived ECDSA public key. This public key can be obtained by calling ecdsa_public_key
with the caller's canister_id
, and the same derivation_path
and key_id
used here.
The signatures are encoded as the concatenation of the SEC1 encodings of the two values r and s. For curve secp256k1
, this corresponds to 32-byte big-endian encoding.
This call requires that the ECDSA feature is enabled, the caller is a canister, and message_hash
is 32 bytes long. Otherwise it will be rejected.
BIG method http_request
This method makes an HTTP request to a given URL and returns the HTTP response, possibly after a transformation.
The canister should aim to issue idempotent requests, meaning that it must not change the state at the remote server, or the remote server has the means to identify duplicated requests. Otherwise, the risk of failure increases.
The responses for all identical requests must match too. However, a web service could return slightly different responses for identical idempotent requests. For example, it may include some unique identification or a timestamp that would vary across responses.
For this reason, the calling canister can supply a transformation function, which the BIG uses to let the canister sanitize the responses from such unique values. The transformation function is executed separately on the corresponding response received for a request. The final response will only be available to the calling canister.
Currently, the GET
, HEAD
, and POST
methods are supported for HTTP requests.
It is important to note the following for the usage of the POST
method:
The calling canister must make sure that the remote server is able to handle idempotent requests sent from multiple sources. This may require, for example, to set a certain request header to uniquely identify the request.
There are no confidentiality guarantees on the request content. There is no guarantee that all sent requests are as specified by the canister. If the canister receives a response, then at least one request that was sent matched the canister's request, and the response was to that request.
For security reasons, only HTTPS connections are allowed (URLs must start with https://
). The BIG uses industry-standard root CA lists to validate certificates of remote web servers.
The size of an HTTP request from the canister or an HTTP response from the remote HTTP server is the total number of bytes representing the names and values of HTTP headers and the HTTP body. The maximal size for the request from the canister is 2MB
(2,000,000B
). Each request can specify a maximal size for the response from the remote HTTP server. The upper limit on the maximal size for the response is 2MB
(2,000,000B
) and this value also applies if no maximal size value is specified. An error will be returned when the request or response is larger than the maximal size.
The following parameters should be supplied for the call:
url
- the requested URL. The URL must be valid according to RFC-3986 and its length must not exceed8192
. The URL may specify a custom port number.max_response_bytes
- optional, specifies the maximal size of the response in bytes. If provided, the value must not exceed2MB
(2,000,000B
). The call will be charged based on this parameter. If not provided, the maximum of2MB
will be used.method
- currently, only GET, HEAD, and POST are supportedheaders
- list of HTTP request headers and their corresponding valuesbody
- optional, the content of the request's bodytransform
- an optional record that includes a function that transforms raw responses to sanitized responses, and a byte-encoded context that is provided to the function upon invocation, along with the response to be sanitized. If provided, the calling canister itself must export this function.
Cycles to pay for the call must be explicitly transferred with the call, i.e., they are not deducted from the caller's balance implicitly (e.g., as for inter-canister calls).
The returned response (and the response provided to the transform
function, if specified) contains the following fields:
status
- the response status (e.g., 200, 404)headers
- list of HTTP response headers and their corresponding valuesbody
- the response's body
The transform
function may, for example, transform the body in any way, add or remove headers, modify headers, etc. The maximal number of bytes representing the response produced by the transform
function is 2MB
(2,000,000B
). Note that the number of bytes representing the response produced by the transform
function includes the serialization overhead of the encoding produced by the canister.
When the transform function is invoked by the system due to a canister HTTP request, the caller's identity is the principal of the management canister. This information can be used by developers to implement access control mechanism for this function.
The following additional limits apply to HTTP requests and HTTP responses from the remote sever:
the number of headers must not exceed
64
,the number of bytes representing a header name or value must not exceed
8KiB
, andthe total number of bytes representing the header names and values must not exceed
48KiB
.
If the request headers provided by the canister do not contain a user-agent
header (case-insensitive),
then the BIG sends a user-agent
header (case-insensitive) with the value ic/1.0
in addition to the headers provided by the canister. Such an additional header does not contribute
to the above limits on HTTP request headers.
Currently, the BigFile mainnet only supports URLs that resolve to IPv6 destinations (i.e., the domain has a AAAA
DNS record) in HTTP requests.
If you do not specify the max_response_bytes
parameter, the maximum of a 2MB
response will be charged for, which is expensive in terms of cycles. Always set the parameter to a reasonable upper bound of the expected network response size to not incur unnecessary cycles costs for your request.
BIG method node_metrics_history
The node metrics management canister API is considered EXPERIMENTAL. Cube developers must be aware that the API may evolve in a non-backward-compatible way.
Given a subnet ID as input, this method returns a time series of node metrics (field node_metrics
). The timestamps are represented as nanoseconds since 1970-01-01 (field timestamp_nanos
) at which the metrics were sampled. The returned timestamps are all timestamps after (and including) the provided timestamp (field start_at_timestamp_nanos
) for which node metrics are available. The maximum number of returned timestamps is 60 and no two returned timestamps belong to the same UTC day.
Note that a sample will only include metrics for nodes whose metrics changed compared to the previous sample. This means that if a node disappears in one sample and later reappears its metrics will restart from 0 and consumers of this API need to adjust for these resets when aggregating over multiple samples.
A single metric entry is a record with the following fields:
node_id
(principal
): the principal characterizing a node;num_blocks_total
(nat64
): the number of blocks proposed by this node;num_block_failures_total
(nat64
): the number of failed block proposals by this node.
BIG method provisional_create_canister_with_cycles
As a provisional method on development instances, the provisional_create_canister_with_cycles
method is provided. It behaves as create_canister
, but initializes the canister's balance with amount
fresh cycles (using DEFAULT_PROVISIONAL_CYCLES_BALANCE
if amount = null
). If specified_id
is provided, the canister is created under this id. Note that canister creation using create_canister
or provisional_create_canister_with_cycles
with specified_id = null
can fail after calling provisional_create_canister_with_cycles
with provided specified_id
. In that case, canister creation should be retried.
The optional sender_canister_version
parameter can contain the caller's canister version. If provided, its value must be equal to ic0.canister_version
.
Cycles added to this call via ic0.call_cycles_add128
are returned to the caller.
This method is only available in local development instances.
BIG method provisional_top_up_canister
As a provisional method on development instances, the provisional_top_up_canister
method is provided. It adds amount
cycles to the balance of canister identified by amount
.
Cycles added to this call via ic0.call_cycles_add128
are returned to the caller.
Any user can top-up any canister this way.
This method is only available in local development instances.
The BIG Bitcoin API
The Bitcoin functionality is exposed via the management canister. Information about Bitcoin can be found in the Bitcoin developer guides. Invoking the functions of the Bitcoin API will cost cycles. We refer the reader to the Bitcoin documentation for further relevant information and the BIG pricing page for information on pricing for the Bitcoin mainnet and testnet.
BIG method bitcoin_get_utxos
Given a get_utxos_request
, which must specify a Bitcoin address and a Bitcoin network (mainnet
or testnet
), the function returns all unspent transaction outputs (UTXOs) associated with the provided address in the specified Bitcoin network based on the current view of the Bitcoin blockchain available to the Bitcoin component. The UTXOs are returned sorted by block height in descending order.
The following address formats are supported:
Pay to public key hash (P2PKH)
Pay to script hash (P2SH)
Pay to witness public key hash (P2WPKH)
Pay to witness script hash (P2WSH)
Pay to taproot (P2TR)
If the address is malformed, the call is rejected.
The optional filter
parameter can be used to restrict the set of returned UTXOs, either providing a minimum number of confirmations or a page reference when pagination is used for addresses with many UTXOs. In the first case, only UTXOs with at least the provided number of confirmations are returned, i.e., transactions with fewer than this number of confirmations are not considered. In other words, if the number of confirmations is c
, an output is returned if it occurred in a transaction with at least c
confirmations and there is no transaction that spends the same output with at least c
confirmations.
There is an upper bound of 144 on the minimum number of confirmations. If a larger minimum number of confirmations is specified, the call is rejected. Note that this is not a severe restriction as the minimum number of confirmations is typically set to a value around 6 in practice.
It is important to note that the validity of transactions is not verified in the Bitcoin component. The Bitcoin component relies on the proof of work that goes into the blocks and the verification of the blocks in the Bitcoin network. For a newly discovered block, a regular Bitcoin (full) node therefore provides a higher level of security than the Bitcoin component, which implies that it is advisable to set the number of confirmations to a reasonably large value, such as 6, to gain confidence in the correctness of the returned UTXOs.
There is an upper bound of 10,000 UTXOs that can be returned in a single request. For addresses that contain sufficiently many UTXOs, a partial set of the address's UTXOs are returned along with a page reference.
In the second case, a page reference (a series of bytes) must be provided, which instructs the Bitcoin component to collect UTXOs starting from the corresponding "page".
A get_utxos_request
without the optional filter
results in a request that considers the full blockchain, which is equivalent to setting min_confirmations
to 0.
The recommended workflow is to issue a request with the desired number of confirmations. If the next_page
field in the response is not empty, there are more UTXOs than in the returned vector. In that case, the page
field should be set to the next_page
bytes in the subsequent request to obtain the next batch of UTXOs.
BIG method bitcoin_get_utxos_query
This method is identical to bitcoin_get_utxos
, but exposed as a query.
This query is only accessible in non-replicated mode. Calls in replicated mode are rejected.
The response of a query comes from a single replica, and is therefore not appropriate for security-sensitive applications.
BIG method bitcoin_get_balance
Given a get_balance_request
, which must specify a Bitcoin address and a Bitcoin network (mainnet
or testnet
), the function returns the current balance of this address in Satoshi
(10^8 Satoshi = 1 Bitcoin) in the specified Bitcoin network. The same address formats as for bitcoin_get_utxos
are supported.
If the address is malformed, the call is rejected.
The optional min_confirmations
parameter can be used to limit the set of considered UTXOs for the calculation of the balance to those with at least the provided number of confirmations in the same manner as for the bitcoin_get_utxos
call.
Given an address and the optional min_confirmations
parameter, bitcoin_get_balance
iterates over all UTXOs, i.e., the same balance is returned as when calling bitcoin_get_utxos
for the same address and the same number of confirmations and, if necessary, using pagination to get all UTXOs for the same tip hash.
BIG method bitcoin_get_balance_query
This method is identical to bitcoin_get_balance
, but exposed as a query.
This query is only accessible in non-replicated mode. Calls in replicated mode are rejected.
The response of a query comes from a single replica, and is therefore not appropriate for security-sensitive applications.
BIG method bitcoin_send_transaction
Given a send_transaction_request
, which must specify a blob
of a Bitcoin transaction and a Bitcoin network (mainnet
or testnet
), several checks are performed:
The transaction is well formed.
The transaction only consumes unspent outputs with respect to the current (longest) blockchain, i.e., there is no block on the (longest) chain that consumes any of these outputs.
There is a positive transaction fee.
If at least one of these checks fails, the call is rejected.
If the transaction passes these tests, the transaction is forwarded to the specified Bitcoin network. Note that the function does not provide any guarantees that the transaction will make it into the mempool or that the transaction will ever appear in a block.
BIG method bitcoin_get_current_fee_percentiles
The transaction fees in the Bitcoin network change dynamically based on the number of pending transactions. It must be possible for a canister to determine an adequate fee when creating a Bitcoin transaction.
This function returns fee percentiles, measured in millisatoshi/vbyte (1000 millisatoshi = 1 satoshi), over the last 10,000 transactions in the specified network, i.e., over the transactions in the last approximately 4-10 blocks.
The standard nearest-rank estimation method, inclusive, with the addition of a 0th percentile is used. Concretely, for any i from 1 to 100, the ith percentile is the fee with rank ⌈i * 100⌉
. The 0th percentile is defined as the smallest fee (excluding coinbase transactions).
Certification
Some parts of the BIG state are exposed to users in a tamperproof way via certification: the BIG can reveal a partial state tree which includes just the data of interest, together with a signature on the root hash of the state tree. This means that a user can be sure that the response is correct, even if the user happens to be communicating with a malicious node, or has received the certificate via some other untrusted way.
To validate a value using a certificate, the user conceptually
checks the validity of the partial tree using
verify_cert
,looks up the value in the certificate using
lookup
at a given path, which uses the subroutinelookup_path
on the certificate's tree.
This mechanism is used in the read_state
request type, and eventually also for other purposes.
Root of trust
The root of trust is the root public key, which must be known to the user a priori. In a local canister execution environment, the key can be fetched via the /api/v2/status
endpoint.
Certificate
A certificate consists of
a tree
a signature on the tree root hash valid under some public key
an optional delegation that links that public key to root public key.
The BIG will certify states by issuing certificates where the tree is a partial state tree. The state tree can be pruned by replacing subtrees with their root hashes (yielding a new and potentially smaller but still valid certificate) to only include paths pertaining to relevant data but still preserving enough information to recover the tree root hash.
More formally, a certificate is described by the following data structure:
Certificate = {
tree : HashTree
signature : Signature
delegation : NoDelegation | Delegation
}
HashTree
= Empty
| Fork HashTree HashTree
| Labeled Label HashTree
| Leaf blob
| Pruned Hash
Label = Blob
Hash = Blob
Signature = Blob
A certificate is validated with regard to the root of trust by the following algorithm (which uses check_delegation
defined in Delegation):
verify_cert(cert) =
let root_hash = reconstruct(cert.tree)
// see section Delegations below
if check_delegation(cert.delegation) = false then return false
let bls_key = delegation_key(cert.delegation)
verify_bls_signature(bls_key, cert.signature, domain_sep("ic-state-root") · root_hash)
reconstruct(Empty) = H(domain_sep("ic-hashtree-empty"))
reconstruct(Fork t1 t2) = H(domain_sep("ic-hashtree-fork") · reconstruct(t1) · reconstruct(t2))
reconstruct(Labeled l t) = H(domain_sep("ic-hashtree-labeled") · l · reconstruct(t))
reconstruct(Leaf v) = H(domain_sep("ic-hashtree-leaf") · v)
reconstruct(Pruned h) = h
domain_sep(s) = byte(|s|) · s
where H
is the SHA-256 hash function,
verify_bls_signature : PublicKey -> Signature -> Blob -> Bool
is the BLS signature verification function, ciphersuite BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_. See that document also for details on the encoding of BLS public keys and signatures.
All state trees include the time at path /time
(see Time). Users that get a certificate with a state tree can look up the timestamp to guard against working on obsolete data.
Lookup
Given a (verified) tree, the user can fetch the value at a given path, which is a sequence of labels (blobs). In this document, we write paths suggestively with slashes as separators; the actual encoding is not actually using slashes as delimiters.
The following algorithm looks up a path
in a certificate, and returns either
Found v
: the requestedpath
has an associated valuev
in the tree,Absent
: the requested path is not in the tree,Unknown
: it cannot be syntactically determined if the requestedpath
was pruned or not; i.e., there exist at least two trees (one containing the requested path and one not containing the requested path) from which the given tree can be obtained by pruning some subtrees,Error
: the requested path does not have an associated value in the tree, but the requested path is in the tree:
lookup(path, cert) = lookup_path(path, cert.tree)
lookup_path([], Empty) = Absent
lookup_path([], Leaf v) = Found v
lookup_path([], Pruned _) = Unknown
lookup_path([], Labeled _ _) = Error
lookup_path([], Fork _ _) = Error
lookup_path(l::ls, tree) =
match find_label(l, flatten_forks(tree)) with
| Absent -> Absent
| Unknown -> Unknown
| Error -> Error
| Found subtree -> lookup_path ls subtree
flatten_forks(Empty) = []
flatten_forks(Fork t1 t2) = flatten_forks(t1) · flatten_forks(t2)
flatten_forks(t) = [t]
find_label(l, _ · Labeled l1 t · _) | l == l1 = Found t
find_label(l, _ · Labeled l1 _ · Labeled l2 _ · _) | l1 < l < l2 = Absent
find_label(l, Labeled l2 _ · _) | l < l2 = Absent
find_label(l, _ · Labeled l1 _ ) | l1 < l = Absent
find_label(l, [Leaf _]) = Absent
find_label(l, []) = Absent
find_label(l, _) = Unknown
The BIG will only produce well-formed state trees, and the above algorithm assumes well-formed trees. These have the property that labeled subtrees appear in strictly increasing order of labels, and are not mixed with leaves. More formally:
well_formed(tree) =
(tree = Leaf _) ∨ (well_formed_forest(flatten_forks(tree)))
well_formed_forest(trees) =
strictly_increasing([l | Label l _ ∈ trees]) ∧
∀ Label _ t ∈ trees. well_formed(t) ∧
∀ t ∈ trees. t ≠ Leaf _
Delegation
The root key can delegate certification authority to other keys.
A certificate by the root subnet does not have a delegation field. A certificate by other subnets include a delegation, which is itself a certificate that proves that the subnet is listed in the root subnet's state tree (see Subnet information), and reveals its public key.
The certificate included in the delegation (if present) must not itself again contain a delegation.
Delegation =
Delegation {
subnet_id : Principal;
certificate : Certificate;
}
A delegation is verified using the following algorithm:
check_delegation(NoDelegation) = true
check_delegation(Delegation d) = verify_cert(d.certificate) and lookup(["subnet",d.subnet_id,"public_key"],d.certificate) = Found _ and d.certificate.delegation = NoDelegation
The delegation key (a BLS key) is computed by the following algorithm:
delegation_key(NoDelegation) : public_bls_key = root_public_key
delegation_key(Delegation d) : public_bls_key =
match lookup(["subnet",d.subnet_id,"public_key"],d.certificate) with
Found der_key -> extract_der(der_key)
where root_public_key
is the a priori known root key and
extract_der : Blob -> Blob
implements DER decoding of the public key, following RFC5480 using OID 1.3.6.1.4.1.44668.5.3.1.2.1 for the algorithm and 1.3.6.1.4.1.44668.5.3.2.1 for the curve.
Delegations are scoped, i.e., they indicate which set of canister principals the delegatee subnet may certify for. This set can be obtained from a delegation d
using lookup(["subnet",d.subnet_id,"canister_ranges"],d.certificate)
, which must be present, and is encoded as described in Subnet information. The various applications of certificates describe if and how the subnet scope comes into play.
Encoding of certificates
The binary encoding of a certificate is a CBOR (see CBOR) value according to the following CDDL (see CDDL). You can also download the file.
The values in the The system state tree are encoded to blobs as follows:
natural numbers are leb128-encoded.
text values are UTF-8-encoded
blob values are encoded as is
Example
Consider the following tree-shaped data (all single character strings denote labels, all other denote values)
─┬╴ "a" ─┬─ "x" ─╴"hello"
│ └╴ "y" ─╴"world"
├╴ "b" ──╴ "good"
├╴ "c"
└╴ "d" ──╴ "morning"
A possible hash tree for this labeled tree might be, where ┬
denotes a fork. This is not a typical encoding (a fork with Empty
on one side can be avoided), but it is valid.
─┬─┬╴"a" ─┬─┬╴"x" ─╴"hello"
│ │ │ └╴Empty
│ │ └╴ "y" ─╴"world"
│ └╴"b" ──╴"good"
└─┬╴"c" ──╴Empty
└╴"d" ──╴"morning"
This tree has the following CBOR (see CBOR) encoding
8301830183024161830183018302417882034568656c6c6f810083024179820345776f726c6483024162820344676f6f648301830241638100830241648203476d6f726e696e67
and the following root hash
eb5c5b2195e62d996b84c9bcc8259d19a83786a2f59e0878cec84c811f669aa0
Pruning this tree with the following paths
/a/y
/ax
/d
would lead to this tree (with pruned subtree represented by their hash):
─┬─┬╴"a" ─┬─ 1B4FEFF9BEF8131788B0C9DC6DBAD6E81E524249C879E9F10F71CE3749F5A638
│ │ └╴ "y" ─╴"world"
│ └╴"b" ──╴7B32AC0C6BA8CE35AC82C255FC7906F7FC130DAB2A090F80FE12F9C2CAE83BA6
└─┬╴EC8324B8A1F1AC16BD2E806EDBA78006479C9877FED4EB464A25485465AF601D
└╴"d" ──╴"morning"
Note that the "b"
label is included (without content) to prove the absence of the /ax
path.
This tree encodes to CBOR as
83018301830241618301820458201b4feff9bef8131788b0c9dc6dbad6e81e524249c879e9f10f71ce3749f5a63883024179820345776f726c6483024162820458207b32ac0c6ba8ce35ac82c255fc7906f7fc130dab2a090f80fe12f9c2cae83ba6830182045820ec8324b8a1f1ac16bd2e806edba78006479c9877fed4eb464a25485465af601d830241648203476d6f726e696e67
and (obviously) the same root hash.
In the pruned tree, the lookup_path
function behaves as follows:
lookup_path(["a", "a"], pruned_tree) = Unknown
lookup_path(["a", "y"], pruned_tree) = Found "world"
lookup_path(["aa"], pruned_tree) = Absent
lookup_path(["ax"], pruned_tree) = Absent
lookup_path(["b"], pruned_tree) = Unknown
lookup_path(["bb"], pruned_tree) = Unknown
lookup_path(["d"], pruned_tree) = Found "morning"
lookup_path(["e"], pruned_tree) = Absent
The HTTP Gateway protocol
The HTTP Gateway Protocol has been moved into its own specification.
Abstract behavior
The previous sections describe the interfaces, i.e. outer edges of the BigFile, but give only intuitive and vague information in prose about what these interfaces actually do.
The present section aims to address that question with great precision, by describing the abstract state of the whole BigFile, and how this state can change in response to API function calls, or spontaneously (modeling asynchronous, distributed or non-deterministic execution).
The design of this abstract specification (e.g. how and where pending messages are stored) are not to be understood to in any way prescribe a concrete implementation or software architecture. The goals here are formal precision and clarity, but not implementability, so this can lead to different ways of phrasing.
Notation
We specify the behavior of the BigFile using ad-hoc pseudocode.
The manipulated values are primitive values (numbers, text, binary blobs), aggregate values (lists, unordered lists a.k.a. bags, partial maps, records with fixed fields, named constructors) and functions.
We use a concatenation operator ·
with various types: to extend sets and maps, or to concatenate lists with lists or lists with elements.
The shape of values is described using a hand-wavy type system. We use Foo = Nat
to define type aliases; now Foo
can be used instead of Nat
. Often, the right-hand side is a more complex type here, e.g. a record, or multiple possible types separated by a vertical bar (|
). Partial maps are written as Key ↦ Value
and the function type as Argument → Result
.
All values are immutable! State change is specified by describing the new state, not by changing the existing state.
Record fields are accessed using dot-notation (e.g. S.request_id > 0
). To create a new record from an existing record R
with some fields changed, the syntax R where field = new_value
is used. This syntax can also be used to create new records with some deeply nested field changed: R where some_map[key].field = new_value
.
In the state transitions, upper-case variables (S
, C
, Req_id
) are free variables: The state transition may be taken for any possible value of these variables. S
always refers to the previous state. A state transition often comes with a list of conditions, which may restrict the values of these free variables. The state after is usually described using the record update syntax by starting with S where
.
For example, the condition S.messages = Older_messages · M · Younger_messages
says that M
is some message in field messages
of the record S
, and that Younger_messages
and Older_messages
are the other messages in the state. If the "state after" specifies S with messages = Older_messages · Younger_messages
, then the message M
is removed from the state.
Abstract state
In this specification, we describe the BigFile as a state machine. In particular, there is a single piece of data that describes the complete state of the BIG, called S
.
Of course, this is a huge simplification: The real BigFile is distributed and has a multi-component architecture, and the state is spread over many different components, some physically separated. But this simplification allows us to have a concise description of the behavior, and to easily make global decisions (such as, "is there any pending message"), without having to specify the bookkeeping that allows such global decisions.
Identifiers
Principals (canister ids and user ids) are blobs, but some of them have special form, as explained in Special forms of Principals.
type Principal = Blob
The function
mk_self_authenticating_id : PublicKey -> Principal
mk_self_authenticating_id pk = H(pk) · 0x02
calculates self-authenticating ids.
The function
mk_derived_id : Principal -> Blob -> Principal
mk_derived_id p nonce = H(|p| · p · nonce) · 0x03
calculates derived ids. With |p|
we denote the length of the principal, in bytes, encoded as a single byte.
The principal of the anonymous user is fixed:
anonymous_id : Principal
anonymous_id = 0x04
The principal of the management canister is the empty blob (i.e. aaaaa-aa
):
ic_principal : Principal = ""
These function domains and fixed values are mutually disjoint.
Method names can be arbitrary pieces of text:
MethodName = Text
Abstract canisters
The WebAssembly System API is relatively low-level, and some of its details (e.g. that the argument data is queried using separate calls, and that closures are represented by a function pointer and a number, that method names need to be mangled) would clutter this section. Therefore, we abstract over the WebAssembly details as follows:
The state of a WebAssembly module (memory, tables, globals) is hidden behind an abstract
WasmState
. TheWasmState
contains theStableMemory
, which can be extracted usingpre_upgrade
and passed topost_upgrade
.A canister module
CanisterModule
consists of an initial state, and a (pure) function that models function invocation. It either indicates that the canister function traps, or returns a new state together with a description of the invoked asynchronous System API calls.WasmState = (abstract)
StableMemory = (abstract)
Callback = (abstract)
ChunkStore = Hash -> Blob
Arg = Blob;
CallerId = Principal;
Timestamp = Nat;
CanisterVersion = Nat;
Env = {
time : Timestamp;
controllers : List Principal;
global_timer : Nat;
balance : Nat;
reserved_balance : Nat;
reserved_balance_limit : Nat;
compute_allocation : Nat;
memory_allocation : Nat;
memory_usage_raw_module : Nat;
memory_usage_canister_history : Nat;
freezing_threshold : Nat;
subnet_size : Nat;
certificate : NoCertificate | Blob;
status : Running | Stopping | Stopped;
canister_version : CanisterVersion;
}
RejectCode = Nat
Response = Reply Blob | Reject (RejectCode, Text)
MethodCall = {
callee : CanisterId;
method_name: MethodName;
arg: Blob;
transferred_cycles: Nat;
callback: Callback;
}
UpdateFunc = WasmState -> Trap { cycles_used : Nat; } | Return {
new_state : WasmState;
new_calls : List MethodCall;
new_certified_data : NoCertifiedData | Blob;
new_global_timer : NoGlobalTimer | Nat;
response : NoResponse | Response;
cycles_accepted : Nat;
cycles_used : Nat;
}
QueryFunc = WasmState -> Trap { cycles_used : Nat; } | Return {
response : Response;
cycles_used : Nat;
}
CompositeQueryFunc = WasmState -> Trap { cycles_used : Nat; } | Return {
new_state : WasmState;
new_calls : List MethodCall;
response : NoResponse | Response;
cycles_used : Nat;
}
SystemTaskFunc = WasmState -> Trap { cycles_used : Nat; } | Return {
new_state : WasmState;
new_calls : List MethodCall;
new_certified_data : NoCertifiedData | Blob;
new_global_timer : NoGlobalTimer | Nat;
cycles_used : Nat;
}
AvailableCycles = Nat
RefundedCycles = Nat
CanisterModule = {
init : (CanisterId, Arg, CallerId, Env) -> Trap { cycles_used : Nat; } | Return {
new_state : WasmState;
new_certified_data : NoCertifiedData | Blob;
new_global_timer : NoGlobalTimer | Nat;
cycles_used : Nat;
}
pre_upgrade : (WasmState, Principal, Env) -> Trap { cycles_used : Nat; } | Return {
stable_memory : StableMemory;
new_certified_data : NoCertifiedData | Blob;
cycles_used : Nat;
}
post_upgrade : (CanisterId, StableMemory, Arg, CallerId, Env) -> Trap { cycles_used : Nat; } | Return {
new_state : WasmState;
new_certified_data : NoCertifiedData | Blob;
new_global_timer : NoGlobalTimer | Nat;
cycles_used : Nat;
}
update_methods : MethodName ↦ ((Arg, CallerId, Env, AvailableCycles) -> UpdateFunc)
query_methods : MethodName ↦ ((Arg, CallerId, Env) -> QueryFunc)
composite_query_methods : MethodName ↦ ((Arg, CallerId, Env) -> CompositeQueryFunc)
heartbeat : (Env) -> SystemTaskFunc
global_timer : (Env) -> SystemTaskFunc
callbacks : (Callback, Response, RefundedCycles, Env, AvailableCycles) -> UpdateFunc
composite_callbacks : (Callback, Response, Env) -> UpdateFunc
inspect_message : (MethodName, WasmState, Arg, CallerId, Env) -> Trap | Return {
status : Accept | Reject;
}
}
This high-level interface presents a pure, mathematical model of a canister, and hides the bookkeeping required to provide the System API as seen in Section Cube interface (System API).
The CanisterId
parameter of init
and post_upgrade
is merely passed through to the canister, via the canister.self
system call.
The Env
parameter provides synchronous read-only access to portions of the system state and canister metadata that are always available.
The parsing of a blob to a canister module and its public and private custom sections is modelled via the (possibly implicitly failing) functions
parse_wasm_mod : Blob -> CanisterModule
parse_public_custom_sections : Blob -> Text ↦ Blob
parse_private_custom_sections : Blob -> Text ↦ Blob
The concrete mapping of this abstract CanisterModule
to actual WebAssembly concepts and the System API is described separately in section Abstract Canisters to System API.
Call contexts
The BigFile provides certain messaging guarantees: If a user or a canister calls another canister, it will eventually get a single response (a reply or a rejection), even if some canister code along the way fails.
To ensure that only one response is generated, and also to detect when no response can be generated any more, the BIG maintains a call context. The needs_to_respond
field is set to false
once the call has received a response. Further attempts to respond will now fail.
Request = {
nonce : Blob;
ingress_expiry : Nat;
sender : UserId;
canister_id : CanisterId;
method_name : Text;
arg : Blob;
}
CallId = (abstract)
CallOrigin
= FromUser {
request : Request;
}
| FromCanister {
calling_context : CallId;
callback: Callback;
}
| FromSystemTask
CallCtxt = {
canister : CanisterId;
origin : CallOrigin;
needs_to_respond : bool;
deleted : bool;
available_cycles : Nat;
}
Calls and Messages
Calls into and within the BIG are implemented as messages passed between canisters. During their lifetime, messages change shape: they begin as a call to a public method, which is resolved to a WebAssembly function that is then executed, potentially generating a response which is then delivered.
Therefore, a message can have different shapes:
Queue = Unordered | Queue { from : System | CanisterId; to : CanisterId }
EntryPoint
= PublicMethod MethodName Principal Blob
| Callback Callback Response RefundedCycles
| Heartbeat
| GlobalTimer
Message
= CallMessage {
origin : CallOrigin;
caller : Principal;
callee : CanisterId;
method_name : Text;
arg : Blob;
transferred_cycles : Nat;
queue : Queue;
}
| FuncMessage {
call_context : CallId;
receiver : CanisterId;
entry_point : EntryPoint;
queue : Queue;
}
| ResponseMessage {
origin : CallOrigin;
response : Response;
refunded_cycles : Nat;
}
The queue
field is used to describe the message ordering behavior. Its concrete value is only used to determine when the relative order of two messages must be preserved, and is otherwise not interpreted. Response messages are not ordered, as explained above, so they have no queue
field.
A reference implementation would likely maintain a separate list of messages
for each such queue to efficiently find eligible messages; this document uses a single global list for a simpler and more concise system state.
API requests
We distinguish between the asynchronous API requests (type Request
) passed to /api/v2/…/call
, which may be present in the BIG state, and the synchronous API requests passed to /api/v2/…/read_state
and /api/v2/…/query
, which are only ephemeral.
These are the synchronous read messages:
Path = List(Blob)
APIReadRequest
= StateRead = {
nonce : Blob;
ingress_expiry : Nat;
sender : UserId;
paths : List(Path);
}
| CanisterQuery = {
nonce : Blob;
ingress_expiry : Nat;
sender : UserId;
canister_id : CanisterId;
method_name : Text;
arg : Blob;
}
Signed delegations contain the (unsigned) delegation data in a nested record, next to the signature of that data.
PublicKey = Blob
Signature = Blob
SignedDelegation = {
delegation : {
pubkey : PublicKey;
targets : [CanisterId] | Unrestricted;
expiration : Timestamp
};
signature : Signature
}
For the signatures in a Request
, we assume that the following function implements signature verification as described in Authentication. This function picks the corresponding signature scheme according to the DER-encoded metadata in the public key.
verify_signature : PublicKey -> Signature -> Blob -> Bool
Envelope = {
content : Request | APIReadRequest;
sender_pubkey : PublicKey | NoPublicKey;
sender_sig : Signature | NoSignature;
sender_delegation: [SignedDelegation]
}
The evolution of a Request
goes through these states, as explained in Overview of canister calling:
RequestStatus
= Received
| Processing
| Rejected (RejectCode, Text)
| Replied Blob
| Done
A Path
may refer to a request by way of a request id, as specified in Request ids:
RequestId = { b ∈ Blob | |b| = 32 }
hash_of_map: Request -> RequestId
The system state
Finally, we can describe the state of the BIG as a record having the following fields:
CanState
= EmptyCanister | {
wasm_state : WasmState;
module : CanisterModule;
raw_module : Blob;
public_custom_sections: Text ↦ Blob;
private_custom_sections: Text ↦ Blob;
}
CanStatus
= Running
| Stopping (List (CallOrigin, Nat))
| Stopped
ChangeOrigin
= FromUser {
user_id : PrincipalId;
}
| FromCanister {
canister_id : PrincipalId;
canister_version : CanisterVersion | NoCanisterVersion;
}
CodeDeploymentMode
= Install
| Reinstall
| Upgrade
ChangeDetails
= Creation {
controllers : [PrincipalId];
}
| CodeUninstall
| CodeDeployment {
mode : CodeDeploymentMode;
module_hash : Blob;
}
| ControllersChange {
controllers : [PrincipalId];
}
Change = {
timestamp_nanos : Timestamp;
canister_version : CanisterVersion;
origin : ChangeOrigin;
details : ChangeDetails;
}
CanisterHistory = {
total_num_changes : Nat;
recent_changes : [Change];
}
Subnet = {
subnet_id : Principal;
subnet_size : Nat;
}
S = {
requests : Request ↦ (RequestStatus, Principal);
canisters : CanisterId ↦ CanState;
controllers : CanisterId ↦ Set Principal;
compute_allocation : CanisterId ↦ Nat;
memory_allocation : CanisterId ↦ Nat;
freezing_threshold : CanisterId ↦ Nat;
canister_status: CanisterId ↦ CanStatus;
canister_version: CanisterId ↦ CanisterVersion;
canister_subnet : CanisterId ↦ Subnet;
time : CanisterId ↦ Timestamp;
global_timer : CanisterId ↦ Timestamp;
balances: CanisterId ↦ Nat;
reserved_balances: CanisterId ↦ Nat;
reserved_balance_limits: CanisterId ↦ Nat;
certified_data: CanisterId ↦ Blob;
canister_history: CanisterId ↦ CanisterHistory;
system_time : Timestamp
call_contexts : CallId ↦ CallCtxt;
messages : List Message; // ordered!
root_key : PublicKey
}
To convert CanStatus
into status : Running | Stopping | Stopped
from Env
, we define the following conversion function:
simple_status(Running) = Running
simple_status(Stopping _) = Stopping
simple_status(Stopped) = Stopped
To convert CallOrigin
into ChangeOrigin
, we define the following conversion function:
change_origin(principal, _, FromUser { … }) = FromUser {
user_id = principal
}
change_origin(principal, sender_canister_version, FromCanister { … }) = FromCanister {
canister_id = principal
canister_version = sender_canister_version
}
change_origin(principal, sender_canister_version, FromSystemTask) = FromCanister {
canister_id = principal
canister_version = sender_canister_version
}
Cycle bookkeeping and resource consumption
The main cycle balance of canister A
in state S
can be obtained with S.balances(A)
.
In addition to the main balance, each canister has a reserved balance S.reserved_balances(A)
.
The reserved balance contains cycles that were set aside from the main balance for future payments for the consumption of resources such as compute and memory.
The reserved cycles can only be used for resource payments and cannot be transferred back to the main balance.
The (unspecified) function idle_cycles_burned_rate(compute_allocation, memory_allocation, memory_usage, subnet_size)
determines the idle resource consumption rate in cycles per day of a canister given its current compute and memory allocation, memory usage, and subnet size. The function freezing_limit(compute_allocation, memory_allocation, freezing_threshold, memory_usage, subnet_size)
determines the freezing limit in cycles of a canister given its current compute and memory allocation, freezing threshold in seconds, memory usage, and subnet size. The value freezing_limit(compute_allocation, memory_allocation, freezing_threshold, memory_usage, subnet_size)
is derived from idle_cycles_burned_rate(compute_allocation, memory_allocation, memory_usage, subnet_size)
and freezing_threshold
as follows:
freezing_limit(compute_allocation, memory_allocation, freezing_threshold, memory_usage, subnet_size) = idle_cycles_burned_rate(compute_allocation, memory_allocation, memory_usage, subnet_size) * freezing_threshold / (24 * 60 * 60)
The (unspecified) functions memory_usage_wasm_state(wasm_state)
, memory_usage_raw_module(raw_module)
, and memory_usage_canister_history(canister_history)
determine the canister's memory usage in bytes consumed by its Wasm state, raw Wasm binary, and canister history, respectively.
The amount of cycles that is available for spending in calls and execution is computed by the function liquid_balance(balance, reserved_balance, freezing_limit)
:
liquid_balance(balance, reserved_balance, freezing_limit) = balance - max(freezing_limit - reserved_balance, 0)
The reasoning behind this is that resource payments first drain the reserved balance and only when the reserved balance gets to zero, they start draining the main balance.
The amount of cycles that need to be reserved after operations that allocate resources is modeled with an unspecified function cycles_to_reserve(S, CanisterId, compute_allocation, memory_allocation, CanState)
that depends on the old BIG state, the id of the canister, the new allocations of the canister, and the new state of the canister.
Initial state
The initial state of the BIG is
{
requests = ();
canisters = ();
controllers = ();
compute_allocation = ();
memory_allocation = ();
freezing_threshold = ();
canister_status = ();
canister_version = ();
canister_subnet = ();
time = ();
global_timer = ();
balances = ();
reserved_balances = ();
reserved_balance_limits = ();
certified_data = ();
canister_history = ();
system_time = T;
call_contexts = ();
messages = [];
root_key = PublicKey;
}
for some time stamp T
, some DER-encoded BLS public key PublicKey
, and using ()
to denote the empty map or bag.
Invariants
The following is an incomplete list of invariants that should hold for the abstract state S
, and are not already covered by the type annotations in this section.
No pair of update, query, and composite query methods in a CanisterModule can have the same name:
∀ (_ ↦ CanState) ∈ S.canisters:
dom(CanState.module.update_methods) ∩ dom(CanState.module.query_methods) = ∅
dom(CanState.module.update_methods) ∩ dom(CanState.module.composite_query_methods) = ∅
dom(CanState.module.query_methods) ∩ dom(CanState.module.composite_query_methods) = ∅Deleted call contexts were not awaiting a response:
∀ (_ ↦ Ctxt) ∈ S.call_contexts:
if Ctxt.deleted then Ctxt.needs_to_respond = falseResponded call contexts have no available_cycles left:
∀ (_ ↦ Ctxt) ∈ S.call_contexts:
if Ctxt.needs_to_respond = false then Ctxt.available_cycles = 0A stopped canister does not have any call contexts (in particular, a stopped canister does not have any call contexts marked as deleted):
∀ (_ ↦ Ctxt) ∈ S.call_contexts:
S.canister_status[Ctxt.canister] ≠ StoppedReferenced call contexts exist:
∀ CallMessage {origin = FromCanister O, …} ∈ S.messages. O.calling_context ∈ dom(S.call_contexts)
∀ ResponseMessage {origin = FromCanister O, …} ∈ S.messages. O.calling_context ∈ dom(S.call_contexts)
∀ (_ ↦ {needs_to_respond = true, origin = FromCanister O, …}) ∈ S.call_contexts: O.calling_context ∈ dom(S.call_contexts)
∀ (_ ↦ Stopping Origins) ∈ S.canister_status: ∀(FromCanister O, _) ∈ Origins. O.calling_context ∈ dom(S.call_contexts)
State transitions
Based on this abstract notion of the state, we can describe the behavior of the BIG. There are three classes of behaviors:
Asynchronous API requests that are submitted via
/api/v2/…/call
. These transitions describe checks that the request must pass to be considered received.Spontaneous transitions that model the internal behavior of the BIG, by describing conditions on the state that allow the transition to happen, and the state after.
Responses to reads (i.e.
/api/v2/…/read_state
and/api/v2/…/query
). By definition, these do not change the state of the BIG, and merely describe the response based on the read request (or query, respectively) and the current state.
The state transitions are not complete with regard to error handling. For example, the behavior of sending a request to a non-existent canister is not specified here. For now, we trust implementors to make sensible decisions there.
We model the The BIG management canister with one state transition per method. There, we assume a function
candid : Value -> Blob
that represents Candid encoding; this is implicitly taking the method types, as declared in Interface overview, into account. We model the parsing of Candid values in the "Conditions" section using candid
as well, by treating it as a non-deterministic function.
Envelope Authentication
The following predicate describes when an envelope E
correctly signs the enclosed request with a key belonging to a user U
, at time T
: It returns which canister ids this envelope may be used at (as a set of principals).
verify_envelope({ content = C }, U, T)
= { p : p is CanisterID } if U = anonymous_id
verify_envelope({ content = C, sender_pubkey = PK, sender_sig = Sig, sender_delegation = DS}, U, T)
= TS if U = mk_self_authenticating_id E.sender_pubkey
∧ (PK', TS) = verify_delegations(DS, PK, T, { p : p is CanisterId })
∧ verify_signature PK' Sig ("\x0Aic-request" · hash_of_map(C))
verify_delegations([], PK, T, TS) = (PK, TS)
verify_delegations([D] · DS, PK, T, TS)
= verify_delegations(DS, D.pubkey, T, TS ∩ delegation_targets(D))
if verify_signature PK D.signature ("\x1Aic-request-auth-delegation" · hash_of_map(D.delegation))
∧ D.delegation.expiration ≥ T
delegation_targets(D)
= if D.targets = Unrestricted
then { p : p is CanisterId }
else D.targets
Effective canister ids
A Request
has an effective canister id according to the rules in Effective canister id:
is_effective_canister_id(Request {canister_id = ic_principal, method = provisional_create_canister_with_cycles, …}, p)
is_effective_canister_id(Request {canister_id = ic_principal, arg = candid({canister_id = p, …}), …}, p)
is_effective_canister_id(Request {canister_id = p, …}, p), if p ≠ ic_principal
API Request submission
After a node accepts a request via /api/v2/canister/<ECID>/call
, the request gets added to the BIG state as Received
.
This may only happen if the signature is valid and is created with a correct key. Due to this check, the envelope is discarded after this point.
Requests that have expired are dropped here.
Ingress message inspection is applied, and messages that are not accepted by the canister are dropped.
Submitted request
E : Envelope
Conditions
E.content.canister_id ∈ verify_envelope(E, E.content.sender, S.system_time)
|E.content.nonce| <= 32
E.content ∉ dom(S.requests)
S.system_time <= E.content.ingress_expiry
is_effective_canister_id(E.content, ECID)
( E.content.canister_id = ic_principal
E.content.arg = candid({canister_id = CanisterId, …})
E.content.sender ∈ S.controllers[CanisterId]
E.content.method_name ∈
{ "install_code", "install_chunked_code", "uninstall_code", "update_settings", "start_canister", "stop_canister",
"canister_status", "delete_canister" }
) ∨ (
E.content.canister_id = ic_principal
E.content.method_name ∈
{ "provisional_create_canister_with_cycles", "provisional_top_up_canister" }
) ∨ (
E.content.canister_id ≠ ic_principal
S.canisters[E.content.canister_id] ≠ EmptyCanister
S.canister_status[E.content.canister_id] = Running
Env = {
time = S.time[E.content.canister_id];
controllers = S.controllers[E.content.canister_id];
global_timer = S.global_timer[E.content.canister_id];
balance = S.balances[E.content.canister_id];
reserved_balance = S.reserved_balances[E.content.canister_id];
reserved_balance_limit = S.reserved_balance_limits[E.content.canister_id];
compute_allocation = S.compute_allocation[E.content.canister_id];
memory_allocation = S.memory_allocation[E.content.canister_id];
memory_usage_raw_module = memory_usage_raw_module(S.canisters[E.content.canister_id].raw_module);
memory_usage_canister_history = memory_usage_canister_history(S.canister_history[E.content.canister_id]);
freezing_threshold = S.freezing_threshold[E.content.canister_id];
subnet_size = S.canister_subnet[E.content.canister_id].subnet_size;
certificate = NoCertificate;
status = simple_status(S.canister_status[E.content.canister_id]);
canister_version = S.canister_version[E.content.canister_id];
}
liquid_balance(
S.balances[E.content.canister_id],
S.reserved_balances[E.content.canister_id],
freezing_limit(
S.compute_allocation[E.content.canister_id],
S.memory_allocation[E.content.canister_id],
S.freezing_threshold[E.content.canister_id],
memory_usage_wasm_state(S.canisters[E.content.canister_id].wasm_state) +
memory_usage_raw_module(S.canisters[E.content.canister_id].raw_module) +
memory_usage_canister_history(S.canister_history[E.content.canister_id]),
S.canister_subnet[E.content.canister_id].subnet_size,
)
) ≥ 0
S.canisters[E.content.canister_id].module.inspect_message
(E.content.method_name, S.canisters[E.content.canister_id].wasm_state, E.content.arg, E.content.sender, Env) = Return {status = Accept;}
)
State after
S with
requests[E.content] = (Received, ECID)
This is not instantaneous (the BIG takes some time to agree it accepts the request) nor guaranteed (a node could just drop the request, or maybe it did not pass validation). But once the request has entered the BIG state like this, it will be acted upon.
Request rejection
The BIG may reject a received message for internal reasons (high load, low resources) or expiry. The precise conditions are not specified here, but the reject code must indicate this to be a system error.
Conditions
S.requests[R] = (Received, ECID)
Code = SYS_FATAL or Code = SYS_TRANSIENT
State after
S with
requests[R] = (Rejected (Code, Msg), ECID)
Initiating canister calls
A first step in processing a canister update call is to create a CallMessage
in the message queue.
The request
field of the FromUser
origin establishes the connection to the API message. One could use the corresponding hash_of_map
for this purpose, but this formulation is more abstract.
The BIG does not make any guarantees about the order of incoming messages.
Conditions
S.requests[R] = (Received, ECID)
S.system_time <= R.ingress_expiry
C = S.canisters[R.canister_id]
State after
S with
requests[R] = (Processing, ECID)
messages =
CallMessage {
origin = FromUser { request = R };
caller = R.sender;
callee = R.canister_id;
method_name = R.method_name;
arg = R.arg;
transferred_cycles = 0;
queue = Unordered;
} · S.messages
Calls to stopped/stopping canisters are rejected
A call to a canister which is stopping, or stopped is automatically rejected.
Conditions
S.messages = Older_messages · CallMessage CM · Younger_messages
(CM.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ CM.queue)
S.canisters[CM.callee] ≠ EmptyCanister
S.canister_status[CM.callee] = Stopped or S.canister_status[CM.callee] = Stopping
State after:
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = CM.origin;
response = Reject (CANISTER_ERROR, <implementation-specific>);
refunded_cycles = CM.transferred_cycles;
}
Calls to frozen canisters are rejected
A call to a canister which is frozen is automatically rejected.
Conditions
S.messages = Older_messages · CallMessage CM · Younger_messages
(CM.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ CM.queue)
S.canisters[CM.callee] ≠ EmptyCanister
S.canister_status[CM.callee] = liquid_balance(
S.balances[CM.callee],
S.reserved_balances[CM.callee],
freezing_limit(
S.compute_allocation[CM.callee],
S.memory_allocation[CM.callee],
S.freezing_threshold[CM.callee],
memory_usage_wasm_state(S.canisters[CM.callee].wasm_state) +
memory_usage_raw_module(S.canisters[CM.callee].raw_module) +
memory_usage_canister_history(S.canister_history[CM.callee]),
S.canister_subnet[CM.callee].subnet_size,
)
) < 0
State after:
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = CM.origin;
response = Reject (SYS_TRANSIENT, <implementation-specific>);
refunded_cycles = CM.transferred_cycles;
}
Call context creation
Before invoking a heartbeat, a global timer, or a message to a public entry point, a call context is created for bookkeeping purposes. For these invocations the canister must be running (so not stopped or stopping). Additionally, these invocations only happen for "real" canisters, not the BIG management canister.
This "bookkeeping transition" must be immediately followed by the corresponding "Message execution" transition.
Call context creation: Public entry points
For a message to a public entry point, the method is looked up in the list of exports. This happens for both ingress and inter-canister messages.
The position of the message in the queue is unchanged.
Conditions
S.messages = Older_messages · CallMessage CM · Younger_messages
S.canisters[CM.callee] ≠ EmptyCanister
S.canister_status[CM.callee] = Running
liquid_balance(
S.balances[CM.callee],
S.reserved_balances[CM.callee],
freezing_limit(
S.compute_allocation[CM.callee],
S.memory_allocation[CM.callee],
S.freezing_threshold[CM.callee],
memory_usage_wasm_state(S.canisters[CM.callee].wasm_state) +
memory_usage_raw_module(S.canisters[CM.callee].raw_module) +
memory_usage_canister_history(S.canister_history[CM.callee]),
S.canister_subnet[CM.callee].subnet_size,
)
) ≥ MAX_CYCLES_PER_MESSAGE
Ctxt_id ∉ dom(S.call_contexts)
State after
S with
messages =
Older_messages ·
FuncMessage {
call_context = Ctxt_id;
receiver = CM.callee;
entry_point = PublicMethod CM.method_name CM.caller CM.arg;
queue = CM.queue;
} ·
Younger_messages
call_contexts[Ctxt_id] = {
canister = CM.callee;
origin = CM.origin;
needs_to_respond = true;
deleted = false;
available_cycles = CM.transferred_cycles;
}
balances[CM.callee] = S.balances[CM.callee] - MAX_CYCLES_PER_MESSAGE
Call context creation: Heartbeat
If canister C
exports a method with name canister_heartbeat
, the BIG will create the corresponding call context.
Conditions
S.canisters[C] ≠ EmptyCanister
S.canister_status[C] = Running
liquid_balance(
S.balances[C],
S.reserved_balance[C],
freezing_limit(
S.compute_allocation[C],
S.memory_allocation[C],
S.freezing_threshold[C],
memory_usage_wasm_state(S.canisters[C].wasm_state) +
memory_usage_raw_module(S.canisters[C].raw_module) +
memory_usage_canister_history(S.canister_history[C]),
S.canister_subnet[C].subnet_size,
)
) ≥ MAX_CYCLES_PER_MESSAGE
Ctxt_id ∉ dom(S.call_contexts)
State after
S with
messages =
FuncMessage {
call_context = Ctxt_id;
receiver = C;
entry_point = Heartbeat;
queue = Queue { from = System; to = C };
}
· S.messages
call_contexts[Ctxt_id] = {
canister = C;
origin = FromSystemTask;
needs_to_respond = false;
deleted = false;
available_cycles = 0;
}
balances[C] = S.balances[C] - MAX_CYCLES_PER_MESSAGE
Call context creation: Global timer
If canister C
exports a method with name canister_global_timer
, the global timer of canister C
is set, and the current time for canister C
has passed the value of the global timer, the BIG will create the corresponding call context and deactivate the global timer.
Conditions
S.canisters[C] ≠ EmptyCanister
S.canister_status[C] = Running
S.global_timer[C] ≠ 0
S.time[C] ≥ S.global_timer[C]
liquid_balance(
S.balances[C],
S.reserved_balances[C],
freezing_limit(
S.compute_allocation[C],
S.memory_allocation[C],
S.freezing_threshold[C],
memory_usage_wasm_state(S.canisters[C].wasm_state) +
memory_usage_raw_module(S.canisters[C].raw_module) +
memory_usage_canister_history(S.canister_history[C]),
S.canister_subnet[C].subnet_size,
)
) ≥ MAX_CYCLES_PER_MESSAGE
Ctxt_id ∉ dom(S.call_contexts)
State after
S with
messages =
FuncMessage {
call_context = Ctxt_id;
receiver = C;
entry_point = GlobalTimer;
queue = Queue { from = System; to = C };
}
· S.messages
call_contexts[Ctxt_id] = {
canister = C;
origin = FromSystemTask;
needs_to_respond = false;
deleted = false;
available_cycles = 0;
}
global_timer[C] = 0
balances[C] = S.balances[C] - MAX_CYCLES_PER_MESSAGE
The BIG can execute any message that is at the head of its queue, i.e. there is no older message with the same abstract queue
field. The actual message execution, if successful, may enqueue further messages and --- if the function returns a response --- record this response. The new call and response messages are enqueued at the end.
Note that new messages are executed only if the canister is Running and is not frozen.
Message execution
The transition models the actual execution of a message, whether it is an initial call to a public method or a response. In either case, a call context already exists (see transition "Call context creation").
Conditions
S.messages = Older_messages · FuncMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
S.canisters[M.receiver] ≠ EmptyCanister
Mod = S.canisters[M.receiver].module
Is_response = M.entry_point == Callback _ _ _
Env = {
time = S.time[M.receiver];
controllers = S.controllers[M.receiver];
global_timer = S.global_timer[M.receiver];
balance = S.balances[M.receiver]
reserved_balance = S.reserved_balances[M.receiver];
reserved_balance_limit = S.reserved_balance_limits[M.receiver];
compute_allocation = S.compute_allocation[M.receiver];
memory_allocation = S.memory_allocation[M.receiver];
memory_usage_raw_module = memory_usage_raw_module(S.canisters[M.receiver].raw_module);
memory_usage_canister_history = memory_usage_canister_history(S.canister_history[M.receiver]);
freezing_threshold = S.freezing_threshold[M.receiver];
subnet_size = S.canister_subnet[M.receiver].subnet_size;
certificate = NoCertificate;
status = simple_status(S.canister_status[M.receiver]);
canister_version = S.canister_version[M.receiver];
}
Available = S.call_contexts[M.call_contexts].available_cycles
( M.entry_point = PublicMethod Name Caller Arg
F = Mod.update_methods[Name](Arg, Caller, Env, Available)
New_canister_version = S.canister_version[M.receiver] + 1
)
or
( M.entry_point = PublicMethod Name Caller Arg
F = query_as_update(Mod.query_methods[Name], Arg, Caller, Env)
New_canister_version = S.canister_version[M.receiver]
)
or
( M.entry_point = Callback Callback Response RefundedCycles
F = Mod.callbacks(Callback, Response, RefundedCycles, Env, Available)
New_canister_version = S.canister_version[M.receiver] + 1
)
or
( M.entry_point = Heartbeat
F = system_task_as_update(Mod.heartbeat, Env)
New_canister_version = S.canister_version[M.receiver] + 1
)
or
( M.entry_point = GlobalTimer
F = system_task_as_update(Mod.global_timer, Env)
New_canister_version = S.canister_version[M.receiver] + 1
)
R = F(S.canisters[M.receiver].wasm_state)
State after
if
R = Return res
validate_sender_canister_version(res.new_calls, S.canister_version[M.receiver])
res.cycles_used ≤ (if Is_response then MAX_CYCLES_PER_RESPONSE else MAX_CYCLES_PER_MESSAGE)
res.cycles_accepted ≤ Available
(res.cycles_used + ∑ [ MAX_CYCLES_PER_RESPONSE + call.transferred_cycles | call ∈ res.new_calls ]) ≤
(S.balances[M.receiver] + res.cycles_accepted + (if Is_response then MAX_CYCLES_PER_RESPONSE else MAX_CYCLES_PER_MESSAGE))
Cycles_reserved = cycles_to_reserve(S, A.canister_id, S.compute_allocation[A.canister_id], S.memory_allocation[A.canister_id], New_state)
New_balance =
(S.balances[M.receiver] + res.cycles_accepted + (if Is_response then MAX_CYCLES_PER_RESPONSE else MAX_CYCLES_PER_MESSAGE))
- (res.cycles_used + ∑ [ MAX_CYCLES_PER_RESPONSE + call.transferred_cycles | call ∈ res.new_calls ])
- Cycles_reserved
New_reserved_balance = S.reserved_balances[M.receiver] + Cycles_reserved
Min_balance = if Is_response then 0 else freezing_limit(
S.compute_allocation[M.receiver],
S.memory_allocation[M.receiver],
S.freezing_threshold[M.receiver],
memory_usage_wasm_state(res.new_state) +
memory_usage_raw_module(S.canisters[M.receiver].raw_module) +
memory_usage_canister_history(S.canister_history[M.receiver]),
S.canister_subnet[M.receiver].subnet_size,
)
New_reserved_balance ≤ S.reserved_balance_limits[M.receiver]
liquid_balance(
New_balance,
New_reserved_balance,
Min_balance
) ≥ 0
(S.memory_allocation[M.receiver] = 0) or (memory_usage_wasm_state(res.new_state) +
memory_usage_raw_module(S.canisters[M.receiver].raw_module) +
memory_usage_canister_history(S.canister_history[M.receiver]) ≤ S.memory_allocation[M.receiver])
(res.response = NoResponse) or S.call_contexts[M.call_context].needs_to_respond
then
S with
canisters[M.receiver].wasm_state = res.new_state;
canister_version[M.receiver] = New_canister_version;
messages =
Older_messages ·
Younger_messages ·
[ CallMessage {
origin = FromCanister {
call_context = M.call_context;
callback = call.callback;
};
caller = M.receiver;
callee = call.callee;
method_name = call.method_name;
arg = call.arg;
transferred_cycles = call.transferred_cycles
queue = Queue { from = M.receiver; to = call.callee };
}
| call ∈ res.new_calls ] ·
[ ResponseMessage {
origin = S.call_contexts[M.call_context].origin
response = res.response;
refunded_cycles = Available - res.cycles_accepted;
}
| res.response ≠ NoResponse ]
if res.response = NoResponse:
call_contexts[M.call_context].available_cycles = Available - res.cycles_accepted
else
call_contexts[M.call_context].needs_to_respond = false
call_contexts[M.call_context].available_cycles = 0
if res.new_certified_data ≠ NoCertifiedData:
certified_data[M.receiver] = res.new_certified_data
if res.new_global_timer ≠ NoGlobalTimer:
global_timer[M.receiver] = res.new_global_timer
balances[M.receiver] = New_balance
reserved_balances[M.receiver] = New_reserved_balance
else
S with
messages = Older_messages · Younger_messages
balances[M.receiver] =
(S.balances[M.receiver] + (if Is_response then MAX_CYCLES_PER_RESPONSE else MAX_CYCLES_PER_MESSAGE))
- min (R.cycles_used, (if Is_response then MAX_CYCLES_PER_RESPONSE else MAX_CYCLES_PER_MESSAGE))
Depending on whether this is a call message and a response messages, we have either set aside MAX_CYCLES_PER_MESSAGE
or MAX_CYCLES_PER_RESPONSE
, either in the call context creation rule or the Callback invocation rule.
The cycle consumption of executing this message is modeled via the unspecified cycles_used
variable; the variable takes some value between 0 and MAX_CYCLES_PER_MESSAGE
/MAX_CYCLES_PER_RESPONSE
(for call execution and response execution, respectively).
This transition detects certain behavior that will appear as a trap (and which an implementation may implement by trapping directly in a system call):
Responding if the present call context does not need to be responded to
Accepting more cycles than are available on the call context
Sending out more cycles than available to the canister
Consuming more cycles than allowed (and reserved)
If message execution traps (in the sense of a Wasm function), the message gets dropped. No response is generated (as some other message may still fulfill this calling context). Any state mutation is discarded. If the message was a call, the associated cycles are held by its associated call context and will be refunded to the caller, see Call context starvation.
If message execution returns (in the sense of a Wasm function), the state is updated and possible outbound calls and responses are enqueued.
Note that returning does not imply that the call associated with this message now succeeds in the sense defined in section responding; that would require a (unique) call to ic0.reply
. Note also that the state changes are persisted even when the BIG is set to synthesize a CANISTER_ERROR reject immediately afterward (which happens when this returns without calling ic0.reply
or ic0.reject
, the corresponding call has not been responded to and there are no outstanding callbacks, see Call context starvation).
The function validate_sender_canister_version
checks that sender_canister_version
matches the actual canister version of the sender in all calls to the methods of the management canister that take sender_canister_version
:
validate_sender_canister_version(new_calls, canister_version_from_system) =
∀ call ∈ new_calls. (call.callee = ic_principal and (call.method = 'create_canister' or call.method = 'update_settings' or call.method = 'install_code' or call.method = `install_chunked_code` or call.method = 'uninstall_code' or call.method = 'provisional_create_canister_with_cycles') and call.arg = candid(A) and A.sender_canister_version = n) => n = canister_version_from_system
The functions query_as_update
and system_task_as_update
turns a query function (note that composite query methods cannot be called when executing a message during this transition) resp the heartbeat or global timer into an update function; this is merely a notational trick to simplify the rule:
query_as_update(f, arg, env) = λ wasm_state →
match f(arg, env)(wasm_state) with
Trap trap → Trap trap
Return res → Return {
new_state = wasm_state;
new_calls = [];
new_certified_data = NoCertifiedData;
new_global_timer = NoGlobalTimer;
response = res.response;
cycles_accepted = 0;
cycles_used = res.cycles_used;
}
system_task_as_update(f, env) = λ wasm_state →
match f(env)(wasm_state) with
Trap trap → Trap trap
Return res → Return {
new_state = res.new_state;
new_calls = res.new_calls;
new_certified_data = res.new_certified_data;
new_global_timer = res.new_global_timer;
response = NoResponse;
cycles_accepted = 0;
cycles_used = res.cycles_used;
}
Note that by construction, a query function will either trap or return with a response; it will never send calls, and it will never change the state of the canister.
Call context starvation
If the call context needs to respond (in particular, if the call context is not for a system task) and there is no call, downstream call context, or response that references a call context, then a reject is synthesized. The error message below is not indicative. In particular, if the BIG has an idea about why this starved, it can put that in there (e.g. the initial message handler trapped with an out-of-memory access).
Conditions
S.call_contexts[Ctxt_id].needs_to_respond = true
∀ CallMessage {origin = FromCanister O, …} ∈ S.messages. O.calling_context ≠ Ctxt_id
∀ ResponseMessage {origin = FromCanister O, …} ∈ S.messages. O.calling_context ≠ Ctxt_id
∀ (_ ↦ {needs_to_respond = true, origin = FromCanister O, …}) ∈ S.call_contexts: O.calling_context ≠ Ctxt_id
∀ (_ ↦ Stopping Origins) ∈ S.canister_status: ∀(FromCanister O, _) ∈ Origins. O.calling_context ≠ Ctxt_id
State after
S with
call_contexts[Ctxt_id].needs_to_respond = false
call_contexts[Ctxt_id].available_cycles = 0
messages =
S.messages ·
ResponseMessage {
origin = S.call_contexts[Ctxt_id].origin;
response = Reject (CANISTER_ERROR, <implementation-specific>);
refunded_cycles = S.call_contexts[Ctxt_id].available_cycles
}
Call context removal
If there is no call, downstream call context, or response that references a call context, and the call context does not need to respond (because it has already responded or its origin is a system task that does not await a response), then the call context can be removed.
Conditions
S.call_contexts[Ctxt_id].needs_to_respond = false
∀ CallMessage {origin = FromCanister O, …} ∈ S.messages. O.calling_context ≠ Ctxt_id
∀ ResponseMessage {origin = FromCanister O, …} ∈ S.messages. O.calling_context ≠ Ctxt_id
∀ (_ ↦ {needs_to_respond = true, origin = FromCanister O, …}) ∈ S.call_contexts: O.calling_context ≠ Ctxt_id
∀ (_ ↦ Stopping Origins) ∈ S.canister_status: ∀(FromCanister O, _) ∈ Origins. O.calling_context ≠ Ctxt_id
State after
S with
call_contexts[Ctxt_id] = (deleted)
BIG Management Cube: Cube creation
The BIG chooses an appropriate canister id (referred to as CanisterId
) and subnet id (referred to as SubnetId
, SubnetId ∈ Subnets
, where Subnets
is the under-specified set of subnet ids on the BIG) and instantiates a new (empty) canister identified by CanisterId
on the subnet identified by SubnetId
with subnet size denoted by SubnetSize
. The controllers are set such that the sender of this request is the only controller, unless the settings
say otherwise. All cycles on this call are now the canister's initial cycles.
This is also when the System Time of the new canister starts ticking.
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'create_canister'
M.arg = candid(A)
is_system_assigned Canister_id
Canister_id ∉ dom(S.canisters)
SubnetId ∈ Subnets
if A.settings.controllers is not null:
New_controllers = A.settings.controllers
else:
New_controllers = [M.caller]
if New_memory_allocation > 0:
memory_usage_canister_history(New_canister_history) ≤ New_memory_allocation
if A.settings.compute_allocation is not null:
New_compute_allocation = A.settings.compute_allocation
else:
New_compute_allocation = 0
if A.settings.memory_allocation is not null:
New_memory_allocation = A.settings.memory_allocation
else:
New_memory_allocation = 0
if A.settings.freezing_threshold is not null:
New_freezing_threshold = A.settings.freezing_threshold
else:
New_freezing_threshold = 2592000
if A.settings.reserved_cycles_limit is not null:
New_reserved_balance_limit = A.settings.reserved_cycles_limit
else:
New_reserved_balance_limit = 5_000_000_000_000
Cycles_reserved = cycles_to_reserve(S, Canister_id, New_compute_allocation, New_memory_allocation, EmptyCanister.wasm_state)
New_balance = M.transferred_cycles - Cycles_reserved
New_reserved_balance = Cycles_reserved
New_reserved_balance <= New_reserved_balance_limit
if New_compute_allocation > 0 or New_memory_allocation > 0 or Cycles_reserved > 0:
liquid_balance(
New_balance,
New_reserved_balance,
freezing_limit(
New_compute_allocation,
New_memory_allocation,
New_freezing_threshold,
memory_usage_canister_history(New_canister_history),
SubnetSize,
)
) ≥ 0
New_canister_history = {
total_num_changes = 1
recent_changes = {
timestamp_nanos = CurrentTime
canister_version = 0
origin = change_origin(M.caller, A.sender_canister_version, M.origin)
details = Creation {
controllers = New_controllers
}
}
}
State after
S with
canisters[Canister_id] = EmptyCanister
time[Canister_id] = CurrentTime
global_timer[Canister_id] = 0
controllers[Canister_id] = New_controllers
chunk_store[Canister_id] = ()
compute_allocation[Canister_id] = New_compute_allocation
memory_allocation[Canister_id] = New_memory_allocation
freezing_threshold[Canister_id] = New_freezing_threshold
balances[Canister_id] = New_balance
reserved_balances[Canister_id] = New_reserved_balance
reserved_balance_limits[Canister_id] = New_reserved_balance_limit
certified_data[Canister_id] = ""
canister_history[Canister_id] = New_canister_history
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin
response = Reply (candid({canister_id = Canister_id}))
refunded_cycles = 0
}
canister_status[Canister_id] = Running
canister_version[Canister_id] = 0
canister_subnet[Canister_id] = Subnet {
subnet_id : SubnetId
subnet_size : SubnetSize
}
This uses the predicate
is_system_assigned : Principal -> Bool
which characterizes all system-assigned ids.
To avoid clashes with potential user ids or is derived from users or canisters, we require (somewhat handwavy) that
is_system_assigned (mk_self_authenticating_id pk) = false
for possible public keyspk
andis_system_assigned (mk_derived_id p dn) = false
for anyp
that could be a user id or canister id.is_system_assigned p = false
for|p| > 29
.is_system_assigned ic_principal = false
.
BIG Management Cube: Changing settings
Only the controllers of the given canister can update the canister settings.
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'update_settings'
M.arg = candid(A)
M.caller ∈ S.controllers[A.canister_id]
if New_memory_allocation > 0:
memory_usage_wasm_state(S.canisters[A.canister_id].wasm_state) +
memory_usage_raw_module(S.canisters[A.canister_id].raw_module) +
memory_usage_canister_history(New_canister_history) ≤ New_memory_allocation
if A.settings.compute_allocation is not null:
New_compute_allocation = A.settings.compute_allocation
else:
New_compute_allocation = S.compute_allocation[A.canister_id]
if A.settings.memory_allocation is not null:
New_memory_allocation = A.settings.memory_allocation
else:
New_memory_allocation = S.memory_allocation[A.canister_id]
if A.settings.freezing_threshold is not null:
New_freezing_threshold = A.settings.freezing_threshold
else:
New_freezing_threshold = S.freezing_threshold[A.canister_id]
if A.settings.reserved_cycles_limit is not null:
New_reserved_balance_limit = A.settings.reserved_cycles_limit
else:
New_reserved_balance_limit = S.reserved_balance_limits[A.canister_id]
Cycles_reserved = cycles_to_reserve(S, A.canister_id, New_compute_allocation, New_memory_allocation, S.canisters[A.canister_id].wasm_state)
New_balance = S.balances[A.canister_id] - Cycles_reserved
New_reserved_balance = S.reserved_balances[A.canister_id] + Cycles_reserved
New_reserved_balance ≤ New_reserved_balance_limit
if New_compute_allocation > S.compute_allocation[A.canister_id] or New_memory_allocation > S.memory_allocation[A.canister_id] or Cycles_reserved > 0:
liquid_balance(
New_balance,
New_reserved_balance,
freezing_limit(
New_compute_allocation,
New_memory_allocation,
New_freezing_threshold,
memory_usage_wasm_state(S.canisters[A.canister_id].wasm_state) +
memory_usage_raw_module(S.canisters[A.canister_id].raw_module) +
memory_usage_canister_history(New_canister_history),
S.canister_subnet[A.canister_id].subnet_size,
)
) ≥ 0
S.canister_history[A.canister_id] = {
total_num_changes = N;
recent_changes = H;
}
if A.settings.controllers is not null:
New_canister_history = {
total_num_changes = N + 1;
recent_changes = H · {
timestamp_nanos = S.time[A.canister_id];
canister_version = S.canister_version[A.canister_id] + 1;
origin = change_origin(M.caller, A.sender_canister_version, M.origin);
details = ControllersChange {
controllers = A.settings.controllers;
};
};
}
else:
New_canister_history = S.canister_history[A.canister_id]
State after
S with
if A.settings.controllers is not null:
controllers[A.canister_id] = A.settings.controllers
canister_history[A.canister_id] = New_canister_history
compute_allocation[A.canister_id] = New_compute_allocation
memory_allocation[A.canister_id] = New_memory_allocation
freezing_threshold[A.canister_id] = New_freezing_threshold
balances[A.canister_id] = New_balance
reserved_balances[A.canister_id] = New_reserved_balance
reserved_balance_limits[A.canister_id] = New_reserved_balance_limit
canister_version[A.canister_id] = S.canister_version[A.canister_id] + 1
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin
response = Reply (candid())
refunded_cycles = M.transferred_cycles
}
BIG Management Cube: Cube status
The controllers of a canister can obtain detailed information about the canister.
The Memory_usage
is the (in this specification underspecified) total size of storage in bytes.
The idle_cycles_burned_per_day
is the idle consumption of resources in cycles per day.
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'canister_status'
M.arg = candid(A)
M.caller ∈ S.controllers[A.canister_id] ∪ {A.canister_id}
State after
S with
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin
response = candid({
status = simple_status(S.canister_status[A.canister_id]);
settings = {
controllers = S.controllers[A.canister_id];
compute_allocation = S.compute_allocation[A.canister_id];
memory_allocation = S.memory_allocation[A.canister_id];
freezing_threshold = S.freezing_threshold[A.canister_id];
reserved_cycles_limit = S.reserved_balance_limit[A.canister_id];
}
module_hash =
if S.canisters[A.canister_id] = EmptyCanister
then null
else opt (SHA-256(S.canisters[A.canister_id].raw_module));
memory_size = Memory_usage;
cycles = S.balances[A.canister_id];
reserved_cycles = S.reserved_balances[A.canister_id]
idle_cycles_burned_per_day = idle_cycles_burned_rate(
S.compute_allocation[A.canister_id],
S.memory_allocation[A.canister_id],
memory_usage_wasm_state(S.canisters[A.canister_id].wasm_state) +
memory_usage_raw_module(S.canisters[A.canister_id].raw_module) +
memory_usage_canister_history(S.canister_history[A.canister_id]),
S.freezing_threshold[A.canister_id],
S.canister_subnet[A.canister_id].subnet_size,
);
})
refunded_cycles = M.transferred_cycles
}
BIG Management Cube: Cube information
Every canister can retrieve the canister history, current module hash, and current controllers of every other canister (including itself).
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'canister_info'
M.arg = candid(A)
if A.num_requested_changes = null then From = |S.canister_history[A.canister_id].recent_changes|
else From = max(0, |S.canister_history[A.canister_id].recent_changes| - A.num_requested_changes)
End = |S.canister_history[A.canister_id].recent_changes| - 1
State after
S with
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin
response = candid({
total_num_changes = S.canister_history[A.canister_id].total_num_changes;
recent_changes = S.canister_history[A.canister_id].recent_changes[From..End];
module_hash =
if S.canisters[A.canister_id] = EmptyCanister
then null
else opt (SHA-256(S.canisters[A.canister_id].raw_module));
controllers = S.controllers[A.canister_id];
})
refunded_cycles = M.transferred_cycles
}
BIG Management Cube: Upload Chunk
A controller of a canister, or the canister itself can upload chunks to the chunk store of that canister.
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.method_name = 'upload_chunk'
M.arg = candid(A)
|dom(S.chunk_store[A.canister_id]) ∪ {SHA-256(A.chunk)}| <= CHUNK_STORE_SIZE
M.caller ∈ S.controllers[A.canister_id] ∪ {A.canister_id}
State after
S with
chunk_store[A.canister_id](SHA-256(A.chunk)) = A.chunk
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin
response = candid(hash)
}
BIG Management Cube: Clear chunk store
The controller of a canister, or the canister itself can clear the chunk store of that canister.
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.method_name = 'clear_chunk_store'
M.arg = candid(A)
M.caller ∈ S.controllers[A.canister_id] ∪ {A.canister_id}
State after
S with
chunk_store[A.canister_id] = ()
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin
response = candid()
}
BIG Management Cube: List stored chunks
The controller of a canister, or the canister itself can list the hashes of the chunks stored in the chunk store.
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.method_name = 'stored_chunks'
M.arg = candid(A)
M.caller ∈ S.controllers[A.canister_id] ∪ {A.canister_id}
State after
S with
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin
response = candid(dom(S.chunk_store[A.canister_id]))
}
BIG Management Cube: Code installation
Only the controllers of the given canister can install code. This transition installs new code over a canister. This involves invoking the canister_init
method (see Cube initialization), which must succeed.
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'install_code'
M.arg = candid(A)
Mod = parse_wasm_mod(A.wasm_module)
Public_custom_sections = parse_public_custom_sections(A.wasm_module);
Private_custom_sections = parse_private_custom_sections(A.wasm_module);
(A.mode = install and S.canisters[A.canister_id] = EmptyCanister) or A.mode = reinstall
M.caller ∈ S.controllers[A.canister_id]
dom(Mod.update_methods) ∩ dom(Mod.query_methods) = ∅
dom(Mod.update_methods) ∩ dom(Mod.composite_query_methods) = ∅
dom(Mod.query_methods) ∩ dom(Mod.composite_query_methods) = ∅
Env = {
time = S.time[A.canister_id];
controllers = S.controllers[A.canister_id];
global_timer = 0;
balance = S.balances[A.canister_id];
reserved_balance = S.reserved_balances[A.canister_id];
reserved_balance_limit = S.reserved_balance_limits[A.canister_id];
compute_allocation = S.compute_allocation[A.canister_id];
memory_allocation = S.memory_allocation[A.canister_id];
memory_usage_raw_module = memory_usage_raw_module(A.wasm_module);
memory_usage_canister_history = memory_usage_canister_history(New_canister_history);
freezing_threshold = S.freezing_threshold[A.canister_id];
subnet_size = S.canister_subnet[A.canister_id].subnet_size;
certificate = NoCertificate;
status = simple_status(S.canister_status[A.canister_id]);
canister_version = S.canister_version[A.canister_id] + 1;
}
Mod.init(A.canister_id, A.arg, M.caller, Env) = Return {new_state = New_state; new_certified_data = New_certified_data; new_global_timer = New_global_timer; cycles_used = Cycles_used;}
Cycles_reserved = cycles_to_reserve(S, A.canister_id, S.compute_allocation[A.canister_id], S.memory_allocation[A.canister_id], New_state)
New_balance = S.balances[A.canister_id] - Cycles_used - Cycles_reserved
New_reserved_balance = S.reserved_balances[A.canister_id] + Cycles_reserved
New_reserved_balance ≤ S.reserved_balance_limits[A.canister_id]
liquid_balance(
S.balances[A.canister_id],
S.reserved_balances[A.canister_id],
freezing_limit(
S.compute_allocation[A.canister_id],
S.memory_allocation[A.canister_id],
S.freezing_threshold[A.canister_id],
memory_usage_wasm_state(S.canisters[A.canister_id].wasm_state) +
memory_usage_raw_module(S.canisters[A.canister_id].raw_module) +
memory_usage_canister_history(S.canister_history[A.canister_id]),
S.canister_subnet[A.canister_id].subnet_size,
)
) ≥ MAX_CYCLES_PER_MESSAGE
liquid_balance(
New_balance,
New_reserved_balance,
freezing_limit(
S.compute_allocation[A.canister_id],
S.memory_allocation[A.canister_id],
S.freezing_threshold[A.canister_id],
memory_usage_wasm_state(New_state) +
memory_usage_raw_module(A.wasm_module) +
memory_usage_canister_history(New_canister_history),
S.canister_subnet[A.canister_id].subnet_size,
)
) ≥ 0
if S.memory_allocation[A.canister_id] > 0:
memory_usage_wasm_state(New_state) +
memory_usage_raw_module(A.wasm_module) +
memory_usage_canister_history(New_canister_history) ≤ S.memory_allocation[A.canister_id]
S.canister_history[A.canister_id] = {
total_num_changes = N;
recent_changes = H;
}
New_canister_history = {
total_num_changes = N + 1;
recent_changes = H · {
timestamp_nanos = S.time[A.canister_id];
canister_version = S.canister_version[A.canister_id] + 1
origin = change_origin(M.caller, A.sender_canister_version, M.origin);
details = CodeDeployment {
mode = A.mode;
module_hash = SHA-256(A.wasm_module);
};
};
}
State after
S with
canisters[A.canister_id] = {
wasm_state = New_state;
module = Mod;
raw_module = A.wasm_module;
public_custom_sections = Public_custom_sections;
private_custom_sections = Private_custom_sections;
}
certified_data[A.canister_id] = New_certified_data
if New_global_timer ≠ NoGlobalTimer:
global_timer[A.canister_id] = New_global_timer
else:
global_timer[A.canister_id] = 0
canister_version[A.canister_id] = S.canister_version[A.canister_id] + 1
balances[A.canister_id] = New_balance
reserved_balances[A.canister_id] = New_reserved_balance
canister_history[A.canister_id] = New_canister_history
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin;
response = Reply (candid());
refunded_cycles = M.transferred_cycles;
}
BIG Management Cube: Code upgrade
Only the controllers of the given canister can install new code. This changes the code of an existing canister, preserving the state in the stable memory. This involves invoking the canister_pre_upgrade
method, if the skip_pre_upgrade
flag is not set to opt true
, on the old and canister_post_upgrade
method on the new canister, which must succeed and must not invoke other methods.
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'install_code'
M.arg = candid(A)
Mod = parse_wasm_mod(A.wasm_module)
Public_custom_sections = parse_public_custom_sections(A.wasm_module)
Private_custom_sections = parse_private_custom_sections(A.wasm_module)
M.caller ∈ S.controllers[A.canister_id]
S.canisters[A.canister_id] = { wasm_state = Old_state; module = Old_module, …}
dom(Mod.update_methods) ∩ dom(Mod.query_methods) = ∅
dom(Mod.update_methods) ∩ dom(Mod.composite_query_methods) = ∅
dom(Mod.query_methods) ∩ dom(Mod.composite_query_methods) = ∅
Env = {
time = S.time[A.canister_id];
controllers = S.controllers[A.canister_id];
balance = S.balances[A.canister_id];
reserved_balance = S.reserved_balances[A.canister_id];
reserved_balance_limit = S.reserved_balance_limits[A.canister_id];
compute_allocation = S.compute_allocation[A.canister_id];
memory_allocation = S.memory_allocation[A.canister_id];
memory_usage_raw_module = memory_usage_raw_module(S.canisters[A.canister_id].raw_module);
memory_usage_canister_history = memory_usage_canister_history(S.canister_history[A.canister_id]);
freezing_threshold = S.freezing_threshold[A.canister_id];
subnet_size = S.canister_subnet[A.canister_id].subnet_size;
certificate = NoCertificate;
status = simple_status(S.canister_status[A.canister_id]);
}
(
(A.mode = upgrade or A.mode = upgrade {skip_pre_upgrade = false})
Env1 = Env with {
global_timer = S.global_timer[A.canister_id];
canister_version = S.canister_version[A.canister_id];
}
Old_module.pre_upgrade(Old_State, M.caller, Env1) = Return {stable_memory = Stable_memory; new_certified_data = New_certified_data; cycles_used = Cycles_used;}
)
or
(
A.mode = upgrade {skip_pre_upgrade = true}
Stable_memory = Old_State.stable_mem
New_certified_data = NoCertifiedData
Cycles_used = 0
)
Env2 = Env with {
memory_usage_raw_module = memory_usage_raw_module(A.wasm_module);
memory_usage_canister_history = memory_usage_canister_history(New_canister_history);
global_timer = 0;
canister_version = S.canister_version[A.canister_id] + 1;
}
Mod.post_upgrade(A.canister_id, Stable_memory, A.arg, M.caller, Env2) = Return {new_state = New_state; new_certified_data = New_certified_data'; new_global_timer = New_global_timer; cycles_used = Cycles_used';}
Cycles_reserved = cycles_to_reserve(S, A.canister_id, S.compute_allocation[A.canister_id], S.memory_allocation[A.canister_id], New_state)
New_balance = S.balances[A.canister_id] - Cycles_used - Cycles_used' - Cycles_reserved
New_reserved_balance = S.reserved_balances[A.canister_id] + Cycles_reserved
New_reserved_balance ≤ S.reserved_balance_limits[A.canister_id]
liquid_balance(
S.balances[A.canister_id],
S.reserved_balances[A.canister_id],
freezing_limit(
S.compute_allocation[A.canister_id],
S.memory_allocation[A.canister_id],
S.freezing_threshold[A.canister_id],
memory_usage_wasm_state(S.canisters[A.canister_id].wasm_state) +
memory_usage_raw_module(S.canisters[A.canister_id].raw_module) +
memory_usage_canister_history(S.canister_history[A.canister_id]),
S.canister_subnet[A.canister_id].subnet_size,
)
) ≥ MAX_CYCLES_PER_MESSAGE
liquid_balance(
New_balance,
New_reserved_balance,
freezing_limit(
S.compute_allocation[A.canister_id],
S.memory_allocation[A.canister_id],
S.freezing_threshold[A.canister_id],
memory_usage_wasm_state(New_state) +
memory_usage_raw_module(A.wasm_module) +
memory_usage_canister_history(New_canister_history),
S.canister_subnet[A.canister_id].subnet_size,
)
) ≥ 0
if S.memory_allocation[A.canister_id] > 0:
memory_usage_wasm_state(New_state) +
memory_usage_raw_module(A.wasm_module) +
memory_usage_canister_history(New_canister_history) ≤ S.memory_allocation[A.canister_id]
S.canister_history[A.canister_id] = {
total_num_changes = N;
recent_changes = H;
}
New_canister_history = {
total_num_changes = N + 1;
recent_changes = H · {
timestamp_nanos = S.time[A.canister_id];
canister_version = S.canister_version[A.canister_id] + 1
origin = change_origin(M.caller, A.sender_canister_version, M.origin);
details = CodeDeployment {
mode = Upgrade;
module_hash = SHA-256(A.wasm_module);
};
};
}
State after
S with
canisters[A.canister_id] = {
wasm_state = New_state;
module = Mod;
raw_module = A.wasm_module;
public_custom_sections = Public_custom_sections;
private_custom_sections = Private_custom_sections;
}
if New_certified_data' ≠ NoCertifiedData:
certified_data[A.canister_id] = New_certified_data'
else if New_certified_data ≠ NoCertifiedData:
certified_data[A.canister_id] = New_certified_data
if New_global_timer ≠ NoGlobalTimer:
global_timer[A.canister_id] = New_global_timer
else:
global_timer[A.canister_id] = 0
canister_version[A.canister_id] = S.canister_version[A.canister_id] + 1
balances[A.canister_id] = New_balance;
reserved_balances[A.canister_id] = New_reserved_balance;
canister_history[A.canister_id] = New_canister_history
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin;
response = Reply (candid());
refunded_cycles = M.transferred_cycles;
}
BIG Management Cube: Install chunked code
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'install_chunked_code'
if A.storage_canister = null then
storage_canister = A.target_canister
else
storage_canister = A.storage_canister
M.caller ∈ S.controllers[A.target_canister]
M.caller ∈ S.controllers[storage_canister] ∪ {storage_canister}
S.canister_subnet[A.target_canister] = S.canister_subnet[strorage_canister]
∀ h ∈ A.chunk_hashes_list. h ∈ dom(S.chunk_store[storage_canister])
A.chunk_hashes_list = [h1,h2,...,hk]
wasm_module = S.chunk_store[storage_canister][h1] || ... || S.chunk_store[storage_canister][hk]
A.wasm_module_hash = SHA-256(wasm_module)
M' = M with
method_name = 'install_code'
arg = candid(record {A.mode; A.target_canister; wasm_module; A.arg; A.sender_canister_version})
State after
S with
messages = Older_messages · CallMessage M' · Younger_messages
BIG Management Cube: Code uninstallation
Upon uninstallation, the canister is reverted to an empty canister, and all outstanding call contexts are rejected and marked as deleted.
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'uninstall_code'
M.arg = candid(A)
M.caller ∈ S.controllers[A.canister_id]
S.canister_history[A.canister_id] = {
total_num_changes = N;
recent_changes = H;
}
State after
S with
canisters[A.canister_id] = EmptyCanister
certified_data[A.canister_id] = ""
chunk_store = ()
canister_history[A.canister_id] = {
total_num_changes = N + 1;
recent_changes = H · {
timestamp_nanos = S.time[A.canister_id];
canister_version = S.canister_version[A.canister_id] + 1
origin = change_origin(M.caller, A.sender_canister_version, M.origin);
details = CodeUninstall;
};
}
canister_version[A.canister_id] = S.canister_version[A.canister_id] + 1
global_timer[A.canister_id] = 0
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin
response = Reply (candid())
refunded_cycles = M.transferred_cycles
} ·
[ ResponseMessage {
origin = Ctxt.origin
response = Reject (CANISTER_REJECT, <implementation-specific>)
refunded_cycles = Ctxt.available_cycles
}
| Ctxt_id ↦ Ctxt ∈ S.call_contexts
, Ctxt.canister = A.canister_id
, Ctxt.needs_to_respond = true
]
for Ctxt_id ↦ Ctxt ∈ S.call_contexts:
if Ctxt.canister = A.canister_id:
call_contexts[Ctxt_id].deleted := true
call_contexts[Ctxt_id].needs_to_respond := false
call_contexts[Ctxt_id].available_cycles := 0
BIG Management Cube: Stopping a canister
The controllers of a canister can stop a canister. Stopping a canister goes through two steps. First, the status of the canister is set to Stopping
; as explained above, a stopping canister rejects all incoming requests and continues processing outstanding responses. When a stopping canister has no more open call contexts, its status is changed to Stopped
and a response is generated. Note that when processing responses, a stopping canister can make calls to other canisters and thus create new call contexts. In addition, a canister which is stopped or stopping will accept (and respond) to further stop_canister
requests.
We encode this behavior via three (types of) transitions:
First, any
stop_canister
call sets the state of the canister toStopping
; we record in the BIG state the origin (and cycles) of allstop_canister
calls which arrive at the canister while it is stopping (or stopped). Note that every suchstop_canister
call can be rejected by the system at any time (the canister stays stopping in this case), e.g., if thestop_canister
call could not be responded to for a long time.Next, when the canister has no open call contexts (so, in particular, all outstanding responses to the canister have been processed), the status of the canister is set to
Stopped
.Finally, each pending
stop_canister
call (which are encoded in the status) is responded to, to indicate that the canister is stopped.
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'stop_canister'
M.arg = candid(A)
S.canister_status[A.canister_id] = Running
M.caller ∈ S.controllers[A.canister_id]
State after
S with
messages = Older_messages · Younger_messages
canister_status[A.canister_id] = Stopping [(M.origin, M.transferred_cycles)]
canister_version[A.canister_id] = S.canister_version[A.canister_id] + 1
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'stop_canister'
M.arg = candid(A)
S.canister_status[A.canister_id] = Stopping Origins
M.caller ∈ S.controllers[A.canister_id]
State after
S with
messages = Older_messages · Younger_messages
canister_status[A.canister_id] = Stopping (Origins · [(M.origin, M.transferred_cycles)])
canister_version[A.canister_id] = S.canister_version[A.canister_id] + 1
The status of a stopping canister which has no open call contexts is set to Stopped
, and all pending stop_canister
calls are replied to.
Conditions
S.canister_status[CanisterId] = Stopping Origins
∀ Ctxt_id. S.call_contexts[Ctxt_id].canister ≠ CanisterId
State after
S with
canister_status[CanisterId] = Stopped
canister_version[A.canister_id] = S.canister_version[A.canister_id] + 1
messages = S.Messages ·
[ ResponseMessage {
origin = O
response = Reply (candid())
refunded_cycles = C
}
| (O, C) ∈ Origins
]
Sending a stop_canister
message to an already stopped canister is acknowledged (i.e. responded with success) and the canister version is incremented, but is otherwise a no-op:
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'stop_canister'
M.arg = candid(A)
S.canister_status[A.canister_id] = Stopped
M.caller ∈ S.controllers[A.canister_id]
State after
S with
canister_version[A.canister_id] = S.canister_version[A.canister_id] + 1
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin;
response = Reply (candid());
refunded_cycles = M.transferred_cycles;
}
Pending stop_canister
calls may be rejected by the system at any time (the canister stays stopping in this case):
Conditions
S.canister_status[CanisterId] = Stopping (Older_origins · (O, C) · Younger_origins)
State after
S with
canister_status[CanisterId] = Stopping (Older_origins · Younger_origins)
messages = S.Messages ·
ResponseMessage {
origin = O
response = Reject (SYS_TRANSIENT, <implementation-specific>)
refunded_cycles = C
}
BIG Management Cube: Starting a canister
The controllers of a canister can start a stopped
canister. If the canister is already running, the command has no effect on the canister (except for incrementing its canister version).
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'start_canister'
M.arg = candid(A)
S.canister_status[A.canister_id] = Running or S.canister_status[A.canister_id] = Stopped
M.caller ∈ S.controllers[A.canister_id]
State after
S with
canister_status[A.canister_id] = Running
canister_version[A.canister_id] = S.canister_version[A.canister_id] + 1
messages = Older_messages · Younger_messages ·
ResponseMessage{
origin = M.origin
response = Reply (candid())
refunded_cycles = M.transferred_cycles
}
If the status of the canister was 'stopping', then the canister status is set to running
. The pending stop_canister
request(s) are rejected.
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'start_canister'
M.arg = candid(A)
S.canister_status[A.canister_id] = Stopping Origins
M.caller ∈ S.controllers[A.canister_id]
State after
S with
canister_status[A.canister_id] = Running
canister_version[A.canister_id] = S.canister_version[A.canister_id] + 1
messages = Older_messages · Younger_messages ·
ResponseMessage{
origin = M.origin
response = Reply (candid())
refunded_cycles = M.transferred_cycles
} ·
[ ResponseMessage {
origin = O
response = Reject (CANISTER_ERROR, <implementation-specific>)
refunded_cycles = C
}
| (O, C) ∈ Origins
]
BIG Management Cube: Cube deletion
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'delete_canister'
M.arg = candid(A)
S.canister_status[A.canister_id] = Stopped
M.caller ∈ S.controllers[A.canister_id]
State after
S with
canisters[A.canister_id] = (deleted)
controllers[A.canister_id] = (deleted)
compute_allocation[A.canister_id] = (deleted)
memory_allocation[A.canister_id] = (deleted)
freezing_threshold[A.canister_id] = (deleted)
canister_status[A.canister_id] = (deleted)
canister_version[A.canister_id] = (deleted)
canister_subnet[A.canister_id] = (deleted)
time[A.canister_id] = (deleted)
global_timer[A.canister_id] = (deleted)
balances[A.canister_id] = (deleted)
reserved_balances[A.canister_id] = (deleted)
reserved_balance_limits[A.canister_id] = (deleted)
certified_data[A.canister_id] = (deleted)
canister_history[A.canister_id] = (deleted)
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin
response = Reply (candid())
refunded_cycles = M.transferred_cycles
}
BIG Management Cube: Depositing cycles
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'deposit_cycles'
M.arg = candid(A)
A.canister_id ∈ dom(S.balances)
State after
S with
balances[A.canister_id] =
S.balances[A.canister_id] + M.transferred_cycles
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin
response = Reply (candid())
refunded_cycles = 0
}
BIG Management Cube: Random numbers
The management canister can produce pseudo-random bytes. It always returns a 32-byte blob
:
The precise guarantees around the randomness, e.g. unpredictability, are not captured in this formal semantics.
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'raw_rand'
M.arg = candid()
|B| = 32
State after
S with
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin
response = Reply (candid(B))
refunded_cycles = M.transferred_cycles
}
BIG Management Cube: Node Metrics
The node metrics management canister API is considered EXPERIMENTAL. Cube developers must be aware that the API may evolve in a non-backward-compatible way.
The management canister returns metrics for nodes on a given subnet. The definition of the metrics values is not captured in this formal semantics.
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'node_metrics_history'
M.arg = candid(A)
R = <implementation-specific>
State after
S with
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin
response = Reply (candid(R))
refunded_cycles = M.transferred_cycles
}
BIG Management Cube: Cube creation with cycles
This is a variant of create_canister
, which sets the initial cycle balance based on the amount
argument.
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'provisional_create_canister_with_cycles'
M.arg = candid(A)
is_system_assigned Canister_id
Canister_id ∉ dom(S.canisters)
if A.specified_id is not null:
Canister_id = A.specified_id
if A.settings.controllers is not null:
New_controllers = A.settings.controllers
else:
New_controllers = [M.caller]
if New_memory_allocation > 0:
memory_usage_canister_history(New_canister_history) ≤ New_memory_allocation
if A.settings.compute_allocation is not null:
New_compute_allocation = A.settings.compute_allocation
else:
New_compute_allocation = 0
if A.settings.memory_allocation is not null:
New_memory_allocation = A.settings.memory_allocation
else:
New_memory_allocation = 0
if A.settings.freezing_threshold is not null:
New_freezing_threshold = A.settings.freezing_threshold
else:
New_freezing_threshold = 2592000
if A.settings.reserved_cycles_limit is not null:
New_reserved_balance_limit = A.settings.reserved_cycles_limit
else:
New_reserved_balance_limit = 5_000_000_000_000
Cycles_reserved = cycles_to_reserve(S, Canister_id, New_compute_allocation, New_memory_allocation, EmptyCanister.wasm_state)
if A.amount is not null:
New_balance = A.amount - Cycles_reserved
else:
New_balance = DEFAULT_PROVISIONAL_CYCLES_BALANCE - Cycles_reserved
New_reserved_balance = Cycles_reserved
New_reserved_balance ≤ New_reserved_balance_limit
if New_compute_allocation > 0 or New_memory_allocation > 0 or Cycles_reserved > 0:
liquid_balance(
New_balance,
New_reserved_balance,
freezing_limit(
New_compute_allocation,
New_memory_allocation,
New_freezing_threshold,
memory_usage_canister_history(New_canister_history),
SubnetSize,
)
) ≥ 0
New_canister_history {
total_num_changes = 1
recent_changes = {
timestamp_nanos = CurrentTime
canister_version = 0
origin = change_origin(M.caller, A.sender_canister_version, M.origin)
details = Creation {
controllers = New_controllers
}
}
}
State after
S with
canisters[Canister_id] = EmptyCanister
time[Canister_id] = CurrentTime
global_timer[Canister_id] = 0
controllers[Canister_id] = New_controllers
compute_allocation[Canister_id] = New_compute_allocation
memory_allocation[Canister_id] = New_memory_allocation
freezing_threshold[Canister_id] = New_freezing_threshold
balances[Canister_id] = New_balance
reserved_balances[Canister_id] = New_reserved_balance
reserved_balance_limits[Canister_id] = New_reserved_balance_limit
certified_data[Canister_id] = ""
canister_history[Canister_id] = New_canister_history
messages = Older_messages · Younger_messages ·
ResponseMessage {
origin = M.origin
response = Reply (candid({canister_id = Canister_id}))
refunded_cycles = M.transferred_cycles
}
canister_status[Canister_id] = Running
canister_version[Canister_id] = 0
canister_subnet[Canister_id] = Subnet {
subnet_id : SubnetId
subnet_size : SubnetSize
}
BIG Management Cube: Top up canister
Conditions
S.messages = Older_messages · CallMessage M · Younger_messages
(M.queue = Unordered) or (∀ msg ∈ Older_messages. msg.queue ≠ M.queue)
M.callee = ic_principal
M.method_name = 'provisional_top_up_canister'
M.arg = candid(A)
A.canister_id ∈ dom(S.canisters)
State after
S with
balances[A.canister_id] = S.balances[A.canister_id] + A.amount
Callback invocation
When an inter-canister call has been responded to, we can queue the call to the callback.
This "bookkeeping transition" must be immediately followed by the corresponding "Message execution" transition.
Conditions
S.messages = Older_messages · ResponseMessage RM · Younger_messages
RM.origin = FromCanister {
call_context = Ctxt_id
callback = Callback
}
not S.call_contexts[Ctxt_id].deleted
S.call_contexts[Ctxt_id].canister ∈ dom(S.balances)
State after
S with
balances[S.call_contexts[Ctxt_id].canister] =
S.balances[S.call_contexts[Ctxt_id].canister] + RM.refunded_cycles
messages =
Older_messages ·
FuncMessage {
call_context = Ctxt_id
receiver = S.call_contexts[Ctxt_id].canister
entry_point = Callback Callback RM.response RM.refunded_cycles
queue = Unordered
} ·
Younger_messages
If the responded call context does not exist anymore, because the canister has been uninstalled since, the refunded cycles are still added to the canister balance, but no function invocation is enqueued:
Conditions
S.messages = Older_messages · ResponseMessage RM · Younger_messages
RM.origin = FromCanister {
call_context = Ctxt_id
callback = Callback
}
S.call_contexts[Ctxt_id].deleted
S.call_contexts[Ctxt_id].canister ∈ dom(S.balances)
State after
S with
balances[S.call_contexts[Ctxt_id].canister] =
S.balances[S.call_contexts[Ctxt_id].canister] + RM.refunded_cycles + MAX_CYCLES_PER_RESPONSE
messages = Older_messages · Younger_messages
Respond to user request
When an ingress method call has been responded to, we can record the response in the list of queries.
Conditions
S.messages = Older_messages · ResponseMessage RM · Younger_messages
RM.origin = FromUser { request = M }
S.requests[M] = (Processing, ECID)
State after
S with
messages = Older_messages · Younger_messages
requests[M] =
| (Replied R, ECID) if M.response = Reply R
| (Rejected (c, R), ECID) if M.response = Reject (c, R)
NB: The refunded cycles, RM.refunded_cycles
are, by construction, empty.
Request clean up
The BIG will keep the data for a completed or rejected request around for a certain, implementation defined amount of time, to allow users to poll for the data. After that time, the data of the request will be dropped:
Conditions
(S.requests[M] = (Replied _, ECID)) or (S.requests[M] = (Rejected _, ECID))
State after
S with
requests[M] = (Done, ECID)
At the same or some later point, the request will be removed from the state of the BIG. This must happen no earlier than the ingress expiry time set in the request.
Conditions
(S.requests[M] = (Replied _, _)) or (S.requests[M] = (Rejected _, _)) or (S.requests[M] = (Done, _))
M.ingress_expiry < S.system_time
State after
S with
requests[M] = (deleted)
Cube out of cycles
Once a canister runs out of cycles, its code is uninstalled (cf. BIG Management Cube: Code uninstallation), the canister changes in the canister history are dropped (their total number is preserved), and the allocations are set to zero (NB: allocations are currently not modeled in the formal model):
Conditions
S.balances[CanisterId] = 0
S.reserved_balances[CanisterId] = 0
S.canister_history[CanisterId] = {
total_num_changes = N;
recent_changes = H;
}
State after
S with
canisters[CanisterId] = EmptyCanister
certified_data[CanisterId] = ""
canister_history[CanisterId] = {
total_num_changes = N;
recent_changes = [];
}
canister_version[CanisterId] = S.canister_version[CanisterId] + 1
global_timer[CanisterId] = 0
messages = S.messages ·
[ ResponseMessage {
origin = Ctxt.origin
response = Reject (CANISTER_REJECT, <implementation-specific>)
refunded_cycles = Ctxt.available_cycles
}
| Ctxt_id ↦ Ctxt ∈ S.call_contexts
, Ctxt.canister = CanisterId
, Ctxt.needs_to_respond = true
]
for Ctxt_id ↦ Ctxt ∈ S.call_contexts:
if Ctxt.canister = CanisterId:
call_contexts[Ctxt_id].deleted := true
call_contexts[Ctxt_id].needs_to_respond := false
call_contexts[Ctxt_id].available_cycles := 0
Time progressing, cycle consumption, and canister version increments
Time progresses. Abstractly, it does so independently for each canister, and in unspecified intervals.
Conditions
T0 = S.time[CanisterId]
T1 > T0
State after
S with
time[CanisterId] = T1
The canister cycle balances similarly deplete at an unspecified rate, but stay non-negative. If the canister has a positive reserved balance, then the reserved balance depletes before the main balance:
Conditions
R0 = S.reserved_balances[CanisterId]
0 ≤ R1 < R0
State after
S with
reserved_balances[CanisterId] = R1
Once the reserved balance reaches zero, then the main balance starts depleting:
Conditions
S.reserved_balances[CanisterId] = 0
B0 = S.balances[CanisterId]
0 ≤ B1 < B0
State after
S with
balances[CanisterId] = B1
Similarly, the system time, used to expire requests, progresses:
Conditions
T0 = S.system_time
T1 > T0
State after
S with
system_time = T1
Finally, the canister version can be incremented arbitrarily:
Conditions
N0 = S.canister_version[CanisterId]
N1 > N0
State after
S with
canister_version[CanisterId] = N1
Trimming canister history
The list of canister changes can be trimmed, but the total number of recorded canister changes cannot be altered. At least 20 changes are guaranteed to remain in the list of changes.
Conditions
S.canister_history[CanisterId] = {
total_num_changes = N;
recent_changes = Older_changes · Newer_changes;
}
|Newer_changes| ≥ 20
State after
S with
canister_history[CanisterId] = {
total_num_changes = N;
recent_changes = Newer_changes;
}
Query call
Cube query calls to /api/v2/canister/<ECID>/query
can be executed directly. They can only be executed against non-empty canisters which have a status of Running
and are also not frozen.
In query and composite query methods evaluated on the target canister of the query call, a certificate is provided to the canister that is valid, contains a current state tree (or "recent enough"; the specification is currently vague about how old the certificate may be), and reveals the canister's Certified Data.
Composite query methods are EXPERIMENTAL and there might be breaking changes of their behavior in the future. Use at your own risk!
Composite query methods can call query methods and composite query methods up to a maximum depth MAX_CALL_DEPTH_COMPOSITE_QUERY
of the call graph. The total amount of cycles consumed by executing a (composite) query method and all (transitive) calls it makes must be at most MAX_CYCLES_PER_QUERY
. This limit applies in addition to the limit MAX_CYCLES_PER_MESSAGE
for executing a single (composite) query method and MAX_CYCLES_PER_RESPONSE
for executing a single callback of a (composite) query method.
We define an auxiliary method that handles calls from composite query methods by performing a call graph traversal. It can also be (trivially) invoked for query methods that do not make further calls.
composite_query_helper(S, Cycles, Depth, Root_canister_id, Caller, Canister_id, Method_name, Arg) =
let Mod = S.canisters[Canister_id].module
let Cert <- { Cert | verify_cert(Cert) and
lookup(["canister", Canister_id, "certified_data"], Cert) = Found S.certified_data[Canister_id] and
lookup(["time"], Cert) = Found S.system_time // or "recent enough"
}
if Canister_id ≠ Root_canister_id
then
Cert := NoCertificate // no certificate available in query and composite query methods evaluated on canisters other than the target canister of the query call
let Env = { time = S.time[Canister_id];
global_timer = S.global_timer[Canister_id];
balance = S.balances[Canister_id];
reserved_balance = S.reserved_balances[Canister_id];
reserved_balance_limit = S.reserved_balance_limits[Canister_id];
compute_allocation = S.compute_allocation[Canister_id];
memory_allocation = S.memory_allocation[Canister_id];
memory_usage_raw_module = memory_usage_raw_module(S.canisters[Canister_id].raw_module);
memory_usage_canister_history = memory_usage_canister_history(S.canister_history[Canister_id]);
freezing_threshold = S.freezing_threshold[Canister_id];
subnet_size = S.canister_subnet[Canister_id].subnet_size;
certificate = Cert;
status = simple_status(S.canister_status[Canister_id]);
canister_version = S.canister_version[Canister_id];
}
if S.canisters[Canister_id] ≠ EmptyCanister and
S.canister_status[Canister_id] = Running and
(Method_name ∈ dom(Mod.query_methods) or Method_name ∈ dom(Mod.composite_query_methods)) and
Cycles >= MAX_CYCLES_PER_MESSAGE
then
let W = S.canisters[Canister_id].wasm_state
let F = if Method_name ∈ dom(Mod.query_methods) then Mod.query_methods[Method_name] else Mod.composite_query_methods[Method_name]
if liquid_balance(
S.balances[Canister_id],
S.reserved_balances[Canister_id],
freezing_limit(
S.compute_allocation[Canister_id],
S.memory_allocation[Canister_id],
S.freezing_threshold[Canister_id],
memory_usage_wasm_state(S.canisters[Canister_id].wasm_state) +
memory_usage_raw_module(S.canisters[Canister_id].raw_module) +
memory_usage_canister_history(S.canister_history[Canister_id]),
S.canister_subnet[Canister_id].subnet_size,
)
) < 0
then
Return (Reject (SYS_TRANSIENT, <implementation-specific>), Cycles)
let R = F(Arg, Caller, Env)(W)
if R = Trap trap
then Return (Reject (CANISTER_ERROR, <implementation-specific>), Cycles - trap.cycles_used)
else if R = Return {new_state = W'; new_calls = Calls; response = Response; cycles_used = Cycles_used}
then
W := W'
if Cycles_used > MAX_CYCLES_PER_MESSAGE
then
Return (Reject (CANISTER_ERROR, <implementation-specific>), Cycles - MAX_CYCLES_PER_MESSAGE) // single message execution out of cycles
Cycles := Cycles - Cycles_used
if Response = NoResponse
then
while Calls ≠ []
do
if Depth = MAX_CALL_DEPTH_COMPOSITE_QUERY
then
Return (Reject (CANISTER_ERROR, <implementation-specific>), Cycles) // max call graph depth exceeded
let Calls' · Call · Calls'' = Calls
Calls := Calls' · Calls''
if S.canister_subnet[Canister_id].subnet_id ≠ S.canister_subnet[Call.callee].subnet_id
then
Return (Reject (CANISTER_ERROR, <implementation-specific>), Cycles) // calling to another subnet
let (Response', Cycles') = composite_query_helper(S, Cycles, Depth + 1, Root_canister_id, Canister_id, Call.callee, Call.method_name, Call.arg)
Cycles := Cycles'
if Cycles < MAX_CYCLES_PER_RESPONSE
then
Return (Reject (CANISTER_ERROR, <implementation-specific>), Cycles) // composite query out of cycles
Env.Cert = NoCertificate // no certificate available in composite query callbacks
let F' = Mod.composite_callbacks(Call.callback, Response', Env)
let R'' = F'(W')
if R'' = Trap trap''
then Return (Reject (CANISTER_ERROR, <implementation-specific>), Cycles - trap''.cycles_used)
else if R'' = Return {new_state = W''; new_calls = Calls''; response = Response''; cycles_used = Cycles_used''}
then
W := W''
if Cycles_used'' > MAX_CYCLES_PER_RESPONSE
then
Return (Reject (CANISTER_ERROR, <implementation-specific>), Cycles - MAX_CYCLES_PER_RESPONSE) // single message execution out of cycles
Cycles := Cycles - Cycles_used''
if Response'' = NoResponse
then
Calls := Calls'' · Calls
else
Return (Response'', Cycles)
Return (Reject (CANISTER_ERROR, <implementation-specific>), Cycles) // canister did not respond
else
Return (Response, Cycles)
else
Return (Reject (CANISTER_ERROR, <implementation-specific>), Cycles)
Submitted request
E
Conditions
E.content = CanisterQuery Q
Q.canister_id ∈ verify_envelope(E, Q.sender, S.system_time)
|Q.nonce| <= 32
is_effective_canister_id(E.content, ECID)
S.system_time <= Q.ingress_expiry
Query response R
:
if
composite_query_helper(S, MAX_CYCLES_PER_QUERY, 0, Q.canister_id, Q.sender, Q.canister_id, Q.method_name, Q.arg) = (Reject (RejectCode, RejectMsg), _)
then{status: "rejected"; reject_code: RejectCode; reject_message: RejectMsg; error_code: <implementation-specific>, signatures: Sigs}
Else if
composite_query_helper(S, MAX_CYCLES_PER_QUERY, 0, Q.canister_id, Q.sender, Q.canister_id, Q.method_name, Q.arg) = (Reply Res, _)
then{status: "replied"; reply: {arg: Res}, signatures: Sigs}
where the query Q
, the response R
, and a certificate Cert'
that is obtained by requesting the path /subnet
in a separate read state request to /api/v2/canister/<effective_canister_id>/read_state
satisfy the following:
verify_response(Q, R, Cert') ∧ lookup(["time"], Cert') = Found S.system_time // or "recent enough"
Certified state reads
Requesting paths with the prefix /subnet
at /api/v2/canister/<effective_canister_id>/read_state
might be deprecated in the future. Hence, users might want to point their requests for paths with the prefix /subnet
to /api/v2/subnet/<subnet_id>/read_state
.
On the BIG mainnet, the root subnet ID tdb26-jop6k-aogll-7ltgs-eruif-6kk7m-qpktf-gdiqx-mxtrf-vb5e6-eqe
can be used to retrieve the list of all BIG mainnet's subnets by requesting the prefix /subnet
at /api/v2/subnet/tdb26-jop6k-aogll-7ltgs-eruif-6kk7m-qpktf-gdiqx-mxtrf-vb5e6-eqe/read_state
.
The user can read elements of the state tree, using a read_state
request to /api/v2/canister/<ECID>/read_state
or /api/v2/subnet/<subnet_id>/read_state
.
Submitted request to /api/v2/canister/<ECID>/read_state
E
Conditions
E.content = ReadState RS
TS = verify_envelope(E, RS.sender, S.system_time)
|E.content.nonce| <= 32
S.system_time <= RS.ingress_expiry
∀ path ∈ RS.paths. may_read_path_for_canister(S, R.sender, path)
∀ (["request_status", Rid] · _) ∈ RS.paths. ∀ R ∈ dom(S.requests). hash_of_map(R) = Rid => R.canister_id ∈ TS
Read response
A record with
{certificate: C}
The predicate may_read_path_for_canister
is defined as follows, implementing the access control outlined in Request: Read state:
may_read_path_for_canister(S, _, ["time"]) = True
may_read_path_for_canister(S, _, ["subnet"]) = True
may_read_path_for_canister(S, _, ["subnet", sid]) = True
may_read_path_for_canister(S, _, ["subnet", sid, "public_key"]) = True
may_read_path_for_canister(S, _, ["subnet", sid, "canister_ranges"]) = True
may_read_path_for_canister(S, _, ["subnet", sid, "node"]) = True
may_read_path_for_canister(S, _, ["subnet", sid, "node", nid]) = True
may_read_path_for_canister(S, _, ["subnet", sid, "node", nid, "public_key"]) = True
may_read_path_for_canister(S, _, ["request_status", Rid]) =
may_read_path_for_canister(S, _, ["request_status", Rid, "status"]) =
may_read_path_for_canister(S, _, ["request_status", Rid, "reply"]) =
may_read_path_for_canister(S, _, ["request_status", Rid, "reject_code"]) =
may_read_path_for_canister(S, _, ["request_status", Rid, "reject_message"]) =
may_read_path_for_canister(S, _, ["request_status", Rid, "error_code"]) =
∀ (R ↦ (_, ECID')) ∈ dom(S.requests). hash_of_map(R) = Rid => RS.sender == R.sender ∧ ECID == ECID'
may_read_path_for_canister(S, _, ["canister", cid, "module_hash"]) = cid == ECID
may_read_path_for_canister(S, _, ["canister", cid, "controllers"]) = cid == ECID
may_read_path_for_canister(S, _, ["canister", cid, "metadata", name]) = cid == ECID ∧ UTF8(name) ∧
(cid ∉ dom(S.canisters[cid]) ∨
S.canisters[cid] = EmptyCanister ∨
name ∉ (dom(S.canisters[cid].public_custom_sections) ∪ dom(S.canisters[cid].private_custom_sections)) ∨
name ∈ dom(S.canisters[cid].public_custom_sections) ∨
(name ∈ dom(S.canisters[cid].private_custom_sections) ∧ RS.sender ∈ S.controllers[cid])
)
may_read_path_for_canister(S, _, _) = False
where UTF8(name)
holds if name
is encoded in UTF-8.
Submitted request to /api/v2/subnet/<subnet_id>/read_state
E
Conditions
E.content = ReadState RS
TS = verify_envelope(E, RS.sender, S.system_time)
|E.content.nonce| <= 32
S.system_time <= RS.ingress_expiry
∀ path ∈ RS.paths. may_read_path_for_subnet(S, RS.sender, path)
Read response
A record with
{certificate: C}
The predicate may_read_path_for_subnet
is defined as follows, implementing the access control outlined in Request: Read state:
may_read_path_for_subnet(S, _, ["time"]) = True
may_read_path_for_subnet(S, _, ["subnet"]) = True
may_read_path_for_subnet(S, _, ["subnet", sid]) = True
may_read_path_for_subnet(S, _, ["subnet", sid, "public_key"]) = True
may_read_path_for_subnet(S, _, ["subnet", sid, "canister_ranges"]) = True
may_read_path_for_subnet(S, _, ["subnet", sid, "metrics"]) = True
may_read_path_for_subnet(S, _, ["subnet", sid, "node"]) = True
may_read_path_for_subnet(S, _, ["subnet", sid, "node", nid]) = True
may_read_path_for_subnet(S, _, ["subnet", sid, "node", nid, "public_key"]) = True
may_read_path_for_subnet(S, _, _) = False
The response is a certificate cert
, as specified in Certification, which passes verify_cert
(assuming S.root_key
as the root of trust), and where for every path
documented in The system state tree that has a path in RS.paths
or ["time"]
as a prefix, we have
lookup_in_tree(path, cert.tree) = lookup_in_tree(path, state_tree(S))
where state_tree
constructs a labeled tree from the BIG state S
and the (so far underspecified) set of subnets subnets
, as per The system state tree
state_tree(S) = {
"time": S.system_time;
"subnet": { subnet_id : { "public_key" : subnet_pk; "canister_ranges" : subnet_ranges; "metrics" : <implementation-specific>; "node": { node_id : { "public_key" : node_pk } | (node_id, node_pk) ∈ subnet_nodes } } | (subnet_id, subnet_pk, subnet_ranges, subnet_nodes) ∈ subnets };
"request_status": { request_id(R): request_status_tree(T) | (R ↦ (T, _)) ∈ S.requests };
"canister":
{ canister_id :
{ "module_hash" : SHA256(C.raw_module) | if C ≠ EmptyCanister } ∪
{ "controllers" : CBOR(S.controllers[canister_id]) } ∪
{ "metadata": { name: blob | (name, blob) ∈ S.canisters[canister_id].public_custom_sections ∪ S.canisters[canister_id].private_custom_sections } }
| (canister_id, C) ∈ S.canisters };
}
request_status_tree(Received) =
{ "status": "received" }
request_status_tree(Processing) =
{ "status": "processing" }
request_status_tree(Rejected (code, msg)) =
{ "status": "rejected"; "reject_code": code; "reject_message": msg; "error_code": <implementation-specific>}
request_status_tree(Replied arg) =
{ "status": "replied"; "reply": arg }
request_status_tree(Done) =
{ "status": "done" }
and where lookup_in_tree
is a function that returns Found v
for a value v
, Absent
, or Error
, appropriately. See the Section Lookup for more details.
Abstract Canisters to System API
In Section Abstract canisters we introduced an abstraction over the interface to a canister, to avoid cluttering the abstract specification of the BigFile from WebAssembly details. In this section, we will fill the gap and explain how the abstract canister interface maps to the concrete System API and the WebAssembly concepts as defined in the WebAssembly specification.
The concrete WasmState
The abstract WasmState
above models the WebAssembly store S
, which encompasses the functions, tables, memories and globals of the WebAssembly program, plus additional data maintained by the BIG, such as the stable memory:
WasmState = {
store : S; // a store as per WebAssembly spec
self_id : CanId;
stable_mem : Blob
}
As explained in Section "WebAssembly module requirements", the WebAssembly module imports at most one memory and at most one table; in the following, the memory (resp. table) and the fields mem
and table
of S
refer to that. Any system call that accesses the memory (resp. table) will trap if the module does not import the memory (resp. table).
We model mem
as an array of bytes, and table
as an array of execution functions.
The abstract Callback
type above models an entry point for responses:
Closure = {
fun : i32,
env : i32,
}
Callback = {
on_reply : Closure;
on_reject : Closure;
on_cleanup : Closure | NoClosure;
}
The execution state
We can model the execution of WebAssembly functions as stateful functions that have access to the WebAssembly store. In order to also model the behavior of the system imports, which have access to additional data structures, we extend the state as follows:
Params = {
arg : NoArg | Blob;
caller : Principal;
reject_code : 0 | SYS_FATAL | SYS_TRANSIENT | …;
reject_message : Text;
sysenv : Env;
cycles_refunded : Nat;
method_name : NoText | Text;
}
ExecutionState = {
wasm_state : WasmState;
params : Params;
response : NoResponse | Response;
cycles_accepted : Nat;
cycles_available : Nat;
cycles_used : Nat;
balance : Nat;
reply_params : { arg : Blob };
pending_call : MethodCall | NoPendingCall;
calls : List MethodCall;
new_certified_data : NoCertifiedData | Blob;
new_global_timer : NoGlobalTimer | Nat;
ingress_filter : Accept | Reject;
context : I | G | U | Q | CQ | Ry | Rt | CRy | CRt | C | CC | F | T | s;
}
This allows us to model WebAssembly functions, including host-provided imports, as functions with implicit mutable access to an ExecutionState
, dubbed execution functions. Syntactically, we express this using an implicit argument of type ref ExecutionState
in angle brackets (e.g. func<es>(x)
for the invocation of a WebAssembly function with type (x : i32) -> ()
). The lifetime of the ExecutionState
data structure is that of one such function invocation.
It is nonsensical to pass to an execution function a WebAssembly store S
that comes from a different WebAssembly module than one defining the function.
For more convenience when creating a new
ExecutionState
, we define the following partial records:empty_params = {
arg = NoArg;
caller = ic_principal;
reject_code = 0;
reject_message = "";
sysenv = (undefined);
cycles_refunded = 0;
method_name = NoText;
}
empty_execution_state = {
wasm_state = (undefined);
params = (undefined);
response = NoResponse;
cycles_accepted = 0;
cycles_available = 0;
cycles_used = 0;
balance = 0;
reply_params = { arg = "" };
pending_call = NoPendingCall;
calls = [];
new_certified_data = NoCertifiedData;
new_global_timer = NoGlobalTimer;
ingress_filter = Reject;
context = (undefined);
}
The concrete CanisterModule
Finally we can specify the abstract CanisterModule
that models a concrete WebAssembly module.
The
initial_wasm_store
mentioned below is the store of the WebAssembly module after instantiation (as per WebAssembly spec) of the WasmModule contained in the canister module, before executing a potential(start)
function.We define a helper function
start : (CanisterId) -> Trap { cycles_used : Nat; } | Return {
new_state : WasmState;
cycles_used : Nat;
}modelling execution of a potential
(start)
function.If the WebAssembly module does not export a function called under the name
start
, thenstart = λ (self_id) →
Return {
new_state = {store = initial_wasm_store; self_id = self_id; stable_mem = ""};
cycles_used = 0;
}Otherwise, if the WebAssembly module exports a function
func
under the namestart
, it isstart = λ (self_id) →
let es = ref {empty_execution_state with
wasm_state = {store = initial_wasm_store; self_id = self_id; stable_mem = ""};
context = s;
}
try func<es>() with Trap then Trap {cycles_used = es.cycles_used;}
Return {
new_state = es.wasm_state;
cycles_used = es.cycles_used;
}Note that
params
are undefined in the(start)
function's execution state which is fine because the System API does not have access to that part of the execution state during the execution of the(start)
function.The
init
field of theCanisterModule
is defined as follows:If the WebAssembly module does not export a function called under the name
canister_init
, theninit = λ (self_id, arg, caller, sysenv) →
match start(self_id) with
Trap trap → Trap trap
Return res → Return {
new_state = res.wasm_state;
new_certified_data = NoCertifiedData;
new_global_timer = NoGlobalTimer;
cycles_used = res.cycles_used;
}Otherwise, if the WebAssembly module exports a function
func
under the namecanister_init
, it isinit = λ (self_id, arg, caller, sysenv) →
match start(self_id) with
Trap trap → Trap trap
Return res →
let es = ref {empty_execution_state with
wasm_state = res.wasm_state
params = empty_params with {
arg = arg;
caller = caller;
sysenv = sysenv with {
balance = sysenv.balance - res.cycles_used
}
}
balance = sysenv.balance - res.cycles_used
context = I
}
try func<es>() with Trap then Trap {cycles_used = res.cycles_used + es.cycles_used;}
Return {
new_state = es.wasm_state;
new_certified_data = es.new_certified_data;
new_global_timer = es.new_global_timer;
cycles_used = res.cycles_used + es.cycles_used;
}The
pre_upgrade
field of theCanisterModule
is defined as follows:If the WebAssembly module does not export a function called under the name
canister_pre_upgrade
, then it simply returns the stable memory:pre_upgrade = λ (old_state, caller, sysenv) → Return {stable_memory = old_state.stable_mem; new_certified_data = NoCertifiedData; cycles_used = 0;}
Otherwise, if the WebAssembly module exports a function
func
under the namecanister_pre_upgrade
, it ispre_upgrade = λ (old_state, caller, sysenv) →
let es = ref {empty_execution_state with
wasm_state = old_state
params = empty_params with { caller = caller; sysenv }
balance = sysenv.balance
context = G
}
try func<es>() with Trap then Trap {cycles_used = es.cycles_used;}
Return {
stable_memory = es.wasm_state.stable_mem;
new_certified_data = es.new_certified_data;
cycles_used = es.cycles_used;
}The
post_upgrade
field of theCanisterModule
is defined as follows:If the WebAssembly module does not export a function called under the name
canister_post_upgrade
, then the argument blob is ignored and theinitial_wasm_store
is returned:post_upgrade = λ (self_id, stable_mem, arg, caller, sysenv) →
Return {new_state = { store = initial_wasm_store; self_id = self_id; stable_mem = stable_mem }; new_certified_data = NoCertifiedData; new_global_timer = NoGlobalTimer; cycles_used = 0;}Otherwise, if the WebAssembly module exports a function
func
under the namecanister_post_upgrade
, it ispost_upgrade = λ (self_id, stable_mem, arg, caller, sysenv) →
let es = ref {empty_execution_state with
wasm_state = { store = initial_wasm_store; self_id = self_id; stable_mem = stable_mem }
params = empty_params with { arg = arg; caller = caller; sysenv }
balance = sysenv.balance
context = I
}
try func<es>() with Trap then Trap {cycles_used = es.cycles_used;}
Return {
new_state = es.wasm_state;
new_certified_data = es.new_certified_data;
new_global_timer = es.new_global_timer;
cycles_used = es.cycles_used;
}The partial map
update_methods
of theCanisterModule
is defined for all method namesmethod
for which the WebAssembly program exports a functionfunc
namedcanister_update <method>
, and has valueupdate_methods[method] = λ (arg, caller, sysenv, available) → λ wasm_state →
let es = ref {empty_execution_state with
wasm_state = wasm_state;
params = empty_params with { arg = arg; caller = caller; sysenv }
balance = sysenv.balance
cycles_available = available;
context = U
}
try func<es>() with Trap then Trap {cycles_used = es.cycles_used;}
Return {
new_state = es.wasm_state;
new_calls = es.calls;
new_certified_data = es.new_certified_data;
new_global_timer = es.new_global_timer;
response = es.response;
cycles_accepted = es.cycles_accepted;
cycles_used = es.cycles_used;
}The partial map
query_methods
of theCanisterModule
is defined for all method namesmethod
for which the WebAssembly program exports a functionfunc
namedcanister_query <method>
, and has valuequery_methods[method] = λ (arg, caller, sysenv) → λ wasm_state →
let es = ref {empty_execution_state with
wasm_state = wasm_state;
params = empty_params with { arg = arg; caller = caller; sysenv }
balance = sysenv.balance
context = Q
}
try func<es>() with Trap then Trap {cycles_used = es.cycles_used;}
Return {
response = es.response;
cycles_used = es.cycles_used;
}By construction, the (possibly modified)
es.wasm_state
is discarded.The partial map
composite_query_methods
of theCanisterModule
is defined for all method namesmethod
for which the WebAssembly program exports a functionfunc
namedcanister_composite_query <method>
, and has valuecomposite_query_methods[method] = λ (arg, caller, sysenv) → λ wasm_state →
let es = ref {empty_execution_state with
wasm_state = wasm_state;
params = empty_params with { arg = arg; caller = caller; sysenv }
balance = sysenv.balance
context = CQ
}
try func<es>() with Trap then Trap {cycles_used = es.cycles_used;}
Return {
new_state = es.wasm_state;
new_calls = es.calls;
response = es.response;
cycles_used = es.cycles_used;
}The function
heartbeat
of theCanisterModule
is defined if the WebAssembly program exports a functionfunc
namedcanister_heartbeat
, and has valueheartbeat = λ (sysenv) → λ wasm_state →
let es = ref {empty_execution_state with
wasm_state = wasm_state;
params = empty_params with { arg = NoArg; caller = ic_principal; sysenv }
balance = sysenv.balance
context = T
}
try func<es>() with Trap then Trap {cycles_used = es.cycles_used;}
Return {
new_state = es.wasm_state;
new_calls = es.calls;
new_certified_data = es.certified_data;
new_global_timer = es.new_global_timer;
cycles_used = es.cycles_used;
}otherwise it is
heartbeat = λ (sysenv) → λ wasm_state → Trap {cycles_used = 0;}
The function
global_timer
of theCanisterModule
is defined if the WebAssembly program exports a functionfunc
namedcanister_global_timer
, and has valueglobal_timer = λ (sysenv) → λ wasm_state →
let es = ref {empty_execution_state with
wasm_state = wasm_state;
params = empty_params with { arg = NoArg; caller = ic_principal; sysenv }
balance = sysenv.balance
context = T
}
try func<es>() with Trap then Trap {cycles_used = es.cycles_used;}
Return {
new_state = es.wasm_state;
new_calls = es.calls;
new_certified_data = es.certified_data;
new_global_timer = es.new_global_timer;
cycles_used = es.cycles_used;
}otherwise it is
global_timer = λ (sysenv) → λ wasm_state → Trap {cycles_used = 0;}
The function
callbacks
of theCanisterModule
is defined as followscallbacks = λ(callbacks, response, refunded_cycles, sysenv, available) → λ wasm_state →
let params0 = empty_params with {
sysenv
cycles_refunded = refund_cycles;
}
let (fun, env, params, context) = match response with
Reply data ->
(callbacks.on_reply.fun, callbacks.on_reply.env,
{ params0 with data}, Ry)
Reject (reject_code, reject_message)->
(callbacks.on_reject.fun, callbacks.on_reject.env,
{ params0 with reject_code; reject_message}, Rt)
let es = ref {empty_execution_state with
wasm_state = wasm_state;
params = params;
balance = sysenv.balance;
cycles_available = available;
context = context;
}
try
if fun > |es.wasm_state.store.table| then Trap
let func = es.wasm_state.store.table[fun]
if typeof(func) ≠ func (i32) -> () then Trap
func<es>(env)
Return {
new_state = es.wasm_state;
new_calls = es.calls;
new_certified_data = es.certified_data;
new_global_timer = es.new_global_timer;
response = es.response;
cycles_accepted = es.cycles_accepted;
cycles_used = es.cycles_used;
}
with Trap
if callbacks.on_cleanup = NoClosure then Trap {cycles_used = es.cycles_used;}
if callbacks.on_cleanup.fun > |es.wasm_state.store.table| then Trap {cycles_used = es.cycles_used;}
let func = es.wasm_state.store.table[callbacks.on_cleanup.fun]
if typeof(func) ≠ func (i32) -> () then Trap {cycles_used = es.cycles_used;}
let es' = ref { empty_execution_state with
wasm_state = wasm_state;
context = C;
}
try func<es'>(callbacks.on_cleanup.env) with Trap then Trap {cycles_used = es.cycles_used + es'.cycles_used;}
Return {
new_state = es'.wasm_state;
new_calls = [];
new_certified_data = NoCertifiedData;
new_global_timer = es'.new_global_timer;
response = NoResponse;
cycles_accepted = 0;
cycles_used = es.cycles_used + es'.cycles_used;
}Note that if the initial callback handler traps, the cleanup callback (if present) is executed, and the canister has the chance to update its state.
The function
composite_callbacks
of theCanisterModule
is defined as followscomposite_callbacks = λ(callbacks, response, sysenv) → λ wasm_state →
let params0 = empty_params with {
sysenv
}
let (fun, env, params, context) = match response with
Reply data ->
(callbacks.on_reply.fun, callbacks.on_reply.env,
{ params0 with data}, CRy)
Reject (reject_code, reject_message)->
(callbacks.on_reject.fun, callbacks.on_reject.env,
{ params0 with reject_code; reject_message}, CRt)
let es = ref {empty_execution_state with
wasm_state = wasm_state;
params = params;
balance = sysenv.balance;
context = context;
}
try
if fun > |es.wasm_state.store.table| then Trap
let func = es.wasm_state.store.table[fun]
if typeof(func) ≠ func (i32) -> () then Trap
func<es>(env)
Return {
new_state = es.wasm_state;
new_calls = es.calls;
response = es.response;
cycles_used = es.cycles_used;
}
with Trap
if callbacks.on_cleanup = NoClosure then Trap {cycles_used = es.cycles_used;}
if callbacks.on_cleanup.fun > |es.wasm_state.store.table| then Trap {cycles_used = es.cycles_used;}
let func = es.wasm_state.store.table[callbacks.on_cleanup.fun]
if typeof(func) ≠ func (i32) -> () then Trap {cycles_used = es.cycles_used;}
let es' = ref { empty_execution_state with
wasm_state = wasm_state;
context = CC;
}
try func<es'>(callbacks.on_cleanup.env) with Trap then Trap {cycles_used = es.cycles_used + es'.cycles_used;}
Return {
new_state = es'.wasm_state;
new_calls = [];
response = NoResponse;
cycles_used = es.cycles_used + es'.cycles_used;
}Note that if the initial callback handler traps, the cleanup callback (if present) is executed.
The
inspect_message
field of theCanisterModule
is defined as follows.If the WebAssembly module does not export a function called under the name
canister_inspect_message
, then access is always granted:inspect_message = λ (method_name, wasm_state, arg, caller, sysenv) →
Return {status = Accept;}Otherwise, if the WebAssembly module exports a function
func
under the namecanister_inspect_message
, it isinspect_message = λ (method_name, wasm_state, arg, caller, sysenv) →
let es = ref {empty_execution_state with
wasm_state = wasm_state;
params = empty_params with {
arg = arg;
caller = caller;
method_name = method_name;
sysenv
}
balance = sysenv.balance;
cycles_available = 0; // ingress requests have no funds
context = F;
}
try func<es>() with Trap then Trap
Return {status = es.ingress_filter;};
Helper functions
In the following section, we use the these helper functions
copy_to_canister<es>(dst : i32, offset : i32, size : i32, data : blob) =
if offset+size > |data| then Trap {cycles_used = es.cycles_used;}
if dst+size > |es.wasm_state.store.mem| then Trap {cycles_used = es.cycles_used;}
es.wasm_state.store.mem[dst..dst+size] := data[offset..offset+size]
copy_from_canister<es>(src : i32, size : i32) blob =
if src+size > |es.wasm_state.store.mem| then Trap {cycles_used = es.cycles_used;}
return es.wasm_state.store.mem[src..src+size]
Cycles are represented by 128-bit values so they require 16 bytes of memory.
copy_cycles_to_canister<es>(dst : i32, data : blob) =
let size = 16;
if dst+size > |es.wasm_state.store.mem| then Trap {cycles_used = es.cycles_used;}
es.wasm_state.store.mem[dst..dst+size] := data[0..size]
System imports
Upon instantiation of the WebAssembly module, we can provide the following functions as imports.
The pseudo-code below does not explicitly enforce the restrictions of which imports are available in which contexts; for that the table in Overview of imports is authoritative, and is assumed to be part of the implementation.
ic0.msg_arg_data_size<es>() : i32 =
if es.context ∉ {I, U, Q, CQ, Ry, CRy, F} then Trap {cycles_used = es.cycles_used;}
return |es.params.arg|
ic0.msg_arg_data_copy<es>(dst:i32, offset:i32, size:i32) =
if es.context ∉ {I, U, Q, CQ, Ry, CRy, F} then Trap {cycles_used = es.cycles_used;}
copy_to_canister<es>(dst, offset, size, es.params.arg)
ic0.msg_caller_size() : i32 =
if es.context = s then Trap {cycles_used = es.cycles_used;}
return |es.params.caller|
ic0.msg_caller_copy(dst:i32, offset:i32, size:i32) : i32 =
if es.context = s then Trap {cycles_used = es.cycles_used;}
copy_to_canister<es>(dst, offset, size, es.params.caller)
ic0.msg_reject_code<es>() : i32 =
if es.context ∉ {Ry, Rt, CRy, CRt} then Trap {cycles_used = es.cycles_used;}
es.params.reject_code
ic0.msg_reject_msg_size<es>() : i32 =
if es.context ∉ {Rt, CRt} then Trap {cycles_used = es.cycles_used;}
return |es.params.reject_msg|
ic0.msg_reject_msg_copy<es>(dst:i32, offset:i32, size:i32) : i32 =
if es.context ∉ {Rt, CRt} then Trap {cycles_used = es.cycles_used;}
copy_to_canister<es>(dst, offset, size, es.params.reject_msg)
ic0.msg_reply_data_append<es>(src : i32, size : i32) =
if es.context ∉ {U, Q, CQ, Ry, Rt, CRy, CRt} then Trap {cycles_used = es.cycles_used;}
if es.response ≠ NoResponse then Trap {cycles_used = es.cycles_used;}
es.reply_params.arg := es.reply_params.arg · copy_from_canister<es>(src, size)
ic0.msg_reply<es>() =
if es.context ∉ {U, Q, CQ, Ry, Rt, CRy, CRt} then Trap {cycles_used = es.cycles_used;}
if es.response ≠ NoResponse then Trap {cycles_used = es.cycles_used;}
es.response := Reply (es.reply_params.arg)
es.cycles_available := 0
ic0.msg_reject<es>(src : i32, size : i32) =
if es.context ∉ {U, Q, CQ, Ry, Rt, CRy, CRt} then Trap {cycles_used = es.cycles_used;}
if es.response ≠ NoResponse then Trap {cycles_used = es.cycles_used;}
es.response := Reject (CANISTER_REJECT, copy_from_canister<es>(src, size))
es.cycles_available := 0
ic0.msg_cycles_available<es>() : i64 =
if es.context ∉ {U, Rt, Ry} then Trap {cycles_used = es.cycles_used;}
if es.cycles_available >= 2^64 then Trap {cycles_used = es.cycles_used;}
return es.cycles_available
ic0.msg_cycles_available128<es>(dst : i32) =
if es.context ∉ {U, Rt, Ry} then Trap {cycles_used = es.cycles_used;}
let amount = es.cycles_available
copy_cycles_to_canister<es>(dst, amount.to_little_endian_bytes())
ic0.msg_cycles_refunded<es>() : i64 =
if es.context ∉ {Rt, Ry} then Trap {cycles_used = es.cycles_used;}
if es.params.cycles_refunded >= 2^64 then Trap {cycles_used = es.cycles_used;}
return es.params.cycles_refunded
ic0.msg_cycles_refunded128<es>(dst : i32) =
if es.context ∉ {Rt, Ry} then Trap {cycles_used = es.cycles_used;}
let amount = es.params.cycles_refunded
copy_cycles_to_canister<es>(dst, amount.to_little_endian_bytes())
ic0.msg_cycles_accept<es>(max_amount : i64) : i64 =
if es.context ∉ {U, Rt, Ry} then Trap {cycles_used = es.cycles_used;}
let amount = min(max_amount, es.cycles_available)
es.cycles_available := es.cycles_available - amount
es.cycles_accepted := es.cycles_accepted + amount
es.balance := es.balance + amount
return amount
ic0.msg_cycles_accept128<es>(max_amount_high : i64, max_amount_low : i64, dst : i32) =
if es.context ∉ {U, Rt, Ry} then Trap {cycles_used = es.cycles_used;}
let max_amount = max_amount_high * 2^64 + max_amount_low
let amount = min(max_amount, es.cycles_available)
es.cycles_available := es.cycles_available - amount
es.cycles_accepted := es.cycles_accepted + amount
es.balance := es.balance + amount
copy_cycles_to_canister<es>(dst, amount.to_little_endian_bytes())
ic0.cycles_burn128<es>(amount_high : i64, amount_low : i64, dst : i32) =
if es.context ∉ {I, G, U, Ry, Rt, C, T} then Trap {cycles_used = es.cycles_used;}
let amount = amount_high * 2^64 + amount_low
let burned_amount = min(
amount,
liquid_balance(
es.balance,
es.params.sysenv.reserved_balance,
freezing_limit(
es.params.sysenv.compute_allocation,
es.params.sysenv.memory_allocation,
es.params.sysenv.freezing_threshold,
memory_usage_wasm_state(es.wasm_state) + es.params.sysenv.memory_usage_raw_module + es.params.sysenv.memory_usage_canister_history,
es.params.sysenv.subnet_size,
)
)
)
es.balance := es.balance - burned_amount
copy_cycles_to_canister<es>(dst, burned_amount.to_little_endian_bytes())
ic0.canister_self_size<es>() : i32 =
if es.context = s then Trap {cycles_used = es.cycles_used;}
return |es.wasm_state.self_id|
ic0.canister_self_copy<es>(dst:i32, offset:i32, size:i32) =
if es.context = s then Trap {cycles_used = es.cycles_used;}
copy_to_canister<es>(dst, offset, size, es.wasm_state.self_id)
ic0.canister_cycle_balance<es>() : i64 =
if es.context = s then Trap {cycles_used = es.cycles_used;}
if es.balance >= 2^64 then Trap {cycles_used = es.cycles_used;}
return es.balance
ic0.canister_cycles_balance128<es>(dst : i32) =
if es.context = s then Trap {cycles_used = es.cycles_used;}
let amount = es.balance
copy_cycles_to_canister<es>(dst, amount.to_little_endian_bytes())
ic0.canister_status<es>() : i32 =
if es.context = s then Trap {cycles_used = es.cycles_used;}
match es.params.sysenv.canister_status with
Running -> return 1
Stopping -> return 2
Stopped -> return 3
ic0.canister_version<es>() : i64 =
if es.context = s then Trap {cycles_used = es.cycles_used;}
return es.params.sysenv.canister_version
ic0.msg_method_name_size<es>() : i32 =
if es.context ∉ {F} then Trap {cycles_used = es.cycles_used;}
return |es.method_name|
ic0.msg_method_name_copy<es>(dst : i32, offset : i32, size : i32) : i32 =
if es.context ∉ {F} then Trap {cycles_used = es.cycles_used;}
copy_to_canister<es>(dst, offset, size, es.params.method_name)
ic0.accept_message<es>() =
if es.context ∉ {F} then Trap {cycles_used = es.cycles_used;}
if es.ingress_filter = Accept then Trap {cycles_used = es.cycles_used;}
es.ingress_filter = Accept
ic0.call_new<es>(
callee_src : i32,
callee_size : i32,
name_src : i32,
name_size : i32,
reply_fun : i32,
reply_env : i32,
reject_fun : i32,
reject_env : i32,
) =
if es.context ∉ {U, CQ, Ry, Rt, CRy, CRt, T} then Trap {cycles_used = es.cycles_used;}
discard_pending_call<es>()
if es.balance < MAX_CYCLES_PER_RESPONSE then Trap {cycles_used = es.cycles_used;}
es.balance := es.balance - MAX_CYCLES_PER_RESPONSE
callee := copy_from_canister<es>(callee_src, callee_size);
method_name := copy_from_canister<es>(name_src, name_size);
if reply_fun > |es.wasm_state.store.table| then Trap {cycles_used = es.cycles_used;}
if typeof(es.wasm_state.store.table[reply_fun]) ≠ func (anyref, i32) -> () then Trap {cycles_used = es.cycles_used;}
if reject_fun > |es.wasm_state.store.table| then Trap {cycles_used = es.cycles_used;}
if typeof(es.wasm_state.store.table[reject_fun]) ≠ func (anyref, i32) -> () then Trap {cycles_used = es.cycles_used;}
es.pending_call = MethodCall {
callee = callee;
method_name = callee;
arg = "";
transferred_cycles = 0;
callback = Callback {
on_reply = Closure { fun = reply_fun; env = reply_env }
on_reject = Closure { fun = reject_fun; env = reject_env }
on_cleanup = NoClosure
};
}
ic0.call_on_cleanup<es> (fun : i32, env : i32) =
if es.context ∉ {U, CQ, Ry, Rt, CRy, CRt, T} then Trap {cycles_used = es.cycles_used;}
if fun > |es.wasm_state.store.table| then Trap {cycles_used = es.cycles_used;}
if typeof(es.wasm_state.store.table[fun]) ≠ func (anyref, i32) -> () then Trap {cycles_used = es.cycles_used;}