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 CALL
s 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 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.
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.
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.
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. |
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. |
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. |
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. |
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. |
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. |
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. |
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. |
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. |
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. |
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.
CALL
sThe 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.
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.
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.