Comment on page
Unkey is a decentralized open source blockchain that will succeed Ethereum in the future.
In Unkey , the state is made up of objects called "accounts", with each account having a 20-byte address and state transitions being direct transfers of value and information between accounts. An Unkey account contains four fields:
- The nonce, a counter used to make sure each transaction can only be processed once
- The account's current UNK balance
- The account's contract code, if present
- The account's storage (empty by default)
"UNK" is the main internal crypto-fuel of Unkey, and is used to pay transaction fees. In general, there are two types of accounts: externally owned accounts, controlled by private keys, and contract accounts, controlled by their contract code. An externally owned account has no code, and one can send messages from an externally owned account by creating and signing a transaction; in a contract account, every time the contract account receives a message its code activates, allowing it to read and write to internal storage and send other messages or create contracts in turn.
Note that "contracts" in Unkey should not be seen as something that should be "fulfilled" or "complied with"; rather, they are more like "autonomous agents" that live inside of the Unkey execution environment, always executing a specific piece of code when "poked" by a message or transaction, and having direct control over their own balance and their own key/value store to keep track of persistent variables.
The term "transaction" is used in Unkey to refer to the signed data package that stores a message to be sent from an externally owned account. Transactions contain:
- The recipient of the message
- A signature identifying the sender
- The amount to transfer from the sender to the recipient
- An optional data field
STARTGASvalue, representing the maximum number of computational steps the transaction execution is allowed to take
GASPRICEvalue, representing the fee the sender pays per computational step
The first three are standard fields expected in any cryptocurrency. The data field has no function by default, but the virtual machine has an opcode using which a contract can access the data; as an example use case, if a contract is functioning as an on-blockchain domain registration service, then it may wish to interpret the data being passed to it as containing two "fields", the first field being a domain to register and the second field being the IP address to register it to. The contract would read these values from the message data and appropriately place them in storage.
GASPRICEfields are crucial for Unkey's anti-denial of service model. In order to prevent accidental or hostile infinite loops or other computational wastage in code, each transaction is required to set a limit to how many computational steps of code execution it can use. The fundamental unit of computation is "gas"; usually, a computational step costs 1 gas, but some operations cost higher amounts of gas because they are more computationally expensive, or increase the amount of data that must be stored as part of the state. There is also a fee of 5 gas for every byte in the transaction data. The intent of the fee system is to require an attacker to pay proportionately for every resource that they consume, including computation, bandwidth and storage; hence, any transaction that leads to the network consuming a greater amount of any of these resources must have a gas fee roughly proportional to the increment.
Contracts have the ability to send "messages" to other contracts. Messages are virtual objects that are never serialized and exist only in the Unkey execution environment. A message contains:
- The sender of the message (implicit)
- The recipient of the message
- The amount to transfer alongside the message
- An optional data field
Essentially, a message is like a transaction, except it is produced by a contract and not an external actor. A message is produced when a contract currently executing code executes the
CALLopcode, which produces and executes a message. Like a transaction, a message leads to the recipient account running its code. Thus, contracts can have relationships with other contracts in exactly the same way that external actors can.
Note that the gas allowance assigned by a transaction or contract applies to the total gas consumed by that transaction and all sub-executions. For example, if an external actor A sends a transaction to B with 1000 gas, and B consumes 600 gas before sending a message to C, and the internal execution of C consumes 300 gas before returning, then B can spend another 100 gas before running out of gas.
The Unkey state transition function,
APPLY(S,TX) -> S'can be defined as follows:
- 1.Check if the transaction is well-formed (ie. has the right number of values), the signature is valid, and the nonce matches the nonce in the sender's account. If not, return an error.
- 2.Calculate the transaction fee as
STARTGAS * GASPRICE, and determine the sending address from the signature. Subtract the fee from the sender's account balance and increment the sender's nonce. If there is not enough balance to spend, return an error.
GAS = STARTGAS, and take off a certain quantity of gas per byte to pay for the bytes in the transaction.
- 4.Transfer the transaction value from the sender's account to the receiving account. If the receiving account does not yet exist, create it. If the receiving account is a contract, run the contract's code either to completion or until the execution runs out of gas.
- 5.If the value transfer failed because the sender did not have enough money, or the code execution ran out of gas, revert all state changes except the payment of the fees, and add the fees to the miner's account.
- 6.Otherwise, refund the fees for all remaining gas to the sender, and send the fees paid for gas consumed to the miner.
For example, suppose that the contract's code is:
self.storage[calldataload(0)] = calldataload(32)
Note that in reality the contract code is written in the low-level EVM code; this example is written in Serpent, one of our high-level languages, for clarity, and can be compiled down to EVM code. Suppose that the contract's storage starts off empty, and a transaction is sent with 10 UNK value, 2000 gas, 0.001 UNK gasprice, and 64 bytes of data, with bytes 0-31 representing the number
2and bytes 32-63 representing the string
CHARLIE. The process for the state transition function in this case is as follows:
- 1.Check that the transaction is valid and well formed.
- 2.Check that the transaction sender has at least 2000 * 0.001 = 2 UNK. If it is, then subtract 2 UNK from the sender's account.
- 3.Initialize gas = 2000; assuming the transaction is 170 bytes long and the byte-fee is 5, subtract 850 so that there is 1150 gas left.
- 4.Subtract 10 more UNK from the sender's account, and add it to the contract's account.
- 5.Run the code. In this case, this is simple: it checks if the contract's storage at index
2is used, notices that it is not, and so it sets the storage at index
2to the value
CHARLIE. Suppose this takes 187 gas, so the remaining amount of gas is 1150 - 187 = 963
- 6.Add 963 * 0.001 = 0.963 UNK back to the sender's account, and return the resulting state.
If there was no contract at the receiving end of the transaction, then the total transaction fee would simply be equal to the provided
GASPRICEmultiplied by the length of the transaction in bytes, and the data sent alongside the transaction would be irrelevant.
Note that messages work equivalently to transactions in terms of reverts: if a message execution runs out of gas, then that message's execution, and all other executions triggered by that execution, revert, but parent executions do not need to revert. This means that it is "safe" for a contract to call another contract, as if A calls B with G gas then A's execution is guaranteed to lose at most G gas. Finally, note that there is an opcode,
CREATE, that creates a contract; its execution mechanics are generally similar to
CALL, with the exception that the output of the execution determines the code of a newly created contract.
The code in Unkey contracts is written in a low-level, stack-based bytecode language, referred to as "Unkey virtual machine code" or "EVM code". The code consists of a series of bytes, where each byte represents an operation. In general, code execution is an infinite loop that consists of repeatedly carrying out the operation at the current program counter (which begins at zero) and then incrementing the program counter by one, until the end of the code is reached or an error or
RETURNinstruction is detected. The operations have access to three types of space in which to store data:
- The stack, a last-in-first-out container to which values can be pushed and popped
- Memory, an infinitely expandable byte array
- The contract's long-term storage, a key/value store. Unlike stack and memory, which reset after computation ends, storage persists for the long term.
The code can also access the value, sender and data of the incoming message, as well as block header data, and the code can also return a byte array of data as an output.
The formal execution model of EVM code is surprisingly simple. While the Unkey virtual machine is running, its full computational state can be defined by the tuple
(block_state, transaction, message, code, memory, stack, pc, gas), where
block_stateis the global state containing all accounts and includes balances and storage. At the start of every round of execution, the current instruction is found by taking the
pcth byte of
code(or 0 if
pc >= len(code)), and each instruction has its own definition in terms of how it affects the tuple. For example,
ADDpops two items off the stack and pushes their sum, reduces
gasby 1 and increments
pcby 1, and
SSTOREpushes the top two items off the stack and inserts the second item into the contract's storage at the index specified by the first item. Although there are many ways to optimize Unkey virtual machine execution via just-in-time compilation, a basic implementation of Unkey can be done in a few hundred lines of code.