ITPub博客

首页 > 区块链 > 区块链 > 【刘文彬】【源码解读】EOS测试插件:txntestgen_plugin.cpp

【刘文彬】【源码解读】EOS测试插件:txntestgen_plugin.cpp

区块链 作者:圆方圆区块链 时间:2018-12-13 17:33:19 0 删除 编辑


原文链接:醒者呆的博客园,https://www.cnblogs.com/Evsward/p/txn test gen_plugin.html

本文内容本属于《【精解】EOS TPS 多维实测》的内容,但由于在编写时篇幅过长,所以我决定将这一部分单独成文撰写,以便于理解。

关键字:eos, txn test gen plugin, signed transaction, ordered action result, C++, EOS插件

txn test gen_plugin 插件

这个插件是官方开发用来测试块打包交易量的,这种方式由于是直接系统内部调用来模拟transaction,没有中间通讯的损耗,因此效率是非常高的,官方称通过这个插件测试到了8000的tps结果,而就我的测试结果来讲,没有这么恐怖,但也能到2000了,熟不知,其他的测试手段,例如cleos,eosjs可能只有百级的量。下面,我们一同来研究一下这个插件是如何实现以上功能的,过程中,我们也会思考EOS插件的架构体系,以及实现方法。通过本文的学习,如果有好的想法,我们也可以自己开发一个功能强大的插件pr给eos,为EOS社区做出我们自己的贡献。

关于txn_test_gen_plugin插件的使用,非常易于上手,本文不做分析,这方面可以直接参考官方文档。

插件的整体架构

插件代码整体结构中,我们上面介绍的核心功能的实现函数都是包含在一个结构体struct txn test gen plugin impl中。剩余的其他代码都是对插件本身的通讯进行描述,包括如何调用,如何响应等,以及整个插件的生命周期的控制:

  • set program options,设置参数的阶段,是最开始的阶段,内容只设置了txn-reference-block-lag的值,默认是0,-1代表最新头区块。

  • plugin initialize,这一时期就把包含核心功能的结构体txn test gen plugin impl加载到程序运行时内存中了,同时初始化标志位txn reference block lag为txn-reference-block-lag的值。

  • plugin startup,我们通过基础插件http plugin的支持获得了http接口的能力,这一时期,就暴露出来本插件的对外接口。

  • plugin shutdown,调用stop generation函数,重置标志位running为false,计时器关闭,打印关闭提示日志。

下面是对外暴露的三个接口之一的stop generation函数的源码:

void stop_generation() {
    if(!running)
        throw fc::exception(fc::invalid_operation_exception_code);
    timer.cancel();
    running = false;
    ilog("Stopping transaction generation test");
}

接下来,我们主要集中精力在结构体txn test gen plugin_impl上,研究路线是以剩余两个接口分别为入口进行逐一分析。

create test accounts 接口

关于这个接口,调用方法是

curl --data-binary '["eosio", "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"]' http://localhost:8888/v1/txn_test_gen/create_test_accounts

传入的参数是eosio以及其私钥。我们进入到函数create test accounts中去分析源码。

准备知识

首先,整个函数涉及到的所有transaction都是打包存入到一个vector集合std::vector中去。

trxs是一个事务集,它包含很多的trx,而其中每一个trx包含一个actions集合vector

一、准备账户

trxs的第一个trx,内容为账户创建:

  • 定义3个账户:txn.test.a,txn.test.b, txn.test.t

  • 辅助功能:controller& cc = app().get plugin<chain plugin>().chain();,通过cc可以随时调用本地区块链上的任意信息。

  • 通过fc::crypto::private_key::regenerate函数分别生成他们的私钥,要传入生成秘钥的seed。

  • 通过私钥直接调用get public key()即可获得公钥

  • 设置每个账户的owner和active权限对应的公钥,一般来讲他们是相同的

  • 账户的创建者均为我们外部调用create test accounts接口时传入的账户eosio,注意:eosio的私钥是通过字符串传入的,要通过fc::crypto::private_key转换成私钥对象

  • 将每一个账户的创建组装好成为一个action,存入trx的actions集合中去。

  • trx的actions成员已经设置完毕,完成剩余trx的组装工作,包括

    • 当前trx的actions中的元素的data并不是如文首的transaction中的data的加密串的结构,而是明文的,这里的加密是数字摘要技术,感兴趣的朋友可以去《应用密码学初探》进行了解。

    • 摘要的源码函数是:sig digest(chain id, context free data),其中参数使用到了chain id,而context free_data就是上面提到的明文data内容,所以它是要与链id一起做数字摘要的(这一点我在使用eosjs尝试自己做摘要的时候并未想到)

    • expiration,通过cc获得当前头区块的时间,加上延迟时间,这里是30s,fc::seconds(30)

    • reference_block,值为通过cc获取当前的头区块,意思为本transaction的引用区块,所有的信息是引用的这个区块为头区块的环境

    • sign,签名,使用的是创建者eosio的私钥对象,上面我们已经准备好了,签名的数据是data的摘要

这一部分的源码展示如下:

`
name newaccountA("txn.test.a");
name newaccountB("txn.test.b");
name newaccountC("txn.test.t");
name creator(init_name);
abidef currencyabidef = fc::json::fromstring(eosiotokenabi).as<abi_def>();
controller& cc = app().getplugin<chainplugin>().chain();
auto chainid = app().getplugin<chainplugin>().getchainid();
fc::crypto::privatekey txntestreceiverAprivkey = fc::crypto::privatekey::regenerate(fc::sha256(std::string(64, 'a')));
fc::crypto::privatekey txntestreceiverBprivkey = fc::crypto::privatekey::regenerate(fc::sha256(std::string(64, 'b')));
fc::crypto::privatekey txntestreceiverCprivkey = fc::crypto::privatekey::regenerate(fc::sha256(std::string(64, 'c')));
fc::crypto::publickey  txntextreceiverApubkey = txntestreceiverAprivkey.getpublickey();
fc::crypto::publickey  txntextreceiverBpubkey = txntestreceiverBprivkey.getpublickey();
fc::crypto::publickey  txntextreceiverCpubkey = txntestreceiverCprivkey.getpublickey();
fc::crypto::privatekey creatorprivkey = fc::crypto::privatekey(initprivkey);
//create some test accounts
{
    signed_transaction trx;
//create "A" account
{
    auto owner_auth   = eosio::chain::authority{1, {{txn_text_receiver_A_pub_key, 1}}, {}};
    auto active_auth  = eosio::chain::authority{1, {{txn_text_receiver_A_pub_key, 1}}, {}};
    trx.actions.emplace_back(vector<chain::permission_level>{{creator,"active"}}, newaccount{creator, newaccountA, owner_auth, active_auth});
}
//create "B" account
{
    auto owner_auth   = eosio::chain::authority{1, {{txn_text_receiver_B_pub_key, 1}}, {}};
    auto active_auth  = eosio::chain::authority{1, {{txn_text_receiver_B_pub_key, 1}}, {}};
    trx.actions.emplace_back(vector<chain::permission_level>{{creator,"active"}}, newaccount{creator, newaccountB, owner_auth, active_auth});
}
//create "txn.test.t" account
{
    auto owner_auth   = eosio::chain::authority{1, {{txn_text_receiver_C_pub_key, 1}}, {}};
    auto active_auth  = eosio::chain::authority{1, {{txn_text_receiver_C_pub_key, 1}}, {}};
    trx.actions.emplace_back(vector<chain::permission_level>{{creator,"active"}}, newaccount{creator, newaccountC, owner_auth, active_auth});
}
trx.expiration = cc.head_block_time() + fc::seconds(30);
trx.set_reference_block(cc.head_block_id());
trx.sign(creator_priv_key, chainid);
trxs.emplace_back(std::move(trx));
}`

二、token相关

trxs的第二个trx,内容为token创建和issue,为账户转账为之后的测试做准备

  • 为账户txn.test.t设置eosio.token合约,之前在操作cleos set contract的时候可以通过打印结果发现,是有setcode和setabi两个步骤的。

    • 设置handler的账户为txn.test.t

    • 设置handler的abi,将文件eosio token abi(json格式的)转成json转储为abi_def结构,然后通过fc::raw::pack操作将结果赋值给abi

    • 将handler加上相关权限组装成action装入trx的actions集合中。

    • 设置handler的账户为txn.test.t

    • 将wasm设置为handler的code,wasm是通过eosio.token合约的eosio token wast文件获取的,vector<uint8 t> wasm = wast to wasm(std::string(eosio token_wast))

    • 将handler加上相关权限组装成action装入trx的actions集合中。

    • setcode handler:

    • setabi handler:

  • 使用账户txn.test.t创建token,标志位CUR,总发行量十亿,装成action装入trx的actions集合中。

  • issue CUR 给txn.test.t 600枚CUR,装成action装入trx的actions集合中。

  • 从txn.test.t转账给txn.test.a 200枚CUR,装成action装入trx的actions集合中。

  • 从txn.test.t转账给txn.test.b 200枚CUR,装成action装入trx的actions集合中。

  • trx的actions成员已经设置完毕,完成剩余trx的组装工作(同上),这里只介绍不同的部分

    • max net usage_words,指定了网络资源的最大使用限制为5000个词。

这一部分的源码展示如下:

`
//set txn.test.t contract to eosio.token & initialize it
{
    signedtransaction trx;
    vector<uint8t> wasm = wasttowasm(std::string(eosiotokenwast));
    setcode handler;
    handler.account = newaccountC;
    handler.code.assign(wasm.begin(), wasm.end());
    trx.actions.emplaceback( vector<chain::permissionlevel>{{newaccountC,"active"}}, handler);
{
    setabi handler;
    handler.account = newaccountC;
    handler.abi = fc::raw::pack(json::from_string(eosio_token_abi).as<abi_def>());
    trx.actions.emplace_back( vector<chain::permission_level>{{newaccountC,"active"}}, handler);
}
{
    action act;
    act.account = N(txn.test.t);
    act.name = N(create);
    act.authorization = vector<permission_level>{{newaccountC,config::active_name}};
    act.data = eosio_token_serializer.variant_to_binary("create", fc::json::from_string("{\"issuer\":\"txn.test.t\",\"maximum_supply\":\"1000000000.0000 CUR\"}}"));
    trx.actions.push_back(act);
}
{
    action act;
    act.account = N(txn.test.t);
    act.name = N(issue);
    act.authorization = vector<permission_level>{{newaccountC,config::active_name}};
    act.data = eosio_token_serializer.variant_to_binary("issue", fc::json::from_string("{\"to\":\"txn.test.t\",\"quantity\":\"600.0000 CUR\",\"memo\":\"\"}"));
    trx.actions.push_back(act);
}
{
    action act;
    act.account = N(txn.test.t);
    act.name = N(transfer);
    act.authorization = vector<permission_level>{{newaccountC,config::active_name}};
    act.data = eosio_token_serializer.variant_to_binary("transfer", fc::json::from_string("{\"from\":\"txn.test.t\",\"to\":\"txn.test.a\",\"quantity\":\"200.0000 CUR\",\"memo\":\"\"}"));
    trx.actions.push_back(act);
}
{
    action act;
    act.account = N(txn.test.t);
    act.name = N(transfer);
    act.authorization = vector<permission_level>{{newaccountC,config::active_name}};
    act.data = eosio_token_serializer.variant_to_binary("transfer", fc::json::from_string("{\"from\":\"txn.test.t\",\"to\":\"txn.test.b\",\"quantity\":\"200.0000 CUR\",\"memo\":\"\"}"));
    trx.actions.push_back(act);
}
trx.expiration = cc.head_block_time() + fc::seconds(30);
trx.set_reference_block(cc.head_block_id());
trx.max_net_usage_words = 5000;
trx.sign(txn_test_receiver_C_priv_key, chainid);
trxs.emplace_back(std::move(trx));
}`

发起请求

目前trxs集合已经包含了两个trx元素,其中每个trx包含了多个action。下面要将trxs推送到链上执行

  • push transactions函数,遍历trxs元素,每个trx单独发送push next_transaction

  • push next transaction函数,首先将trx取出通过packed_transaction函数进行组装成post的结构

  • packed transaction函数,通过set transaction函数对trx进行摘捡,使用pack_transaction函数进行组装

  • pack transaction函数,就是调用了一下上面提过的fc::raw::pack操作,然后通过accept transaction函数向链发起请求

  • accept transaction函数,是chain plugin的一个函数,它内部调用了incoming transaction async_method异步发起交易请求。

这部分代码比较杂,分为几个部分:

push transactions函数:

void push_transactions( std::vector<signed_transaction>&& trxs, const std::function<void(fc::exception_ptr)>& next ) {
    auto trxs_copy = std::make_shared<std::decay_t<decltype(trxs)>>(std::move(trxs));
    push_next_transaction(trxs_copy, 0, next);
}

push next transaction函数:

static void push_next_transaction(const std::shared_ptr<std::vector<signed_transaction>>& trxs, size_t index, const std::function<void(const fc::exception_ptr&)>& next ) {
      chain_plugin& cp = app().get_plugin<chain_plugin>();
      cp.accept_transaction( packed_transaction(trxs->at(index)), [=](const fc::static_variant<fc::exception_ptr, transaction_trace_ptr>& result){
         if (result.contains<fc::exception_ptr>()) {
            next(result.get<fc::exception_ptr>());
         } else {
            if (index + 1 < trxs->size()) {
               push_next_transaction(trxs, index + 1, next);
            } else {
               next(nullptr);
            }
         }
      });
   }

packed transaction函数,set transaction函数以及pack transaction函数的代码都属于本插件源码之外的EOS库源码,由于本身代码量也较少,含义在上面已经完全解释过了,这里不再粘贴源码。

accept transaction函数也是EOS的库源码

`
void chainplugin::accepttransaction(const chain::packedtransaction& trx, nextfunction<chain::transactiontraceptr> next) {
    my->incomingtransactionasyncmethod(std::makeshared<packedtransaction>(trx), false, std::forward<decltype(next)>(next));
}
incomingtransactionasyncmethod(app().getmethod<incoming::methods::transaction_async>())`

start_generation 接口

该接口的调用方法是:

curl --data-binary '["", 20, 20]' http://localhost:8888/v1/txn test gen/start_generation

参数列表为:

  • 第一个参数为 salt,一般用于“加盐”加密算法的值,这里我们可以留空。

  • 第二个参数为 period,发送交易的间隔时间,单位为ms,这里是20。

  • 第三个参数为 batch_size,每个发送间隔周期内打包交易的数量,这里也是20。

    翻译过来就是:每20ms提交20笔交易。

接下来,以start_generation 函数为入口进行源码分析。

start_generation 函数

  • 校验:

    • period的取值范围为(1, 2500)

    • batch_size的取值范围为(1, 250)

    • batch size必须是2的倍数,batch size & 1结果为假0才可以,这是一个位运算,与&,所以batch_size的值转为二进制时末位不能为1,所以就是2的倍数即可。

    • 对标志位running的控制。

这部分代码展示如下:

`
if(running)
    throw fc::exception(fc::invalidoperationexceptioncode);
if(period < 1 || period > 2500)
    throw fc::exception(fc::invalidoperationexceptioncode);
if(batchsize < 1 || batchsize > 250)
    throw fc::exception(fc::invalidoperationexceptioncode);
if(batchsize & 1)
    throw fc::exception(fc::invalidoperationexception_code);
running = true;`

- 定义两个action,分别是:   

        - 账户txn.test.a给txn.test.b转账1000枚CUR   

        - txn.test.b转给txn.test.a同样1000枚CUR

这部分代码展示如下:

`
//create the actions here
actatob.account = N(txn.test.t);
actatob.name = N(transfer);
actatob.authorization = vector<permissionlevel>{{name("txn.test.a"),config::activename}};
actatob.data = eosiotokenserializer.varianttobinary("transfer", fc::json::fromstring(fc::formatstring("{\"from\":\"txn.test.a\",\"to\":\"txn.test.b\",\"quantity\":\"1.0000 CUR\",\"memo\":\"${l}\"}", fc::mutablevariantobject()("l", salt))));
actbtoa.account = N(txn.test.t);
actbtoa.name = N(transfer);
actbtoa.authorization = vector<permissionlevel>{{name("txn.test.b"),config::activename}};
actbtoa.data = eosiotokenserializer.varianttobinary("transfer", fc::json::fromstring(fc::formatstring("{\"from\":\"txn.test.b\",\"to\":\"txn.test.a\",\"quantity\":\"1.0000 CUR\",\"memo\":\"${l}\"}", fc::mutablevariantobject()("l", salt))));接下来,是对参数period和batch_size的储存为结构体作用域的变量以供结构体内其他函数调用,然后打印日志,最后调用arm_timer函数。timertimeout = period; // timertimeout是结构体的成员变量
batch = batchsize/2; // batch是结构体的成员变量
ilog("Started transaction test plugin; performing ${p} transactions every ${m}ms", ("p", batchsize)("m", period));
armtimer(boost::asio::highresolutiontimer::clocktype::now());`

arm_timer 函数

从start generation 函数过来,传入的参数是当前时间now,该函数主要功能是对计时器的初始化操作(计时器与文首的stop generation函数中的关闭计时器呼应)。具体内容可分为两部分:

  • 设定计时器的过期时间,值为start_generation 接口的参数period与now相加的值,即从现在开始,过period这么久,当前计时器对象timer就过期。

  • 设定计时器的异步定时任务,任务体直接调用send transaction函数,对函数的返回值进行处理,如果有报错信息(一般是服务中止)则调用stop generation函数关闭插件。

    注意stop_generation函数关闭的是定时任务的无限递归,中止定时任务,停止发送测试交易。但它并没有停止插件服务,我们仍旧可以通过再次请求插件接口启动无限测试交易。
    

这部分代码如下:

`
void armtimer(boost::asio::highresolutiontimer::timepoint s) {
    timer.expiresat(s + std::chrono::milliseconds(timertimeout));
    timer.async_wait(this {
        if(!running || ec)
            return;
    send_transaction([this](const fc::exception_ptr& e){
        if (e) {
            elog("pushing transaction failed: ${e}", ("e", e->to_detail_string()));
            stop_generation();
        } else { // 如果没有终止报错,则无限递归调用arm_timer函数,递归时传入的参数代替上面的now是当前timer对象的过期时间,这样在新的递归调用中,timer的创建会以这个时间再加上period,无间隔继续执行。
            arm_timer(timer.expires_at());
        }
    });
});
}`

send_transaction 函数

这个函数是本插件的核心功能部分,主要是发送测试交易,对transaction的处理,将我们上面start_generation 函数中设置的两个action打包到transaction中去,以及对transaction各项属性的设置。具体步骤为:

  • 声明trxs,并为其设置大小为start generation 接口中batch size的值。

    std::vector<signed_transaction> trxs; trxs.reserve(2*batch);

接下来,与上面介绍的create test accounts 接口的账户准备过程相同,准备私钥公钥,不多介绍。继续准备trx的参数:

  • nonce,是用来赋值context free actions的

  • context free actions:官方介绍一大堆,总之就是正常action是需要代价的,要确权,要占用主网资源什么的,所以搞了一个context free actions,字面意思就是上下文免费的action,这里权当测试用,填入的数据也是随机nonce组装的。

  • abi serializer,用来序列化abi的,传入的system account_name的abi值,它是在这里被赋值,然而是在结构体的作用域中被调用的。

  • reference block num的处理,引用区块,上面我们也提到过,而这里面增加了一层判断,是根据标志位txn reference block lag的值来比较,也就是说reference block num最后的值是最新区块号减去txn reference block lag的值,但是最小值为0,不可为负数。

  • 通过reference block num获得reference block id

这部分代码如下:

`
controller& cc = app().getplugin<chainplugin>().chain();
auto chainid = app().getplugin<chainplugin>().getchainid();
fc::crypto::privatekey aprivkey = fc::crypto::privatekey::regenerate(fc::sha256(std::string(64, 'a')));
fc::crypto::privatekey bprivkey = fc::crypto::privatekey::regenerate(fc::sha256(std::string(64, 'b')));
static uint64t nonce = staticcast<uint64t>(fc::timepoint::now().secsinceepoch()) << 32;
abiserializer eosioserializer(cc.db().find<accountobject, byname>(config::systemaccountname)->get_abi());
uint32t referenceblocknum = cc.lastirreversibleblocknum();
if (txnreferenceblocklag >= 0) {
    referenceblocknum = cc.headblocknum();
    if (referenceblocknum <= (uint32t)txnreferenceblocklag) {
        referenceblocknum = 0;
    } else {
        referenceblocknum -= (uint32t)txnreferenceblock_lag;
    }
}
blockidtype referenceblockid = cc.getblockidfornum(referenceblocknum);`

接下来,就是循环打包trx,我们设置的batch_size好比是20,现在我们已有两个action,每个action对应一个trx,则循环只需要执行10次,每次执行两个trx即可实现,每个trx相关的属性在上一阶段都已准备好。直接看代码吧。

for(unsigned int i = 0; i < batch; ++i) {
    {
        signed_transaction trx;
        trx.actions.push_back(act_a_to_b);
        trx.context_free_actions.emplace_back(action({}, config::null_account_name, "nonce", fc::raw::pack(nonce++)));
        trx.set_reference_block(reference_block_id);
        trx.expiration = cc.head_block_time() + fc::seconds(30);
        trx.max_net_usage_words = 100;
        trx.sign(a_priv_key, chainid);
        trxs.emplace_back(std::move(trx));
    }
    {
        signed_transaction trx;
        trx.actions.push_back(act_b_to_a);
        trx.context_free_actions.emplace_back(action({}, config::null_account_name, "nonce", fc::raw::pack(nonce++)));
        trx.set_reference_block(reference_block_id);
        trx.expiration = cc.head_block_time() + fc::seconds(30);
        trx.max_net_usage_words = 100;
        trx.sign(b_priv_key, chainid);
        trxs.emplace_back(std::move(trx));
    }
}

最后,执行

push_transactions(std::move(trxs), next);


这个部分与create test accounts 接口发起请求的部分一致,这里不再重复展示。

总结

到这里为止,我们已经完全分析透了txn test gen plugin 插件的内容。本文首先从大体上介绍了插件的架构,生命周期,通讯请求与返回。接着介绍了核心结构体的内容,然后以对外接口为入口,沿着一条线将每个功能的实现完整地研究清楚。通过本文的学习,我们对于EOS插件的体系有了初步深刻的理解,同时我们也完全搞清楚了txn test gen plugin 插件的功能,以及它为什么会达到一个比较高的tps的表现。

参考资料

  • EOSIO/eos

  • eos官方文档


相关文章和视频推荐

圆方圆学院汇集大批区块链名师,打造精品的区块链技术课程。 在各大平台都长期有优质免费公开课,欢迎报名收看。

公开课地址: https://ke.qq.com/course/345101

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/31522172/viewspace-2285182/,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2018-11-09

  • 博文量
    61
  • 访问量
    25703