This commit is contained in:
Anatoly Yakovenko 2018-06-21 16:19:14 -07:00 committed by Greg Fitzgerald
parent 7ba5d5ef86
commit 475a76e656
1 changed files with 69 additions and 52 deletions

View File

@ -2,9 +2,35 @@
Our approach to smart contract execution is based on how operating systems load and execute dynamic code in the kernel. 
![Figure 1. Smart Contracts Stack](images/smart-contracts-stack.png)
+---------------------+ +---------------------+
| | | |
| +------------+ | | +------------+ |
| | | | | | | |
| | frontend | | | | verifier | |
| | | | | | | |
| +-----+------+ | | +-----+------+ |
| | | | | |
| | | | | |
| +-----+------+ | | +-----+------+ |
| | | | | | | |
| | llvm | | | | loader | |
| | | +------>+ | | |
| +-----+------+ | | +-----+------+ |
| | | | | |
| | | | | |
| +-----+------+ | | +-----+------+ |
| | | | | | | |
| | ELF | | | | runtime | |
| | | | | | | |
| +------------+ | | +------------+ |
| | | |
| userspace | | kernel |
+---------------------+ +---------------------+
In Figure 1. an untrusted client, or *Userspace* in Operating Systems terms, creates a program in the front-end language of her choice, (like C/C++/Rust/Lua), and compiles it with LLVM to the Solana Bytecode object. This object file is a standard ELF. We use the section headers in the ELF format to annotate memory in the object such that the Kernel aka the Solana blockchain, can safely and efficiently load it for execution.
[Figure 1. Smart Contracts Stack]
In Figure 1. an untrusted client, or `Userspace` in Operating Systems terms, creates a program in the front-end language of her choice, (like C/C++/Rust/Lua), and compiles it with LLVM to the Solana Bytecode object. This object file is a standard ELF. We use the section headers in the ELF format to annotate memory in the object such that the Kernel aka the Solana blockchain, can safely and efficiently load it for execution.
The computationally expensive work of converting frontend languages into programs is done locally by the client. The output is a ELF with specific section headers and with a bytecode as its target that is designed for quick verification and conversion to the local machine instruction set that Solana is running on.
@ -17,14 +43,44 @@ Our bytecode is based on Berkley Packet Filter. The requirements for BPF overlap
3. Verified memory accesses
4. Fast to load the object, verify the bytecode and JIT to local machine instruction set
For 1 BPF toolchain is designed for generating code without jumps back. For us this means loops are unrolled, and the stack itself is part of the ELF. Any work that we do here to expand how the kernel can analyze the runtime of the generated code can be ported back to the Linux community.
For 1, we can unroll the loops, and for any jumps back we can guard them with a check against the number of instruction that have been executed at this point. If the limit is reached, the program yields. This involves saving the stack and current instruction index to the RW segment of the elf.
For 2, the bytecode already easily maps to x8664, arm64 and other instruction sets. 
For 2, the BPF bytecode already easily maps to x8664, arm64 and other instruction sets. 
For 3 and 4, is in a single pass we want to check that all the load and stores are pointing to the memory defined in the ELF,and map all the instructions to x86 or SPIR-V, or some other local machine instruction set. Linux already ships with a BPF JIT, but we love Rust, so would likely reimplemnt it in rust.
For 3, every load and store that is PC relative can be checked to be within the ELF. Dynamic load and stores can dynamically guard against load and stores to dynamic memory.
For 4, Statically linked elf with just a signle R/RX/RW segments. Effectively we are linking with `-static --nostd -target bpf` and a linker script to collect everything into a single spot. The R segment is for read only instance data that is populated by the loader.
## Loader aka Dynamic Linker
The loader is our first smart contract. The job of this contract is to load the actual program with its own instance data.
## Loader
The loader is our first smart contract. The job of this contract is to load the actual program with its own instance data.
+----------------------+
| |
| +----------------+ |
| | RX-code | |
| +----------------+ |
| |
| +----------------+ |
| | R-data | |
| +----------------+ |
| |
| +----------------+ |
| | RW-data | |
| +----------------+ |
| elf |
+----------------------+
A client will create a transaction to create a new loader instance.
* `NewLoaderAtPubKey(Loader instance PubKey, proof of key ownership, space i need for my elf)`
A client will then do a bunch of transactions to load its elf into the loader instance they created.
* `LoaderConfigureInstance(Loader instance PubKey, proof of key ownership, amount of space I need for R user data, user data)`
At this point the client may need to upload more R user data to the OS via some more transactions to the loader.
* `LoaderStart(Loader instance PubKey, proof of key owndership)`
At htis point clients can start sending transactions to the instance
## Parallelizable Runtime
To parallelize smart contract execution we plan on breaking up contracts into distinct sections, Map/Collect/Reduce/Finalize. These distinct sections are the interface between the ELF and the Kernel that is executing it. Memory is loaded into the symbols defined in these sections, and relevant constants are set.
@ -34,58 +90,26 @@ struct Vote {
uint64_t amount;
uint8_t favorite;
}
__section("map.data")
struct Vote vote;
__section("map")
void map(struct Transaction *tx)
{
memmove(&vote.from, &tx.from, sizeof(vote.from));
vote.amount = tx.amount;
vote.favorite = tx.userdata[0];
yield(collect);
collect(&vote, sizeof(vote));
}
```
The contract's object file implements a map function and lays out memory that is allocated per transaction. It then yields itself to a collect call that is schedule to run sometime later.
```
__section("collect.map.len")
const uint32_t votelen;
__section("collect.map.data")
const struct Vote vote[votelen];
__section("collect.data")
Vote totals[votelen];
__section("collect.data.used")
uint32_t used;
__section("collect")
void collect()
void collect(void* data[], uint32_t sizes[], uint32_t num)
{
used = sizeof(struct Vote) * votelen;
memmove(totals, vote, used);
yield(reduce)
reduce)
}
Then we simply reduce multiple collect objects into 1 data structure. Reduce additionally sees its own previous output from multiple calls to reduce.
__section("reduce.collect.len")
const uint32_t len;
__section("reduce.collect.data")
const struct Vote *vote[len];
__section("reduce.collect.data.sizes")
const uint32_t sizes[len];
//partially reduced contracts are also available to reduce
//so it can fold over it's own output
__section("reduce.reduce.len")
const uint32_t rlen;
__section("reduce.reduce.data")
Vote *rtotals[rlen];
__section("reduce.reduce.data.sizes")
const uint32_t rsizes[rlen];
//total memory available for reduce `data`
//this is the sum of all the reduce and collect allocations
__section("reduce.data.total")
const uint32_t total;
__section("reduce.data")
Vote totals[total/sizeof(struct Vote)];
__section("reduce.data.used")
uint32_t used;
__section("reduce")
```
Reduce
```
void reduce()
{
int i;
@ -101,13 +125,6 @@ void reduce()
```
finalize is then called when some final condition occurs. This could be when the time expires on the contract, or from a direct call to finalize itself, such as yield(finalize). 
```
__section("finalize.reduce.data.used")
const uint32_t used;
__section("finalize.reduce.data")
Vote totals[used/sizeof(struct Vote)];
__section("finalize.data")
uint64_t votes[256];
__section("finalize")
void finalize() {
int i;
uint64_t total = 0;