原文链接:醒者呆的博客园,https://www.cnblogs.com/Evsward/p/eostps.html
本文主要研究EOS的tps表现,会从插件、cleos、EOSBenchTool以及eosjs四种方式进行分析研究。
关键字:eos, tps, cleos, txn_test_gen_plugin, EOSBenchTool, qt, eosjs, C++源码分析
{
"transaction_id": "7943f613f8cde71bc37d76daf3581ceb62ae6d481fa9b3a11ba73d19d909c666",
"broadcast": false,
"transaction": {
"compression": "none",
"transaction": {
"expiration": "2018-07-12T09:51:14",
"ref_block_num": 526,
"ref_block_prefix": 52869816,
"net_usage_words": 0,
"max_cpu_usage_ms": 0,
"delay_sec": 0,
"context_free_actions": [],
"actions": [
{
"account": "eosio.token",
"name": "transfer",
"authorization": [
{
"actor": "eosiotestay",
"permission": "active"
}
],
"data": "00bcc95865ea305500fcc95865ea3055010000000000000004535953000000000c7061636b696e672074657374"
}
],
"transaction_extensions": []
},
"signatures": [
"SIG_K1_KB6ENT2Ns3QmaPSfvxqCkgZTjK5RUDRFwkZ7p9Jv6p1GpnD67jhMUsw1Spfp7yw4hChsubPeiTc2HSt5hc6YdMH5rk5Kfz"
]
}
}
由于我们在研究eos阶段,大量使用到cleos,因此使用cleos来测试tps是我们第一个能想到的手段。这一节我们将加深理解tps的意义,tps的计算方法,讨论单节点与多节点环境对tps的影响。
单节点的搭建这里不再赘述,直接使用脚本执行,
./bios-boot-tutorial.py -k -w -b -s -c -t -S -T --user-limit 1000 -X
注意参数的顺序不能变。
执行成功以后,我们将得到一个拥有1000个stake账户(简单理解为已抵押完可直接投票的账户)的单节点eos环境,最后一个参数-X会让当前环境不断执行随机转账操作(注意:每一笔转账都是一个action,一个action对应一个transaction)
查看日志
修改脚本的stepLog函数,改为:
def stepLog():
run('tail -f ' + args.nodes_dir + '00-eosio/stderr')
然后在终端执行:
./bios-boot-tutorial.py -l
即可进入同步日志输出的界面。
环境准备完毕,我们来测试一下当前正在不断进行转账的eos链上的tps表现。这里采用的tps计算方式为:
tps = BlockTxs*2
因为eos是半秒出块,所以两个块的打包交易量之和就是tps,为确保数值可靠性,每个块的打包交易量我们要通过大量区块取平均值的方式。
基于以上思想,可以总结出一个shell命令直接在终端执行即可:
for (( i = 12638; i <= 13638; i++ )); do cleos --wallet-url http://localhost:6666 --url http://localhost:8000 get block $i | grep "executed" | wc -l; done | awk '{sum+=$1} END {print NR,"blocks average tps =", sum/NR*2}'
取出区块号从200到1200的区块,分别计算每个区块的打包交易量(通过统计其包含的“executed”即可,因为每个交易对应一个“executed”),然后将这些区块交易量进行累加除以数量得到平均值,再乘以2,辅以可视化备注输出即可。
最终结果不是很理想,至少距离官方声称的几千tps有很大差距。
1001 blocks average tps = 39.2727
所以1000个块统计tps为 39.2727。
由于tps的结果不理想,我也有过很多思考,下面我们换一种计算方式来看:
tps = trxs/time
这里通过一种简单的方式来计算tps:即统计共发出了trxs笔交易所耗费的时间,以秒为单位,然后相除即可得到tps。
基于以上思想,由于这部分代码是无法通过一行shell解决的,所以我通过修改bios脚本来解决,
def stepTPS():
start = time.time()
numtps = args.num_tps
i = 0
while i < numtps :
print ("on: ",i)
randomTransfer(0, args.num_senders,1)
i=i+1
elapsed = (time.time() - start)
print ("Time used:",elapsed,"s tps=",numtps/elapsed)
def randomTransfer(b, e, t):
for j in range(t):
src = accounts[random.randint(b, e - 1)]['name']
dest = src
while dest == src:
dest = accounts[random.randint(b, e - 1)]['name']
run(args.cleos + 'transfer -f ' + src + ' ' + dest + ' "0.0001 ' + args.symbol + '"' + ' || true')
('A', 'tps', stepTPS, False, "calculate the tps"),
parser.add_argument('--num-tps', metavar='', help="Number of tps test trx", type=int, default=1000)
执行A:
注意,在执行前,我们要先停掉单节点环境,将-X去掉,而采用我们的-A来执行随机转账。
./bios-boot-tutorial.py -k -w -b -s -c -t -S -T --user-limit 1000 -X
./bios-boot-tutorial.py -A --num-tps 2000
发起2000笔交易,然后使用脚本函数stepTPS进行测试。
结果:
Time used: 26.172592401504517 s tps= 38.20790790072884
结果与shell方式差不多,都是不到40的tps表现。
tps的结果不尽人意,我又转念想到了是否因为单节点出块的原因。因此我搭建了多节点出块加全节点的环境,搭建环境的方法可以参考《【精解】EOS多节点组网:商业场景分析以及节点启动时序》
我仍旧通过以上两种方式,分别是shell方式和Python脚本的方式去测试,最后结果是并无改变,这也证实了eos不具备多线程处理事务的能力。
插曲:我将python脚本的修改提交了EOSIO/eos的官方pr,结果被拒绝合并,原因是“unrelated change”,转念一想,如果合并至源码,用户可以通过这种方式直白地得到eos的tps就是几十个的结论,那绝对是很不好的。
我对eos的高tps有了深深地怀疑,于是找来了官方的tps测试插件,要亲自感受一下tps的“洗礼”。插件的使用方式很简单,按照官方文档的步骤执行即可,最后我调整参数:
curl --data-binary '[""30, 50]' http:/ /localhost:8888/v1/txn_test_gen/start_generationn
链上日志结果:
通过trxs一列可以看出,每个区块打包的交易量大大提升了,平均tps在2000左右。
插件的测试方法也是bm所推崇的,他说通过cleos无法发挥出真正的eos的性能。那么具体是为什么,我们通过插件的源码txn_test_gen_plugin.cpp进行分析,我将这一部分内容单独成文,请阅读《【源码解读】EOS测试插件:txn_test_gen_plugin.cpp》
EOSBenchTool来自于OracleChain的贡献,虽然他们的节点oraclegogogo没竞选上bp,但我认为bp的竞选更多是市场行为,不是技术实力的“成绩单”,在所有bp中,目前我也仅看到了OracleChain做出的技术方面的贡献,包括对EOSIO/eos的pr,都是OracleChain自身技术气质的体现。多余夸奖的话不多讲了,下面来研究这套工具内容。
EOSBenchTool的思想与以上的cleos有很大不同,与插件的方式(打包交易)比较相似,但它的实现方式却是独具一格的,他并不是像插件那样直接在“服务器端”自我模拟交易来测试tps。他们敢于直接使用C++ 来编写客户端请求主网来打包、发起请求,最终测试得到一个非常不错的结果,大约可以到200到300,这个结果也是我在众多压测手段中得到的比较理想的结果,包括下面要介绍到的eosjs的方式,都不及EOSBenchTool的测试结果。
EOSBenchTool既能不牺牲在真实场景中的模拟,又能通过技术手段优化交易通讯,可以说他的tps结果是比较具备真实性、业务可行性,以及他的技术实现手段也是非常值得业务方来学习并尝试使用的。
官方文档的介绍比较技术范儿,就是不太亲民。这里我给他填点肉,希望层级尝试使用EOSBenchTool却失败的朋友能够在这里找到答案。
一、EOS主网环境
首先,要准备EOS主网环境,可以通过脚本快速获得:python3 ./bios-boot-tutorial.py -k -w -b -s -c -t (不部署system合约,因为部署后无法使用create account创建账户。)
二、获取代码,QT工具,编译代码
源码位置:EOSBenchTool
QT去官网下载community版本即可,注意:QT在安装时要同时勾选安装 QCreator 和 QT source 以及 QT prebuild tool(这里我选择的是mingw)
打开QCreator,一般情况下,上面的步骤准备妥当以后,QCreator会自动检测一套构建套件(Kit),构建套件依赖于Qt Version、编译器、Debuggers,Cmakes,这些工具也都是可以自动检测到的,如果无法检测到,一定是某个工具未安装,请检查相应的工具,并重新下载安装(一般来讲,所有这些工具在QT安装包都会包含,只需再次打开QT安装包,选择更新,重新勾选缺乏的工具安装即可。)最终我的构建套件(Kit) 截图如下:
以上工作都顺利完成以后,在QCreator中,点击左下角三角按钮运行启动EOSBenchTool工具。建议将UI最大化,可以更方便地查看日志。填写好setting内容,如下:
关于几个参数:
其他参数不多介绍。设置好参数以后,点击OK保存,然后切换到 Benchmark Testing 点击Prepare:创建测试账户、给测试账户转账、每个测试账户发起测试交易并打包。
等待Prepare结束,1万笔测试交易大约两到三分钟,视客户端机器本地性能。然后点击Start,得到tps结果,这里由于界面都是可视化的,我不再赘述。
这部分我们将一起通过源码学习EOSBenchTool打包交易的原理。
Prepare阶段,正如上面在EOSBenchTool使用中介绍到的那样,包括创建账户,转账,打包。
创建账户
下面先来看创建账户的源码:
CreateAccount createAccount;
int count = createAccount.create(thread_num, [=](const QString& name, bool res) { // lambda格式的回调函数:打印日志
commonOutput(QString("Create %1 %2.").arg(name).arg(res ? "succeed" : "failed"));
});
进入createaccount.cpp文件,查看create函数:
int CreateAccount::create(int threadNum, const create_account_callback& func)
{
if (threadNum <= 0) { // 根据threadNum个数创建对应数量的账户。
return 0;
}
// 清空其他账户
AccountManager::instance().removeAll();
for (int i = 0; i < threadNum; ++i) {
eos_key owner, active;
keys.clear(); // 头文件中的 QVector keys;
keys.push_back(owner); // 添加owner和active权限到keys对象
keys.push_back(active);
newAccountName = createNewName();
bool res = false;
QEventLoop loop;
// WINSOCK_API_LINKAGE int PASCAL connect (SOCKET, const struct sockaddr *, int);
// 通过connect开启一个socket通道
connect(this, &CreateAccount::oneRoundFinished, &loop, &QEventLoop::quit);
if (httpc) { // httpc(new HttpClient)
httpc->request(FunctionID::get_info); // 通过http请求get info
// 以上的get_info回调函数,实际功能函数:get_info_returned,由connect开启socket访问进去。
connect(httpc, &HttpClient::responseData, this, &CreateAccount::get_info_returned);
}
loop.exec();
// 返回执行结果res,成功为true,失败为false
res = !(AccountManager::instance().listKeys(newAccountName).first.empty());
// 执行回调函数:打印日志
func(newAccountName, res);
}
return AccountManager::instance().count() - 1; // 除了super account以外的集合中的账户个数
}
查看一下AccountManager的源码:
class AccountManager
{
public:
AccountManager();
static AccountManager& instance();
void addAccounts(const QString& name, const QPair& keypairs);
void removeAll();
QPair listKeys(const QString& account);
QVector listAccounts();
int count() const;
private: // 私有属性,QMap集合对象 accounts
QMap> accounts;
};
removeAll的实现方法:
void AccountManager::removeAll()
{
QPair superKey = accounts[super_account];
accounts.clear();
accounts.insert(super_account, superKey);
}
super_account和superKey是全局变量,在mainwindow.cpp前面标明:
QString super_account = "eosio";
实际上,是对QMap集合对象 accounts的操作。接着,账户名的生成方式:
QString CreateAccount::createNewName()
{
// eos的命名规则
static const char *char_map = "12345abcdefghijklmnopqrstuvwxyz";
int map_size = strlen(char_map);
QString newName;
for (int i = 0; i < 5; ++i) {
int r = rand() % map_size; // 随机选出char_map的下标位置
newName += char_map[r];
} // 返回的是一个五位的名字
return newName;
}
AccountManager的实例也是个static的单例
AccountManager &AccountManager::instance()
{
static AccountManager manager;
return manager;
}
get_info_returned函数,
void CreateAccount::get_info_returned(const QByteArray &data)
{
//先关闭进来的socket通道
disconnect(httpc, &HttpClient::responseData, this, &CreateAccount::get_info_returned);
getInfoData.clear();
getInfoData = data;
QByteArray param = packGetRequiredKeysParam();
if (param.isNull()) {
emit oneRoundFinished();
return;
}
if (httpc) {
// 通过http请求链的get_required_keys接口,传入对应事务的json格式作为入参。
httpc->request(FunctionID::get_required_keys, param);
// get_required_keys的回调函数,通过socket建立通道去访问get_required_keys_returned函数。
connect(httpc, &HttpClient::responseData, this, &CreateAccount::get_required_keys_returned);
}
}
转到函数packGetRequiredKeysParam(),该函数是创建账户的实际生效函数:
QByteArray CreateAccount::packGetRequiredKeysParam()
{
if (getInfoData.isEmpty()) {
return QByteArray();
}
// 组装了newAccount的请求数据
EOSNewAccount newAccount(EOS_SYSTEM_ACCOUNT, newAccountName.toStdString(),
keys.at(0).get_eos_public_key(), keys.at(1).get_eos_public_key(),
EOS_SYSTEM_ACCOUNT);
std::vector hexData = newAccount.dataAsHex(); // 将data对象转为十六进制
// 通过ChainManager创建事务,是创建账户的事务。
signedTxn = ChainManager::createTransaction(EOS_SYSTEM_ACCOUNT, newAccount.getActionName(), std::string(hexData.begin(), hexData.end()),
ChainManager::getActivePermission(EOS_SYSTEM_ACCOUNT), getInfoData);
QJsonObject txnObj = signedTxn.toJson().toObject();
QJsonArray avaibleKeys;
std::string pub = eos_key::get_eos_public_key_by_wif(super_private_key.toStdString());// 通过私钥获得公钥
avaibleKeys.append(QJsonValue(QString::fromStdString(pub)));
QJsonObject obj;
obj.insert("available_keys", avaibleKeys);
obj.insert("transaction", txnObj);
return QJsonDocument(obj).toJson();// 最终获得json格式的创建账户的事务对象
}
进入get_required_keys_returned函数,
void CreateAccount::get_required_keys_returned(const QByteArray &data)
{
disconnect(httpc, &HttpClient::responseData, this, &CreateAccount::get_required_keys_returned);
getRequiredKeysData.clear();
getRequiredKeysData = data;
QByteArray param = packPushTransactionParam();
if (param.isNull()) {
emit oneRoundFinished();
return;
}
if (httpc) {
// 相同的套路,通过packPushTransactionParam()函数组装好的推送交易接口的入参param,然后通过http发起请求。
httpc->request(FunctionID::push_transaction, param);
// 通过connect建立socket连接访问push_transaction的回调函数push_transaction_returned,继续处理。
connect(httpc, &HttpClient::responseData, this, &CreateAccount::push_transaction_returned);
}
}
packPushTransactionParam(),开始组装push transaction的参数,由于代码中对于数据的处理较多,这里只展示结果的部分:
// 给上面由函数packGetRequiredKeysParam()组装的交易signedTxn签名。
signedTxn.sign(pri, TypeChainId::fromHex(info.value("chain_id").toString().toStdString()));
PackedTransaction packedTxn(signedTxn, "none");
QJsonObject obj = packedTxn.toJson().toObject();
return QJsonDocument(obj).toJson(); // 获得签名后的交易数据
push_transaction_returned,我们经过大量的组合校验,与链上的信息进行同步组装获得了合法的签名交易对象,然后通过http接口请求了push_transaction接口将签名交易对象推送到链上执行,执行结果通过回调函数处理,回调函数的主要作用是将处理结果 -> 成功创建了的这个账户,存入集合accounts中,由于accounts是私有属性,所以通过方法AccountManager::instance().addAccounts执行。
客户端本地保存了一个对象accounts用来同步自己创建过的账户。大部分代码是对accounts的处理。
账户转账
在上一个创建账户的部分,我们详细解读了通讯的过程,仍旧是通过http去发起请求,通过每个请求的回调函数进行处理,组装,维护了本地的集合accounts。由于篇幅过大,在之后的介绍中,不会再过多介绍,而专注于实现方式的核心代码。转账的核心代码:
QVector accounts = AccountManager::instance().listAccounts(); // 通过accounts获得测试账户们
int accountSize = accounts.size();
int balance = total_tokens / accountSize; // 平均分配测试用币
for (int i = 0; i < accountSize; ++i) {
PushManager push;
QString quantity = QString("%1.0000 %2").arg(balance).arg(token_name); // 拼串,转账额度
QString to = QString::fromStdString(accounts.at(i)); // 遍历接收转账的账户
commonOutput(QString("Transfering %1 to %2 ...").arg(quantity).arg(to)); // 日志
bool ret = push.transferToken(super_account, to, quantity); // 核心生效代码,是PushManager的transferToken函数。
commonOutput(ret ? "Succeed." : "Failed.");
}
PushManager的transferToken函数是本地组装了标准的转账请求参数,json字符串格式的from, to, quality以及memo信息。然后跳转到make_push函数。make_push函数需要通过http请求接口abi_json_to_bin,而针对该接口的入参,都需要在这个函数处理获取到,入参包括action,code以及args。code就是对应的合约的code,例如我们使用账户eosio部署了合约eosio.system,那么eosio.system的code就可以通过get code eosio获得。action就是转账:transfer。args就是上面PushManager的transferToken函数组装的参数对象。http请求成功以后,通过回调函数abi_json_to_bin_returned处理响应结果。
if (httpc) {
httpc->request(FunctionID::abi_json_to_bin, QJsonDocument(obj).toJson());
connect(httpc, &HttpClient::responseData, this, &PushManager::abi_json_to_bin_returned);
}
接口abi_json_to_bin:序列化json数据为二进制数据。这个结果的数据通常用在push_transaction的data字段。
action.setData(hexData); // action的hexData字段就是以上接口**abi\_json\_to\_bin**获得的结果。
剩余部分与上面介绍“创建账户”相同,get_info -> get_required_keys -> push_transaction 的流程。
总结一下,转账由于涉及到合约,所以多了一步abi_json_to_bin,而创建账户不需要这一步,但创建账户需要本地的集合对象同步存储。
打包交易
首先说明,打包的交易是测试交易,不是以上的创建账户和账户转账。先看源码部分:
trxpool = new TransactionPool; // 创建交易池
trxpool->setTargetSize(trx_size); // 设置交易池的大小
// packedTrxTransferFinished,打包测试交易发送链全部结束
connect(trxpool, &TransactionPool::finished, this, &MainWindow::packedTrxTransferFinished);
// packedTrxReady,prepare阶段完成,可以点击start
connect(trxpool, &TransactionPool::packedTrxPoolFulfilled, this, &MainWindow::packedTrxReady);
enablePacker(true);// 核心打包内容
enablePacker(),触发打包流程
QVector accounts = AccountManager::instance().listAccounts();
for (int i = 0; i < accounts.size(); ++i) {
Packer *p = new Packer;
connect(p, &Packer::finished, p, &QObject::deleteLater); // auto delete
// A:稍后重点讲
connect(p, &Packer::newPackedTrx, trxpool, &TransactionPool::incomingPackedTrxs);
// 为Packer的对象设置属性的值
p->setAccountName(QString::fromStdString(accounts.at(i)));
p->setCallback([=] (const QString& msg) {
commonOutput(msg);
});
p->start(); // 执行Packer
packers.push_back(p);
}
进入incomingPackedTrxs函数,
void TransactionPool::incomingPackedTrxs(const QByteArray &data)
{
// 上锁,data推入packedTransactions,QVector packedTransactions;
QMutexLocker locker(&mutex);
packedTransactions.push_back(data);
if (packedTransactions.size() >= targetSize) { // 通过我们设置的交易池的大小来控制总测试交易量
emit packedTrxPoolFulfilled();
}
}
Packer开始执行,
void Packer::run()
{
while(!needStop) {
PushManager push(false);
// 这是一个包含lambda为回调函数的connect语句
connect(&push, &PushManager::trxPacked, this, [&](const QByteArray& data){
emit newPackedTrx(data); // emit 发送signal给newPackedTrx B:稍后重点讲
func(QString("PACKED: %1 to %2.").arg(accountName).arg(super_account));// 打印日志
});
// 以下部分与账户转账接口一致,后续内容均同上。
push.transferToken(accountName, super_account, QString("0.0001 %1").arg(token_name));
}
}
当Packer开始run的时候,它是一个无线循环,直到灌满trxPool为止,而其中,我们注意观察,这一connect翻译过来就是:我先注册一个signals trxPacked在这,等待某处代码将该信号发射,会被这里捕捉到,将它传入回调函数,就是这个lambda回调函数的参数data中,这个lambda回调函数我们先放一放,来讲这个signals trxPacked:
signals 对应的触发是 emit
trxPacked 作为一个signals 是在PushManager::get_required_keys_returned中被发射emit的(注意这个是与上面讲到的CreateAccount::get_required_keys_returned是不同的。)
QByteArray param = packPushTransactionParam();
emit trxPacked(param);
...
httpc->request(FunctionID::push_transaction, param);
这个emit发送的param是仅在push_transaction发送之前的transaction,会将这个对象传入回调函数。下面来看一下lambda回调函数的内部,获取到transaction数据对象以后,会将该对象再次emit到一个signals newPackedTrx,我们去找一下这个signals的注册位置:MainWindow::enablePacker,就是上面展示过的代码,我注释为“A:稍后重点讲”,因此相同的原理,这个data又被传入了incomingPackedTrxs函数,最终被打包进packedTransactions集合中。
关于QT的signals emit slot connect 的具体语法介绍的内容可以查看这篇文章我们没有QT开发的需求,所以没必要在此过多介绍语法内容,只需要捋清楚业务逻辑即可。
packedTransactions的内容是属于TransactionPool的,它会在TransactionPool被启动时(也就是start按钮被按下时)使用,而这个对象是在prepare阶段被储存。(据说这个时间只有5分钟,机器性能不太好的不要将trxPool设置地太高,否则执行不完,打包好的packedTransactions并未做持久化,就会消失掉,最终导致测试结果失真)
这个按钮点击事件的内容看上去比较简单,只有一个enableTrxpool(true)是生效代码,其他都是一些日志。下面直接进入enableTrxpool函数,不张贴了,直接转到核心代码trxpool->start(); 那么我们进入到transactionpool.cpp,start对应run函数,源码如下:
void TransactionPool::run()
{
DataManager::instance().setBeginBlockNum(get_block_info());// get_block_info()是通过http请求链获取的
HttpClient httpc;
int sz = packedTransactions.size();
for (int i = 0; i < sz && !needStop; i += batch_size) {
QEventLoop loop;
connect(&httpc, &HttpClient::responseData, &loop, &QEventLoop::quit);
QJsonArray array;
int range = sz - i > batch_size ? batch_size : sz - i;
for (int j = 0; j < range; ++j) {
QJsonObject val = QJsonDocument::fromJson(packedTransactions.at(i+j)).object();
array.append(val);
}
// http请求push_transactions接口,推送打包交易到链
httpc.request(FunctionID::push_transactions, QJsonDocument(array).toJson());
loop.exec();
}
DataManager::instance().setEndBlockNum(get_block_info());
packedTransactions.clear();
}
这段代码就是上面提到的对 packedTransactions 的“消费”,核心代码是按照设置的打包(后称小包)大小来逐渐“消费”packedTransactions,然后通过http的push_transactions接口,将这些“小包”推送到链执行。
没想到EOSBenchTool的源码解读一下子搞了这么长的篇幅,我没控制住,读者又要吃力了。其实到这里我们来总结一下,EOSBenchTool主要是使用了QT的界面系统,同时也用到了QT的signals,emit,connect等专有语法,不懂qt的同学看起来有些吃力。然而,抛开这些语言或者类库的语法来讲,我们专注于代码逻辑,EOSBenchTool的实现是容易被人理解的:
上面我们介绍了:
Way | Business | TPS | memo |
---|---|---|---|
cleos | 可直接使用 | 70-80 | (单节点、多节点)shell方式,python脚本 |
txn_test_gen_plugin | 不可使用 | 1500-2000 | 官方用来测试的一种方式,这个插件纯粹是为了测tps而设的 |
EOSBenchTool | 可修改使用 | 200-300 | C++门槛较高且无对外封装接口 |
通过以上总结,我们可以推论出,如果有一种方式,支持:
那么它对于业务方来讲,是完全可以接受并享受基于eos的区块链带来的红利的。
下面就到了引出eosjs的时刻了,eosjs是官方EOSIO组织承认的客户端调用技术,它不仅仅是对rpc协议的封装,更多的还有大量的eos本身的特性,这些特性都可以做到在客户端本地实现,例如本地签名,本地生成交易id等等,这些技术可以让我们在业务方的客户端角度充分挖掘需求,自定义接口,上乘业务方,下启公有链eos环境,这种目前为止最为合适的承上启下的技术就是eosjs。
eos环境,可通过脚本快速搭建:
python3 ./bios-boot-tutorial.py -k -w -b -s -c -t
继续调用
python3 ./bios-boot-tutorial.py -l
将终端界面的输出内容保持链日志的同步输出。
eosjs是使用JavaScript语言,nodejs框架构成。
nodejs框架天生可以让我们便携地封装导出以及依赖导入某个“组件”,监于这种特性,我们也可以为业务方开发自己的sdk。
const Eos = require('../src')
const ecc = require('eosjs-ecc')
const keyProvider = [
"5K463ynhZoCDDa4RDcr63cUwWLTnKqmdcoTKTHBjqoKfv4u5V7p",
ecc.seedPrivate('test-tps')
]
const eos = Eos({
httpEndpoint: 'http://39.107.152.239:8000',
chainId: '1c6ae7719a2a3b4ecb19584a30ff510ba1b6ded86e1fd8b8fc22f1179c622a32',
keyProvider: keyProvider,
expireInSeconds: 60,
broadcast: false,
verbose: true
})
eos对象的能力:
{ getCurrencyBalance: [Function],
getCurrencyStats: [Function],
getProducers: [Function],
getInfo: [Function],
getBlock: [Function],
getAccount: [Function],
getCode: [Function],
getTableRows: [Function],
getAbi: [Function],
abiJsonToBin: [Function],
abiBinToJson: [Function],
getRequiredKeys: [Function],
pushBlock: [Function],
pushTransaction: [Function],
pushTransactions: [Function],
getActions: [Function],
getControlledAccounts: [Function],
getKeyAccounts: [Function],
getTransaction: [Function],
createTransaction: [Function],
api: { createTransaction: [Function: createTransaction] },
transaction: [AsyncFunction],
nonce: [Function],
bidname: [Function],
buyram: [Function],
buyrambytes: [Function],
canceldelay: [Function],
claimrewards: [Function],
delegatebw: [Function],
deleteauth: [Function],
linkauth: [Function],
newaccount: [Function],
onerror: [Function],
refund: [Function],
regproducer: [Function],
regproxy: [Function],
reqauth: [Function],
rmvproducer: [Function],
sellram: [Function],
setalimits: [Function],
setglimits: [Function],
setprods: [Function],
setabi: [Function],
setcode: [Function],
setparams: [Function],
setpriv: [Function],
setram: [Function],
undelegatebw: [Function],
unlinkauth: [Function],
unregprod: [Function],
updateauth: [Function],
voteproducer: [Function],
create: [Function],
issue: [Function],
transfer: [Function],
contract: [Function],
fc:
{ structs:
{ extensions_type: [Object],
transaction_header: [Object],
transaction: [Object],
signed_transaction: [Object],
field_def: [Object],
producer_key: [Object],
producer_schedule: [Object],
chain_config: [Object],
type_def: [Object],
struct_def: [Object],
clause_pair: [Object],
error_message: [Object],
abi_def: [Object],
table_def: [Object],
action: [Object],
action_def: [Object],
block_header: [Object],
packed_transaction: [Object],
nonce: [Object],
authority: [Object],
bidname: [Object],
blockchain_parameters: [Object],
buyram: [Object],
buyrambytes: [Object],
canceldelay: [Object],
claimrewards: [Object],
connector: [Object],
delegatebw: [Object],
delegated_bandwidth: [Object],
deleteauth: [Object],
eosio_global_state: [Object],
exchange_state: [Object],
key_weight: [Object],
linkauth: [Object],
namebid_info: [Object],
newaccount: [Object],
onerror: [Object],
permission_level: [Object],
permission_level_weight: [Object],
producer_info: [Object],
refund: [Object],
refund_request: [Object],
regproducer: [Object],
regproxy: [Object],
require_auth: [Object],
rmvproducer: [Object],
sellram: [Object],
set_account_limits: [Object],
set_global_limits: [Object],
set_producers: [Object],
setabi: [Object],
setcode: [Object],
setparams: [Object],
setpriv: [Object],
setram: [Object],
total_resources: [Object],
undelegatebw: [Object],
unlinkauth: [Object],
unregprod: [Object],
updateauth: [Object],
user_resources: [Object],
voteproducer: [Object],
voter_info: [Object],
wait_weight: [Object],
account: [Object],
create: [Object],
currency_stats: [Object],
issue: [Object],
transfer: [Object],
fields: [Object] },
types:
{ bytes: [Function],
string: [Function],
vector: [Function],
optional: [Function],
time: [Function],
map: [Function],
static_variant: [Function],
fixed_string16: [Function],
fixed_string32: [Function],
fixed_bytes16: [Function],
fixed_bytes20: [Function],
fixed_bytes28: [Function],
fixed_bytes32: [Function],
fixed_bytes33: [Function],
fixed_bytes64: [Function],
fixed_bytes65: [Function],
uint8: [Function],
uint16: [Function],
uint32: [Function],
uint64: [Function],
uint128: [Function],
uint224: [Function],
uint256: [Function],
uint512: [Function],
varuint32: [Function],
int8: [Function],
int16: [Function],
int32: [Function],
int64: [Function],
int128: [Function],
int224: [Function],
int256: [Function],
int512: [Function],
varint32: [Function],
float64: [Function],
name: [Function],
public_key: [Function],
symbol: [Function],
extended_symbol: [Function],
asset: [Function],
extended_asset: [Function],
signature: [Function],
config: [Object],
checksum160: [Function],
checksum256: [Function],
checksum512: [Function],
message_type: [Function],
symbol_code: [Function],
field_name: [Function],
account_name: [Function],
permission_name: [Function],
type_name: [Function],
token_name: [Function],
table_name: [Function],
scope_name: [Function],
action_name: [Function],
time_point: [Function],
time_point_sec: [Function],
timestamp: [Function],
block_timestamp_type: [Function],
block_id: [Function],
checksum_type: [Function],
checksum256_type: [Function],
checksum512_type: [Function],
checksum160_type: [Function],
sha256: [Function],
sha512: [Function],
sha160: [Function],
weight_type: [Function],
block_num_type: [Function],
share_type: [Function],
digest_type: [Function],
context_free_type: [Function],
unsigned_int: [Function],
bool: [Function],
transaction_id_type: [Function] },
fromBuffer: [Function],
toBuffer: [Function],
abiCache: { abiAsync: [Function: abiAsync], abi: [Function: abi] } },
modules:
{ format:
{ ULong: [Function: ULong],
isName: [Function: isName],
encodeName: [Function: encodeName],
decodeName: [Function: decodeName],
encodeNameHex: [Function: encodeNameHex],
decodeNameHex: [Function: decodeNameHex],
DecimalString: [Function: DecimalString],
DecimalPad: [Function: DecimalPad],
DecimalImply: [Function: DecimalImply],
DecimalUnimply: [Function: DecimalUnimply],
printAsset: [Function: printAsset],
parseAsset: [Function: parseAsset] } } }
通过以上列出的eos对象的提供的这些功能,我们可以满足大部分业务方的需求,这里展示一个创建用户的代码实例:
const nameRule = "12345abcdefghijklmnopqrstuvwxyz"
const config = {
trx_pool_size: 10,
optBCST: {expireInSeconds: 120, broadcast: true},
opts: {expireInSeconds: 60, broadcast: false},
ok: true,
no: false
}
function createAccount(account, publicKey, callback) {
eos.transaction(tr => {
tr.newaccount({
creator: 'eosio',
name: account,
owner: publicKey,
active: publicKey
})
tr.buyrambytes({
payer: 'eosio',
receiver: account,
bytes: 4096
})
tr.delegatebw({
from: 'eosio',
receiver: account,
stake_net_quantity: '0.0002 SYS',
stake_cpu_quantity: '0.0002 SYS',
transfer: 0
})
}).then(callback)
}
function generateAccounts(nameroot) {
for (i = 0; i < 31; i++) {
let accountname = nameroot + nameRule.charAt(i)
console.log("create account: ", accountname)
createAccount(accountname, ecc.privateToPublic(keyProvider[1]), asset => {
eos.transfer("eosio", accountname, "40.0000 SYS", "initial distribution", config.optBCST)
})
}
}
function getAccountsBalance(nameroot) {
for (i = 0; i < 31; i++) {
let accountname = nameroot + nameRule.charAt(i)
eos.getCurrencyBalance("eosio.token", accountname, "SYS").then(tx => {
console.log(accountname + " balance: " + tx[0])
})
}
}
打包交易接口目前我还未封装完毕,这篇文章更适合作为学习研究而不是代码段粘贴,因此对于打包交易的功能,研究好以上内容的朋友可以有自己的想法,这里我简单说一下我的实现思路:
每笔transaction是可以包含多个action的,在上面介绍过的插件的实现中,也是它的实现思路。另外push_transactions接口是链提供的http接口,我们打包多笔transaction成一个transactions对象请求这个接口,正如插件和EOSBenchTool的实现方式。然后中间要经过大量的优化,这其中较为重要的是我们的本地交易池,这个概念在EOSBenchTool中也研究过,那里的内存对象最多存活5分钟,而我们这里要如何设计呢?是否采用内存变量?还是引入队列?这都是架构师的工作,也是根据不同的业务场景大有所为的地方。
更新添加打包交易时序图:
更新打包交易源码: Templar
本篇文章全面而详细地分析了EOS中关于tps的一切手段,包括了cleos,插件,EOSBenchTool,eosjs的方式,这其中,我们仔细研究了EOSBenchTool的源码,过程中也涉及到了qt的部分语法,对比了这几种方式的利弊,讨论了tps的计算方式,tps的现实意义,插件的“作弊”行为,EOSBenchTool的良好思路和贡献,eosjs的最终确型,以及针对transaction,action等内部元素的深入理解与研究。最后也思考了未来eos商业实现的架构设想:通过eosjs作为承上启下的sdk。
圆方圆学院汇集大批区块链名师,打造精品的区块链技术课程。 在各大平台都长期有优质免费公开课,欢迎报名收看。
公开课地址:https://ke.qq.com/course/345101?flowToken=1007330