Chapter 9: Error Handling and Assertions
Smart contracts on the Kortana blockchain operate within an immutable, multi-tenant execution context where invalid state transitions can lead to lost assets or broken system invariants. The Quorlin Smart Contract Language and the Kortana Virtual Machine (KVM) provide a robust, dual-layered architecture for handling exceptional conditions.
Quorlin pairs human-readable English syntax with strict type checking and precise opcode emission. Under the hood, the KVM manages gas consumption, execution halting, and transactional rollback state.
9.1 Overview of Error Handling Philosophy
Error handling in Quorlin differentiates between two fundamental categories of runtime failures:
- Precondition and Input Violations (
requireand explicitrevert): Used to validate external inputs, permissions, contextual state (such as account balances), and cross-contract call responses. When a precondition fails, the operation halts, reverts all state modifications made during the transaction, and returns remaining gas to the caller along with an error reason string. - Internal Invariant Violations (
assert): Used to detect bugs, arithmetic anomalies, or impossible state configurations within the contract code itself. A failed assertion indicates a catastrophic logical failure. Rather than returning remaining gas, it triggers an invalid execution path that drains the current context's gas allocation, penalizing malicious or severely broken execution paths.
+-----------------------------------+
| Runtime Condition Check |
+-----------------------------------+
|
Is this a precondition/input check
or an internal invariant check?
|
+------------------+------------------+
| |
[ Precondition Failure ] [ Invariant Failure ]
| |
`require` / `revert` `assert`
| |
Opcode::Revert Opcode::Invalid
| |
- Base Gas Cost: Free (0) - Base Gas Cost: 0
- Reverts State Changes - Reverts State Changes
- Preserves Unspent Gas - Drains ALL Remaining Gas
- Optional Error Message - Halts Immediately
9.2 Precondition Checking with require
The require statement is the primary construct for input validation and precondition assertion in Quorlin. It evaluates an expression of primitive type truth (bool). If the expression evaluates to yes (true), execution proceeds to the next statement. If it evaluates to no (false), the execution context immediately halts and reverts.
Syntax and Basic Usage
A require statement accepts a mandatory boolean condition and an optional textual error message:
require <condition_expression>, "<error_message>";
The condition must strictly resolve to type truth. The compiler's semantic analyzer (sema.cpp) checks that non-boolean types (such as number or address) are not implicitly coerced into booleans.
contract TokenVault { number totalVaultSupply; map<address, number> deposits; writes truth deposit(number amount) { // Validate input parameter require amount > 0, "deposit amount must be greater than zero"; deposits[caller] = deposits[caller] + amount; totalVaultSupply = totalVaultSupply + amount; return yes; } writes truth withdraw(number amount) { number balance = deposits[caller]; // Check contextual state precondition require balance >= amount, "insufficient vault balance"; deposits[caller] = balance - amount; totalVaultSupply = totalVaultSupply - amount; return yes; } }
String Literals and Context Bounds
Error messages in Quorlin are stored as text literals. Standard text bounds enforced by kMaxTextBytes limit string literal lengths to prevent excessive gas charges during contract deployment and memory allocation.
During compilation, string literals in require statements are embedded into the instruction stream or loaded from the module’s constant table, which is parsed by the KVM module reader (kvm/module.cpp).
9.3 Explicit Control Flow Reversion (revert)
In complex control structures—such as nested conditional branches or iterative loops—using require may lead to redundant logical tests. Quorlin allows developers to explicitly trigger a state reversion using conditional blocks combined with error dispatching.
contract EscrowManager { address seller; address buyer; number state; // 0: Pending, 1: Approved, 2: Disputed writes truth resolveDispute(truth refundBuyer) { require caller == seller, "only seller can initiate resolution"; if (state == 0) { // Unhandled workflow state require no, "escrow is not disputed yet"; } else if (state == 2) { if (refundBuyer == yes) { // Perform refund logic return yes; } else { // Perform payout logic return yes; } } else { // Revert execution for any unknown state require no, "invalid escrow state for resolution"; } return no; } }
When require no, "..." is hit, the compiler emits instructions that load the reason string offset and execute KVM's Opcode::Revert.
9.4 Internal Invariants and assert
While require handles external inputs and expected failure states, assert is reserved for conditions that should never evaluate to no unless the contract code has a critical bug.
Syntax and Invariant Testing
contract SafeMathVault { number totalShares; number totalAssets; writes truth verifyInvariants() { if (totalShares == 0) { // Invariant check: Assets must be zero if total shares are zero assert totalAssets == 0; } return yes; } }
If an assert statement fails, the code generator emits the Opcode::Invalid instruction.
9.5 KVM Architecture: Revert vs. Invalid Opcodes
Underneath the high-level syntax of Quorlin, execution is driven by the KVM interpreter (kvm/interpreter.cpp) and gas manager (kvm/gas.cpp). Understanding how KVM implements Opcode::Revert and Opcode::Invalid is vital for low-level contract optimization and debugging.
Halting Opcode Mechanics
In the KVM Instruction Set Architecture (kvm/isa.cpp), both Revert and Invalid belong to zero-operand or specialized control formats. However, their behaviors in kvm/gas.cpp and kvm/interpreter.cpp differ significantly:
// Excerpt from kvm/gas.cpp showing base gas costs uint64_t base_cost(Opcode opcode, const params::GasSchedule& schedule) noexcept { switch (opcode) { // Free: these halt, and charging for stopping would price the same work // differently depending on how a contract chose to end. case Opcode::Stop: case Opcode::Return: case Opcode::Revert: return kGasZero; // `INVALID` consumes everything remaining rather than a fixed amount, so its // base is zero and the interpreter drains the counter. A fixed price would // make deliberately aborting cheaper than running out of gas, which is a // refund by another name. case Opcode::Invalid: return kGasZero; ... } }
Comparing Revert and Invalid
| Property | Opcode::Revert (require) | Opcode::Invalid (assert) |
|---|---|---|
| Primary Intent | Precondition & input validation failure | Invariant check failure / Internal bug |
| State Modifications | Rolled back entirely via StateHost | Rolled back entirely via StateHost |
| Unspent Gas | Preserved and refunded to caller | Drained completely (0 remaining gas) |
| Base Gas Charge | kGasZero | kGasZero (drains remaining balance) |
| Return Data | Returns ABI-encoded error message | Returns no payload; halts immediately |
Unified State Trie Unwinding
When Opcode::Revert or Opcode::Invalid is executed, the interpreter halts the execution step loop and signals the StateHost (kvm/state_host.cpp). The StateHost manages the transactional boundary to the underlying world state trie (state::WorldState). All storage modifications (set_storage), balance updates, and context mutations accumulated during the call scope are discarded, guaranteeing atomic failure semantics across external boundaries.
9.6 Compiler Analysis and Semantic Checks
Error checking begins at compile time. Quorlin's multi-stage compiler pipeline validates error handling constructs during lexical analysis (lexer.cpp), syntactic parsing (parser.cpp), and semantic analysis (sema.cpp) before reaching bytecode generation (codegen.cpp).
+--------------+ +--------------+ +--------------+ +--------------+
| Source Code | | Lexer | | Parser | | Semantic |
| (*.ql) | --> | (lexer.cpp) | --> | (parser.cpp) | --> | Analysis |
+--------------+ +--------------+ +--------------+ | (sema.cpp) |
+--------------+
|
Validation Check:
- Type must be `truth`
- Expressions valid
|
v
+--------------+ +--------------+ +--------------+
| KVM Bytecode | <-- | CodeGen | <----------------------- | Analysis |
| Execution | | (codegen.cpp)| Passed Verification | Result |
+--------------+ +--------------+ +--------------+
Type Invariance in Conditions
The Analyzer class in sema.cpp enforces strict type checking. Passing non-boolean types to require or assert triggers a diagnostic error:
// Analysis snippet concept from sema.cpp if (condition_type != Type::Bool) { diagnostics_.error( statement.location(), "type mismatch: require condition must be `truth`, found `" + std::string(type_name(condition_type)) + "`" ); }
This prevents common bugs found in dynamically-typed smart contract systems, such as non-zero integers accidentally evaluating to true.
Diagnostic Example
Consider the following invalid Quorlin code:
// INCORRECT number activeStatus = 1; require activeStatus, "contract is inactive"; // Error!
The compiler reports an explicit semantic error:
error: type mismatch: require condition must be `truth`, found `number` | 2 | require activeStatus, "contract is inactive"; | ^^^^^^^^^^^^ expected `truth`
To resolve this, explicitly compare the numerical value against a threshold:
// CORRECT number activeStatus = 1; require activeStatus == 1, "contract is inactive";
9.7 Comprehensive Design Pattern: Checks-Effects-Interactions
To write secure contracts, input validation using require must be combined with proper execution ordering. Quorlin enforces clear state transitions using the Checks-Effects-Interactions pattern.
Pattern Rules
- Checks: Validate all incoming parameters, caller privileges, and internal state requirements using
require. - Effects: Mutate local contract storage and update balances before invoking standard libraries or external contracts.
- Interactions: Perform cross-contract calls or external interactions after all internal storage transformations are committed.
Complete Example: Token Staking Pool
contract StakingPool { number totalStaked; map<address, number> stakedBalances; map<address, truth> isStaking; event Staked(address indexed user, number amount); event Unstaked(address indexed user, number amount); writes truth stake(number amount) { // --- 1. CHECKS --- require amount > 0, "cannot stake zero tokens"; // --- 2. EFFECTS --- stakedBalances[caller] = stakedBalances[caller] + amount; totalStaked = totalStaked + amount; isStaking[caller] = yes; emit Staked(caller, amount); // --- 3. INTERACTIONS --- // External call or token transfer logic here... return yes; } writes truth unstake(number amount) { // --- 1. CHECKS --- require amount > 0, "cannot unstake zero tokens"; number currentBalance = stakedBalances[caller]; require currentBalance >= amount, "unstake amount exceeds staked balance"; // --- 2. EFFECTS --- stakedBalances[caller] = currentBalance - amount; totalStaked = totalStaked - amount; if (stakedBalances[caller] == 0) { isStaking[caller] = no; } emit Unstaked(caller, amount); // --- 3. INVARIANT SAFETY CHECK --- assert totalStaked >= 0; return yes; } reads number getStakedBalance(address user) { return stakedBalances[user]; } }
By following this structure and using require for preconditions alongside assert for invariant guarantees, Quorlin contracts remain resilient, deterministic, and safe against state corruption on the Kortana Virtual Machine.