25. Cryptographic Primitives
Cryptographic primitives form the bedrock of trust, state integrity, and execution safety within the Kortana ecosystem. In the Kortana Virtual Machine (KVM) and the Quorlin smart contract language, cryptography governs identity, address space formatting, function selector computation, event topic hashing, and state trie persistence.
This chapter details the low-level implementation and high-level language interfaces for Kortana’s cryptographic primitives, referencing the backend implementation in kvm and the frontend compiler pipeline in quorlin.
1. Cryptographic Hash Functions in KVM
The KVM relies on two primary cryptographic hash functions: Keccak-256 and BLAKE3. These functions are embedded directly within the interpreter execution core (kvm/interpreter.cpp).
Keccak-256
Keccak-256 is the standard cryptographic hash function used across Ethereum-compatible virtual machines. In Kortana, Keccak-256 is essential for:
- Computing 4-byte ABI function selectors.
- Generating 32-byte event topics.
- Deriving deterministic storage slots and contract addresses.
BLAKE3
BLAKE3 is integrated into the KVM backend as a high-performance, tree-hash primitive. It provides fast cryptographic checksums, proof-of-inclusion generation, and internal system hashing.
Converting Digests to 256-Bit Machine Words
The KVM operates natively on 256-bit registers and stack values (uint256_t). Cryptographic digest outputs—represented as raw 32-byte arrays or specialized hash types (Hash256)—must be converted into big-endian 256-bit machine words before being loaded into execution registers.
In kvm/interpreter.cpp, the KVM relies on specialized conversion helper functions:
/// A hash as a 256-bit value. [[nodiscard]] uint256_t hash_to_word(const Hash256& hash) noexcept { const auto result = uint256_t::from_be_bytes(ByteView{hash.data(), Hash256::kSize}); return result.value_or(uint256_t::zero()); } /// A raw 32-byte digest as a 256-bit value. The hash functions return `std::array` rather than /// `Hash256`, so this is the overload the hashing opcodes use. [[nodiscard]] uint256_t digest_to_word(const std::array<uint8_t, 32>& digest) noexcept { const auto result = uint256_t::from_be_bytes(ByteView{digest.data(), digest.size()}); return result.value_or(uint256_t::zero()); }
By enforcing strict big-endian decoding (from_be_bytes), Kortana guarantees uniform numerical interpretation of cryptographic digests across heterogeneous hardware platforms.
2. Address Representation and Byte Packing
Account identities and smart contract addresses in Kortana are 20-byte (160-bit) cryptographic identifiers (Address::kSize = 20). However, because KVM registers operate on 32-byte (256-bit) words, address values are mapped to and from machine words using zero-extension (left-padding) and truncation (low-byte extraction).
Address Word Padding and Extraction
When an address is placed onto the execution stack or loaded into a KVM register, the 20-byte payload is placed in the low-order 20 bytes of a 32-byte big-endian word, leaving the high-order 12 bytes zero-padded:
/// An address as a 256-bit value, left-padded — how a contract sees one. [[nodiscard]] uint256_t address_to_word(const Address& address) noexcept { const auto result = uint256_t::from_be_bytes(ByteView{address.data(), Address::kSize}); return result.value_or(uint256_t::zero()); } /// The low 20 bytes of a word, as an address — how a contract names one. [[nodiscard]] Address address_from_word(const uint256_t& word) noexcept { Address out; const auto bytes = word.to_be_bytes(); std::memcpy(out.data(), bytes.data() + (32 - Address::kSize), Address::kSize); return out; }
When converting a 256-bit machine word back into a 20-byte Kortana address (address_from_word), the interpreter discards the upper 12 bytes (32 - 20 = 12) and copies the low 20 bytes into the destination Address structure.
3. ABI Serialization and Type Name Normalization
Quorlin intentionally uses human-readable, English-like keywords for standard data types in source code. However, when compiling contracts to output JSON ABIs or generating cryptographic function selectors, the Quorlin compiler standardizes type names into standard Ethereum ABI signatures.
Standard Type Mapping vs. ABI Type Mapping
The internal compiler logic (quorlin/parser.cpp) separates developer-facing syntax from cryptographic signature string representation:
| Quorlin Keyword | Internal AST Type (Type) | Developer Diagnostic (type_name) | ABI Signature Type (abi_type_name) |
|---|---|---|---|
number | Type::U256 | "number" | "uint256" |
truth | Type::Bool | "truth" | "bool" |
address | Type::Address | "address" | "address" |
text | Type::Text | "text" | "string" |
nothing | Type::Void | "nothing" | "void" |
The compiler routine enforces this distinction explicitly:
std::string_view type_name(Type type) noexcept { // What a person reads in a diagnostic: the words the language actually uses. switch (type) { case Type::U256: return "number"; case Type::Bool: return "truth"; case Type::Address: return "address"; case Type::Text: return "text"; case Type::Void: return "nothing"; case Type::Invalid: return "<invalid>"; } return "<unknown>"; } std::string_view abi_type_name(Type type) noexcept { // What goes into a selector or an event topic — a different question from what a person reads. // The ABI is Ethereum's, so these are Ethereum's names: `uint256`, not `number` and not `u256`. switch (type) { case Type::U256: return "uint256"; case Type::Bool: return "bool"; case Type::Address: return "address"; case Type::Text: return "string"; case Type::Void: return "void"; case Type::Invalid: return "<invalid>"; } return "<unknown>"; }
Computing Function Selectors
A function selector is defined as the first 4 bytes of the Keccak-256 hash of the normalized canonical function signature string.
For example, given a Quorlin function signature:
writes truth transfer(address recipient, number amount)
- The compiler constructs the canonical ABI signature using
abi_type_name: $$\text{Signature String} = \text{"transfer(address,uint256)"}$$ - The signature string undergoes Keccak-256 hashing: $$\text{Keccak256}("transfer(address,uint256)") = \text{0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b}$$
- The high-order 4 bytes are sliced to derive the function selector: $$\text{Selector} = \text{0xa9059cbb}$$
If the compiler mistakenly hashed "transfer(address,number)", external EVM tools and smart contracts would be unable to interface with Quorlin contracts. Standardizing canonical ABI signature generation guarantees interoperability across the Kortana ecosystem.
4. Cryptographic Events and Topic Hashing
Quorlin supports indexed log emission via the event and emit constructs. Cryptographically, event logs in Kortana consist of:
- Topic 0: The Keccak-256 hash of the normalized event signature (e.g.,
Transfer(address,address,uint256)). - Topics 1..3: Up to three 32-byte
indexedparameters. Parameters marked asindexedare hashed (if dynamic) or padded directly into a 32-byte event topic, enabling fast log filtering by off-chain indexers. - Data: Non-indexed event parameters serialized sequentially in memory.
In quorlin/abi.cpp, parameter metadata for events is serialized to JSON ABI definitions, indicating the indexed flag status:
[[nodiscard]] std::string parameter_json(std::string_view name, Type type, bool indexed, bool with_indexed) { std::string out = "{\"name\":" + quoted(name) + ",\"type\":" + quoted(abi_type_name(type)) + ",\"internalType\":" + quoted(type_name(type)); if (with_indexed) out += ",\"indexed\":" + std::string{indexed ? "true" : "false"}; return out + "}"; }
5. Unified State Trie and Storage Keys
Kortana maintains account state and smart contract persistent storage using a unified Merkle State Trie (kvm/state_host.cpp).
Storage Keys
Every persistent state variable in a Quorlin contract (such as scalar variables or key-value entries in a map) maps directly to a 256-bit storage key (uint256_t).
- Simple Fields: Allocated sequential numeric slots starting at index $0, 1, 2, \dots$
- Mappings (
map<K, V>): The storage slot for a specific key $K$ at mapping location $p$ is derived cryptographically by concatenating $K$ and $p$, then hashing the result with Keccak-256: $$\text{StorageSlot}(K, p) = \text{Keccak256}(K \mathbin{\Vert} p)$$
Reading and Writing Cryptographic State
The KVM interacts with storage via the StateHost interface (kvm/state_host.cpp):
Result<uint256_t> StateHost::get_storage(const Address& address, const uint256_t& key) const { // Straight to the shared trie. This one line is where §21's "unified state trie" is actually // satisfied — the KEVM reads the same slot through the same call. return world_.get_storage(address, key); } Result<uint256_t> StateHost::set_storage(const Address& address, const uint256_t& key, const uint256_t& value) { // The previous value is returned so the interpreter can price the write without a second read: // filling an empty slot costs several times an overwrite, and it needs to know which this is. KORTANA_TRY_ASSIGN(const uint256_t previous, world_.get_storage(address, key)); KORTANA_TRY(world_.put_storage(address, key, value)); return previous; }
Because state reads and writes query the unified Merkle trie, write operations are dynamically metered according to state transitions (e.g., zero-to-non-zero vs non-zero-to-non-zero slot updates).
6. Binary Module Cryptographic Structure
Compiled KVM bytecode is structured into a binary module format (kvm/module.cpp). To prevent tampering, facilitate fast binary verification, and maintain strict deterministic decoding, module headers are strictly formatted using big-endian numeric encoding:
offset size field
0 4 magic "KVM\0"
4 2 version (big endian)
6 4 constant count (big endian)
10 4 instruction count (big endian)
14 4 entry point (big endian, an instruction index)
18 ... constants, 32 bytes each, big endian
... ... code, 4 bytes per instruction, big endian
The 4-byte ASCII magic bytes KVM\0 (0x4B 0x56 0x4D 0x00) immediately validate the module format prior to constant pool and instruction deserialization.
7. Practical Quorlin Code Examples
The following smart contracts demonstrate how cryptographic addresses, function selectors, and event topics operate in high-level Quorlin code.
Example 1: Token Vault with Event Logging and Address Cryptography
contract SecurityVault { address owner; number totalDeposited; map<address, number> vaultBalances; event Deposit(address indexed sender, number amount); event Withdraw(address indexed recipient, number amount); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); constructor { owner = caller; totalDeposited = 0; } reads address getOwner() { return owner; } reads number getBalance(address user) { return vaultBalances[user]; } writes truth deposit(number amount) { require amount > 0, "Deposit must be greater than zero"; vaultBalances[caller] = vaultBalances[caller] + amount; totalDeposited = totalDeposited + amount; emit Deposit(caller, amount); return yes; } writes truth withdraw(number amount) { number currentBalance = vaultBalances[caller]; require currentBalance >= amount, "Insufficient vault balance"; vaultBalances[caller] = currentBalance - amount; totalDeposited = totalDeposited - amount; emit Withdraw(caller, amount); return yes; } writes truth transferOwnership(address newOwner) { require caller == owner, "Only owner can transfer ownership"; require newOwner != 0x0000000000000000000000000000000000000000, "Invalid new owner"; address oldOwner = owner; owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); return yes; } }
Example 2: Interfacing with Standard Token Selectors (IERC20)
Quorlin standard library declarations (quorlin/standard.cpp) allow developers to invoke standard token interfaces. The function signatures below are mapped to standard EVM Keccak-256 selectors:
interface IERC20 { reads number totalSupply() reads number balanceOf(address account) reads number allowance(address owner, address spender) writes truth transfer(address recipient, number amount) writes truth approve(address spender, number amount) writes truth transferFrom(address sender, address recipient, number amount) } contract TokenEscrow { address tokenAddress; constructor { // Set target ERC-20 contract address tokenAddress = caller; } reads number checkEscrowBalance(address account) { IERC20 token = IERC20(tokenAddress); // Interoperable Keccak-256 selector generation for `balanceOf(address)` return token.balanceOf(account); } }
8. Cryptographic Operation Gas Metering
Cryptographic operations (such as hashing, state reads, and cryptographic address verification) are priced according to explicit execution tiers in kvm/gas.cpp.
uint64_t base_cost(Opcode opcode, const params::GasSchedule& schedule) noexcept { switch (opcode) { // ... case Opcode::Add: case Opcode::Sub: case Opcode::Lt: case Opcode::Gt: case Opcode::Eq: case Opcode::And: case Opcode::Or: case Opcode::Xor: return kGasVeryLow; // 3 gas units case Opcode::Mul: case Opcode::Div: return kGasLow; // 5 gas units // ... } }
By enforcing strict base execution costs and memory expansion costs, the KVM ensures that contract operations involving cryptographic digest calculations, byte packing, and Merkle trie interactions remain bounded and predictable.