Assessment reports>Radix>Threat Model>Manifests

Manifests

In the Radix engine, every transaction starts from a manifest. The manifest describes the list of actions that will occur in the transaction as well as how resources are intended to move between components, payments of transaction fees, and so forth.

Once the manifest ends up in the Radix engine, the engine initially verifies that every component that is referenced in the manifest actually exists in the global blockchain state. This is done in three steps:

  1. If a referenced component is a component that is within the ALWAYS_VISIBLE_GLOBAL_NODES list, then this component is allowed to be referenced in the manifest. This list includes all the native blueprints.

  1. If a referenced component is a global virtual component, then it is allowed. In Radix, components of the accounts and identity blueprints are considered global virtual. These are allowed by default because even if no such components exist on the chain at the time of the transaction, they can be created in the future, and thus other components should still be allowed to interact with them.

  1. If neither of the above cases are true, then the global blockchain state is accessed to ensure that this node actually exists. This would then mean that this component is a globalized component (for example, a user-created blueprint that has been globalized previously) and thus can be accessed through a transaction manifest.

Once the manifest passes these checks, the instructions in the manifest and the list of referenced components are passed into the native transaction-processor blueprint's run() function. This function handles executing all the instructions in the manifest.

The call to this run() function triggers the creation of a new call frame.

Call frames

A call frame is created for each new function/method call made in a transaction. The CallFrame structure is as follows:

pub struct CallFrame { /// The frame id depth: usize, /// Call frame system layer data call_frame_data: C, /// Owned nodes which by definition must live on heap owned_root_nodes: IndexSet, /// References to non-GLOBAL nodes, obtained from substate loading, ref counted. /// These references may NOT be passed between call frames as arguments transient_references: NonIterMap, /// Stable references points to nodes in track, which can't moved/deleted. /// Current two types: `GLOBAL` (root, stored) and `DirectAccess`. /// These references MAY be passed between call frames stable_references: BTreeMap, next_handle: SubstateHandle, open_substates: IndexMap>, }

In the above structure, the owned_root_nodes set contains all nodes that are owned by the current call frame (which essentially means that the node was created within this call frame). The nodes in this set must be on the heap, which means that these nodes are not globalized. This includes nodes created as part of blueprints and components, such as vaults, buckets, and so on.

The transient_references map also contains nodes that are only on the heap, but instead of the nodes here being created within this call frame, these nodes are instead loaded when other substates are loaded. The TransientReference structure is as follows:

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct TransientReference { ref_count: usize, ref_origin: ReferenceOrigin, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum ReferenceOrigin { FrameOwned, Global(GlobalAddress), DirectlyAccessed, SubstateNonGlobalReference(SubstateDevice), }

The stable_references map contains nodes that are stored in the Store substate device, meaning they are part of the permanent blockchain state and can never be removed. The StableReferenceType is an enum that describes the node. These nodes can either be Global or DirectAccess, and these nodes can either be created within the current call frame (and subsequently globalized) or loaded through other substates.

The open_substates structure tracks the current open substates within this call frame. All open substates must be closed when the call frame is dropped.

When a new call frame is created for a function/method call in a transaction, the arguments to the function/method call are passed in as an IndexedScryptoValue. This tracks all the references and owned nodes of the arguments. These are then moved into the new call frame, and so the call frame starts off with the required references needed to execute the function/method call.

When a call frame is dropped, the return value of the function/method call is used to do the same thing as above but in reverse. The return value is an IndexedScryptoValue, which contains any references and owned nodes that must be moved back to the parent call frame.

It is important to note that transient references cannot be passed between call frames. This invariant exists in the code base because it prevents the need for adding additional checks to ensure the references are still alive and have not been dropped when the references are passed to another call frame.

Node visibility

Nodes are identified by unique node IDs. A node ID may look like the following:

f8305dfa7742a66ce8c8c299cfdeb6f597b44dd381fb28cb808a72e2cf10

A user-defined blueprint potentially has the ability to spoof nodes, which essentially means that they can construct a component with the exact same node ID as another one, and then try to access it. How does Radix prevent this?

Radix has a concept known as node visibilities. Whenever a node is accessed (for example, a substate is opened for reading), the engine makes sure that the node in question is visible in the current context. This means the node has to fall in one of two categories:

  1. The node must be visible in the current call frame (that is, existing in the owned_root_nodes, transient_references, or stable_references).

  1. If the node is not visible in the current call frame, it must be one of the ALWAYS_VISIBLE_GLOBAL_NODES.

These rules ensure that it is impossible for nodes to be spoofed and accessed through other nodes that do not have permissions for it.

Zellic © 2025Back to top ↑