Virtual machine
This page specifies qubits-vm, the sandbox contracts run in: the entry point, the wasmi configuration, what validation refuses, how instructions are metered, every gas price and limit, every host call with its exact signature, and how determinism is pinned. Everything here is consensus: the same code and input must give the same result, gas included, on every validator. It is written for auditors and for anyone reading or changing the VM. How a transaction reaches the VM is on Contract model.
Why WebAssembly and wasmi
The chain does not run the EVM. ecrecover would bring back a signature that quantum computers break, and addresses would drop to 20 bytes. Contracts check ML-DSA and SLH-DSA signatures through the verify host call instead.
wasmi is an interpreter. No JIT compiler enters the attack surface, and it has a fuel meter built in. A gas is one unit of wasmi fuel.
Entry point
pub fn run(call: Invocation<'_>, host: &mut dyn Host) -> Result<Finish, Fatal>;
pub struct Invocation<'a> {
pub code: &'a [u8],
pub function: &'a str,
pub input: &'a [u8],
pub contract: Address,
pub caller: Address,
pub value: Amount,
pub gas: u64,
}
pub struct Finish {
pub outcome: Outcome,
pub output: Vec<u8>,
pub gas_left: u64,
}
/// The node itself failed: a database error, not anything a contract did. Stops the block.
pub struct Fatal(pub String);
| Field | Meaning |
|---|---|
code | The module's bytes |
function | The export to run. It must have type () -> () |
input | The arguments, handed over by input_size and input |
contract | The address this run is, used for storage, events and self |
caller | The account or contract that called |
value | The native tokens that came along, already in contract's balance |
gas | The gas this run may use |
run returns Err only for Fatal. Everything a contract can cause is an Ok(Finish). Outcome is defined on Contract model.
Steps
- Subtract
CALL + code.len() × LOAD_BYTEfromgas. On underflow, finishOutOfGas. - Build a new
EngineandModule::newthe code. This validates and translates it. On failure, finishInvalid. - Create the
Storewith its limits and link the host calls. - Instantiate. Data and element segments are applied here. On failure, finish
Invalid. - Find the memory exported as
memory. If there is none, finishInvalid. - Subtract
pages × PAGE, using the instantiated size. On underflow, finishOutOfGas. - Find
functionwith type() -> (). If that fails, finishDonewhenfunctionisinitand no export has that name, otherwiseInvalid. Either way the remaining gas stays. - Set the fuel to the remaining gas and call the function.
- Read the fuel back and take the output buffer. Map the result as below.
Steps 1 to 7 run with no fuel set, so wasmi charges nothing for them. LOAD_BYTE and PAGE pay for that work instead.
Host trait
Host is the chain as a contract sees it. qubits-execution implements it with undo, on Run (see Undo log). tests/vm.rs implements it with a map.
pub trait Host {
fn height(&self) -> u64;
fn timestamp_ms(&self) -> u64;
fn balance(&mut self, who: &Address) -> Result<Amount, Fatal>;
fn read(&mut self, contract: &Address, key: &[u8]) -> Result<Option<Vec<u8>>, Fatal>;
fn write(&mut self, contract: &Address, key: &[u8], value: Option<&[u8]>) -> Result<(), Fatal>;
/// Only a contract's own balance can leave; returns false if it is too low.
fn transfer(&mut self, from: &Address, to: &Address, amount: Amount) -> Result<bool, Fatal>;
fn emit(&mut self, contract: &Address, topic: Vec<u8>, data: Vec<u8>);
fn call(
&mut self,
from: &Address,
to: &Address,
function: &str,
args: &[u8],
value: Amount,
gas: u64,
) -> Result<Called, Fatal>;
}
/// What came of a contract calling another. A failed callee has already been undone.
pub struct Called {
pub ok: bool,
pub output: Vec<u8>,
pub gas_left: u64,
}
Each run has its own frame in the store. The contract argument of read, write and emit, and the from of transfer and call, always come from the frame and never from the contract, which is why a contract cannot reach another's storage or balance.
pub(crate) struct Frame<'h> {
pub host: &'h mut dyn Host,
pub limits: StoreLimits,
pub contract: Address,
pub caller: Address,
pub value: Amount,
pub input: Vec<u8>,
pub output: Vec<u8>,
/// What the last contract called gave back.
pub result: Vec<u8>,
}
wasmi configuration
Version and features
wasmi = { version = "=2.0.0", default-features = false, features = ["std", "validate", "portable-dispatch"] }
The version is exact because gas used goes into receipts, and receipts into blocks. An upgrade is a fork and happens only on purpose.
portable-dispatch is mandatory. It runs instructions in a loop. The other dispatch hopes the compiler turns every instruction into a tail call, and where it does not, an endless wasm loop overflows the native stack and kills the node.
validate enables wasm validation. default-features = false leaves out wasmi's memory64 and auto-dispatch, and simd is not requested. cargo tree -e features shows qubits-vm as the only crate enabling features of wasmi, so nothing else adds them back.
Engine config
let mut config = Config::default();
config
.consume_fuel(true)
.floats(false)
.allow_start_fn(false)
.wasm_saturating_float_to_int(false)
.wasm_multi_memory(false)
.wasm_tail_call(false)
.wasm_custom_page_sizes(false)
.wasm_wide_arithmetic(false)
.set_max_recursion_depth(limits::MAX_RECURSION)
.compilation_mode(CompilationMode::Eager);
| Proposal or setting | State |
|---|---|
Floats, f32 and f64 | Off |
| Start functions | Off |
| Saturating float to int | Off |
| Multi-memory | Off |
| Tail calls | Off |
| Custom page sizes | Off |
| Wide arithmetic | Off |
memory64, SIMD, relaxed SIMD | Off, not compiled in |
| Mutable globals | On, the wasmi default |
| Multi-value | On, the wasmi default |
| Sign extension | On, the wasmi default |
| Bulk memory | On, the wasmi default |
| Reference types | On, the wasmi default |
| Extended constant expressions | On, the wasmi default |
| Compilation | Eager: the whole module is translated in Module::new |
| Wasm call depth | 256 frames, MAX_RECURSION |
The configuration is built once, in a OnceLock. Each run clones it into a new engine.
One engine per run
An engine keeps all code it ever translated until it is dropped. One shared engine loses 23 KB a call, and a node dies of its own uptime. So run builds a fresh Engine every time. memory_does_not_grow_with_calls in qubits-execution/tests/gas.rs checks that memory stays flat over 200,000 calls. See Measurements.
Store limits
StoreLimitsBuilder::new()
.memory_size(limits::MAX_MEMORY_BYTES)
.memories(1)
.tables(1)
.instances(1)
.build()
trap_on_grow_failure keeps its default of false. memory.grow past the limit gives -1, as the wasm specification says, and the contract can see that. memory_stops_at_the_limit pins it.
table_elementsis not set, so a table's size is neither limited nor charged.(table 1000000 funcref)deployed for 7,188 gas in a test. Only governance deploys today, so only reviewed code reaches this.
What is refused
Each of these was deployed on a test chain with 2,000,000 gas. All ended Invalid, with gas_left 0, and the deployment left nothing behind.
| Module | Refused because |
|---|---|
| Bytes that are not wasm | Parsing |
| A float instruction, or a function with a float parameter | Floats are off |
A start section | Start functions are off |
i32.trunc_sat_f32_s | Saturating float to int is off |
return_call | Tail calls are off |
A v128 value | SIMD is not compiled in |
| A 64-bit memory | memory64 is not compiled in |
| Two memories | Multi-memory is off |
| Two tables | The store allows one |
An import from any module but qubits | Linking |
A qubits import with the wrong signature | Linking |
| An imported memory | Linking: the linker provides functions only |
| No memory exported, or one exported under another name | No memory export |
| 65 initial pages | Above MAX_MEMORY_BYTES. 64 pages deploy |
| A data or element segment out of bounds | Instantiation |
code_that_could_differ_between_machines_is_refused in tests/contracts.rs pins part of this list.
A function that is exported with parameters or a result cannot be called. The module is valid, and a call of that function ends Invalid with the remaining gas kept. init with the wrong type does the same, and the deployment fails.
Modules using multi-value, bulk memory, sign extension, reference types, mutable globals or extended constants deployed Done.
Instruction metering
wasmi charges fuel when it enters a basic block, for the whole block. The translator opens each block with a ConsumeFuel instruction of at least 1, then adds the cost of each operator.
| Operators | Cost |
|---|---|
nop, drop, block, loop, unreachable, return, else, end | 0 |
Every other operator, including local.get and i32.const | 1 |
Each ConsumeFuel | 1 |
Bulk memory (memory.copy, memory.fill, memory.init) | Also length / 64, rounded down |
memory.grow | Also added bytes / 64, which is 1,024 per page |
These are wasmi's defaults, and run does not change them. So the gas of a module of L bytes with p pages whose function does nothing is 2,000 + 16 × L + 500 × p + 1. Measured: 3,237 gas for 46 bytes and one page, and 3,737 with two pages.
Three i64.add statements, each local.get, i64.const, i64.add, local.set, cost 13 fuel in one block: 1 for the block and 4 for each statement. memory.fill of 6,400 bytes adds 100 gas to the operator's own cost, and each memory.grow of one page adds 1,024.
Pages are priced differently depending on when they appear. A page present at instantiation costs PAGE (500) on every run. A page added by memory.grow costs 1,024, charged by wasmi, and no PAGE.
Metering happens at block entry, so a block already paid for runs to its end even if a host call inside it leaves the fuel at 0. A contract whose callee burned all the gas finished Done when nothing metered followed in that block, and OutOfGas when it entered another.
Gas table
All prices are in qubits_vm::gas, and all are part of the on-chain format. A gas is about one interpreted instruction: 0.5 ns on the fast core these were measured on. Every price keeps the work under about 1.5 ns a gas, so a slow cloud core stays near 5.
| Constant | Gas | Pays for |
|---|---|---|
HOST | 100 | Crossing into the host at all. No host call is free |
CALL | 2,000 | Starting any run, and reaching for another contract |
LOAD_BYTE | 16 | Reading the code and translating it, per byte |
PAGE | 500 | Instantiating a page of 64 KiB of memory |
COPY_BYTE | 1 | Copying between the contract's memory and the host, per byte |
READ | 20,000 | A storage or balance read, priced for a read that misses the cache on a network disk, not for the usual one |
WRITE | 15,000 | A storage write or delete |
STATE_BYTE | 64 | A byte kept in state for good, per byte of key and value written |
TRANSFER | 20,000 | Moving native tokens |
EVENT | 500 | An event |
EVENT_BYTE | 8 | A byte of topic or data in an event |
HASH | 500 | The hash host call |
VERIFY_ML_DSA | 100,000 | Verifying an ML-DSA-65 signature |
VERIFY_SLH_DSA | 800,000 | Verifying an SLH-DSA-SHA2-192s signature |
A price change changes gas counts, so it is a fork. Deleting storage returns no gas: state pays by the byte written, and del is one WRITE.
Limits
These are in qubits_vm::limits. What a single run may hold:
| Constant | Value | Applies to |
|---|---|---|
MAX_MEMORY_BYTES | 4,194,304 (4 MiB, 64 pages) | Linear memory |
MAX_KEY_BYTES | 128 | A storage key |
MAX_VALUE_BYTES | 16,384 (16 KiB) | A storage value. Also the input of hash, and the message of verify |
MAX_TOPIC_BYTES | 64 | An event's topic |
MAX_EVENT_BYTES | 4,096 (4 KiB) | An event's data |
MAX_OUTPUT_BYTES | 16,384 (16 KiB) | output, equal to MAX_ARGS_BYTES |
MAX_RECURSION | 256 | Wasm call depth within a run |
A revert reason may be up to MAX_TOPIC_BYTES × 4, which is 256 bytes.
These are in qubits_types::limits:
| Constant | Value |
|---|---|
MAX_ARGS_BYTES | 16,384 |
MAX_FUNCTION_NAME | 64 |
MAX_CODE_BYTES | 163,840 |
MAX_CALL_DEPTH | 8 |
MAX_TX_GAS | 5,000,000 |
MAX_BLOCK_GAS | 10,000,000 |
Host calls
A contract reaches the chain only through the imports of the qubits module. There are 20. They and their signatures are part of the on-chain format.
(import "qubits" "input_size" (func (result i32)))
(import "qubits" "input" (func (param i32))) ;; ptr
(import "qubits" "output" (func (param i32 i32))) ;; ptr len
(import "qubits" "revert" (func (param i32 i32))) ;; ptr len, does not return
(import "qubits" "caller" (func (param i32))) ;; ptr
(import "qubits" "self" (func (param i32))) ;; ptr
(import "qubits" "value" (func (param i32))) ;; ptr
(import "qubits" "height" (func (result i64)))
(import "qubits" "timestamp_ms" (func (result i64)))
(import "qubits" "balance" (func (param i32 i32))) ;; who out
(import "qubits" "get" (func (param i32 i32 i32 i32) (result i32))) ;; key key_len out cap
(import "qubits" "set" (func (param i32 i32 i32 i32))) ;; key key_len value value_len
(import "qubits" "del" (func (param i32 i32))) ;; key key_len
(import "qubits" "transfer" (func (param i32 i32) (result i32))) ;; to amount
(import "qubits" "emit" (func (param i32 i32 i32 i32))) ;; topic topic_len data data_len
(import "qubits" "call" (func (param i32 i32 i32 i32 i32 i32) (result i32))) ;; to name name_len args args_len value
(import "qubits" "result_size" (func (result i32)))
(import "qubits" "result" (func (param i32))) ;; ptr
(import "qubits" "hash" (func (param i32 i32 i32))) ;; ptr len out
(import "qubits" "verify" (func (param i32 i32 i32 i32 i32 i32 i32) (result i32))) ;; scheme key key_len msg msg_len sig sig_len
Charges
Every host call charges before it does anything, so whatever it allocates is already paid for. In the table, copy(n) means HOST + n × COPY_BYTE, one copy across the boundary.
| Call | Gas |
|---|---|
input_size | HOST |
input | copy(input length) |
output | copy(len) |
revert | copy(len) |
caller, self | copy(32) |
value | copy(16) |
height, timestamp_ms | HOST |
balance | READ + copy(32) + copy(16) |
get | READ + copy(key_len), plus copy(bytes copied) when the key exists |
set | WRITE + (key_len + value_len) × STATE_BYTE + copy(key_len) + copy(value_len) |
del | WRITE + copy(key_len) |
transfer | TRANSFER + copy(32) + copy(16) |
emit | EVENT + (topic_len + data_len) × EVENT_BYTE + copy(topic_len) + copy(data_len) |
call | CALL + 2 × READ + copy(32) + copy(name_len) + copy(args_len) + copy(16), then what the callee uses |
result_size | HOST |
result | copy(result length) |
hash | HASH + copy(len) + copy(32) |
verify | VERIFY_ML_DSA or VERIFY_SLH_DSA, then copy(key_len) + copy(msg_len) + copy(sig_len) |
call charges two reads whether or not they find anything, for finding the callee and its code.
Measured against a run with the same imports and no call, less 16 gas for each added byte of code, these formulas match to within the few instructions that push the arguments. set with a 10-byte key and a 20-byte value is 17,150, measured at 17,155 with four i32.const and the call. emit of 8 and 80 bytes is 1,492, measured at 1,497. hash of 64 bytes is 796, measured at 800. verify of nothing with ML-DSA-65 is 100,300, measured at 100,308.
Failure modes
A host call stops a contract in one of four ways:
pub(crate) enum Stop {
Revert(Vec<u8>),
OutOfGas,
/// Broke a limit or handed the host something that makes no sense.
Misuse,
Fatal(Fatal),
}
Misuse becomes Trapped. A pointer range outside memory is Misuse in every call that reads or writes memory.
| Call | Stops with | When |
|---|---|---|
output, revert | Misuse | len over 16,384, or over 256 for revert |
get, del | Misuse | key_len over 128 |
set | Misuse | key_len over 128, or value_len over 16,384 |
emit | Misuse | topic_len over 64, or data_len over 4,096 |
call | Misuse | name_len over 64, a name that is not UTF-8, or args_len over 16,384 |
hash | Misuse | len over 16,384 |
verify | Misuse | scheme not 1 or 2, a key or signature longer than the scheme's, or msg_len over 16,384 |
| Any | OutOfGas | The charge is more than the gas left |
balance, get, set, del, transfer, call | Fatal | The database failed |
The order of checks differs and is consensus. load checks its limit before it charges, so output with a huge length is Trapped. set and emit charge for key_len + value_len first, so lengths of 0xFFFFFFFF end OutOfGas, not Trapped, and neither overflows or allocates. hostile_host_calls_stop_the_contract_and_not_the_node pins both.
Lengths from a contract are u32. The host adds them as u64s with saturating multiplication, because a hostile contract could otherwise panic the node through overflow checks.
Returns
| Call | Returns |
|---|---|
input_size | The length of the arguments |
height, timestamp_ms | The block's height and time, as the Host gives them |
get | -1 if the key has no value, otherwise the value's full length. At most cap bytes are copied, so a result above cap means it was cut |
transfer | 0 if it went, 1 if not |
call | 0 if the callee finished, 1 if it failed, in which case everything it did is undone |
result_size | The length of the last callee's output |
verify | 0 if the signature is good, 1 if not |
transfer sends from this contract's own balance. It returns 1 if the balance is too low or the recipient's balance would overflow. With amount 0 or the contract itself as recipient, it returns 0 and changes nothing, provided the balance covers amount. It creates the recipient's account if needed and runs no code there.
call gives the callee all the gas left. Its output replaces result. A failed callee's output is empty. See Calls between contracts.
output may be called several times, and the last wins. It does not end the run. The output counts only if the run ends Done.
hash is contract_hash: BLAKE3 in derive-key mode with the context qubits.sh 2026-11 contract hash, so its result is not plain BLAKE3. It writes 32 bytes to out.
verify checks a raw signature over msg in Domain::Message, whose FIPS context string is qubits/message/v1. This is the domain qubits sign uses, so a transaction signature does not pass, as a_contract_checks_a_post_quantum_signature shows.
scheme | Algorithm | Key bytes | Signature bytes |
|---|---|---|---|
| 1 | ML-DSA-65 | 1,952 | 3,309 |
| 2 | SLH-DSA-SHA2-192s | 48 | 16,224 |
A key or signature shorter than these returns 1 and still pays. Longer ones are Misuse. Both are raw bytes, without the leading SchemeId of the tagged forms that qubits key show and qubits sign print. See Cryptography.
Memory conventions
The module must export its linear memory as memory. Pointers are i32 values read as u32 byte offsets. Nothing needs alignment, because the host copies bytes.
| Value | Layout |
|---|---|
| Address | 32 bytes |
| Amount | 16 bytes, u128 little-endian |
| Hash | 32 bytes |
height, timestamp_ms | i64 results, u64 values |
The host never allocates in the contract's memory. The contract gives a pointer to room it owns: 32 bytes for caller, self and hash, 16 for value and balance, input_size bytes for input, result_size bytes for result, and cap for get.
Outcomes and gas
Finish::early ends a run with an empty output, so only Done carries one. revert is the one way a running contract stops and hands unused gas back.
| Cause | Outcome | gas_left |
|---|---|---|
| The function returned | Done | What remains |
init was run and no export has that name | Done | What remains after loading |
The contract called revert | Reverted with its bytes | What remains |
Refused by enter: re-entry, depth, value | Reverted with text from the chain | All of it, since nothing ran |
Gas below CALL + code.len() × LOAD_BYTE, or below the page cost | OutOfGas | 0 |
| Fuel ran out in wasm | OutOfGas | 0 |
| A host call's charge failed | OutOfGas | 0 |
A wasm trap: unreachable, division by zero, access outside memory, recursion past 256 | Trapped | 0 |
Stop::Misuse | Trapped | 0 |
The module fails validation, instantiation, or has no memory export | Invalid | 0 |
| No such export, or an export of the wrong type | Invalid | What remains after loading |
No code at the address, in enter | Invalid | All of it |
stopped reads the error as a trap code first, then tries to downcast it to Stop, and treats anything else as Trapped. wasmi's error text goes nowhere: nothing it says in words reaches a result, because Outcome is an enum and receipts are hashed into the block. A Reverted reason is bytes the contract chose, or one of the two fixed strings from enter.
Determinism
The same code and input must give the same result on every validator, so these are fixed:
- The interpreter version, to the patch level, and with it the fuel cost of each instruction.
- The compiler, by
chain/rust-toolchain.toml, at 1.97.1. Release builds keep overflow checks on. - Floats are refused, so no NaN bits or rounding modes can differ.
- The engine configuration is one static value, and no run shares an engine or store with another.
- A contract sees only what
Hostgives it: height, block time, its own storage, balances, its call. There is no randomness and no wall clock. - All wasm is translated before metering starts, so translation adds no fuel at run time.
Golden tests
qubits-vm/tests/vm.rs runs the sandbox on the simplest host there is, a BTreeMap with constant height 7, time 1,000 and balance 5, whose call returns output [1] and costs nothing. Each run gets 5,000,000 gas. The numbers are the point: if one changes, on any machine, nodes running different builds disagree. A change there is a fork and must be meant.
| Test | What runs | Pinned |
|---|---|---|
the_token_costs_exactly_this | token.wasm: init, transfer, balance_of, a transfer that reverts | Gas 252,105, 190,562, 130,597 and 130,952, and a fingerprint |
the_market_costs_exactly_this | amm.wasm: add_liquidity and swap on reserves near 2^111, where the 128-bit multiplication and long division run in 64-bit wasm instructions | Gas 522,451 and 385,760, the swap's output, and a fingerprint |
instructions_cost_exactly_this | A 10,000-round loop with function calls and i64.store, then memory.fill, memory.copy, memory.grow | Gas 290,478 |
running_out_of_gas_stops_at_the_same_place | A loop of set until the gas runs out | 317 writes stored |
A fingerprint hashes everything the runs left behind: each storage key and value in key order, each event's topic and data, then each run's output and gas_left as little-endian bytes, all through contract_hash.
| Fingerprint | Value |
|---|---|
| Token | bd0b2efd98a3152ecd1fbbd2076b0d21f08c1cac2b360863ee30c5ab07df9a65 |
| Market | 9d2b1b32fbe62df47bf533e503c41e13b33a96951ef25c27e028ae3b8fd1ca9c |
Because the tests include_bytes! the files in chain/contracts/out/, rebuilding a contract with different bytes changes them too.
These results have been checked equal on arm64 and x86-64, with the same test built for x86_64-apple-darwin:
cargo test -p qubits-vm --test vm # native
cargo test -p qubits-vm --test vm --target x86_64-apple-darwin # under Rosetta
Measurements
qubits-execution/tests/gas.rs measures what gas buys on a real RocksDB store, with 200,000 keys in one contract's storage. Each row is a contract doing one expensive thing until MAX_TX_GAS runs out. A row far above the others is a price that is too low, and what a full block can be made to cost.
cargo test --release -p qubits-execution --test gas -- --ignored --nocapture --test-threads=1
This run was on an Apple M5 Max with 18 cores and 48 GiB of memory, macOS, rustc 1.97.1, release profile, with other work running on the machine. Rates are from one run.
| Work | Gas | ms | ns per gas | Full block, ms |
|---|---|---|---|---|
| branch | 5,000,000 | 2.60 | 0.52 | 5.2 |
| arithmetic | 5,000,000 | 2.02 | 0.40 | 4.0 |
| memory load and store | 5,000,000 | 2.13 | 0.43 | 4.3 |
memory.fill of 60 KB | 5,000,000 | 2.61 | 0.52 | 5.2 |
| wasm function call | 5,000,000 | 3.77 | 0.75 | 7.5 |
| storage read, present | 5,000,000 | 1.31 | 0.26 | 2.6 |
| storage read, absent | 5,000,000 | 0.92 | 0.18 | 1.8 |
| account balance | 5,000,000 | 0.45 | 0.09 | 0.9 |
| hash of nothing | 5,000,000 | 0.96 | 0.19 | 1.9 |
| hash of 16 KB | 5,000,000 | 2.11 | 0.42 | 4.2 |
| event | 5,000,000 | 0.58 | 0.12 | 1.2 |
| output | 5,000,000 | 1.71 | 0.34 | 3.4 |
| failed call, no contract | 5,000,000 | 0.30 | 0.06 | 0.6 |
| call of the largest code | 5,000,000 | 3.10 | 0.62 | 6.2 |
| call of the largest memory | 5,000,000 | 1.91 | 0.38 | 3.8 |
| ML-DSA-65 signature | 5,000,000 | 3.89 | 0.78 | 7.8 |
| SLH-DSA signature | 5,000,000 | 3.76 | 0.75 | 7.5 |
| state root, 666 writes | 9,990,000 | 13.17 | 1.32 | 13.2 |
"Full block" scales the rate to MAX_BLOCK_GAS. The state root row hashes a block's writes into the tree, which is the slowest work a block can ask for. The largest row among the runs themselves is 0.78 ns a gas. call of the largest code installs a module of about 160 KiB directly in state, which no deployment could.
The second test made 200,000 balance_of calls on the 6,677-byte token after 20,000 warm-up calls. Resident memory was 279,424 KB before and 275,328 KB after, at 95.6 µs a call.
Source
chain/crates/qubits-vm/src/lib.rs:run,Invocation,Finish,Host,gas,limits, the engine configurationchain/crates/qubits-vm/src/host.rs: every host call,charge,load,store,linkerchain/crates/qubits-vm/Cargo.toml: the wasmi pin and its featureschain/crates/qubits-vm/tests/vm.rs: the golden testschain/crates/qubits-execution/tests/gas.rs: the measurementschain/crates/qubits-execution/tests/contracts.rs: hostile code, limits, what is refusedchain/crates/qubits-crypto/src/scheme.rs:SchemeId,Domainchain/crates/qubits-types/src/lib.rs:limitschain/rust-toolchain.toml: the compiler