Introduction
Zink is a powerful toolchain revolutionizing smart contract development on Ethereum, it is designed to bridge the gap between WebAssembly and the Ethereum Virtual Machine.
At its core, zinkc, the Zink compiler, takes WASM bytecode as the source code and transforms it into EVM bytecode for seamless execution on the Ethereum blockchain. This optimization ensures efficient and reliable execution of smart contracts on Ethereum.
This guide is intended to serve a number of purposes and within you’ll find:
…and more! The source for this guide lives on GitHub and contributions are welcome!
Tutorial
This tutorial walks through creating a simple add-two program and compiles it with description how everything works.
For the overall instructions:
# Install zink toolchain
cargo install zinkup
# Create project
elko new add-two
# Compile it
cd add-two && elko build
ls target/add-two.bin
Creating Zink Project
For creating a zink project, we need to install the zink toolchain zinkup
from crates.io
first, the package manager elko
will be installed along
with other tools:
cargo install zinkup
elko -h
Now, let’s create a zink project:
elko new my-awesome-contract
Created zink project `my-awesome-contract`
the Zink projects are based on the cargo projects, you can install
dependencies you need with cargo
, the basic Cargo.toml
will be like:
# ...
[lib]
crate-type = [ "cdylib" ]
[dependencies]
zink = "0.1.0"
# ...
open my-awesome-contract/src/lib.rs
//! my-awesome-project
#![no_std]
// For the panic handler.
#[cfg(not(test))]
extern crate zink;
/// Adds two numbers together.
#[no_mangle]
pub extern "C" fn addition(x: u64, y: u64) -> u64 {
x + y
}
you’ll see a standard WASM
library in rust:
#![no_std]
means we don’t need the std library in this project.extern crate zink
is for importing the panic handler from libraryzink
for this project.#[no_mangle]
is for exporting functionaddition
to WASM, and this will be one the methods of your contracts.
Compiling Zink Project
We have got a zink project after creating-zink-project, now it’s time to compile it to EVM bytecode!
# Enter our project
cd my-awesome-project
# Build the project
elko build
# Check the outputs
ls target/zink
my-awesome-project.wasm my-awesome-project.bin
you’ll see a my-awesome-project.bin
file under target/zink
, and that’s it!
How it works?
first, elko
compiles the cargo project to WASM with:
cargo b --target wasm32-unknown-unknown --release
then, there will be some logic inside elko
, running wasm-opt
for our output
WASM binary
# if you have wasm-opt installed on your machine, you can try the same
mkdir -p target/zink
wasm-opt -O4 target/wasm32-unknown/unknown/release/my-awesome-project.wasm -o target/zink/my-awesome-project.wasm
finally we use zinkc
to compile the wasm to EVM bytecode:
# For reproducing it in your command line
zinkc target/zink/my-awesome-project.wasm
mv my-awesome-project.bin target/zink
Future plans (TODO)
- Generate the ABI as well.
- Add command for deploying the bytecode to EVM chain with RPC endpoints.
- Test suite
- …
Examples
This chapter provides various zink examples in rust:
name | knowledges | description |
---|---|---|
add-two | params | basic program in zink |
fibonacci | calls , recursion , if-block | recursion implementation |
log | log | log APIs |
select | wasm built-in functions | program with extra instruction select from WASM |
storage | storage | storage operations |
AddTwo
//! Addition example.
#![no_std]
// for the panic handler.
#[cfg(not(test))]
extern crate zink;
/// Adds two numbers together.
#[no_mangle]
pub extern "C" fn addition(x: u64, y: u64) -> u64 {
x + y
}
A basic addition program in zink
(module
(func (param i32) (param i32) (result i32)
(local.get 0)
(local.get 1)
(i32.add)
)
)
Requires:
- Get params from locals
- Process basic operand
- Return data from the result type
6000356020350160005260206000f3
Fibonacci
//! fibonacci example.
#![no_std]
// for the panic handler.
#[cfg(not(test))]
extern crate zink;
/// Calculates the nth fibonacci number.
#[no_mangle]
pub extern "C" fn fibonacci(n: usize) -> usize {
recursion(n)
}
/// Calculates the nth fibonacci number using recursion.
#[no_mangle]
pub extern "C" fn recursion(n: usize) -> usize {
if n < 2 {
n
} else {
recursion(n - 1) + recursion(n - 2)
}
}
A recursion example, complex in bytecode
(module
(type (;0;) (func (param i32) (result i32)))
(func (;0;) (type 0) (param i32) (result i32)
local.get 0
call 1)
(func (;1;) (type 0) (param i32) (result i32)
(local i32)
local.get 0
i32.const 2
i32.ge_u
if ;; label = @1
loop ;; label = @2
local.get 0 ;; 1
i32.const 1 ;; 2
i32.sub ;; 1
call 1 ;; 1
local.get 1 ;; 2
i32.add ;; 1
local.set 1 ;; 0
local.get 0 ;; 1
i32.const 2 ;; 2
i32.sub ;; 1
local.tee 0 ;; 1
i32.const 1 ;; 2
i32.gt_u ;; 1
br_if 0 (;@2;) ;; 2 -> 0
end
end
local.get 0
local.get 1
i32.add))
A more complex implementation of locals ( params + local variables) is introduced in this example,
control flow br_if
and loop
are compiled as well.
600035586010565b60005260206000f35b906000816002600190031015603d575b8160019003586010565b8101905081600290038092506001106020575b8181019150509060040156
Log
//! Addition example.
#![no_std]
// for the panic handler.
#[cfg(not(test))]
extern crate zink;
use zink::Event;
/// A `Ping` event.
#[derive(Event)]
struct Ping;
#[no_mangle]
pub extern "C" fn log1() {
Ping.log1(b"pong");
}
The log API of zink is derived by the trait Event
which provides methods
log0
, log1
, log2
, log3
, log4
. We current only supports static
bytes in this API.
(module
(type (;0;) (func))
(type (;1;) (func (param i32 i32 i32 i32)))
(import "evm" "log1" (func (;1;) (type 1)))
(import "env" "memory" (memory (;0;) 17))
(func (;1;) (type 0)
i32.const 1048576
i32.const 4
i32.const 1048580
i32.const 4
call 0)
(export "log1" (func 1))
(data (;0;) (i32.const 1048576) "Pingpong"))
The static byte array will be compiled to the data section of wasm, zinkc
gets it from the data section then process it to the logging interfaces.
63706f6e676350696e6760005260206000a15f5ff3
Select
//! if-else example.
#![no_std]
// for the panic handler.
#[cfg(not(test))]
extern crate zink;
/// Simple if-else condition
#[no_mangle]
pub extern "C" fn if_else(x: u64, y: u64) -> u64 {
if x > y {
x
} else {
y
}
}
Code block selecting value with if-else will be compiled to instruction select
in WASM
(module
(type (;0;) (func (param i64 i64) (result i64)))
(func $if_else (type 0) (param i64 i64) (result i64)
local.get 0
local.get 1
local.get 0
local.get 1
i64.gt_u
select))
Since EVM bytecode doesn’t have similar instruction, we have to implement it ourselves, the solution
is introduce a select
function in the extra code section provided by zink compiler, jump to there
and jump back just like calling a real function.
60003560203560003560203510589190601c575b60005260206000f35b5060060156
Storage
// for the panic handler.
#[cfg(not(test))]
extern crate zink;
use zink::Storage;
/// It gets expanded to 'Counter' struct
/// that implements zink::Storage trait
/// (::set and ::get)
///
/// Storage key is taken based on macro order
/// (e.g this macro is first and only in this project,
/// so it will take 0x0 contract storage key)
#[zink::storage]
pub type Counter = i32;
/// Set value to storage and get it
#[no_mangle]
pub unsafe extern "C" fn set_and_get(value: i64) -> i64 {
Counter::set(value);
Counter::get()
}
Simple storage IO for numbers.
(module
(type (;0;) (func (param i64) (result i64)))
(type (;1;) (func (param i64 i64)))
(import "zink" "sload" (func (;0;) (type 0)))
(import "zink" "sstore" (func (;1;) (type 1)))
(func (type 0) (param i64) (result i64)
i64.const 0
local.get 0
call 1
i64.const 0
call 0))
Set and get number parameter with storage here.
60006000355891601b565b600058906021565b60005260206000f35b55600501565b549060050156
Style Guide
This style guide is for writing zink projects with rust
as the
source language.
We don’t need any styles for now, just writing anything you want, and the zinkc compiler will handle the rest of everything.
For the futures plans of events
, selector
, storage
and ...
plz keep tuned, they will be implemented in v0.2.0
: )
Compiler
The chapter illustrates the design of the zink compiler, so mostly,
we are talking about wat
and EVM bytecode Mnemonic
here:
Arithmetic
The arithmetic operators have a lot of differences between WASM and EVM bytecode, all of the operand requires the order of the stack are reserved…
Sub, Div, Mod and Bitwise Operand
i32.const 2 ;; PUSH1 0x02
i32.const 1 ;; PUSH1 0x01
sub ;; SWAP1
;; SUB
This SWAP1
sticks to all of these reversed order instructions, will introduce
macros to optimize it in v0.3.0
.
Comparison
The order of comparison are reversed as well, but however, they are paired!
i32.const 1 ;; PUSH1 0x01
i32.const 0 ;; PUSH1 0x00
gt ;; LT
This is insane, but works perfectly, don’t think too much about it, focus
on if the output is 0
or 1
;)
Calls
The function calls are compiled by order in zink, the first call in order (index 0) will be the main function ( to be updated ).
Functions
There are only internal functions and external functions in zink project for now.
Internal Functions
The parameters of the internal functions will be queued to the
locals of the them and taking the first n
stack
for storing them.
External Functions
Same as internal functions, will be updated once have the design of selector in v0.2.0
Extended Functions
We have also introduces extended functions inside the compiler for complete the difference between EVM bytecode and WASM, see the implementation select as example.
Main Function
You may never meet this because it is embedded in the compiled bytecode, but it is the entry of zink programs.
It takes parameters from the calldata
by order, for loading
32-byte parameters, it will process
// parameter 1
PUSH1 0x00
calldataload
// parameter 2
PUSH1 0x20
calldataload
Layout
Each function in zink is started with JUMPDEST
in the layout
of the bytecode for the insane jumping…
Each function call’s stack starts with PC
which stores the last active
program counter for the program for jumping back to the main process since
the callee functions could be called by any functions but not only one.
There is a tricky problem that how to detect the last pc before jumping,
for solving this, zinkc
registers the original PC
to the jump table when
meeting jumps and relocates them after compiling all functions.
Example Addition
(module
(func (export "main") (param i32) (param i32) (result i32)
(call $add (local.get 0) (local.get 1))
)
(func $add (param i32 i32) (result i32)
(local.get 0)
(local.get 1)
(i32.add)
)
)
Let’s assume we are calling an add
function with parameters 1, 1
and
now we are at the first byte right before it:
/* 0x00 */ PUSH1 0x01 // push the first parameter on the stack
/* 0x02 */ PUSH1 0x01 // push the second parameter on the stack
/* */ //
/* */ //
/* 0x04 */ pc // the first byte before calling the callee function
/* */ //
/* */ //
/* 0x05 */ PUSH1 0x42 // This 0x42 will be reloacted by `zinkc`
/* */ //
/* */ //
/* 0x07 */ jump // jump to the callee function
/* */ //
/* */ //
/* 0x08 */ jumpdest // the pc for jumping back from the callee function.
/* */ //
/* */ // the rest logic of the main process.
/* */ //
/* */ //
/* 0x42 */ jumpdest // the first byte of the callee function
/* */ //
/* */ //
/* 0x43 */ add // for the current stack: [PC, 0x02]
/* */ //
/* */ //
/* 0x44 */ SWAP1 // shift the stored PC to the top of the stack
/* */ //
/* */ //
/* 0x45 */ PUSH1 0x04 // the jumpdest is the original pc + 4 bcz we have
/* 0x47 */ add // `push1`, `0x42`, `jump`, `jumpdest` queued after
/* */ // `pc`.
/* */ //
/* */ //
/* 0x48 */ jump // This 0x07 will be reloacted by `zinkc`
Control Flow
EVM doesn’t have instructions for the custom control flows, however
zink implements them with JUMPI
and JUMP
, which includes:
if
block
loop
else
select
br
br_if
br_table
If-Else
The beginning of an if construct with an implicit then block, plus and else block.
The basic logic is, if non-zero, enter the if block, otherwise jump to the else block or the end of the if condition.
(if (result i32)
(local.get 0)
(then (i32.const 7))
(else (i32.const 8)))
The expected result is
Input | Result |
---|---|
1 | 7 |
0 | 8 |
so in the compiled bytecode, the code snippet above will be
PUSH1 0x00 // Load the params at 0x00
calldataload
iszero // if is zero, jump to 0x0c, the else block.
PUSH1 0x0c
jumpi
push1 0x07 // if is non-zero, enters the if block.
// push 0x07 on stack.
PUSH1 0x0f // jump to the end of the else block.
jump
jumpdest // destination of the else block, push 0x08
push1 0x08 // on stack.
jumpdest // the end of the else block.
PUSH1 0x00 // pack the result and return...
mstore
PUSH1 0x20
PUSH1 0x00
return
Select
The select (0x1B)
instruction comes from WebAssembly, it selects
one of its first two operands based on whether its third operand is
zero or not.
Simple rust conditions in rust will be compiled to select
.
pub extern "C" fn if_else(x: u64, y: u64) -> u64 {
if x > y {
x
} else {
y
}
}
As we can see in the example above, we simply returns the bigger
number from the 2 parameters, the logic in the two blocks of
if-else
is explicit direct, that will be compiled to select
.
(module
(type (;0;) (func (param i64 i64) (result i64)))
(func $if_else (type 0) (param i64 i64) (result i64)
local.get 0
local.get 1
local.get 0
local.get 1
i64.gt_u
select))
Since EVM doesn’t have instruction like select
, we need to provide
it ourselves in our implementation like an external function, if zero
pop the value on the top of the stack.
const SELECT: [OpCode; 6] = [
OpCode::JUMPDEST,
OpCode::POP,
OpCode::PUSH1,
OpCode::Data(0x06),
OpCode::ADD,
OpCode::JUMP,
];
In the compiled code, we need to combine this function select
with
jumpi
in EVM.
PUSH1 0x00 // Load the parameters.
calldataload
PUSH1 0x20
calldataload
PUSH1 0x00
calldataload
PUSH1 0x20
calldataload
lt // Compiled to `lt` because of the result of this
// instruction is oppsited between EVM and WASM.
pc
swap2 // shift
swap1
PUSH1 0x1c
jumpi // `jumpi` for the if condition.
JUMPDEST
PUSH1 0x00 // Returns the value.
mstore
PUSH1 0x20
PUSH1 0x00
return
JUMPDEST // Function select starts here.
pop
PUSH1 0x06
add
jump
Locals
There are two usages of locals in zink.
- The parameters of functions are loaded as locals.
- local defined variables in functions.
fn params | local variables | |
---|---|---|
stack | locals[..n] | locals[n..] |
Function Parameters
Let’s go through the add-two
example:
(module
(func (param (;0;) i32) (param (;1;) i32) (result i32)
(local.get 0)
(local.get 1)
(i32.add)
)
)
(param (;0;) i32)
and (param (;1;) i32)
will be pushed to the function
locals with index 0
and index 1
with their type i32
recorded.
zinkc
gets the defined local at index 0
when reaching (local.get 0)
,
at index 1
for (local.get 1)
, for example, for (local.get 0)
, it will
be translated to:
push1 0x00
calldataload
for (local.get 1)
, that would be
push1 0x20
calldataload
You may have problem why we PUSH1 0x20
while getting local at index 1
, the
answer is that this offset is calculated by the size of the parameters.
The CALLDATALOAD
operator has stack input i
and output data[i]
while data[i]
is a 32-byte
value starting from the given offset of the calldata, so the minimal
size of our types will be 32-byte
, therefore, we align all types sizes to 32-byte
in zinkc
.
WARN: We don’t care about the originals offset of the parameters in WASM bcz we will serialize them into our locals and calculate the offsets on our own when need anyway.
type | size | aligned size |
---|---|---|
i32 | [u8;4] | [u8;32] |
i64 | [u8;8] | [u8;32] |
It is a waste of resources but sadly this is also how EVM works ))
Local Variables
The locals variables will take the stack items right after the function parameters, for example:
(func (result i32)
(local i32)
i32.const 42
local.set 0
local.get 0)
In the program above, we set and get 42
to local variable 0
and returns it.
While compiling this function, zinkc
will push local variable 0
on the stack
with an initializing value 0
first, getting with dup
and setting with swap
and drop
.
PUSH1 0x00 // initializing value 0 for local 0
//
//
PUSH1 0x28 // push value 42 on stack, the current stack is [0, 42]
//
//
SWAP1 // swap the value on the top of the stack to local 0 and
DROP // drop the previous value of it for cleaning stack, `swapn`
// is calculated by `zinkc`. current stack: [42]
//
//
DUP1 // dup the value of local 0 and push it on the top of the
// stack, `dupn` is calculated by `zinkc`.
//
//
DROP // clean the stack before returning results.
As we can see, the usages of get
and set
is verbose with swap
and dup
,
it is for adapting any usages but not necessary for all of them, however, we
will introduce optimizer for this in v0.4.0
!
Recursion
The main function of zink project doesn’t support recursion for now :)
The callee functions support, plz see fibonacci for example.
Storage
The storage keys in Zink is slot based, for example, the first detected
storage in compilation will be using 0
as storage key.
// Loading storage at 0
PUSH0
SLOAD
// Loading storage at 1
PUSH1 0x01
SLOAD
Key-Value
As mentioned above, all key-value pairs follows using number as storage key, however, the value will be limited with 32 bytes, dynamic value like string is currently not supported.
Mapping
Mapping keys are generated via keccak256(slot, key)
Array
Similar to mappings, but the keys will be using u32
/ u64
for indexing due to the optimization
on the wasm side in the zink compiler, which means, the max size of an array is max(u64)
.
Stability
This project is still under active development, please DO NOT use it in production.
version | description |
---|---|
v0.1.0 | The MVP of the zink project. |
v0.1.0
- MVP
The MVP of the zink project, provides various tools for developing
EVM contracts with rust
and WASM
.
Binaries
name | description |
---|---|
elko | Zink’s package manager, can create and build zink project. |
zinkc | The zink compiler, can compile simple wasm to EVM bytecode. |
Components
name | description |
---|---|
zinkgen | Zink code generator |
zinkc | Zink compiler |
zink | Rust library for developing program with zink |
zint | Basic test utils including evm wrapper for testing usages |
zinkup | Zink toolchain installer |
Functionality
We provide basic functionalities in v0.1.0
to verify thoughts, the final target
of it is example fibonaaci
, which means, everything used in the fibonacci
example
now works!
For supporting nearly everything, plz keep tuned for v0.3.0
.
Arithmetic
add
, sub
, mul
are available now, plus all comparison operand like gt
, lt
,
ge
, le
, bitwise
also have implementations operators like shr
require the
order of the stack will have bugs.
Locals
The compilation of locals currently works without any hardcode, ideally, we don’t need to refactor it in the future!
Calls
Same as locals, works without any hardcode, but some logic related to the jump table
need to be refactored after introducing selector
.
Control Flow
if
, else
, block
, loop
, br_if
now works without any hardcode, need to add
br_table
, select
… to align wasm MVP in the future releases.
Security
TBS
Mapping of Instructions
Type Conversions
WASM have i32
, i64
, f32
, f64
as number types while EVM bytecode
only supports arithmetic operations for 256-bits integers.
TODO: Add more risk conditions.
Stack Operations
TBA
Memory Operations
The memory related operations in WASM are dangerous for Zink’s implementation.
WASM is using 32-bits offsets from the MVP spec while EVM is using 256-bits offsets, so it may cause memory overwrite problems.
The instructions need to be checked:
i32.store
i64.store
f32.store
f64.store
i32.store8
i64.store8
i32.store16
i64.store16
memory.size
memory.grow
TODO: check if it is possible to manage this issue with handling memory.size
and memory.grow
in a proper way.
Benchmarks
This chapter terms to record the benchmarks of zink projects comparing
with solidity
and vyper
.
We are not going to compare with yul
or huff
since we are on different
level, however, if zink is even faster than yul
or huff
in some cases,
don’t be surprised, we are born with high performance.
NOTE
since we haven’t implemented the selector logic yet, the bytecode of zink is
a bit shorter, similar in the gas cost, the main reason that zink contracts
cost less gas is that we are currently missing conditions for situations that
are more complex, we will get a more actual answer of these after the release of
v0.2.0
.
Fibonacci
Benchmarks for fibonacci.
Vyper is not included since it doesn’t support cyclic function call :(
Gas Cost
fib(n) | Zink | Solidity@0.8.21 |
---|---|---|
0 | 110 | 614 |
1 | 110 | 614 |
2 | 262 | 1322 |
3 | 414 | 2030 |
4 | 718 | 3446 |
5 | 1174 | 5570 |
Runtime Code
zink | solidity |
---|---|
146 | 1052 |
zink
//! Zink fibonacci recursion
#[no_mangle]
pub extern "C" fn fib(n: usize) -> usize {
if n < 2 {
n
} else {
recursion(n - 1) + recursion(n - 2)
}
}
600035586010565b60005260206000f35b906000816002600190031015603d575b8160019003586010565b8101905081600290038092506001106020575b8181019150509060040156
solidity
/**
* Solidity fibonacci recursion
**/
function fib(uint n) public view returns (uint) {
if (n < 2) {
return n;
} else {
return fib(n - 1) + fib(n - 2);
}
}
608060405234801561001057600080fd5b506004361061002b5760003560e01c8063c6c2ea1714610030575b600080fd5b61004a600480360381019061004591906100ea565b610060565b6040516100579190610126565b60405180910390f35b60006002821015610073578190506100aa565b6100886002836100839190610170565b610060565b61009d6001846100989190610170565b610060565b6100a791906101a4565b90505b919050565b600080fd5b6000819050919050565b6100c7816100b4565b81146100d257600080fd5b50565b6000813590506100e4816100be565b92915050565b600060208284031215610100576100ff6100af565b5b600061010e848285016100d5565b91505092915050565b610120816100b4565b82525050565b600060208201905061013b6000830184610117565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061017b826100b4565b9150610186836100b4565b925082820390508181111561019e5761019d610141565b5b92915050565b60006101af826100b4565b91506101ba836100b4565b92508282019050808211156101d2576101d1610141565b5b9291505056fea2646970667358221220f28552ff642c48025f3617233333427ae50d06ce8b168d3e3e9c18f0cf9bc34d64736f6c63430008120033
Log
Zink only supports static byte array for log arguments for now, see issue #129 for more details.
Zink | Vyper@0.3.9 | Solidity@0.8.21 | |
---|---|---|---|
Gas Cost | 1031 | 2777 | 2894 |
Runtime Code | 42 | 632 | 774 |
zink
//! Addition example.
#![no_std]
// for the panic handler.
#[cfg(not(test))]
extern crate zink;
use zink::Event;
/// A `Ping` event.
///
/// TODO: generate this with proc-macro.
struct Ping;
/// TODO: generate this with proc-macro.
impl Event for Ping {
const NAME: &'static [u8] = b"Ping";
}
#[no_mangle]
pub extern "C" fn log1() {
Ping.log1(b"pong");
}
63706f6e676350696e6760005260206000a15f5ff3
vyper
event Ping:
name: String[4]
topic1: String[4]
@external
def l():
log Ping("Ping", "pong")
6003361161000c57610127565b5f3560e01c3461012b5763ece866b98118610125577fcf8d08d4ab9d61004e3c20715af5b44c3badc3d3f41ddccbedbef447355ebff460408060c05260046040527f50696e670000000000000000000000000000000000000000000000000000000060605260408160c00181516020830160208301815181525050808252508051806020830101601f825f03163682375050601f19601f8251602001011690509050810190508060e05260046080527f706f6e670000000000000000000000000000000000000000000000000000000060a05260808160c00181516020830160208301815181525050808252508051806020830101601f825f03163682375050601f19601f82516020010116905090508101905060c0a1005b505b5f5ffd5b5f80fda165767970657283000309000b
solidity
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
contract Test {
event Ping(string name, string topic);
function l() public {
emit Ping("Ping", "pong");
}
}
608060405234801561001057600080fd5b506004361061002b5760003560e01c8063ece866b914610030575b600080fd5b61003861003a565b005b7fcf8d08d4ab9d61004e3c20715af5b44c3badc3d3f41ddccbedbef447355ebff46040516100679061011a565b60405180910390a1565b600082825260208201905092915050565b7f50696e6700000000000000000000000000000000000000000000000000000000600082015250565b60006100b8600483610071565b91506100c382610082565b602082019050919050565b7f706f6e6700000000000000000000000000000000000000000000000000000000600082015250565b6000610104600483610071565b915061010f826100ce565b602082019050919050565b60006040820190508181036000830152610133816100ab565b90508181036020830152610146816100f7565b905091905056fea26469706673582212201af2a7b1c1d4743d1d089d3eaa1bbd6ecb4186fc95a43320108b18882d7a6dfc64736f6c63430008120033
Storage
Result for a simple storage IO.
Have to say Vyper
is super on this even it contains the logic of function
selector!
Gas Cost
Zink | Vyper@0.3.9 | Solidity@0.8.21 |
---|---|---|
22237 | 22345 | 27738 |
The gas costs here are measured by transaction cost
+ execution cost
,
for example, the transaction of this function in solidity is 24120
, and
2916
for the execution cost, 27738
in total.
Since revm doesn’t support this and we haven’t implemented the constructor yet, we don’t have the separated costs for zink for now ))
Issues: zink-lang/zink#102, bluealloy/revm#619
Runtime Code
zink | vyper | solidity |
---|---|---|
42 | 204 | 724 |
zink
/// TODO: generate this storage interface with proc macro.
struct Counter;
impl Counter {
fn get() -> i64 {
unsafe { sload(0) }
}
fn set(value: i64) {
unsafe { sstore(0, value) }
}
}
/// Set value to the storage and get it.
#[no_mangle]
pub unsafe extern "C" fn set_and_get(value: i64) -> i64 {
Counter::set(value);
Counter::get()
}
6000600035589155600058905460005260206000f3
vyper
n: public(int256)
@external
def sg(_n: int256) -> int256:
self.n = _n
return self.n
6003361161000c57610051565b5f3560e01c3461005557632e52d606811861002c575f5460405260206040f35b63da48b556811861004f5760243610610055576004355f555f5460405260206040f35b505b5f5ffd5b5f80fda165767970657283000309000b
solidity
pragma solidity >=0.7.0 <0.9.0;
contract Storage {
int public number;
function sg(int n) public returns (int) {
number = n;
return number;
}
}
608060405234801561001057600080fd5b50600436106100365760003560e01c80638381f58a1461003b578063da48b55614610059575b600080fd5b610043610089565b60405161005091906100bb565b60405180910390f35b610073600480360381019061006e9190610107565b61008f565b60405161008091906100bb565b60405180910390f35b60005481565b6000816000819055506000549050919050565b6000819050919050565b6100b5816100a2565b82525050565b60006020820190506100d060008301846100ac565b92915050565b600080fd5b6100e4816100a2565b81146100ef57600080fd5b50565b600081359050610101816100db565b92915050565b60006020828403121561011d5761011c6100d6565b5b600061012b848285016100f2565b9150509291505056fea264697066735822122052e14a565911c984f75788fb539e44d7692065628b2042665fc4abfc95e680d264736f6c63430008120033
Command Line Tool
The zink toolchain are gathered in zinkup
You can install all of the components directly with:
cargo install zinkup
For installing only specified binaries:
cargo install zinkup --features elko,zinkc
Available binaries:
Name | Description |
---|---|
elko | Zink's package manager |
zinkc | The zink compiler |
elko
- Zink’s package manager
Installation
cargo install zinkup --features elko
Usages
elko
Package manager of zink.
Usage: elko [OPTIONS] <COMMAND>
Commands:
new Create a new zink project
build Build zink project to EVM bytecode
help Print this message or the help of the given subcommand(s)
Options:
-v, --verbose... Verbose mode (-v, -vv, -vvv, etc.)
-h, --help Print help
-V, --version Print version
The zink compiler
Installation
cargo install zinkup --features zinkc
Usage
The Zink Compiler
Usage: zinkc [OPTIONS] <INPUT>
Arguments:
<INPUT> The path of the wasm file
Options:
-o, --output <OUTPUT> Write output to <filename>
-v, --verbose... Verbose mode (-v, -vv, -vvv, etc.)
-h, --help Print help
-V, --version Print version
Contributing
We’re excited to work on Zink
together with you! this guide should help you
get up and running with Zink development!
Join Our Chat
We chat about Zink development on telegram – join us!
If you’re having building Zink, aren’t sure why a test is failing, or have any other questions, feel free to ask on telegram.
As always, you’re more than welcome to open an issue too!
Join or Support the Team
Zink is developed by @clearloop with his spare time for now, really need more hands for the revolution : )
My handle on telegram is @clearloop
as well, if you’d like to support this
project or join the team, dm me plz!
Architecture of Zink
Compiler
The path of the compiler crate is /compiler
, as its name, it’s the zink compiler
zinkc
, currently just a wrapper of zingen
, the codegen library.
So if you want to contribute to the compiler, the code inside /compiler
and /codegen
will be interested for you!
Zink
Located at /zink
, it is a rust library for compiling cargo project to zink program
with provided apis, selector
, events
…any sugar or asm macro for zink will be
embedded in this library.
Test utils
/zint
is the testing library for zink projects, it is currently just a wrapper
of evm
, we need really a lot of features in this in v0.3.0
.
Building
This section describes everything required to build and run zink.
Prerequisites
Before we can actually build Zink, we’ll need to make sure these things are installed first.
The Rust toolchain
Install the Rust toolchain here. This includes rustup
, cargo
, rustc
, etc…
Add target wasm32-unknown-unknown
rustup target add wasm32-unknown-unknown
This is required for compiling our rust projects to wasm.
Build the zinkc
CLI
cd cli
cargo b -p zinkup --release --features zinkc
The built executable will be located at target/release/zinkc
.
Build examples
cd examples
cargo b --release
The built wasm binaries will be localted at examples/target/wasm32-unknown-unknown/realease/*.wasm
,
then, you can you zinkc
to compile them to EVM bytecode!
Testing
This section describes how to run Zink’s tests and add new tests.
Before continuing, make sure you can build Zink successfully. Can’t run the tests if you can’t build it!
Running All Tests
cargo test --all
Adding New Tests for the Compiler
At the current stage, we are lack of the tests of the compiler,
the tests of it are gathered in compiler/tests
.
Each file under compiler/tests
are named by instruction, for
example add.rs
, it includes the tests related to instruction
ADD
.
For adding a new test for compiler/tests/add.rs
, we need to write
a wasm program for it first, for example, the wasm program of the
params
test of ADD
is located at compiler/wat/i32add/params.wat
.
(module
(func (param i32) (param i32) (result i32)
(local.get 0)
(local.get 1)
(i32.add)
)
)
In compiler/tests/add.rs
:
#[test]
fn params() -> Result<()> {
let bytecode = common::load("i32add", "params")?;
// add(1, 2)
let input = [1.to_bytes32(), 2.to_bytes32()].concat();
let info = EVM::run(&bytecode, &input);
assert_eq!(info.ret, [3.to_bytes32()].concat());
Ok(())
}
We use common::load("i32add", "params")
to load wat file to EVM bytecode
from compiler/tests/wat/i32add/params.wat
, execute with the EVM
provided
by zint
, compare if the result is as expected, that’s it!
Appendix
The following sections contain reference material you may find useful in your Zink journey.
A - Optimizations
The optimizations of Zink projects now are mainly benefited from
the optimizer of wasm – wasm-opt
, for the details of it please
check Binary Optimizations.