Table of contents
Runtime
In this section, we define Solri’s execution environment and initial runtime.
Execution environment
The following section defines how the execution environment for Solri works.
Memory management
A WebAssembly instance for Solri is expected to handle its own memory management. The instance is expected to be long-standing, and might handle executing multiple blocks.
First, the guest is expected to export its linear memory. This is to make it possible that host can read slice of data (blocks, code, and metadata) from guest. The host will not attempt to allocate any memory, nor handle any other memory management functionality for the guest.
The guest should also exports the following functions:
- 
fn write_code(len: i32) → i32: indicates that the host wants to write thecodeparameter prior of callingexecutefunction. The return value should be the address in guest where host can write the data slice.
- 
fn write_block(len: i32) → i32: indicates that the host wants to write theblockparameter prior of callingexecutefunction. The return value should be the address in guest where host can write the data slice.
- 
fn read_metadata() → i32: available afterexecuteis called. This indicates that the host can read the metadata result of the block at the given address.
- 
fn free(): frees wrote code, block parameters, and metadata result, if any.
Block execution
This section defines the execute export, which exposes the method to
execute a block for a runtime, assuming parent block provided and code
provided is valid, defined as:
fn execute() -> i32;Prior of calling this function, the host should first call
write_code and write_block to feed in the code and block data
slice. If the values are not available, it is an invalid call and the
guest should trap.
The runtime can change code being executed for the next block by
modifying memory location and pass the result values in metadata. The
function returns -1 if the block is deemed invalid. Otherwise, the
result is 0.
If the runtime cannot execute on the current WebAssembly executor, it
should trap (for example, by using the unreachable opcode).
If the execution is successful, the runtime is expected to return the
necessary metadata for the host to further consider the validity of
the block and be able to store it in database. The structure should be
set at result_ptr, using C representation:
#[repr(C)]
struct Metadata {
  timestamp: i64,
  difficulty: i64,
  parent_hash_ptr: i32,
  parent_hash_len: i32,
  hash_ptr: i32,
  hash_len: i32,
  code_ptr: i32,
  code_len: i32,
}The full block is up to interpretation of the runtime. The runtime
should return metadata required as defined above. The host can fetch
the metadata using read_metadata function. If the execution was
invalid, the host should not assume data under result_ptr.
execute should always be used together with memory management
functions. A typical cycle looks like below:
- 
Call write_codefor guest to allocate memory for code data slice. Then copy code parameter to guest memory.
- 
Call write_blockfor guest to allocate memory for block data slice. Then copy block parameter to guest memory.
- 
Call execute.
- 
Call read_metadataif execution was successful, and copy metadata result from guest back to host.
- 
Call freeto deallocate parameters and results.
Timestamp validation and fork choice
When mining, it is always expected that the miner uses a client whose
native version supports the current WebAssembly runtime. In this case,
the miner should be able to decode timestamp from incoming
blocks. The client is then responsible to verify that timestamp is
reasonable. The margin is up to each client implementation. When only
validating blocks, one can use the returned metadata to know the
validity of the block, if native version and WebAssembly runtime
mismatches.
Fork choice rule is defined as choosing the block with the highest total difficulty. Notice that although we don’t limit what the PoW algorithm is, the fork choice rule is indeed limited by total difficulty, to ensure backward compatibility.
Initial runtime and block structure
The actual runtime (which exposes execute) function is expected to
be stateless. Notice we don’t provide any imports for the execution
environment. The initial runtime only supports two operations — transfer, and runtime upgrade.
All below structures are stored in binary merkle tree (bm is the
reference implementation), and encoded via SCALE (parity-codec is
the reference implementation).
Types and structures
We define State, which consists of the following structures:
type AccountId = u64;
type UpgradeId = u64;
type Balance = u128;
type Nonce = u64;
type Public = H256;
struct Account {
  balance: Balance,
  nonce: Nonce,
  public: Public
}
struct Upgrade {
  votes: VecArray<bool, U4096>,
  code: Vec<u8>,
}
struct State {
  accounts: Vec<Account>,
  upgrades: Vec<Upgrade>,
}The block structures are defined as follows:
type Difficulty = u128;
type Timestamp = u64;
type Signature = H512;
type StateProof = bm::CompactValue<bm_le::Value>;
enum TransferId {
  Coinbase,
  Existing(AccountId),
  New(Public),
}
enum CoinbaseId {
  Existing(AccountId),
  New(Public),
}
enum UnsealedTransaction {
  UpgradeProposal {
    from: AccountId,
    code: Vec<u8>,
  },
  Transfer {
    from: AccountId,
    to: Vec<(TransferId, Balance)>,
  },
}
struct Transaction {
  unsealed: UnsealedTransaction,
  signature: Signature,
}
struct UnsealedBlock {
  parent_id: Option<H256>,
  coinbase: CoinbaseId,
  timestamp: Timestamp,
  difficulty: Difficulty,
  state_proof: StateProof,
  upgrade_vote: Option<UpgradeId>,
  transactions: Vec<Transaction>,
}
struct Block {
  unsealed: UnsealedBlock,
  pow_proof: Vec<u8>,
}Block execution
- 
Validity of Proof of Work Proof: Upon receiving a new block structure, the executor should first decode difficultyandtimestamp, and check whetherpow_proofis valid under the given difficulty and timestamp. We’re still deciding on the actual proof of work algorithm and difficulty adjustment algorithm. The block time is tentatively set to one minute.
- 
Validity of State Proof: Given a transaction or a coinbase id, it is possible to know all the state (defined as generalized merkle index) it is going to touch. Check all values exist in block’s given state proof. 
- 
Validity of Transaction Signatures: Check that transaction.fromexists instate.accounts, and check that thesignatureis valid againststate.accounts[i].public. Increasestate.accounts[i].nonceby one.
- 
Execution of Transfer Transaction: Check that fromaccount has balance greater than alltoamount combined. Transfer value offrominto alltoaccount, with balance specified as the second item in the tuple. Iftois coinbase, transfer to coinbase account. Iftois new account, create a new account with all fields set to0, and transfer to the new account. Note that we don’t have the concept of transaction fees — it is fulfilled bycoinbasedestination.
- 
Execution of Upgrade Proposal Transaction: Deduct PROPOSAL_COSTfromfromaccount. After that, pushcodeas a new upgrade proposal instate.upgrades, and set itsvotestofalse.
- 
Evaluation of Current Upgrade Proposals: Iterate over all state.upgrades, shift allvotesto the right. Pushtrueifblock.upgrade_voteequals to the proposal index. Otherwise, pushfalse. If a given proposal’svoteshas more than 3072true(more than 75% of blocks voted for the proposal in the past 4096 blocks), then set the runtime code as in the upgrade proposal. At this moment, the initial runtime continue its execution, and reaches its end-of-life after the current block finishes.
- 
Block Rewards: Increase block.coinbase’s balance by `BLOCK_REWARD. If coinbase points to a new account, create it with all fields set to0.