Assessment reports>Radix>Threat Model>Costing

Costing

Costing in Radix combines both static analysis and runtime enforcement to ensure the fair and efficient use of computational and storage resources. Although we will mostly focus on execution-costing mechanisms in this section, subsystems roughly fall into the following categories:

  1. Execution cost

    • Native-code execution

    • Guest-code execution (WASM)

  2. Finalization cost

    • Commit events

    • Commit logs

    • Commit state updates

Execution cost: Native code

Native-code execution here refers to the cost of executing any of the functions exported as the VM runtime. Unlike the guest code, the exact cost cannot be measured during the runtime for the sake of determinism, but the implemented mechanism comes very close to estimating the real cost of each call.

The costing mechanism is implemented as follows:

  1. Marking. A proc macro is used at the specific functions that the guest code interacts with and is of interest to the costing runtime. It can also specify arguments to monomorphize the costing mechanism if any of them affect the cost of the call significantly (e.g., a function that processes a Vec<T> argument).

  2. Sampling. When executed under QEMU, this macro times the execution of the function and writes a sample. After executing a number of sample transactions, the profiler writes a costing table to be used by production runtime.

  3. Costing. Upon each call to the priorly marked functions, the costing runtime uses the costing table to estimate the cost of the call and charges it to the transaction.

While this is not an exact approach, it provides a reasonable approximation of the cost of each call.

Execution cost: Guest code (WASM)

The guest code's execution cost is measured and enforced by instrumenting the WASM module during the preparation phase. The instrumentation ensures that every operation in the WASM module is metered to track resource usage and enforce runtime limits.

  1. Validation phase

    The ScryptoV1WasmValidator ensures that the WASM module adheres to predefined limits. It enforces constraints on the following:

    • Maximum memory size in pages

    • Maximum stack size

    • Maximum number of instructions

    • Maximum number of function parameters

    • Maximum number of function locals

    • Maximum number of tables

    • Maximum number of globals

    • Maximum number of functions

    • Maximum number of br_table targets

  2. Metering

    The metering process involves several transformations to the WASM code to ensure proper resource tracking and safety:

    • The code is instrumented with additional instructions that call a gas function to deduct execution-cost units for each block of instructions.

    • Every function is wrapped into an indirect call stub to track the current stack height when used by call_indirect callers.

    • If the stack height exceeds the maximum limit, the execution halts to prevent stack overflows.

Given the following input WASM —

(module (type $sig (func (result i64))) (table 1 anyfunc) (elem (i32.const 0) $indirect) (func $indirect (type $sig) (i64.const 42) ) (func $Test_f (param $0 i64) (result i64) (if (result i64) (i64.eqz (call_indirect (type $sig) (i32.const 0))) (then (i64.const 0)) (else (i64.add (get_local $0) (i64.const 1))) ) ) (memory $0 1) (export "memory" (memory $0)) (export "Test_f" (func $Test_f)) )

First, the control-flow graph is walked and the cost of each instruction within basic blocks are clustered, emitting a gas function prior.

;; instrumented version of $Test_f (func (;2;) (type 1) (param i64) (result i64) i64.const 31251 call 0 ;; the gas call for the first basic block (cost = 31251) i32.const 0 call_indirect (type 0) i64.eqz if (result i64) i64.const 1372 call 0 ;; the gas call for the first branch (cost = 1372) i64.const 0 else i64.const 5811 call 0 ;; the gas call for the second branch (cost = 5811) local.get 0 i64.const 1 i64.add end)

Afterwards, a thunk is created for each function, wrapping the instrumented function and adding the necessary stack checks.

;; thunk for $Test_f (func (;4;) (type 1) (param i64) (result i64) local.get 0 global.get 0 ;; stack height stored as a global variable i32.const 5 ;; 5 is the size of the stack frame for $Test_f i32.add global.set 0 global.get 0 i32.const 1024 ;; 1024 is the max stack height i32.gt_u if unreachable ;; trap if stack height is exceeded end call 2 ;; the actual $Test_f implementation global.get 0 i32.const 5 ;; subtract the stack frame size and return i32.sub global.set 0)

From this point on, every reference to the Test_f function is replaced with the thunk, with the exception of direct calls where the stack guard is inlined into the flow.

Costing API

The runtime also exposes an API for querying costing parameters.

Function: COSTING_GET_USD_PRICE

This function retrieves the USD price for the use of stable costing. This is mainly used for fixed USD fees seen in blueprint royalties.

fn usd_price(&mut self) -> Result;

Function: COSTING_GET_EXECUTION_COST_UNIT_LIMIT

This function retrieves the execution-cost unit limit.

fn execution_cost_unit_limit(&mut self) -> Result;

Function: COSTING_GET_EXECUTION_COST_UNIT_PRICE

This function retrieves the execution-cost unit price.

fn execution_cost_unit_price(&mut self) -> Result;

Function: COSTING_GET_FINALIZATION_COST_UNIT_LIMIT

This function retrieves the finalization-cost unit limit.

fn finalization_cost_unit_limit(&mut self) -> Result;

Function: COSTING_GET_FINALIZATION_COST_UNIT_PRICE

This function retrieves the finalization-cost unit price.

fn finalization_cost_unit_price(&mut self) -> Result;

Function: COSTING_GET_TIP_PERCENTAGE

This function retrieves the tip percentage.

fn tip_percentage(&mut self) -> Result;

Function: COSTING_GET_FEE_BALANCE

This function retrieves the fee balance.

fn fee_balance(&mut self) -> Result;
Zellic © 2025Back to top ↑