EOS 智能合约

作者: cenkai88 | 来源:发表于2018-01-09 17:34 被阅读5826次

    1. EOS智能合约的介绍

    1.1. 所需背景知识

    C / C++ 经验

    基于EOS.IO的区块链使用Web Assembly(WASM)执行开发者提供的应用代码。WASM是一个已崭露头角的web标准,受到Google, Microsoft, Apple及其他大公司的广泛支持。目前为止,最成熟的用于构建应用及WASM代码编译的工具链是clang/llvm及其C/C++编译器。

    其他由第三方开发中的工具链包括:Rust, Python, and Solidiity。尽管用其他语言更简单,但是他们的性能很可能制约你所构建的应用规模。我们希望C++ 将成为开发高性能及安全智能合约的最佳语言。

    Linux / Mac OS 经验

    EOS.IO软件仅官方支持如下环境

    • Ubuntu 16.10 或更高
    • MacOS Sierra 或更高

    命令行知识

    EOS.IO提供了一系列工具,需要基本的命令行知识来操作它们。

    1.2. EOS智能合约基础知识

    通信模型

    EOS智能合约通过messages 及 共享内存数据库(比如只要一个合约被包含在transaction的读取域中with an async vibe,它就可以读取另一个合约的数据库)相互通信。异步通信导致的spam问题将由资源限制算法来解决。下面是两个在合约里可定义的通信模型:

    • Inline. Inline保证执行当前的transaction或unwind;无论成功或失败都不会有通知。Inline 操作的scopes和authorities和原来的transaction一样。

    • Deferred. Defer将稍后由区块生产者来安排;结果可能是传递通信结果或者只是超时。Deferred可以触及不同的scopes,可以携带发送它的合约的authority*此特性在STAT不可用

    Message vs Transaction

    一个message代表单个操作, 一个transaction是一个或多个messages的集合。合约和账户通过messages通信。Messages可以单个地发送,如果希望一次执行批处理也可以集合起来发送。

    单message的Transaction.

    {
      "ref_block_num": "100",
      "ref_block_prefix": "137469861",
      "expiration": "2017-09-25T06:28:49",
      "scope": ["initb","initc"],
      "messages": [
        {
          "code": "eos",
          "type": "transfer",
          "authorization": [
            {
              "account": "initb",
              "permission": "active"
            }
          ],
          "data": "000000000041934b000000008041934be803000000000000"
        }
      ],
      "signatures": [],
      "authorizations": []
    }
    
    

    多messages的Transaction,这些messages将全部成功或全部失败。

    {
      "ref_block_num": "100",
      "ref_block_prefix": "137469861",
      "expiration": "2017-09-25T06:28:49",
      "scope": [...],
      "messages": [{
          "code": "...",
          "type": "...",
          "authorization": [...],
          "data": "..."
        }, {
          "code": "...",
          "type": "...",
          "authorization": [...],
          "data": "..."
        }, ...
      ],
      "signatures": [],
      "authorizations": []
    }
    
    

    Message名的限定

    Message的类型实际上是base32编码的64位整数。所以Message名的前12个字符需限制在字母a-z, 1-5, 以及'.' 。第13个以后的字符限制在前16个字符('.' and a-p)。

    Transaction确认

    获得一个transaction哈希并不等于transaction完成,它只表示该节点无报错地接受了,而其他区块生产者很可能也会接受它。

    但要确认该transaction,你需要在transaction历史中查看含有该transaction的区块数。

    1.3. 技术限制

    • 无浮点数. 合约不接受浮点小数计算因为这在CPU层级上是一个不确定的行为,会导致意想不到的分叉。
    • Transaction需要在1 ms内执行. transaction的执行时间需要在*小于等于1ms否则transaction将会失败。
    • 最大 30 tps. 目前根据测试公网设置,每个账户最多每秒可发布30个transactions.

    2 智能合约文件

    为简单起见我们创造了一个工具叫 eoscpp,它可以用来引导产生一个新合约。eoscpp将创造三个智能合约文件,他们是你起步开发的框架。

    $ eoscpp -n ${contract}
    

    以上将在'./${project}'文件夹下创建一个新项目,包含三个文件:

    ${contract}.abi ${contract}.hpp ${contract}.cpp
    

    2.1. HPP

    HPP是包含CPP文件所引用的变量、常量、函数的头文件。

    2.2. CPP

    CPP文件是包含合约功能的源文件。

    如果您通过eoscpp工具产生CPP文件,产生的文件将和如下相似:

    #include <${contract}.hpp>
    
    /**
     *  init() 和 apply() 方法一定要有C调用约定 so that the blockchain can lookup and
     *  call these methods.
     */
    extern "C" {
    
        /**
         *  This method is called once when the contract is published or updated.
         */
        void init()  {
           eosio::print( "Init World!\n" ); // Replace with actual code
        }
    
        /// The apply method implements the dispatch of events to this contract
        void apply( uint64_t code, uint64_t action ) {
           eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" ); 
        }
    
    } // extern "C"
    
    

    这里您可以看到我们创建了两个函数,initapply。他们所做的是记录所有提交的messages 到日志中且并不作检查。只要区块生产者同意,任何人在任何时间都可以提交任何message。如果缺少所需的签名,合约将会被收取消耗带宽的费用。

    init

    init仅在被初次部署的时候执行一次。它是用于初始化合约变量的,例如货币合约中提供token的数量。

    apply

    apply是message处理器,它监听所有输入的messages并根据函数中的规定进行反馈。apply函数需要两个输入参数,codeaction

    code filter

    为了响应特定message,您可以如下构建您的apply函数。您也可以忽略code filter来构建一个响应通用messages的函数。

    if (code == N(${contract_name}) {
        //响应特定message的处理器
    }
    

    在其中您可以定义对不同actions的响应。

    action filter

    为了相应特定action,您可以如下构建您的apply函数。常和code filter一起使用。

    if (action == N(${action_name}) {
        //响应该action的处理器
    }
    

    2.3. WAST

    想要部署到EOS.IO区块链上的任何程序都需要先编译成WASM格式。这是区块链接受的唯一格式。

    一旦您完成了CPP文件的开发,您可以用eoscpp工具将它编译成一个文本版本的WASM (.wast) 文件。

    $ eoscpp -o ${contract}.wast ${contract}.cpp
    

    2.4. ABI

    Application Binary Interface (ABI)是一个基于JSON的描述文件,是关于转换JSON和二进制格式的用户actions的。ABI还描述了如何将数据库状态和JSON的互相转换。一旦您通过ABI描述了您的合约,开发者和用户就能够用JSON和您的合约无缝交互了。

    ABI文件可通过eoscpp工具从HPP文件生成:

    $ eoscpp -g ${contract}.abi ${contract}.hpp
    

    这里是一个合约的骨架ABI的例子:

    {
      "types": [{
          "new_type_name": "account_name",
          "type": "name"
        }
      ],
      "structs": [{
          "name": "transfer",
          "base": "",
          "fields": {
            "from": "account_name",
            "to": "account_name",
            "quantity": "uint64"
          }
        },{
          "name": "account",
          "base": "",
          "fields": {
            "account": "name",
            "balance": "uint64"
          }
        }
      ],
      "actions": [{
          "action": "transfer",
          "type": "transfer"
        }
      ],
      "tables": [{
          "table": "account",
          "type": "account",
          "index_type": "i64",
          "key_names" : ["account"],
          "key_types" : ["name"]
        }
      ]
    }
    
    

    您肯定注意到了这个ABI 定义了一个叫transfer的action,它的类型也是transfer。这就告诉EOS.IO当${account}->transfer的message发生时,它的payload是transfer类型的。 transfer类型是在structs的列表中定义的,其中有个对象,name属性是transfer

    ...
      "structs": [{
          "name": "transfer",
          "base": "",
          "fields": {
            "from": "account_name",
            "to": "account_name",
            "quantity": "uint64"
          }
        },{
    ...
    
    

    这部分包括from, toquantity等字段。这些字段都有对应的类型:account_nameuint64account_name 是一个用base32编码来表示uint64的内置类型。 要了解更多的可用内置类型,请点击这里.

    {
      "types": [{
          "new_type_name": "account_name",
          "type": "name"
        }
      ],
    ...
    
    

    上述types列表内,我们定义了一系列现有类型的别名。这里,我们把account_name定义为name的别名。

    3. 清单

    在开始EOS智能合约开发之前,我们需要搞清下面的内容:

    构建最新的版本

    请确认您环境中的是最新的版本,您才能获取到eoscppeosc这些对于您开发非常重要的工具。 如何获取最新构建版本可以在 环境 章节找到。

    一旦您安装了最新版本的eosio/eos代码,请确认您的环境变量中有${CMAKE_INSTALL_PREFIX}/bin,如果没有的话您可以用下面的命令安装。

    cd build 
    make install
    

    连接到EOS.IO区块链

    您可以用以下命令连接到一个节点

    $ eosc -H ${node_ip} -p ${port_num}
    

    node_ip可以是私有的节点IP。如果您连接的是测试公网,您需要用节点的公共IP这里.

    port_num 是8888或8889,具体取决于配置。

    创建钱包获取账户

    为了在区块链上部署合约,您需要在EOS.IO区块链上创建一个账户。每个合约都需要一个相关联的账户。

    如果您已经有EOS Tokens,您应该在测试公网上已经有一个账户了。如果您需要新建一个测试账户,请参考以下信息:

    4. 与智能合约交互的例子

    在深入了解如何构建一个智能合约前,我们这里提供了一些智能合约的例子,供您参考以更快的理解EOS智能合约是如何工作的。

    为了和这些样例合约交互,您需要先完成清单上面的步骤并且部署样例合约到EOS.IO区块链上。

    4.1. 货币合约

    部署样例合约

    样例货币合约可在这里找到,如果您已经下载了EOSIO仓库的话,那您应当可以在本地磁盘上找到。

    文件夹中包括.abi, .cpp 和 .hpp 文件,在您部署合约前您需要编译生成.wast文件。

    $ eoscpp -o currency.wast currency.cpp
    

    您成功生成.wast文件后,您可以使用set contract命令来部署。

    $ eosc set contract ${contract_account_name} ../contracts/currency.wast ../contracts/currency.abi
    Reading WAST...
    Assembling WASM...
    Publishing contract...
    {
      "transaction_id": "1abb46f1b69feb9a88dbff881ea421fd4f39914df769ae09f66bd684436443d5",
      "processed": {
        "ref_block_num": 144,
        "ref_block_prefix": 2192682225,
        "expiration": "2017-09-14T05:39:15",
        "scope": [
          "eos",
          "${account}"
        ],  
        ...
    }
    
    

    请确认您的钱包是解锁状态,且您已经将对应${contract_account_name}的有效的key导入到钱包。

    了解合约

    现在我们已经部署了合约,任何人都可以用eoscget code命令来获取合约的.abi文件并且了解此合约有哪些可用接口。

    $ eosc get code currency -a currency.abi
    code hash: 86968a9091ce32255777e2017fccaede8cea2d4978b30f25b41ee97b9d77bed0
    saving abi to currency.abi
    $ cat currency.abi
    {
      "types": [{
          "newTypeName": "account_name",
          "type": "Name"
        }
      ],
      "structs": [{
          "name": "transfer",
          "base": "",
          "fields": {
            "from": "account_name",
            "to": "account_name",
            "quantity": "uint64"
          }
        },{
          "name": "account",
          "base": "",
          "fields": {
            "account": "name",
            "balance": "uint64"
          }
        }
      ],
      "actions": [{
          "action": "transfer",
          "type": "transfer"
        }
      ],
      "tables": [{
          "table": "account",
          "indextype": "i64",
          "keynames": [
            "account"
          ],
          "keytype": [],
          "type": "account"
        }
      ]
    }
    
    

    备注

    • 合约接受一个叫 transfer的transaction,此transaction接受一个有fromtoquantity字段的message。
    • 同时有一个叫account的table用于存储数据。

    既然我们有一个transfer action,而这个账户table可以用来查余额,我们可以用eosc来和他们交互。

    读取账户余额

    要从一个表中读取数据,需使用get table命令:eosc get table ${account} ${contract} ${table}.

    $ eosc get table ${account} currency account
    {
      "rows": [{
         "key": "account",
         "balance": 1000000000
         }
      ],
      "more": false
    }
    

    资金转账

    任何人都可以在任何时间向任何合约发任何message但是合约有可能拒绝那些没有特定权限的messages。Messages实际上并不是从任何“人”发出的,它们是“伴随一个或多个账户及其特定等级的permission”发出的。

    下面的命令将在货币合约中从account_a到account_b转账50个token

    $ eosc push message currency transfer '{"from":"${account_a}","to":"${account_b}","quantity":50}' --scope ${account_a},${account_b} --permission ${account_a}@active
    

    我们指定了--scope参数来给与货币合约对于那些能修改自身余额的用户的读写权限。在下个版本中,scope将会自动确定。

    您将会收到如下的包含transaction_id字段的JSON输出,作为本次transaction成功提交的确认信息。

    1589302ms thread-0   currency.cpp:271  operator()  ] Converting argument to binary...
    1589304ms thread-0   currency.cpp:290  operator()  ] Transaction result:
    {
      "transaction_id": "1c4911c0b277566dce4217edbbca0f688f7bdef761ed445ff31b31f286720057",
      "processed": {
        "refBlockNum": 1173,
        "refBlockPrefix": 2184027244,
        "expiration": "2017-08-24T18:28:07",
        "scope": [...],
        "signatures": [],
        "messages": [...]
      }
    }
    
    

    一旦您获得了这个成功结果,您就可以像刚才一样从账户table中查看账户的余额和状态了。

    4.2. Tic-Tac-Toe

    tic-tac-toe是一个两人玩的纸笔游戏,用X和O,两人分别轮流在3×3的格子里标记,先完成横向,纵向或对角线三个格子的标记的人获得胜利。

    游戏规则

    • 每对玩家可以有最多2轮游戏,第一轮是1号玩家是host,2号玩家是challenger,第二轮反过来。
    • 游戏数据存储于“host”的游戏表格中,而"challenger"是key。
      例子
    Coordinate 0 1 2
    0 - o x
    1 - x -
    2 x o o

    用数字在板上表示:

    • 0 代表空格子
    • 1 代表被host占据的
    • 2 代表被challenger占据的

    因此,假设x是host,o是challenger,上面的比赛板可以在此局游戏的对象中如下表示:[0, 2, 1, 0, 1, 0, 1, 2, 2]。

    部署样例合约

    tic_tac_toe合约在这里,如果您已经下载了EOSIO仓库的话,那您应当可以在本地磁盘上找到。

    文件夹中包括.abi, .cpp 和 .hpp 文件,在您部署合约前您需要编译生成.wast文件。

    $ eoscpp -o tic_tac_toe.wast tic_tac_toe.cpp
    

    您成功生成.wast文件后,您可以使用set contract命令来部署。对于此例来说,我们希望在tic.tac.toe账户上部署。注意EOS.IO区块链只支持base32字符作为账户名,这也是为什么下划线被替换成了'.'。如果您要在除了tic.tac.toe的其他账户部署此应用,您需要将.hpp,.cpp,和 .abi文件中的tic.tac.toe替换为您自己的账户名。

    $ eosc set contract tic.tac.toe tic_tac_toe.wast tic_tac_toe.abi
    Reading WAST...
    Assembling WASM...
    Publishing contract...
    {
      "transaction_id": "1abb46f1b69feb9a88dbff881ea421fd4f39914df769ae09f66bd684436443d5",
      "processed": {
        "ref_block_num": 144,
        "ref_block_prefix": 2192682225,
        "expiration": "2017-09-14T05:39:15",
        "scope": [
          "eos",
          "tic.tac.toe"
        ],  
        ...
    }
    
    

    了解合约

    现在我们已经部署了合约,任何人都可以用eoscget code命令来获取合约的.abi文件并且了解此合约有哪些可用接口。

    $ eosc get code tic.tac.toe. -a tic_tac_toe.abi
    code hash: c78d16396a5a63b1be47fd570633084cb5fe2eaa9980ca87ec25061d68299294
    saving abi to tic_tac_toe.abi
    $ cat tic_tac_toe.abi
    {
      "types": [{
          "new_type_name": "account_name",
          "type": "name"
        }
      ],
      "structs": [{
          "name": "game",
          "base": "",
          "fields": {
            "challenger": "account_name",
            "host": "account_name",
            "turn": "account_name",
            "winner": "account_name",
            "board": "uint8[]"
          }
        },{
          "name": "create",
          "base": "",
          "fields": {
            "challenger": "account_name",
            "host": "account_name"
          }
        },{
          "name": "restart",
          "base": "",
          "fields": {
            "challenger": "account_name",
            "host": "account_name",
            "by": "account_name"
          }
        },{
          "name": "close",
          "base": "",
          "fields": {
            "challenger": "account_name",
            "host": "account_name"
          }
        },{
          "name": "movement",
          "base": "",
          "fields": {
            "row": "uint32",
            "column": "uint32"
          }
        },{
          "name": "move",
          "base": "",
          "fields": {
            "challenger": "account_name",
            "host": "account_name",
            "by": "account_name",
            "movement": "movement"
          }
        }
      ],
      "actions": [{
          "action_name": "create",
          "type": "create"
        },{
          "action_name": "restart",
          "type": "restart"
        },{
          "action_name": "close",
          "type": "close"
        },{
          "action_name": "move",
          "type": "move"
        }
      ],
      "tables": [{
            "table_name": "games",
            "type": "game",
            "index_type": "i64",
            "key_names" : ["challenger"],
            "key_types" : ["account_name"]
          }
      ]
    }
    
    

    注释

    • 此合约接受createrestartclosemove的actions,每个actions接受具有不同字段的messages。
    • games的table用于保存数据

    如何玩游戏:

    • 使用create来创建游戏,设置您的账户为host其他人的为challenger。
    $ eosc push message tic.tac.toe create '{"challenger":"${challenger_account_name}","host":"${your_account_name}"}' --permission ${your_account}@active
    
    • 第一步host走,用moveaction指定需要填入哪行哪列的格子来完成一次移动。
    $ eosc push message tic.tac.toe move '{"challenger":"${challenger_account_name}","host":"${your_account_name}","by":"${your_account_name}","''{"row":0,"column":1}"}' --permission ${your_account}@active
    
    • 然后让challenger走,然后再是host走。不断重复直至决出赢家。
    $ eosc push message tic.tac.toe move '{"challenger":"${challenger_account_name}","host":"${your_account_name}","by":"${your_account_name}","''{"row":1,"column":1}"}' --permission ${challenger_account}@active
    
    • restart action重启游戏
    $ eosc push message tic.tac.toe restart '{"challenger":"${challenger_account_name}","host":"${your_account_name}","by":"${your_account_name}"}' --permission ${your_account}@active
    
    • close action来将此游戏从数据库清除,这样会在游戏结束后释放空间。
    $ eosc push message tic.tac.toe close '{"challenger":"${challenger_account_name}","host":"${your_account_name}"}' --permission ${your_account}@active
    

    5. 完成您的第一个EOS智能合约

    Hello World

    此章节中,我们将一步步地构建一个hello world合约。

    开始前,您需要先完成清单上的所有步骤。

    开始吧 首先,我们使用eoscpp来生成智能合约的骨架。这将在hello文件夹里产生一个空白工程,里面有abi,hpp和cpp文件。

    $ eoscpp -n hello
    

    CPP文件含有一个当收到message后打印 Hello World: ${account}->${action}的样例代码。

        void apply( uint64_t code, uint64_t action ) {
           eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
        }
    
    

    我们从.cpp文件生成.wast文件。

    $ eoscpp -o hello.wast hello.cpp
    

    您获得.wast 和 .abi 文件后,就可以将合约部署到区块链上了。

    假设您的钱包已经解锁了并且有${account}的keys,您就可以上传用此命令把合约上传到区块链上:

    $ eosc set contract ${account} hello.wast hello.abi
    Reading WAST...
    Assembling WASM...
    Publishing contract...
    {
      "transaction_id": "1abb46f1b69feb9a88dbff881ea421fd4f39914df769ae09f66bd684436443d5",
      "processed": {
        "ref_block_num": 144,
        "ref_block_prefix": 2192682225,
        "expiration": "2017-09-14T05:39:15",
        "scope": [
          "eos",
          "${account}"
        ],
        "signatures": [
          "2064610856c773423d239a388d22cd30b7ba98f6a9fbabfa621e42cec5dd03c3b87afdcbd68a3a82df020b78126366227674dfbdd33de7d488f2d010ada914b438"
        ],
        "messages": [{
            "code": "eos",
            "type": "setcode",
            "authorization": [{
                "account": "${account}",
                "permission": "active"
              }
            ],
            "data": "0000000080c758410000f1010061736d0100000001110460017f0060017e0060000060027e7e00021b0203656e76067072696e746e000103656e76067072696e7473000003030202030404017000000503010001071903066d656d6f7279020004696e69740002056170706c7900030a20020600411010010b17004120100120001000413010012001100041c00010010b0b3f050041040b04504000000041100b0d496e697420576f726c64210a000041200b0e48656c6c6f20576f726c643a20000041300b032d3e000041c0000b020a000029046e616d6504067072696e746e0100067072696e7473010004696e697400056170706c790201300131010b4163636f756e744e616d65044e616d6502087472616e7366657200030466726f6d0b4163636f756e744e616d6502746f0b4163636f756e744e616d6506616d6f756e740655496e743634076163636f756e740002076163636f756e74044e616d650762616c616e63650655496e74363401000000b298e982a4087472616e736665720100000080bafac6080369363401076163636f756e7400076163636f756e74"
          }
        ],
        "output": [{
            "notify": [],
            "deferred_transactions": []
          }
        ]
      }
    }
    
    

    如果您查看您的eosd进程的输出的话,您会看到:

    ...] initt generated block #188249 @ 2017-09-13T22:00:24 with 0 trxs  0 pending
    Init World!
    Init World!
    Init World!
    
    

    您可以看到"Init World!"被执行了三次,这其实并不是个bug。区块链处理transactions的流程是:

    1: eosd收到一个新transaction (正在验证的transaction)

    • 创建一个新的临时会话
    • 尝试应用此transaction
    • 成功并打印出"Init World!"
    • 失败则回滚所做的变化 (也有可能打印"Init World!"后失败)

    2 : eosd开始产出区块

    • 撤销所有pending状态
    • pushes all transactions as it builds the block
    • 第二次打印"Init World!"
    • 完成区块
    • 撤销所有创造区块时的临时变化

    3rd : eosd如同从网络上获得区块一样将区块追加到链上。

    • 第三次打印 "Init World!"

    此时,您的合约就可以开始接受messages了。因为默认message处理器接受所有messages,我们可以发送任何我们想发的东西。我们试一下发一个空的message:

    $ eosc push message ${account} hello '"abcd"' --scope ${account}
    

    此命令将"hello"message及16进制字符串"abcd"所代表的二进制文件传出。注意,后面我们将展示如何定义ABI来用一个好看易读的JSON对象替换16进制字符串。以上,我们只是想证明“hello”类型的message是如何发送到账户的。

    结果是:

    {
      "transaction_id": "69d66204ebeeee68c91efef6f8a7f229c22f47bcccd70459e0be833a303956bb",
      "processed": {
        "ref_block_num": 57477,
        "ref_block_prefix": 1051897037,
        "expiration": "2017-09-13T22:17:04",
        "scope": [
          "${account}"
        ],
        "signatures": [],
        "messages": [{
            "code": "${account}",
            "type": "hello",
            "authorization": [],
            "data": "abcd"
          }
        ],
        "output": [{
            "notify": [],
            "deferred_transactions": []
          }
        ]
      }
    }
    
    

    如果您继续查看eosd的输出,您将在屏幕上看到:

    Hello World: ${account}->hello
    Hello World: ${account}->hello
    Hello World: ${account}->hello
    
    

    再一次,您的合约在transaction被第三次应用并成为产出的区块之前被执行和撤销了两次。

    如果我们查看ABI文件,您将会注意到这个ABI 定义了一个叫transfer的action,它的类型也是transfer。这就告诉EOS.IO当${account}->transfer的message发生时,它的payload是transfer类型的。 transfer类型是在structs的列表中定义的,其中有个对象,name属性是transfer

    ...
      "structs": [{
          "name": "transfer",
          "base": "",
          "fields": {
            "from": "account_name",
            "to": "account_name",
            "quantity": "uint64"
          }
        },{
    ...
    
    

    在弄清骨架ABI后,我们可以构造一个transfer类型的message:

    eosc push message ${account} transfer '{"from":"currency","to":"inita","quantity":50}' --scope initc
    2570494ms thread-0   main.cpp:797                  operator()           ] Converting argument to binary...
    {
      "transaction_id": "b191eb8bff3002757839f204ffc310f1bfe5ba1872a64dda3fc42bfc2c8ed688",
      "processed": {
        "ref_block_num": 253,
        "ref_block_prefix": 3297765944,
        "expiration": "2017-09-14T00:44:28",
        "scope": [
          "initc"
        ],
        "signatures": [],
        "messages": [{
            "code": "initc",
            "type": "transfer",
            "authorization": [],
            "data": {
              "from": "currency",
              "to": "inita",
              "quantity": 50
            },
            "hex_data": "00000079b822651d000000008040934b3200000000000000"
          }
        ],
        "output": [{
            "notify": [],
            "deferred_transactions": []
          }
        ]
      }
    }
    
    

    如果您继续观察eosd的输出,您将看到:

    Hello World: ${account}->transfer
    Hello World: ${account}->transfer
    Hello World: ${account}->transfer
    
    

    根据ABI,transfer message应该是如下格式的:

      "fields": {
        "from": "account_name",
        "to": "account_name",
        "quantity": "uint64"
      }
    
    

    我们也知道account_name -> uint64表示这个message的二进制表示如同:

    struct transfer {
        uint64_t from;
        uint64_t to;
        uint64_t quantity;
    };
    

    EOS.IO的C API通过Message API提供获取message的payload的能力:

    uint32_t message_size();
    uint32_t read_message( void* msg, uint32_t msglen );
    
    

    让我们修改hello.cpp来打印出消息内容:

    #include <hello.hpp>
    
    /**
     *  The init() and apply() methods must have C calling convention so that the blockchain can lookup and
     *  call these methods.
     */
    extern "C" {
    
        /**
         *  This method is called once when the contract is published or updated.
         */
        void init()  {
           eosio::print( "Init World!\n" );
        }
    
        struct transfer {
           uint64_t from;
           uint64_t to;
           uint64_t quantity;
        };
    
        /// The apply method implements the dispatch of events to this contract
        void apply( uint64_t code, uint64_t action ) {
           eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
           if( action == N(transfer) ) {
              transfer message;
              static_assert( sizeof(message) == 3*sizeof(uint64_t), "unexpected padding" );
              auto read = read_message( &message, sizeof(message) );
              assert( read == sizeof(message), "message too short" );
              eosio::print( "Transfer ", message.quantity, " from ", eosio::name(message.from), " to ", eosio::name(message.to), "\n" );
           }
        }
    
    } // extern "C"
    
    

    这样我们就可以重编译并部署了:

    eoscpp -o hello.wast hello.cpp 
    eosc set contract ${account} hello.wast hello.abi
    
    

    eosd因为重部署将再次调用init()

    Init World!
    Init World!
    Init World!
    
    

    然后我们执行transfer:

    $ eosc push message ${account} transfer '{"from":"currency","to":"inita","quantity":50}' --scope ${account}
    {
      "transaction_id": "a777539b7d5f752fb40e6f2d019b65b5401be8bf91c8036440661506875ba1c0",
      "processed": {
        "ref_block_num": 20,
        "ref_block_prefix": 463381070,
        "expiration": "2017-09-14T01:05:49",
        "scope": [
          "${account}"
        ],
        "signatures": [],
        "messages": [{
            "code": "${account}",
            "type": "transfer",
            "authorization": [],
            "data": {
              "from": "currency",
              "to": "inita",
              "quantity": 50
            },
            "hex_data": "00000079b822651d000000008040934b3200000000000000"
          }
        ],
        "output": [{
            "notify": [],
            "deferred_transactions": []
          }
        ]
      }
    }
    
    

    后面我们将看到eosd有如下输出:

    Hello World: ${account}->transfer
    Transfer 50 from currency to inita
    Hello World: ${account}->transfer
    Transfer 50 from currency to inita
    Hello World: ${account}->transfer
    Transfer 50 from currency to inita
    
    

    使用 C++ API来读取 Messages

    目前我们使用是C API因为这是EOS.IO直接暴露给WASM虚拟机的最底层的API。幸运的是,eoslib提供了一个更高级的API,移除了很多不必要的代码。

    /// eoslib/message.hpp
    namespace eosio {
         template<typename T>
         T current_message();
    }
    
    

    我们可以向下面一样更新 hello.cpp 把它变得更简洁:

    #include <hello.hpp>
    
    /**
     *  The init() and apply() methods must have C calling convention so that the blockchain can lookup and
     *  call these methods.
     */
    extern "C" {
    
        /**
         *  此方法仅在合约发布或升级时调用一次
         */
        void init()  {
           eosio::print( "Init World!\n" );
        }
    
        struct transfer {
           eosio::name from;
           eosio::name to;
           uint64_t quantity;
        };
    
        /// apply 方法实现了合约事件的分发
        void apply( uint64_t code, uint64_t action ) {
           eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
           if( action == N(transfer) ) {
              auto message = eosio::current_message<transfer>();
              eosio::print( "Transfer ", message.quantity, " from ", message.from, " to ", message.to, "\n" );
           }
        }
    
    } // extern "C"
    
    

    您可以注意到我们更新了transfer的struct,直接使用eosio::name 类型并将read_message前后的类型检查压缩为一个单个的current-Message调用。

    在编译和上传后,您将看到和C语言版本同样的结果。

    获取发送者的Authority来进行转账

    合约最普遍的需求之一就是定义谁可以进行这样的操作。比如在货币转账的例子里,我们就需要定义为from字段的账户核准此message。

    EOS.IO软件负责加强和验证签名,您需要做的是获取所需的authority。

        ...
        void apply( uint64_t code, uint64_t action ) {
           eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
           if( action == N(transfer) ) {
              auto message = eosio::current_message<transfer>();
              eosio::require_auth( message.from );
              eosio::print( "Transfer ", message.quantity, " from ", message.from, " to ", message.to, "\n" );
           }
        }
        ...
    
    

    建立和部署后,我们可以再试一次转账:

     $ eosc push message ${account} transfer '{"from":"initb","to":"inita","quantity":50}' --scope ${account}
     1881603ms thread-0   main.cpp:797                  operator()           ] Converting argument to binary...
     1881630ms thread-0   main.cpp:851                  main                 ] Failed with error: 10 assert_exception: Assert Exception
     status_code == 200: Error
     : 3030001 tx_missing_auth: missing required authority
     Transaction is missing required authorization from initb
         {"acct":"initb"}
             thread-0  message_handling_contexts.cpp:19 require_authorization
    ...
    
    

    如果您查看eosd ,您将看到:

    Hello World: initc->transfer
    1881629ms thread-0   chain_api_plugin.cpp:60       operator()           ] Exception encountered while processing chain.push_transaction:
    ...
    
    

    这表示此操作尝试请求应用您的transaction,打印出了初始的"Hello World",然后当eosio::require_auth没能成功获取initb账户的authorization后,操作终止了。

    我们可以通过让eosc增加所需的permission来修复这个问题:

    $ eosc push message ${account} transfer '{"from":"initb","to":"inita","quantity":50}' --scope ${account} --permission initb@active
    

    --permission 命令定义了账户和permission等级,此例中我们使用active authority,也就是默认值。

    这次转账应该就成功了,如同我们之前看到的一样。

    发生错误时终止Message

    绝大多数合约开发中有非常多的前置条件,比如转账的金额要大于0。如果用户尝试进行一个非法action,合约必须终止且已做出的任何变动都必须自动回滚。

        ...
        void apply( uint64_t code, uint64_t action ) {
           eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
           if( action == N(transfer) ) {
              auto message = eosio::currentMessage<transfer>();
              assert( message.quantity > 0, "Must transfer a quantity greater than 0" );
              eosio::requireAuth( message.from );
              eosio::print( "Transfer ", message.quantity, " from ", message.from, " to ", message.to, "\n" );
           }
        }
        ...
    
    

    我们编译、部署并尝试进行一次金额为0的转账。

     $ eoscpp -o hello.wast hello.cpp
     $ eosc set contract ${account} hello.wast hello.abi
     $ eosc push message ${account} transfer '{"from":"initb","to":"inita","quantity":0}' --scope initc --permission initb@active
     3071182ms thread-0   main.cpp:851                  main                 ] Failed with error: 10 assert_exception: Assert Exception
     status_code == 200: Error
     : 10 assert_exception: Assert Exception
     test: assertion failed: Must transfer a quantity greater than 0
    
    

    到此为止您已经完成了Hello World教程,您可以自己编写您的第一个智能合约了。

    6. 部署和升级智能合约

    如上面在教程里所提到的,将合约部署到区块链上可以通过set contract命令简单的完成。并且如果您有权限的话,set contract命令还可更新现有合约

    使用下面的命令来:

    • 部署一个新合约
    • 更新现存合约
    $ eosc set contract ${account} ${contract}.wast ${contract}.abi
    

    7. 命令小结

    下载并构建最新的 EOS.IO 软件 $ build.sh ${architecture} ${build_mode}

    开发智能合约

    • 使用 eoscpp工具创建骨架 $ eoscpp -n ${contract}
    • 在.cpp 和 .hpp文件中编辑您的智能合约
    • 生成.abi文件 $ eoscpp -g ${contract}.abi ${contract}.hpp
    • 生成.wast文件 $ eoscpp -o ${contract}.wast ${contract}.cpp

    部署智能合约

    • 连接到一个节点上 $ eosc -H ${node_ip} -p ${port_num}
    • 创建钱包 $ eosc wallet create
    • [创建账户] 如果您没有EOS keys的话
    • 导入账户的key $ eosc wallet import ${private_key}
    • 解锁钱包 $ eosc wallet unlock ${wallet}
    • 部署合约 $ eosc set contract ${account} ${contract}.wast ${contract}.abi

    8. 调试智能合约

    为调试智能合约,您需要安装本地的eosd节点。本地的eosd节点可以以单独的调试私网运行也可以作为调试公网(或官方的调试网络)的延伸来运行。当您在第一次创建智能合约的时候,最好先在测试私网中测试调试完毕您的智能合约,因为您可以完全掌握整个区块链。这使得您有无限的eos而且可以随时重置区块链的状态。当合约可以上生产环境时,可以通过将您的本地eosd和测试公网(或官方的调试网络)连接起来以完成公网的调试,这样您就可以在本地的eosd上看到测试网络的数据了。

    因为概念是一致的,所以接下来的指南中将会介绍在测试私网中的调试。

    如果您还没有安装您的本地eosd请根据安装指南安装。默认情况下,您的本地eosd将只在测试私网中运行,除非您修改config.ini 文件来将其与测试公网(或官方的调试网络)节点连接,就像该指南中提到的一样。

    8.1. 方法

    用于调试智能合约的主要方法是 Caveman调试法,我们使用打印的方法来监控一个变量并检查合约的流程。在智能合约中打印信息可以通过打印API (CC++)来完成。 C++ API 是 C API的封装,因此大多数情况下我们用的是C++ API。

    8.2. 打印

    C API 支持打印如下的数据类型:

    • prints - a null terminated char array (string)
    • prints_l - any char array (string) with given size
    • printi - 64-bit unsigned integer
    • printi128 - 128-bit unsigned integer
    • printd - double encoded as 64-bit unsigned integer
    • printn - base32 string encoded as 64-bit unsigned integer
    • printhex - hex given binary of data and its size

    打印时,C++ API 通过重写print()方法封装了一些上面的C API使得用户不需要关心需要调用那个打印函数。C++ 打印 API支持

    • a null terminated char array (string)
    • integer (128-bit unsigned, 64-bit unsigned, 32-bit unsigned, signed, unsigned)
    • base32 string encoded as 64-bit unsigned integer
    • struct that has print() method

    8.3. 例子

    让我们写一个新的合约作为调试的例子

    • debug.hpp
    #include <eoslib/eos.hpp>
    #include <eoslib/db.hpp>
    
    namespace debug {
        struct foo {
            account_name from;
            account_name to;
            uint64_t amount;
            void print() const {
                eosio::print("Foo from ", eosio::name(from), " to ",eosio::name(to), " with amount ", amount, "\n");
            }
        };
    }
    
    • debug.cpp
    #include <debug.hpp>
    
    extern "C" {
    
        void init()  {
        }
    
        void apply( uint64_t code, uint64_t action ) {
            if (code == N(debug)) {
                eosio::print("Code is debug\n");
                if (action == N(foo)) {
                     eosio::print("Action is foo\n");
                    debug::foo f = eosio::current_message<debug::foo>();
                    if (f.amount >= 100) {
                        eosio::print("Amount is larger or equal than 100\n");
                    } else {
                        eosio::print("Amount is smaller than 100\n");
                        eosio::print("Increase amount by 10\n");
                        f.amount += 10;
                        eosio::print(f);
                    }
                }
            }
        }
    } // extern "C"
    
    • debug.hpp
    {
      "structs": [{
          "name": "foo",
          "base": "",
          "fields": {
            "from": "account_name",
            "to": "account_name",
            "amount": "uint64"
          }
        }
      ],
      "actions": [{
          "action_name": "foo",
          "type": "foo"
        }
      ]
    }
    
    

    让我们部署并发个message给它。假设您已经创建了debug账户,钱包中也有对应的key。

    $ eoscpp -o debug.wast debug.cpp
    $ eosc set contract debug debug.wast debug.abi
    $ eosc push message debug foo '{"from":"inita", "to":"initb", "amount":10}' --scope debug
    

    当您查看本地eosd节点日志时,您将看到前一条message发送后的如下内容

    Code is debug
    Action is foo
    Amount is smaller than 100
    Increase amount by 10
    Foo from inita to initb with amount 20
    
    

    这样您就可以确认您的message经过了正确的控制流且数据被正确地更新了。您可能会看到上面的至少两次,这很正常因为每个transaction在验证、生产区块及区块应用的阶段都会被应用一次。

    相关文章

      网友评论

      • 8337ea5e8883:您好,看到您的文章质量非常高,想邀请您成为虫洞社区的首批优质内容签约作者。虫洞社区是专业的区块链技术学习社区。虫洞社区鼓励内容生产者产生高质量内容,并给予合理的回报,也希望能帮助内容消费者获得高质量的区块链内容,并让数字货币投资者获得有价值的投资洞见。同时,虫洞社区已经积累了大量的区块链深度从业者,便于作者建立个人品牌。不知道是否方便加您微信细聊?
      • 编程狂魔:谢谢分享,安利个EOS智能合约与DApp开发入门:

        http://xc.hubwiz.com/course/5b52c0a2c02e6b6a59171ded?affid=823jianshu
      • songguo123:分享个适合新手的EOS开发视频,https://www.lanzous.com/b325759/ 密码:d55l
      • 486881ee7a2e:code == N(debug) 这而的N(debug)可以解释下吗?
      • NSGoGo:eosiocpp
      • tclabs:你好, 欢迎加入EOS中文社区讨论智能合约开发: eosfans.io
      • b39a109db2df:直接执行 $ eoscpp -n hello 可能会报错:

        Error message after eoscpp -n hello
        cp: /usr/local/share/skeleton/.: No such file or directory

        下面这样可能有用:
        cd ~/eos/build/
        sudo make install
        cenkai88:这里是默认前面已经build过了的,就是前面一句讲的清单上的步骤。

      本文标题:EOS 智能合约

      本文链接:https://www.haomeiwen.com/subject/xacgnxtx.html