2. States and Transitions
Definition 1. Discrete State Machine (DSM)
A Discrete State Machine (DSM) is a state transition system that admits a starting state and whose set of states and set of transitions are countable. Formally, it is a tuple of
is the countable set of all possible inputs.
is a countable set of all possible states.
is the initial state.
is the state-transition function, known as Runtime in the Polkadot vocabulary, such that
Definition 2. Path Graph
A path graph or a path of nodes formally referred to as , is a tree with two nodes of vertex degree 1 and the other n-2 nodes of vertex degree 2. Therefore, can be represented by sequences of where for is the edge which connect and .
Definition 3. Blockchain
A blockchain is a directed path graph. Each node of the graph is called Block and indicated by . The unique sink of is called Genesis Block, and the source is called the of . For any vertex where we say is the parent of , which is the child of , respectively. We indicate that by:
The parent refers to the child by its hash value (Definition 10), making the path graph tamper proof since any modifications to the child would result in its hash value being changed.
The term "blockchain" can also be used as a way to refer to the network or system that interacts or maintains the directed path graph.
2.1.1. Block Tree
In the course of formation of a (distributed) blockchain, it is possible that the chain forks into multiple subchains in various block positions. We refer to this structure as a block tree:
Definition 4. Block
The block tree of a blockchain, denoted by is the union of all different versions of the blockchain observed by the Polkadot Host such that every block is a node in the graph and is connected to if is a parent of .
When a block in the block tree gets finalized, there is an opportunity to prune the block tree to free up resources into branches of blocks that do not contain all of the finalized blocks or those that can never be finalized in the blockchain (Chapter 6).
Definition 5. Pruned Block Tree
By Pruned Block Tree, denoted by , we refer to a subtree of the block tree obtained by eliminating all branches which do not contain the most recent finalized blocks (Definition 90). By pruning, we refer to the procedure of . When there is no risk of ambiguity and is safe to prune BT, we use to refer to .
Definition 6 gives the means to highlight various branches of the block tree.
Definition 6. Subchain
Let be the root of the block tree and be one of its nodes. By , we refer to the path graph from to in . Conversely, for a chain , we define the head of to be , formally noted as . We define , the length of as a path graph.
If is another node on , then by we refer to the subgraph of path graph which contains and ends at and by we refer to its length.
Accordingly, is the set of all subchains of rooted at . The set of all chains of , is denoted by or simply , for the sake of brevity.
Definition 7. Longest Chain
We define the following complete order over as follows. For chains we have that if either or .
If we say if and only if the block arrival time (Definition 67) of is less than the block arrival time of , from the subjective perspective of the Host. We define the to be the maximum chain given by this order.
Definition 8. Longest Path
returns the path graph of which is the longest among all paths in and has the earliest block arrival time (Definition 67). returns the head of chain.
Because every block in the blockchain contains a reference to its parent, it is easy to see that the block tree is de facto a tree. A block tree naturally imposes partial order relationships on the blocks as follows:
Definition 9. Descendant and Ancestor
We say is descendant of , formally noted as , if . Respectively, we say that is an ancestor of , formally noted as , if .
2.2. State Replication
Polkadot nodes replicate each other’s state by syncing the history of the extrinsics. This, however, is only practical if a large set of transactions are batched and synced at the time. The structure in which the transactions are journaled and propagated is known as a block of extrinsics (Section 2.2.1.). Like any other replicated state machines, state inconsistency can occure between Polkadot replicas. Section 2.4.5. gives an overview of how a Polkadot Host node manages multiple variants of the state.
2.2.1. Block Format
A Polkadot block consists a block header (Definition 10) and a block body (Definition 13). The block body in turn is made up out of extrinsics , which represent the generalization of the concept of transactions. Extrinsics can contain any set of external data the underlying chain wishes to validate and track.
Image 1. Block
Definition 10. Block Header
The header of block B, , is a 5-tuple containing the following elements:
parent_hash: formally indicated as , is the 32-byte Blake2b hash (Section A.1.1.1.) of the SCALE encoded parent block header (Definition 12).
number: formally indicated as , is an integer, which represents the index of the current block in the chain. It is equal to the number of the ancestor blocks. The genesis state has number 0.
state_root: formally indicated as , is the root of the Merkle trie, whose leaves implement the storage for the system.
extrinsics_root: is the field which is reserved for the Runtime to validate the integrity of the extrinsics composing the block body. For example, it can hold the root hash of the Merkle trie which stores an ordered list of the extrinsics being validated in this block. The extrinsics_root is set by the runtime and its value is opaque to the Polkadot Host. This element is formally referred to as .
digest: this field is used to store any chain-specific auxiliary data, which could help the light clients interact with the block without the need of accessing the full storage as well as consensus-related data including the block signature. This field is indicated as (Definition 11).
Image 2. Block Header
Definition 11. Header Digest
The header digest of block formally referred to by is an array of digest items ’s, known as digest items of varying data type (Definition 188) such that:
where each digest item can hold one of the following type identifiers:
is a 4-byte ASCII encoded consensus engine identifier
is a scale encoded byte array containing the message payload
Consensus Message, contains scale-encoded message from the Runtime to the consensus engine. The receiving engine is determined by the id identifier:
- id = BABE: a message to BABE engine (Definition 58)
- id = FRNK: a message to GRANDPA engine (Definition 86)
- id = BEEF: a message to BEEFY engine (Definition 87)
Seal, is produced by the consensus engine and proves the authorship of the block producer. The engine used for this is provided through id (at the moment
BABE), while contains the scale-encoded signature (Definition 70) of the block producer. In particular, the Seal digest item must be the last item in the digest array and must be stripped off by the Polkadot Host before the block is submitted to any Runtime function including for validation. The Seal must be added back to the digest afterward.
Pre-Runtime digest, contains messages from the consensus engines to the runtime. Currently only used by BABE to pass the scale encoded BABE Header (Definition 69) in with id =
Runtime Environment Updated digest, indicates that changes regarding the Runtime code or heap pages (Section 184.108.40.206.) occurred. No additional data is provided.
Image 3. Digest
Definition 12. Header Hash
The block header hash of block , , is the hash of the header of block encoded by simple codec:
Definition 13. Block Body
The block body consists of an sequence of extrinsics, each encoded as a byte array. The content of an extrinsic is completely opaque to the Polkadot Host. As such, from the point of the Polkadot Host, and is simply a SCALE encoded array of byte arrays. The body of Block represented as is defined to be:
Where each is a SCALE encoded extrinsic.
Image 4. Block Body
The block body consists of an array of extrinsics. In a broad sense, extrinsics are data from outside of the state which can trigger state transitions. This section describes extrinsics and their inclusion into blocks.
The extrinsics are divided into two main categories defined as follows:
Transaction extrinsics are extrinsics which are signed using either of the key types (Section A.1.4.) and broadcasted between the nodes. Inherent extrinsics are unsigned extrinsics which are generated by Polkadot Host and only included in the blocks produced by the node itself. They are broadcasted as part of the produced blocks rather than being gossiped as individual extrinsics.
The Polkadot Host does not specify or limit the internals of each extrinsics and those are defined and dealt with by the Runtime (Definition 1). From the Polkadot Host point of view, each extrinsics is simply a SCALE-encoded blob (Section A.2.2.).
Transaction are submitted and exchanged through Transactions network messages (Section 4.8.5.). Upon receiving a Transactions message, the Polkadot Host decodes the SCALE-encoded blob and splits it into individually SCALE-encoded transactions.
Alternative transaction can be submitted to the host by offchain worker through the Host API (Section B.6.2.).
Any new transaction should be submitted to the Runtime (Section C.7.1.). This will allow the Polkadot Host to check the validity of the received transaction against the current stat and if it should be gossiped to other peers. If it considers the submitted transaction as valid, the Polkadot Host should store it for inclusion in future blocks. The whole process of handling new transactions is described in more detail by Validate-Transactions-and-Store.
Additionally valid transactions that are supposed to be gossiped are propagated to connected peers of the Polkadot Host. While doing so the Polkadot Host should keep track of peers already aware of each transaction. This includes peers which have already gossiped the transaction to the node as well as those to whom the transaction has already been sent. This behavior is mandated to avoid resending duplicates and unnecessarily overloading the network. To that aim, the Polkadot Host should keep a transaction pool and a transaction queue defined as follows:
Definition 14. Transaction Queue
The Transaction Queue of a block producer node, formally referred to as is a data structure which stores the transactions ready to be included in a block sorted according to their priorities (Section 4.8.5.). The Transaction Pool, formally referred to as , is a hash table in which the Polkadot Host keeps the list of all valid transactions not in the transaction queue.
Furthermore Validate-Transactions-and-Store updates the transaction pool and the transaction queue according to the received message:
Algorithm 1. Validate Transactions and Store
is the transaction message (offchain transactions?)
decodes the SCALE encoded message.
is defined in Definition 7.
is a Runtime entrypoint specified in Section C.7.1. and , and refer to the corresponding fields in the tuple returned by the entrypoint when it deems that is valid.
is the list of tags that transaction provides. The Polkadot Host needs to keep track of tags that transaction provides as well as requires after validating it.
places into approperietly such that the transactions providing the tags which requires or have higher priority than are ahead of .
is described in Maintain-Transaction-Pool.
indictes whether the transaction should be propagated based on the
Propagatefield in the
ValidTransactiontype as defined in Definition 225, which is returned by .
sends to all connected peers of the Polkadot Host who are not already aware of .
Algorithm 2. Maintain Transaction Pool
This has not been defined yet.
Inherents are unsigned extrinsics inserted into a block by the block author and as a result are not stored in the transaction pool or gossiped across the network. Instead they are generated by the Polkadot Host by passing the required inherent data, as listed in Table 1, to the Runtime method (Section C.6.3.). Then the returned extrinsics should be included in the current block as explained in Build-Block.
Table 1. Inherent Data
|timstap0||Unsigned 64-bit integer||Unix epoch time (Definition 181)|
|babeslot||Unsigned 64-bit integer||The babe slot (DEPRECATED) (Definition 54)|
|parachn0||Parachain inherent data (Definition 103)||Parachain candidate inclusion (Section 8.2.2.)|
Definition 15. Inherent Data
Inherent-Data is a hashtable (Definition 192), an array of key-value pairs consisting of the inherent 8-byte identifier and its value, representing the totality of inherent extrinsics included in each block. The entries of this hash table which are listed in Table 1 are collected or generated by the Polkadot Host and then handed to the Runtime for inclusion (Build-Block).
2.4. State Storage Trie
For storing the state of the system, Polkadot Host implements a hash table storage where the keys are used to access each data entry. There is no assumption either on the size of the key nor on the size of the data stored under them, besides the fact that they are byte arrays with specific upper limits on their length. The limit is imposed by the encoding algorithms to store the key and the value in the storage trie (Section A.2.2.1.).
2.4.1. Accessing System Storage
The Polkadot Host implements various functions to facilitate access to the system storage for the Runtime (Section 2.6.1.). Here we formalize the access to the storage when it is being directly accessed by the Polkadot Host (in contrast to Polkadot runtime).
Definition 16. Stored Value
The function retrieves the value stored under a specific key in the state storage and is formally defined as:
where and are respectively the set of all keys and values stored in the state storage. can be an empty value.
2.4.2. General Structure
In order to ensure the integrity of the state of the system, the stored data needs to be re-arranged and hashed in a radix tree, which hereafter we refer to as the State Trie or just Trie. This rearrangment is necessary to be able to compute the Merkle hash of the whole or part of the state storage, consistently and efficiently at any given time.
The trie is used to compute the merkle root (Section 2.4.4.) of the state, (Definition 10), whose purpose is to authenticate the validity of the state database. Thus, the Polkadot Host follows a rigorous encoding algorithm to compute the values stored in the trie nodes to ensure that the computed Merkle hash, , matches across the Polkadot Host implementations.
The trie is a radix-16 tree (Definition 17). Each key value identifies a unique node in the tree. However, a node in a tree might or might not be associated with a key in the storage.
Definition 17. Radix-r Tree
A Radix-r tree is a variant of a trie in which:
Every node has at most children where for some ;
Each node that is the only child of a parent, which does not represent a valid key is merged with its parent.
As a result, in a radix tree, any path whose interior vertices all have only one child and does not represent a valid key in the data set, is compressed into a single edge. This improves space efficiency when the key space is sparse.
When traversing the trie to a specific node, its key can be reconstructed by concatenating the subsequences of the keys which are stored either explicitly in the nodes on the path or implicitly in their position as a child of their parent.
To identify the node corresponding to a key value, , first we need to encode in a way consistent with the trie structure. Because each node in the trie has at most 16 children, we represent the key as a sequence of 4-bit nibbles:
Definition 18. Key Encode
For the purpose of labeling the branches of the trie, the key is encoded to using functions:
where is the set of all nibbles of 4-bit arrays and and are 4-bit nibbles, which are the big endian representations of :
where is the remainder and is the integer division operators.
By looking at as a sequence of nibbles, one can walk the radix tree to reach the node identifying the storage value of .
2.4.3. Trie Structure
In this subsection, we specify the structure of the nodes in the trie as well as the trie structure:
Definition 19. Set of Nodes
We refer to the set of the nodes of Polkadot state trie by . By to refer to an individual node in the trie.
Definition 20. State Trie
The state trie is a radix-16 tree (Definition 17). Each node in the trie is identified with a unique key such that:
- is the shared prefix of the key of all the descendants of in the trie.
and, at least one of the following statements holds:
corresponds to an existing entry in the State Storage.
has more than one child.
Conversely, if is an entry in the state trie then there is a node such that .
Definition 21. Branch
A branch node is a node which has one child or more. A branch node can have at most 16 children. A leaf node is a childless node. Accordingly:
For each node, part of is built while the trie is traversed from the root to and another part of is stored in (Definition 22).
Definition 22. Aggregated Prefix Key
For any , its key is divided into an aggregated prefix key, , aggregated by Aggregate-Key and a partial key, of length in nibbles such that:
where is a prefix subsequence of ; is the length of in nibbles and so we have:
Part of is explicitly stored in ’s ancestors. Additionally, for each ancestor, a single nibble is implicitly derived while traversing from the ancestor to its child included in the traversal path using the function (Definition 23).
Definition 23. Index
For and child of , we define function as:
Algorithm 3. Aggregate-Key
Assuming that is the path (Definition 2) from the trie root to node , Aggregate-Key rigorously demonstrates how to build while traversing .
Definition 24. Node Value
A node stores the node value, , which consists of the following concatenated data:
Formally noted as:
is the node header from Definition 25
is the partial key from Definition 22
is hex encoding (Definition 199)
is the node subvalue from Definition 27
Definition 25. Node Header
The node header, consisting of bytes, , specifies the node variant and the partial key length (Definition 22). Both pieces of information can be represented in bits within a single byte, , where the amount of bits of the variant, , and the bits of the partial key length, varies.
If the value of is equal to the maximum possible value the bits can hold, such as 63 () in case of the variant, then the value of the next 8 bits () are added the the length. This process is repeated for every where . Any value smaller than the maximum possible value of implies that the next value of should not be added to the length. The hashed subvalue for variants and is described in Definition 28.
Formally, the length of the partial key, , is defined as:
as long as , and , where is the maximum possible value that can hold.
2.4.4. Merkle Proof
To prove the consistency of the state storage across the network and its modifications both efficiently and effectively, the trie implements a Merkle tree structure. The hash value corresponding to each node needs to be computed rigorously to make the inter-implementation data integrity possible.
The Merkle value of each node should depend on the Merkle value of all its children as well as on its corresponding data in the state storage. This recursive dependency is encompassed into the subvalue part of the node value which recursively depends on the Merkle value of its children. Additionally, as Section 2.5.1. clarifies, the Merkle proof of each child trie must be updated first before the final Polkadot state root can be calculated.
We use the auxiliary function introduced in Definition 26 to encode and decode information stored in a branch node.
Definition 26. Children Bitmap
Suppose and is a child of . We define bit if and only if has a child with index , therefore we define ChildrenBitmap functions as follows:
Definition 27. Subvalue
For a given node , the subvalue of , formally referred to as , is determined as follows:
where the first variant is a leaf node and the second variant is a branch node.
with are the children nodes of the branch node .
is defined in Section A.2.2..
, where can be empty, is defined in Definition 16.
is defined in Definition 29.
is defined in Definition 26.
The trie deviates from a traditional Merkle tree in that the node value (Definition 24), , is presented instead of its hash if it occupies less space than its hash.
Definition 28. Hashed Subvalue
To increase performance, a merkle proof can be generated by inserting the hash of a value into the trie rather than the value itself (which can be quite large). If merkle proof computation with node hashing is explicitly executed via the Host API (Section B.2.8.2.), then any value larger than 32 bytes is hashed, resulting in that hash being used as the subvalue (Definition 27) under the corresponding key. The node header must specify the variant and respectively for leaves containing a hash as their subvalue and for branches containing a hash as their subvalue (Definition 25).
Definition 29. Merkle Value
For a given node , the Merkle value of , denoted by is defined as follows:
Where is the node value of (Definition 24) and is the root of the trie. The Merkle hash of the trie is defined to be .
2.4.5. Managing Multiple Variants of State
Unless a node is committed to only update its state according to the finalized block (Definition 90), it is inevitable for the node to store multiple variants of the state (one for each block). This is, for example, necessary for nodes participating in the block production and finalization.
While the state trie structure (Section 2.4.3.) facilitates and optimizes storing and switching between multiple variants of the state storage, the Polkadot Host does not specify how a node is required to accomplish this task. Instead, the Polkadot Host is required to implement (Definition 30):
Definition 30. Set State At Block
in which is a block in the block tree (Definition 4), sets the content of state storage equal to the resulting state of executing all extrinsics contained in the branch of the block tree from genesis till block B including those recorded in Block .
For the definition of the state storage see Section 2.4..
2.5. Child Storage
As clarified in Section 2.4., the Polkadot state storage implements a hash table for inserting and reading key-value entries. The child storage works the same way but is stored in a separate and isolated environment. Entries in the child storage are not directly accessible via querying the main state storage.
The Polkadot Host supports as many child storages as required by Runtime and identifies each separate child storage by its unique identifying key. Child storages are usually used in situations where Runtime deals with multiple instances of a certain type of objects such as Parachains or Smart Contracts. In such cases, the execution of the Runtime entrypoint might result in generating repeated keys across multiple instances of certain objects. Even with repeated keys, all such instances of key-value pairs must be able to be stored within the Polkadot state.
In these situations, the child storage can be used to provide the isolation necessary to prevent any undesired interference between the state of separated instances. The Polkadot Host makes no assumptions about how child storages are used, but provides the functionality for it via the Host API (Section B.3.).
2.5.1. Child Tries
The child trie specification is the same as the one described in Section 2.4.3.. Child tries have their own isolated environment. Nonetheless, the main Polkadot state trie depends on them by storing a node () which corresponds to an individual child trie. Here, is the child storage key associated to the child trie, and is the Merkle value of its corresponding child trie computed according to the procedure described in Section 2.4.4..
The Polkadot Host API (Section B.3.) allows the Runtime to provide the key in order to identify the child trie, followed by a second key in order to identify the value within that child trie. Every time a child trie is modified, the Merkle proof of the child trie stored in the Polkadot state must be updated first. After that, the final Merkle proof of the Polkadot state can be computed. This mechanism provides a proof of the full Polkadot state including all its child states.
2.6. Runtime Interactions
Like any transaction-based transition system, Polkadot’s state is changed by executing an ordered set of instructions. These instructions are known as extrinsics. In Polkadot, the execution logic of the state transition function is encapsulated in a Runtime (Definition 1). For easy upgradability this Runtime is presented as a Wasm blob. Nonetheless, the Polkadot Host needs to be in constant interaction with the Runtime (Section 2.6.1.).
In Section 2.3., we specify the procedure of the process where the extrinsics are submitted, pre-processed and validated by Runtime and queued to be applied to the current state.
To make state replication feasible, Polkadot journals and batches series of its extrinsics together into a structure known as a block, before propagating them to other nodes, similar to most other prominent distributed ledger systems. The specification of the Polkadot block as well as the process of verifying its validity are both explained in Section 2.2..
2.6.1. Interacting with the Runtime
The Runtime (Definition 1) is the code implementing the logic of the chain. This code is decoupled from the Polkadot Host to make the the logic of the chain easily upgradable without the need to upgrade the Polkadot Host itself. The general procedure to interact with the Runtime is described by Interact-With-Runtime.
Algorithm 4. Interact With Runtime
is the runtime entrypoint call.
is the block hash indicating the state at the end of .
are arguments to be passed to the runtime entrypoint.
In this section, we describe the details upon which the Polkadot Host is interacting with the Runtime. In particular, and procedures called by Interact-With-Runtime are explained in Definition 32 and Definition 30 respectively. is the Runtime code loaded from , as described in Definition 31, and is the Polkadot Host API, as described in Definition 201.
2.6.2. Loading the Runtime Code
The Polkadot Host expects to receive the code for the Runtime of the chain as a compiled WebAssembly (Wasm) Blob. The current runtime is stored in the state database under the key represented as a byte array:
which is the ASCII byte representation of the string
:code (Section A.3.). As a result of storing the Runtime as part of the state, the Runtime code itself becomes state sensitive and calls to Runtime can change the Runtime code itself. Therefore the Polkadot Host needs to always make sure to provide the Runtime corresponding to the state in which the entrypoint has been called. Accordingly, we define (Definition 31).
The initial Runtime code of the chain is provided as part of the genesis state (Section A.3.) and subsequent calls to the Runtime have the ability to, in turn, upgrade the Runtime by replacing this Wasm blob with the help of the storage API (Section B.2.). Therefore, the executor must always load the latest Runtime from storage - or preferably detect Runtime upgrades (Definition 11) - either based on the parent block when importing blocks or the best/highest block when creating new blocks.
Definition 31. Runtime Code at State
By , we refer to the Runtime code stored in the state storage at the end of the execution of block .
The WASM blobs maybe compressed using zstd. In such cases, there is a 8-byte magic indentifier at the head of the blob, indicating that it should be decompressed with zstd compression. The magic identifier prefix
ZSTD_PREFIX = [82, 188, 83, 118, 70, 219, 142, 5] is different from the WASM magic bytes. The decompression has to be applied on the blob excluding the
ZSTD-PREFIX and has a Bomb Limit of
CODE_BLOB_BOMB_LIMIT = 50 * 1024 * 1024 to mitigate compression bomb attacks.
2.6.3. Code Executor
The Polkadot Host executes the calls of Runtime entrypoints inside a Wasm Virtual Machine (VM), which in turn provides the Runtime with access to the Polkadot Host API. This part of the Polkadot Host is referred to as the Executor.
Definition 32 introduces the notation for calling the runtime entrypoint which is used whenever an algorithm of the Polkadot Host needs to access the runtime.
It is acceptable behavior that the Runtime panics during execution of a function in order to indicate an error. The Polkadot Host must be able to catch that panic and recover from it.
In this section, we specify the general setup for an Executor that calls into the Runtime. In Appendix C we specify the parameters and return values for each Runtime entrypoint separately.
Definition 32. Call Runtime Entrypoint
we refer to the task using the executor to invoke the while passing an argument to it and using the encoding described in Section 220.127.116.11..
18.104.22.168. Memory Management
The Polkadot Host is responsible for managing the WASM heap memory starting at the exported symbol as a part of implementing the allocator Host API (Section B.10.) and the same allocator should be used for any other heap allocation to be used by the Polkadot Runtime.
The size of the provided WASM memory should be based on the value of the storage key (an unsigned 64-bit integer), where each page has the size of 64KB. This memory should be made available to the Polkadot Runtime for import under the symbol name
22.214.171.124. Sending Data to a Runtime Entrypoint
In general, all data exchanged between the Polkadot Host and the Runtime is encoded using SCALE codec described in Section A.2.2.. Therefore all runtime entrypoints have the following identical Wasm function signatures:
(func $runtime_entrypoint (param $data i32) (param $len i32) (result i64))
In each invocation of a Runtime entrypoints, the argument(s) which are supposed to be sent to the entrypoint, need to be SCALE encoded into a byte array (Section A.2.2.) and copied into a section of Wasm shared memory managed by the shared allocator described in Section 126.96.36.199..
When the Wasm method, corresponding to the entrypoint, is invoked, two integers are passed as arguments. The first argument is set to the memory address of the byte array in Wasm memory. The second argument sets the length of the encoded data stored in .
188.8.131.52. Receiving Data from a Runtime Entrypoint
The value which is returned from the invocation is an integer, representing two consecutive integers in which the least significant one indicates the pointer to the offset of the result returned by the entrypoint encoded in SCALE codec in the memory buffer. The most significant one provides the size of the blob.
184.108.40.206. Runtime Version Custom Section
For newer Runtimes, the Runtime version (Section C.4.1.) can be read directly from the Wasm custom section with the name
runtime_version. The content is a SCALE encoded structure as described in Section C.4.1..
Retrieving the Runtime version this way is preferred over calling the
Core_version entrypoint since it involves significantly less overhead.