EOS 搭建指南

EOS 与 ETH 相比,其架构更加复杂,由于其主链刚刚上线不久因此 EOS 中的合约数量也不多,为了学习和理解 EOS 的相关知识,笔者跟随 EOS 官网 的教程对 EOS 进行了初步的了解,由于网上目前的资料参差不齐,因此将学习过程中遇到的问题记录在此,以备查阅。

EOS 源码编译

编译过程比较繁琐,网上也有很多教程,这里不再赘述,仅记一下笔者搭建过程中遇到的一些坑。由于 EOS 代码版本迭代十分频繁,因此网上许多基于旧版本 EOS 源码的搭建教程并不适用。这里推荐使用其源码中提供的脚本 eosio_build.sh 完成工作。这个脚本会根据编译时的实际环境调用不同系统的专用脚本来完成工作,笔者的环境是 Ubuntu_16 因此最后会调用 /scripts/eosio_build_ubuntu.sh 进行编译。其中比较重要的两个点是首先需要安装最新版本的 boost ,其次是需要安装 OpenSSL,安装完成之后一般来说就可以成功的编译起来了。

如果编译过程中提示系统硬件配置等信息不符合要求,如内存硬盘等等不够大,可以直接修改脚本中的判断条件。比如默认状态下脚本要求 EOS 编译的内存要有 8G ,但是如果我们在虚拟机中进行编译内存常常不满足要求,此时只需要修改用于进行硬件检测的编译脚本即可,将这个判断条件改小。

1
2
3
4
5
if [ "${MEM_MEG}" -lt 7000 ]; then
printf "\\tYour system must have 7 or more Gigabytes of physical memory installed.\\n"
printf "\\tExiting now.\\n"
exit 1
fi

EOS 私有链搭建

搭建完成之后开始运行 eos ,EOS 编译完成之后有三个主要的接口程序 nodeos, cleoskeosd, nodeos 主要负责启动 EOS 链并且执行编译好的 wasm ; cleos 主要负责编译 wasm 并且和 EOS 链进行交互, keosd 是钱包管理模块。

使用参数添加运行时所需的插件。 查看 nodeos 源码可以发现 nodeos 本质上只是一个运行插件的框架,EOS 的实际功能均由这些插件实现,因此运行时必须指定插件名称。

1
> nodeos -e -p eosio --plugin eosio::chain_api_plugin --plugin eosio::history_api_plugin

ubuntu 系统中 eos 运行时的数据信息保存在 /.local/share/eosio/nodeos 路径下,删除之可以从新构建一个私有链

其中文件 config.ini 用于指定 nodeos 运行时的一些配置,可以免除每次运行 nodeos 时都输入一大串参数

运行时可能遇到的问题可以参考下面两个 issue 解决。
https://github.com/EOSIO/eos/issues/3545
https://github.com/EOSIO/eos/issues/3522

接着需要创建钱包,钱包的默认存储位置在 /home/*username*/eosio-wallet 中,删除这个文件夹就删除了之前创建的钱包,没有这个文件夹时会提示创建钱包失败,此时只要再新建一个同名文件夹即可,创建钱包的工作是通过 EOS 启动时的插件 进行的

1
2
> mkdir /home/root/eosio-wallet
> cleos wallet create

和以太坊一样,钱包在不使用时处于 lock 状态,需要输入密码解锁才能使用

1
2
> ./cleos wallet unlock -n default
passwd

使用

1
> ./cleos -p 8888 get info

命令可以查看当前搭建的私有链的信息

在 build 文件夹下会有内置的一些合约信息,可以先尝试加载这些合约。

以 BIOS 合约为例,进入 /eos/build/programs/cleos 目录,加载合约,操作之前记得要解锁钱包

1
2
3
4
> cd /eos/build/programs/cleos
> ./cleos wallet unlock -n default
Password:
> ./cleos set contract eosio ../../contracts/eosio.bios -p eosio

为货币合约创建一个账户currency,首先生成两组key,分别对应OwnerKey和ActiveKey

1
2
3
4
5
6
> ./cleos create key # OwnerKey
Private key: 5KHhXyJuAAFQsWCMJcbKiZd9owuud2YBtzMj8d5qQrFYFC3y4so
Public key: EOS5HVWpMi6V1ycGrQSA4xheZD1ohaDmNi2MerVLoXP7b3Byxdefy
> ./cleos create key
Private key: 5Jbj9supqWXWZkBm6hRrrH7aqX74HvoqjfNK3RXSkj9dqARhSmd
Public key: EOS7oVtnSQ9iZ3mdq4BYRPbQWT7GBCjN8fEeUhtMwro8pMaQLz9QY

接着导入两个私钥

1
2
> .cleos wallet import 5KHhXyJuAAFQsWCMJcbKiZd9owuud2YBtzMj8d5qQrFYFC3y4so
> .cleos wallet import 5Jbj9supqWXWZkBm6hRrrH7aqX74HvoqjfNK3RXSkj9dqARhSmd

接着创建一个账户导入公钥,创建账户的语法是 cleos create account {创建者账户名} {新的账户名} 公钥1 公钥2 其中 创建者账户名 是为这个创建动作支付EOS的账户,公钥1和公钥2分别是两个不同权限的密钥对的公钥。

1
> ./cleos create account eosio currency EOS5HVWpMi6V1ycGrQSA4xheZD1ohaDmNi2MerVLoXP7b3Byxdefy EOS7oVtnSQ9iZ3mdq4BYRPbQWT7GBCjN8fEeUhtMwro8pMaQLz9QY

可以使用命令查看上述操作是否成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> ./cleos get account currency
privileged: false
permissions:
owner 1: 1 EOS5HVWpMi6V1ycGrQSA4xheZD1ohaDmNi2MerVLoXP7b3Byxdefy
active 1: 1 EOS7oVtnSQ9iZ3mdq4BYRPbQWT7GBCjN8fEeUhtMwro8pMaQLz9QY
memory:
quota: -1 bytes used: 2.66 Kb
net bandwidth: (averaged over 3 days)
used: -1 bytes
available: -1 bytes
limit: -1 bytes
cpu bandwidth: (averaged over 3 days)
used: -1 us
available: -1 us
limit: -1 us

账户创建完成之后就可以使用账户部署合约了

1
2
3
4
5
6
7
8
9
10
11
12
> ./cleos get code currency
code hash: 0000000000000000000000000000000000000000000000000000000000000000
> ./cleos set contract currency ../../contracts/eosio.bios/ p currency
Reading WAST/WASM from ../../contracts/eosio.bios/eosio.bios.wasm...
Using already assembled WASM...
Publishing contract...
executed transaction: 480dd8f208f332751190a1342d3485bb206b41444c818938f6c4ff38682e1fab 3728 bytes 945 us
# eosio <= eosio::setcode {"account":"currency","vmtype":0,"vmversion":0,"code":"0061736d0100000001621260037f7e7f0060057f7e7e7...
# eosio <= eosio::setabi {"account":"currency","abi":"0e656f73696f3a3a6162692f312e30050c6163636f756e745f6e616d65046e616d650f7...
> ./cleos get code currency
code hash: 5e8e655a05b34e782467684d7148244404b591a6c1ec2687eacb268926a37e59

此时 currency 就是一个合约账户,部署的合约会以 currency 的名义运行在区块上.命令最后的 -p eosio 的含义是:使用账户 eosio(使用的是账户的私钥)来为这个action签名。

部署自己编写的合约与之类似,首先需要将 cpp 编译成 wasm。

首先编写合约 Hello

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <eosiolib/eosio.hpp>
#include <eosiolib/print.hpp>
using namespace eosio;
//所有的智能合约都继承自contract类
class Hello : public eosio::contract {
public:
using contract::contract;
/// @abi action
void hi( account_name user ) {
print( "Hello, ", name{user} );
}
};
EOSIO_ABI( Hello, (hi) ) // 通知编译器,需要编译成 abi 的接口

使用 eosiocpp 编译之

1
2
> eosiocpp -o Hello.wast Hello.cpp
> eosiocpp -g Hello.abi Hello.cpp

按上面的步骤部署合约,还时部署在 currency 账户上就可以,新部署的合约会覆盖之前部署的内容

运行合约

1
> cleos push action currency hi '{"user":"lalala"}' -p currency

第一个 currency 是合约运行的账户,用来做合约寻址,第二个 currency 则表示发送这条请求的账户,只要是当前 wallet 中已经导入的账户都可以用作这个参数,否则会报错

1
2
3
4
5
>$ ./cleos push action hello hi '["user"]' -p xxxx
Error 3090003: provided keys, permissions, and delays do not satisfy declared authorizations
Ensure that you have the related private keys inside your wallet and your wallet is unlocked.
Error Details:
transaction declares authority '{"actor":"xxxx","permission":"active"}', but does not have signatures for it.

如果一切正常的话,运行合约会输出代码中 print 的内容。但是,有时候我们发现运行不报错然而就是不输出,这是因为 eos 运行时的配置文件中一个属性没有修改,打开 config.ini 可以发现其中有这样一条配置

1
2
# print contract's output to console (eosio::chain_plugin)
contracts-console = true

这里默认是 false,要把它改成 true ,在 console 里才能看到输出!!!

1
2
3
4
5
> ./cleos push action hello hi '["user"]' -p currency
executed transaction: 3f03ce3f210826f042e323c17a2ff5a790054612e2fe5e6186a053356133e278 104 bytes 570 us
# hello <= hello::hi {"user":"user"}
>> Hello, user
warning: transaction executed locally, but may not be confirmed by the network yet

WASM 解析引擎搭建

EOS 使用 C++ 作为编程语言,将 C++ 转化成 WebAssembly ,实际运行在 EOS 链上的合约就是一个一个的 WebAssembly 代码段。EOS 用来解析 WASM 的引擎使用的是第三方库 WAVM,我们可以将这个 WAVM 单独编译起来。由于作者并没有给出在 Linux 系统上编译的说明,因此自己编译的过程中还是会踩许多坑的。

源码中提供了编译的脚本 travis-build.sh ,直接运行之会发现报错。

1
2
3
4
5
6
7
8
9
10
11
12
/WAVM/Source/WASM/WASMSerialization.cpp:27:30: error: ‘elem’ may be used uninitialized in this function [-Werror=maybe-uninitialized]
I8 encodedValueType = -(I8)type;
^
/WAVM/Source/WASM/WASMSerialization.cpp:40:14: note: ‘elem’ was declared here
ValueType elem;
^
/WAVM/Source/WASM/WASMSerialization.cpp:27:30: error: ‘elem’ may be used uninitialized in this function [-Werror=maybe-uninitialized]
I8 encodedValueType = -(I8)type;
^
/WAVM/Source/WASM/WASMSerialization.cpp:40:14: note: ‘elem’ was declared here
ValueType elem;
^

检查源码并未发现问题,因此直接忽略之,将编译选项 CMakelist.txt 中的相关语句删除

1
2
# Compile with all warnings and fatal warnings
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror")

继续运行脚本,发现在编译到一定程度时会报错

1
2
../Runtime/libRuntime.so: undefined reference to `llvm::RTDyldMemoryManager::getSymbolAddressInProcess(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
../Runtime/libRuntime.so: undefined reference to `llvm::sys::getProcessTriple[abi:cxx11]()'

搜索了一下发现这个问题是由于 cmake 和 llvm 不匹配造成的,而编译过程中的 cmake 和 llvm 从编译脚本中可以看出,是自己下载并重新渲染的,因此这里就出现了两种方法:其一,直接注释脚本中准备编译环境的部分,自己编译 LLVM 并指定环境变量; 其二,将脚本中的下载路径设置为匹配的 cmake 和 llvm。 笔者这里为了简单起见选择第二种方案,将 cmake 和 llvm 均值指定为当前可以下载的最新版本

1
2
export CMAKE_URL="https://cmake.org/files/v3.12/cmake-3.12.0-Linux-x86_64.tar.gz";
export LLVM_URL="http://releases.llvm.org/6.0.1/clang+llvm-6.0.1-x86_64-linux-gnu-ubuntu-16.04.tar.xz";

继续运行脚本,正常情况下编译成功

Refenrence

https://github.com/EOSIO/eos/issues/3545
https://github.com/EOSIO/eos/issues/3522