BCSkill (Block chain skill )
区块链中文技术社区

只讨论区块链底层技术
遵守一切相关法律政策!

智能合约之 eosio.cdt 我们需要知道的那些事

eosio.cdt 在 1.2.x 和 1.3.x 的改动比较大, 虽然虚拟机是向后兼容的, 但是为了避免意外情况, 我们都会将陆续将合约代码升级。下面来介绍一下大致的改动

# 安装 eosio.cdt, 因为 llvm 库比较大, 所以执行 clone 的时候比较慢
$ git clone https://github.com/EOSIO/eosio.cdt.git
$ git submodule update --init --recursive
$ ./build.sh
$ sudo ./install.sh

1.2.x 和 1.3.x 的区别

eoslib C API

uint64_t 别名 account_name, permission_name, scope_name, table_name, action_name 全部移除, 新增 typedef capi_name uint64_t
symbol_name 别名移除,用 symbol_code 代替
移除 time , weight_type typedefs
移除 transaction_id_type, block_id_type typedefs
checksum160 -> capi_checksum160, checksum256 -> capi_checksum256, checksum512 -> capi_checksum512, public_key -> capi_public_key, signature -> capi_signature
移除掉未实现的 api : require_write_lock 和 require_read_lock

eoslib C++ API

移除 bytes typedefs
移除文件 eosiolib/types.hpp
移除文件 eosiolib/optional.hpp, 用 std::optional 代替
移除 eosiolib/core_symbol.hpp 文件, 以后合约需要自行声明 core_symbol
增加文件 eosiolib/name.hpp

eoslib/types.hpp

将 typedef eosio::extensions_types 移到 eosiolib/transaction.hpp
移除掉对 checksum struct 的 == 和 != 的重载
移除掉 API eosio::char_to_symbol, eosio::string_to_name, eosio::name_suffix, 都整合进了 name struct
移除掉宏命令 N(X), 重载运算符 ""_n ,例如 "foo"_n 或者 name("foo") 来转换成 name struct 类型
将 eosio::name struct 的定义 和 ""_n 运算符 移至 eosiolib/name.hpp
ps: 读者可以使用 #define N(X) name(#X) 来减少代码的改动了哈。

eosiolib/name.hpp

移除name 显式 隐式转换成 uint64_t
添加 enum class eosio::name::raw : uint64_t 用于从 name struct 隐式转换成 raw
添加 bool 类型转换,会返回 name struct 转化成 uint64_t 是否为 0
构造函数都用 constexpr, 确保 name struct 实例化时,都会给 value 赋初值
添加新的 constexpr 方法 eosio::name::length, eosio::name::suffix
添加 name struct 的比较函数

eosiolib/symbol.hpp

移除 eosio::symbol_type strcut , 用 eosio::symbol class 代替
添加 eosio::symbol_code struct
移除掉 eosio::string_to_symbol, eosio::is_valid_symbol, eosio::symbol_name_length 方法,都整合进了 symbol_code struct
移除宏命令#define S(P,X) ::eosio::string_to_symbol(P,#X), 直接实例化 symbol class eg: symbol(symbol_code("SYS"), 4) or symbol("SYS", 4)
重构 eosio::extended_symbol struct

eosiolib/asset.hpp

构造器现需要显式传入 quantity 和 symbol, 不再有默认值

eosiolib/contract.hpp

Rename EOSIO_ABO to EOSIO_DISPATCH, 更加明确的表达该宏指令的作用
根据 contract 的改动重构 EOSIO_DISPATCH

eosiolib/multi_index.hpp

索引不能直接用 name struct 需要使用 eosio::name::raw
multi_index code 不再使用 uint64_t, 使用 eosio::name

eosiolib/singleton.hpp

同 multi_index, 用 eosio::name 替代 uint64_t

eosiolib/action.hpp

添加 inline function: eosio::require_auth, eosio::has_auth, eosio::is_account
重构 eosio::permission_level, 用 eosio::name 替换 uint64_t
移除 宏命令 ACTION,整合到了 eosio.hpp
新增 action_wrapper struct, 它的出现,让我们对inline action 的使用更加便利化,相当于把 inline action 封装成一个 struct,直接实例化便可以发送一个 inline action, 下面会写例子。

eosiolib/permission.hpp

修改 eosio::check_transaction_authorization 参数类型 std::set to std::set , 使得能和 eosio 的 public_key 兼容。
eosio::check_permission_authorization 参数 account, permission 类型从 uint64_t 修改成 eosio::name

eosiolib/ignore.hpp

新增 ignore struct, 会让ABI 生成对应的类型, 但datastream 不会去序列化它
新增 ignore_wrapper, 方便其他合约调用声明的 action。

下面我们挑些改动比较大的地方来说下。

1.移除 uint64_t 的多数别名,只留下了一个 capi_name。

其中最大的地方当属 去掉了 uint64_t 的别名,需要用 name struct 来代替, 不应该用新的别名 capi_name。 不说了,笔者改代码改到想哭了。但为什么要做这个改动呢, 目前对于 account_name 等所使用的都是 implicit, 这意味着可能有一些 bad implicit。
Eg:

//@abi action
void hi(){
  name acc = name{10};
  print(acc==10);
}

我本意是要判断 两个 name struct 是否相等, 但是隐式转换使我直接比较整数 10 也能返回 true。
所以重构了 name struct,消除了这种风险。
这次的改动也变得比较偏面向对象思维, 像 eosio::char_to_symbol, eosio::string_to_name, eosio::name_suffix 都被整合进了 name struct 里面。
symbol 和 symbol_code 也被重构了。宏命令 S 被移除,不能直接用 S(4, SYS) 去声明一个 token symbol, 要用 symbol(symbol_code("SYS"), 4) or symbol("SYS", 4)去实例化一个symbol 对象, 也将一些针对 symbol 的函数整合进了 class。

2.重构了contract.hpp , EOSIO_ABI 修改成 EOSIO_DISPATCH

contract( name receiver, name code, datastream<const char*> ds ):_self(receiver),_code(code),_ds(ds) {}

构造函数增加 code 和 ds 参数。增加 ds 参数是为了方便我们手动解析数据。 这跟后面要说到的 ignore struct 有比较大的关系。
这种改动也意味着我们重写 apply 的方式要改动.
Eg:


extern "C" {
  void apply( uint64_t receiver, uint64_t code, uint64_t action ) {
    auto self = receiver;
    // 拦截 失败的 deferred_trx
    if( code == "eosio"_n.value && action == "onerror"_n.value ) {
      const auto act_data = unpack_action_data<onerror>();
      auto sender = uint64_t( act_data.sender_id >> 64);
      if( sender == self){
        test bos(eosio::name(receiver), eosio::name(code),datastream<const char*>(nullptr, 0));
        bos.resend( act_data.unpack_sent_trx(), uint64_t( act_data.sender_id) );
      }
    // 拦截 eosio.token 的 EOS 转账操作
    } else if ( code == "eosio.token"_n.value ){
      test bos(eosio::name(receiver), eosio::name(code),datastream<const char*>(nullptr, 0));
      const auto t = unpack_action_data<transfer_args>();
      if(t.from.value != self && t.to.value == self){
        bos._transfer(t.from, t.to, t.quantity, t.memo);   
      }
    }else if ( code == self || action == "onerror"_n.value ) {
      switch( action ) {
        EOSIO_DISPATCH_HELPER( test, (hi))
      }
    }
  }
}

3. ignore struct , ignore_wrapper 和 action_wrapper 的使用

在 action 的参数加上 ignore struct, 会告诉虚拟机,不要解析此数据, 让自己手动解析。
使用 action_wrapper 把 hello:hi action 包装起来。
使用inline action 时,用 ignore_wrapper 表明该参数是一个 ignore 类型。
Eg:

#include <eosiolib/eosio.hpp>
#include<eosiolib/ignore.hpp>
using namespace eosio;

CONTRACT hello : public eosio::contract {
 public:
   using contract::contract;

   ACTION hi( name user, ignore<uint64_t>, ignore<std::string>) {
     print_f( "Hello % from hello", user );

     // 读取 ignore 数据。
     uint64_t test;
     _ds >> test;
     printui(test);
     std::string str;
     _ds >> str;
     prints_l(str.c_str(),str.size());
   }

   // 用 action_wrapper , 把 hello::hi action 包装起来
   using hi_action = action_wrapper<"hi"_n, &hello::hi>;

   ACTION inlineaction( name user, name inlinecode ){
     print_f( "Hello % from send_inline", user );
     // constructor takes two arguments (the code the contract is deployed on and the set of permissions)
     // 实例化 hi_action, 并进行调用。
     // inlinecode 参数及对应的 hi action 的合约账号。
     hello::hi_action hi(inlinecode, {_self, "active"_n});
     hi.send(user,ignore_wrapper(22),ignore_wrapper("asdfa"));
   }

};
EOSIO_DISPATCH( hello, (hi)(inlineaction) )

结论:两个版本的主要改动是消除隐式转换的风险。 也重构了一些模块的代码, 变得更加直观。 新增了 action_wrapper struct, 使得 inline action 的使用更加方便快捷。

转载自 https://segmentfault.com/a/1190000017092129#articleHeader15

如何为EOS DAPP开发设置VS CODE和CLION

每个开发人员都需要一个良好的IDE,它是为他的项目的开发过程而设置的。这就是为什么我们创建了一个关于如何为EOS dApp开发设置VS Code和/或CLion的快速教程 。
我们还为VS Code创建了一些脚本,它们将自动执行您在终端中使用的一些命令。

Visual Studio代码设置

首先,如果您还没有安装一些VS代码扩展,请安装它们。通过dApp开发,它们将非常有用:

  • C / C ++ - VS Code的IntelliSense,调试和代码浏览
  • CMake - 对Visual Studio Code的CMake语言支持
  • CMake工具 - Visual Studio Code中的扩展CMake支持
  • WebAssembly - WebAssembly文本表示的语法高亮显示

当我们开发EOSIO dApp时,我们在.hpp和.cpp文件中编写代码。但是,这只是整个过程的一小部分。大多数情况下,我们需要生成一些其他文件,我们将使用这些文件在区块链上部署合同,对其进行单元测试等。这就是 CMake派上用场的地方。

CMake是一个用于控制软件编译过程的命令行工具。一旦在IDE中正确设置,它就会变得非常容易。
现在我们将使用CMake工具,我们应该对项目结构进行一些更改。我们将重用EOSIO项目的骨架,因为它拥有我们需要的一切。当然,我们做了一些小改动。

我们有一张图片显示了新的项目结构。让我们来看看。

首先,我们有build文件夹。这是放置所有构建内容的地方。您将使用的每个生成的文件都在那里。接下来是CMakeModules,其中包含一些有用的cmake模块,其中包含用于编译过程的自定义函数。

该合同是我们的核心文件夹中。这是我们准备智能合约的地方。目前,eosiolib,libc ++和musl默认存在,因为它们用于编译。行中的下一行是外部和库。这两个文件夹都包含用于使整个编译过程更容易的库。

项目结构中最后一个重要的事情是配置文件 - CMakeLists.txt。每个目录都有自己的带有命令的CMakeLists.txt文件。

你可以找到我们的回购里面的所有文件夹和脚本的新项目结构在这里

CMakeLists

让我们看看一些配置文件,因为您需要知道如何使用它们。

1.CMakeLists.txt(4)

这是设置编译过程的主要配置文件。您应该知道,在开发dApp时,需要设置项目名称。版本和语言是可选的。

2. CMakeLists.txt(3)

第二个配置文件位于contracts文件夹中。应将每个新的智能合约添加为此配置中的子目录。因为合同不会编译,所以不要忘记这一步很重要。CMake不会知道。

3. CMakeLists.txt(2)

每个智能合约都有自己的配置文件。在这里你需要注意每个合约有不同的目标,基本上,它是文件夹的名称。

现在,当我们拥有新的项目结构时,我们必须制作自定义命令,这些命令将编译和构建我们所做的一切。但是怎么样?幸运的是,VS Code有一些很酷的东西叫做 Tasks。它帮助我们只需点击几下即可自动执行每个命令。

VS代码中的任务

首先,我们必须生成将包含我们的自定义命令的tasks.json文件。按⇧+⌘+ P在VS Code中打开命令选项板,然后键入“ 任务 ”并选择“ 配置任务 ”。

下一步选择“从模板创建tasks.json文件”,然后选择“其他”

VS Code将创建一个名为“ .vscode ” 的文件夹,在其中,您可以找到tasks.json。现在我们需要添加命令。将以下代码复制并粘贴到tasks.json中:

我们创建了三个名为CMake, Build和Generate ABI的自定义命令。它们执行三个shell脚本 - compile.sh,build.sh和generate.sh。前两个脚本基本上做同样的事情,除了build.sh同时编译和构建。可能大多数时候你会使用第二个。

另一方面,第三个脚本 - generate.sh(Generate ABI)用于生成智能合约的abi。在构建期间需要生成一些文件。您必须在合同所在的文件夹中执行该命令。选择例如.cpp文件并运行它。

真棒!我们几乎准备好了VS代码设置。为了使整个过程变得更加容易,我们将为命令创建快捷方式。当你还在VS Code中时,请转到首选项 - 键盘快捷键。将打开一个快捷方式窗口 - 找到并打开keybindings.json(它位于顶部):

一旦打开keybindings.json,我们将创建我们的快捷方式。对于我们的命令,我们选择了“ cmd + e ”,“ cmd + r ”和“ cmd + i ”,但您可以选择其他命令。这是你必须添加的json:

完成所有操作后,您就可以开始在VS Code上开发EOS dApp

CLION设置

与VS Code相比,设置CLion非常简单。在CLion中加载框架时,IDE会自动在cmake-build-debug文件夹中创建所有构建文件。准备就绪后,您可以使用⌘+ F9快捷键执行实际构建。这就是你应该做的一切,太容易了吗?

但是,如果要为CMake设置其他设置,可以从首选项 - 构建,执行,部署中执行此操作:

有关在CLion中配置CMake的更多信息,请参阅IDE 的官方文档。真的很棒!

准备开始开发EOS dApps了吗?检查我们的教程
1.第一步在EOS Blockchain发展
2.最终的端到端EOS dApp开发教程 - 第1部分

英文原文:https://infinitexlabs.com/setup-ide-for-eos-development/

EOS nodejs 与链上合约交互事例

链上部署合约部分代码

Table
struct [[eosio::table("users"), eosio::contract("some.game")]] user{
        capi_name account_name;
        uint64_t qualifications;
        auto primary_key() const { return account_name; }
    };
Action
ACTION addqualify(capi_name account);
void some_contract::addqualify(capi_name account){
    require_auth( _self.value );
    user_tables user_table(_self, _self.value);
    auto itr = user_table.find(account);
    eosio_assert(itr != user_table.end(), "account not find" );
    user_table.modify( itr, _self, [&]( auto& u ) {
        u.qualifications += 1;
    });
}

NodeJs 代码部分

EosService.js

const Eos = require('eosjs');
const config = require('../config/EosNetwork');

class EosService {
  constructor() {
    this.eosApi = Eos({
      httpEndpoint: `${config.network.protocol}://${config.network.host}${config.network.port ? ':' : ''}${config.network.port}`,
      chainId: config.network.chainId,
      keyProvider: config.self.privateKey,
      fetchConfiguration: {
        timeout: config.network.timeout,
      },
    });
    this.authorization = [`${config.contract.account}@active`];
    this.transactionOptions = { authorization: this.authorization };
    this.contractResult = null;
  }

  async init() {
    try {
      this.contractResult = await this.eosApi.contract(config.contract.code);
    } catch (e) {
      console.log(e); // TODO throw
    }
  }

  api() {
    return this.eosApi;
  }

  async pushAction(action, params) {
    if (this.contractResult === null) {
      await this.init();
    }
    try {
      this.contractResult[action](params, this.transactionOptions);
    } catch (e) {
      console.log(e); // TODO throw
    }
  }
}

module.exports = EosService;

chain.js

const EosService = require('../services/EosService');

class Chain {
  constructor() {
    this.eosService = new EosService();
  }

  async addQualify(account) {
    await this.eosService.pushAction('addqualify', account);
  }
}

module.exports = Chain;

game.js

const Chain = require('./chain');
const chain = new Chain();
....
await chain.addQualify(item.challenger); //此时,链上users 对应账户的qualifications将会+1

谈谈 EOS 的出块时间,不可逆时间,BFT

EOS出块时间

我们知道,新生产节点必须基于上一个区块生产新区块,因此新生产节点必须在指定的时候内接收到上一个区块的内容,否则只能跳过(只能基于上上一个区块生产)。如果出块时间太短,新节点很大可能接收不到上一个区块的内容,进而频繁出现跳块。只要有跳块,系统就会出现临时分叉,尽管EOS的DPOS的定时出块和最长链共识让系统很大可能最终达成共识,但是也会造成更多缺块,进而降低了有效单位出块数量,得不偿失。

因而一个合适的出块时间就显的很重要了,比如STEAM, BTXS的出块时间就设置为3S。那凭啥EOS能将出块时间设置为0.5S呢?因为EOS做了如下改进:

  • 1个生产节点连续出12个块
    1个生产节点内的12个块不存在接收等待问题,是0等待,肯定也不存在跳块问题。比如生产区块2时,肯定能拿到区块1的数据
  • 21个生产节点的出块顺序是固定的,且直连
    当A个节点完成12个区块时,系统会切换生产者,新的生产者B就需要通过网络接收上一个生产者的区块。由于A的区块是生产一个就广播一个,12区块传播到生产者B的可用时间最短, 由于网络时延和拥堵问题,B不一定能接收A的12区块。因此为了减少跳块,必须降低A->B的时延。由于STEEM,BTSX中,生产节点是随机排序的,A->B的时延是不确定的,可能A在美国,B在中国,美国和中国的来回传播就可能需要600ms, 同时A和B可能没有直连还需要中间节点跳转才能传播,时延就更久了,因而需要设置一个比较大的出块时间,最后测试调试下来,STEEM和BTXS就设置了一个比较合理的3S经验值。EOS为了减少相邻两个节点的时延,按照节点之间的时延安排出块顺序,即时延小的安排在邻近,比如A安排为香港,B为中国就比A是美国,B为中国好太多。同时,由于这21个节点的信息是透明的,这21个节点可以直连,进一步降低传播时延。有了这两个改进后,0.5s变为可能了。当然这种固定出块顺序的确定性也带来了不安全因素,比如攻击者可以准确预估每个出块节点,就可以更容易发起攻击行为。

然后我们来看下节点地域和实际出块顺序:
![]()
中国的节点没有将服务器部署在中国大陆的,要么在香港要么在日本,新加坡,甚至美国。

EOS区块不可逆时间

EOS中,21个生产节点轮流出块。一个区块是通过后续节点基于该区块的生产行为来间接确认的,为了实现BFT级别的不可逆,自然需要得到2/3+1的节点确认。由于每个节点生产12个区块,所以需要12(2/321+1)=12*15=180个区块确认。因而大家第一感觉是1个区块只需要后续180个区块即可变成不可逆状态。然而,如果大家在EOS区块链上查询区块的实时信息,就会发现一个区块都是在300多区块的时候才会被标识为不可逆状态。这是因为区块的确认分为两个阶段,第一个阶段是pre-commit阶段,该阶段需要接受2/3+1个节点的确认表明,超过2/3节点认可该区块。但是此时并不意味着超过2/3的节点已经了解到这个2/3确认信息。因而再次需要2/3的commit签名确认过程。两阶段签名确认流程类似下图:

EOS的特殊BFT

常规的BFT都是生产一个区块,等待共识,然后再生产一个区块,比如Tendermint和Cosmos里的PBFT的实现。由于PBFT共识需要比较长的时间(至少1s以上),因而采用传统的PBFT是没法满足0.5s出块需求的。因而EOS 的BFT采取了不同的实现方式,出块和共识是流水并行工作的,区块生产完成后,不等待PBFT共识,继续生产同时参与并处理上一个区块的PBFT共识,当PBFT共识完成后即修改为不可逆状态。

EOS是否已采用BFT

其实很多人都在问我这个问题,我只能说,根据我目前了解点到的信息是代码和测试代码都有,但还没实行。其实从目前不可逆时间也可推测出BFT没有启用,如果BFT已经启用,1个区块在一个BFT共识完成后(该共识一般只需1s多)即可被确认,而不是目前的3分钟。

转载自 https://mp.weixin.qq.com/s/DlKA5xBhNUFC203t7sKy4A

scatter getArbitrarySignature

今天要实现通过ScatterDesktop,使用选中的账号私钥加密数据做签名,传到中心服务器,做身份验证
先粗略记下,稍后整理

首先由scatter.js发起 (View Code)

getArbitrarySignature(publicKey, data, whatfor = '', isHash = false){
        throwNoAuth();
        return SocketService.sendApiRequest({
            type:'requestArbitrarySignature',
            payload:{
                publicKey,
                data,
                whatfor,
                isHash
            }
        });
    }

然后传到 ScatterDesktop (View Code)

static async [Actions.REQUEST_ARBITRARY_SIGNATURE](request, identityKey = null){
        return new Promise(async resolve => {

            const {payload} = request;
            const {origin, publicKey, data, whatFor, isHash} = request.payload;

            if(identityKey) payload.identityKey = identityKey;
            else {
                const possibleId = PermissionService.identityFromPermissions(origin, false);
                if (!possibleId) return resolve({id: request.id, result: Error.identityMissing()});
                payload.identityKey = possibleId.publicKey;
            }

            const keypair = KeyPairService.getKeyPairFromPublicKey(publicKey);
            if(!keypair) return resolve({id:request.id, result:Error.signatureError("signature_rejected", "User rejected the signature request")});

            const blockchain = keypair.publicKeys.find(x => x.key === publicKey).blockchain;

            // Blockchain specific plugin
            const plugin = PluginRepository.plugin(blockchain);

            // Convert buf and abi to messages
            payload.messages = [{
                code:`${blockchain.toUpperCase()} Blockchain`,
                type:'Arbitrary Signature',
                data:{
                    signing:data
                }
            }];

            PopupService.push(Popup.popout(Object.assign(request, {}), async ({result}) => {
                if(!result || (!result.accepted || false)) return resolve({id:request.id, result:Error.signatureError("signature_rejected", "User rejected the signature request")});

                resolve({id:request.id, result:await plugin.signer(payload, publicKey, true, isHash)});
            }));
        });
    }

关键点在

plugin.signer(payload, publicKey, true, isHash)

然后到 eos.js (View Code)

async signer(payload, publicKey, arbitrary = false, isHash = false){
        let privateKey = KeyPairService.publicToPrivate(publicKey);
        if (!privateKey) return;

        if(typeof privateKey !== 'string') privateKey = this.bufferToHexPrivate(privateKey);

        if (arbitrary && isHash) return ecc.Signature.signHash(payload.data, privateKey).toString();
        return ecc.sign(Buffer.from(arbitrary ? payload.data : payload.buf, 'utf8'), privateKey);
    }

eosjs-ecc

Verify signed data.

ecc.verify(signature, 'I am alive', pubkey) === true

Recover the public key used to create the signature.

ecc.recover(signature, 'I am alive') === pubkey