Try  HackMD Logo HackMD

The Async VM (AVM)

tags: Architecture Technical Overview

The Async VM (AVM) is Firechain's virtual machine and execution engine. It is a turing-complete VM with a rich set of instructions that can be used to build complex applications that run in a deterministic and trustless environment. The AVM natively supports asynchronous execution, which allows Firechain to execute smart contracts in parallel. In an effort to maintain compatibility with existing Web3 standards, most of the AVM's instructions are similar or identical to those of the Ethereum Virtual Machine (EVM). However, it's important to note that the AVM is not an EVM fork, and there are a number of differences in the way they operate that are important to understand.

For example, the AVM handles CALLs asynchronously. This means that a CALL instruction will not block the execution of the current function. Instead, it will spawn an async request and return a message ID, and the execution of the current context will resume immediately. The spawned request might be executed in parallel to the current context, at some arbitrary later time, or perhaps not at all. In other words, it's not guaranteed when or if a remote execution request will return or have any observable effects. This is a powerful concept, and it's also a major difference between the AVM and the EVM which developers must consider when building on Firechain.

The AVM, like other popular blockchain VMs, uses a call stack to control the context and flow of execution. Unlike most smart contract platforms, the AVM doesn't use a global stack. Instead, each account has its own execution stack, and each message is executed on its own stack. This means that the network can execute messages asynchronously, and each of those executions can further spawn reactive executions elsewhere in the system. It also allows contracts to schedule code to run at a specific time in the future, which opens the door to a wide range of use cases. It's easy to see how this model adds utility that the synchronous execution engines of other blockchains simply can't offer.

The Power of Async

The AVM's asynchronous execution engine is based on the actor model. In this model, each account is an actor that can send and receive messages to and from other actors in the system. Each account effectively has its own private state and its own message queue. When an account receives a message, it's placed into the account's queue until a validator executes the code associated with that message. The validator executes the code and then sends a response back to the sender. This response is placed into the sender's queue, and the sender can then process the response however it sees fit.

The AVM's unique architecture allows for an inherently more efficient and scalable network. It also enables smart contract automation through on-chain event listeners and scheduled execution, as well as provably secure on-chain randomness.

Account Abstraction

In Firechain, the concept of an account is somewhat abstract. That's because all addresses are technically represented as entities (i.e. code) on the network. There's a basic interface that all addresses support by default, but it's possible to arbitrarily extend that functionality with custom implementations. For example, an address can represent a single person, a business, a group of people, a group of businesses, a group of groups, or etc practically anything that can be logically represented and programmatically enforced. The current implementation for a given address is controlled by its signer, and it can be replaced at any time*. This is a powerful feature that enables Firechain to support a huge variety of use cases, and it's one of the reasons why Firechain is so flexible.

NB: An account can assign a new implementation by registering it with the FNS Router. This is possible at any time, unless the current implementation has been sealed.

The default implementation for an account's base address (its public key hash) acts as a "dumb" queue for inbound token transfer requests. The desktop wallet application can be configured to auto-approve incoming requests, or display a prompt for manual review. Of course, custom on-chain logic can be deployed to handle these cases programmatically. In this case, a deterministic address will be generated for the deployed code, and that address can then be registered as the canonical implementation.

Instruction Set

The following table lists the instructions supported by the Async VM. The table is organized by instruction category. Each instruction is listed with its opcode, name, and description.

Arithmetic

The following instructions perform arithmetic operations on the stack.

Opcode Name Description
0x01 ADD Add two operands and return the sum.
0x02 MUL Multiply two operands and return the product.
0x03 SUB Subtract two operands and return the difference.
0x04 DIV Perform integer division on two operands and return the quotient.
0x05 SDIV Same as DIV, but for signed integers.
0x06 MOD Perform integer modulus on two operands and return the remainder.
0x07 SMOD Same as MOD, but for signed integers.
0x08 ADDMOD Add two operands and return the sum modulo a third operand.
0x09 MULMOD Multiply two operands and return the product modulo a third operand.
0x0a EXP Raise a base to an exponent and return the result.
0x0b SIGNEXTEND Extend the length of a signed integer.

Comparison and Bitwise Logic

The following instructions perform comparison and bitwise logic operations on the stack.

Opcode Name Description
0x10 LT Return 1 if the first operand is less than the second operand, otherwise return 0.
0x11 GT Return 1 if the first operand is greater than the second operand, otherwise return 0.
0x12 SLT Same as LT but for signed operands.
0x13 SGT Same as GT but for signed operands.
0x14 EQ Return 1 if the operands are equal, otherwise return 0.
0x15 ISZERO Return 1 if the operand is zero, otherwise return 0.
0x16 AND Bitwise AND two operands and return the result.
0x17 OR Bitwise OR two operands and return the result.
0x18 XOR Bitwise XOR two operands and return the result.
0x19 NOT Bitwise NOT the operand and return the result.
0x1a BYTE Return the nth byte of the operand.
0x1b SHL Shift the operand left by the number of bits specified by the second operand.
0x1c SHR Shift the operand right by the number of bits specified by the second operand.
0x1d SAR Arithmetic right shift. The sign bit is copied.

Hashing and Cryptography

The following instructions perform synchronous hashing and cryptography operations on the stack. Note that there are many other cryptographic primitives supported by precompiled contracts which are not listed here, including functionality related to zero-knowledge proofs and elliptic curve cryptography.

Opcode Name Description
0x20 SHA3 Compute the SHA3-256 hash of the operand and return the result.
0x21 SHA256 Compute the SHA256 hash of the operand and return the result.
0x22 BLAKE2B Compute the BLAKE2B hash of the operand and return the result.
0x23 RIPEMD160 Compute the RIPEMD160 hash of the operand and return the result.
0x24 KECCAK256 Compute the KECCAK256 hash of the operand and return the result.

Environment

The following instructions provide information about the environment in which the smart contract is executing.

Opcode Name Description
0x30 ADDRESS Return the address of the current account context.
0x31 BALANCE Return the balance of an account. Accepts an address and token ID as operands.
0x32 ORIGIN Return the address of the current message's sender or event source.
0x33 CALLER Same as ORIGIN but returns the current account's address in an event listener.
0x34 CALLVALUE Return the token value of the current message. Accepts a token ID as an operand.
0x35 CALLDATALOAD Return the data from the current message's body. Accepts an offset and length as operands.
0x36 CALLDATASIZE Return the size of the current message's body.
0x37 CALLDATACOPY Copy the data from the current message's body to memory. Accepts an offset and length as operands.
0x38 CODESIZE Return the size of the current contract's code.
0x39 CODECOPY Copy the current contract's code to memory. Accepts an offset and length as operands.
0x3a HEAT Return the amount of heat generated by the current message.
0x3b RAND Return a random number. Always returns a fresh random number.
0x3c RRAND Return a random number. Always returns the same number within the same execution context.
0x3d RETURNDATASIZE Return the size of an external call's return data, or 0 if not available. Accepts a message ID as an operand.
0x3e RETURNDATACOPY Copy an external call's return data to memory. Accepts a message ID, data offset and length as operands.
0x40 MSGHASH Return the ID of the current message.
0x41 COINBASE Return the address of the current validator.
0x42 TIMESTAMP Return the current execution's timestamp. Will change if execution is suspended.
0x43 HEIGHT Return the current account's height, including the current message.
0x44 LCVCOUNT Return the number of validators in the local consensus group.
0x45 GCVCOUNT Return the number of validators in the global consensus group.
0x46 QUEUETIME Return the timestamp when the current message was added to the queue.
0x47 COMMITTIME Return the timestamp when the current message was committed by the LCG.
0x48 CONFTIME Return the timestamp when the current message was first globally confirmed. Returns 0 if not yet confirmed.
0x49 MSGTIME Return the timestamp from the current message's header.
0x4a CHAINID Return the chain ID of the current chain.
0x4b CONFDEPTH Return the number of global confirmations. Returns 0 if not yet confirmed.

Stack and Memory

The following instructions manipulate the stack and memory.

Opcode Name Description
0x50 POP Remove the top item from the stack.
0x51 MLOAD Load a word from memory. Accepts an offset as an operand.
0x52 MSTORE Store a word to memory. Accepts an offset as an operand.
0x53 MSTORE8 Store a byte to memory. Accepts an offset as an operand.
0x54 SLOAD Load a word from storage. Accepts a key as an operand.
0x55 SSTORE Store a word to storage. Accepts a key and a value as operands.
0x56 JUMPDEST Mark a valid jump destination.
0x57 JUMP Jump to a label. Accepts a label as an operand.
0x58 JUMPI Jump to a label if the top item on the stack is not zero. Accepts a label as an operand.
0x59 PC Return the current program counter.
0x5a MSIZE Return the current size of memory.

PUSH (Push a value onto the stack)

Opcode Name Description
0x60 PUSH1 Push a 1-byte value onto the stack.
0x61 PUSH2 Push a 2-byte value onto the stack.
0x62 PUSH3 Push a 3-byte value onto the stack.
0x7d PUSH30 Push a 30-byte value onto the stack.
0x7e PUSH31 Push a 31-byte value onto the stack.
0x7f PUSH32 Push a 32-byte value onto the stack.

DUP (Duplicate an item on the stack)

Opcode Name Description
0x80 DUP1 Duplicate the top item on the stack.
0x81 DUP2 Duplicate the 2nd item on the stack.
0x82 DUP3 Duplicate the 3rd item on the stack.
0x8d DUP14 Duplicate the 14th item on the stack.
0x8e DUP15 Duplicate the 15th item on the stack.
0x8f DUP16 Duplicate the 16th item on the stack.

Swap (Swap two items on the stack)

Opcode Name Description
0x90 SWAP1 Swap the top two items on the stack.
0x91 SWAP2 Swap the 1st and 3rd items on the stack.
0x92 SWAP3 Swap the 1st and 4th items on the stack.
0x9d SWAP14 Swap the 1st and 15th items on the stack.
0x9e SWAP15 Swap the 1st and 16th items on the stack.
0x9f SWAP16 Swap the 1st and 17th items on the stack.

Logging

The following instructions are used to log data. Logs are stored in the current message's receipt. They're always accessible off-chain, and they can also be observed in real-time through on-chain event listeners.

Opcode Name Description
0xa0 LOG0 Log an event with no topics. Accepts a memory offset and length as operands.
0xa1 LOG1 Log an event with one topic. Accepts a memory offset, length and topic as operands.
0xa2 LOG2 Log an event with two topics. Accepts a memory offset, length and two topics as operands.
0xa3 LOG3 Log an event with three topics. Accepts a memory offset, length and three topics as operands.
0xa4 LOG4 Log an event with four topics. Accepts a memory offset, length and four topics as operands.

System operations

The following instructions are used to interact with the broader network.

Opcode Name Description
0xf0 CALL Create an async request to a remote contract. Accepts a target account, token id, token value, and message data as operands. Returns a message ID.
0xf1 WAIT Wait for the result of a request. Accepts a message ID and deadline as operands.
0xf2 RETURN Return from the current function. Accepts a value, length and offset as operands.
0xf3 REVERT Revert the current message. Accepts a value, length and offset as operands.
0xf4 FREEZE Freeze the current account. Suspends job execution, disables outbound messages, and freezes storage.
0xf5 UNFREEZE Unfreeze the current account. Resumes scheduled job execution, enables outbound messaging, and restores storage.

Notes & Gotchas

As noted elsewhere, the AVM is designed to be largely compatible with the EVM, but it's not an EVM fork. As such, there are a few key things developers need to consider when building decentralized applications on Firechain.

Remote CALLs

The CALL opcode is used to create a new request to another entity. The request is not executed synchronously, but is instead added to the message queue of the recipient. A message ID is synchronously returned as the result of the CALL opcode, and it can be used to query the status of the message or retrieve the return data at a later time. However, it's important to remember that the return data, if any, will only become available once a response is generated.

Async functionality

Ideally, developers should write event-driven code consisting of reactive functions as this is the best way to take full advantage of Firechain's asynchronous design. However, it's possible to make things "feel" more synchronous by using CALL in combination with WAIT to suspend execution until a remote request is complete. The WAIT instruction will block execution until the CALL returns or the deadline passes. The WAIT opcode may be deprecated in the future due to complexity and security concerns. For now, it simply generates a lot of heat which reflects the relatively high demand it puts on the network.

Time orientation

Several time-related opcodes are available to contracts. These opcodes return the timestamp when the current execution started, when the message was committed to the account's chain by the local consensus group, when the message was first globally confirmed, and the timestamp indicated by the message itself. These opcodes can be used to implement time-based logic in contracts.