目录
- 1-从零开发EOS区块链小游戏系列 - 使用EOS Studio
- 2-从零开发EOS区块链小游戏系列 - 智能合约设计与实现
- 3-从零开发EOS区块链小游戏系列 - 游戏公平性及安全性
- 4-从零开发EOS区块链小游戏系列 - 加入Token体系
- 5-从零开发EOS区块链小游戏系列 - 实现玩家免CPU玩游戏(终)
前面几篇文章实现了一个基于EOS的区块链小游戏开发,由于这个小游戏逻辑简单,所以我们将整个游戏的逻辑都放在链上执行。但可能你的游戏并没有那么简单,所以其实普遍的做法还是游戏逻辑放在链下,重要的资产放在链上,这里资产不限于游戏中的金币,应该包含所有属于玩家的物品,比如:装备、皮肤、宠物等等。如果开发了多款游戏,那么通过区块链在你的生态就可以实现资产跨游戏交易,甚至你还可以与其他开发者的游戏资产进行交易,资产就变得更流通更有价值了。
当你开发完一款EOS小游戏,发布上线后发给你的几个好朋友玩家帮你内测,一天过去了,你发现游戏对战记录没几条,于是你查看服务器日志,发现大量异常信息:
...3080004: Transaction exceeded the current CPU usage limit imposed on the transactionx...
调查后你发现异常信息是EOS节点返回,该信息就是告诉你:你的玩家账号里面的CPU不足,无法发起交易。
没错,EOS
发起交易是需要消耗资源的,也就是手续费,ETH
交易也需要支付手续费,那边叫做GAS
。这里资源包括CPU和NET,分别用于计算和打包后网络传输,EOS
的资源可以抵押、赎回、定期释放,关于资源我们第一章1-从零开发EOS区块链小游戏系列 - 使用EOS Studio也有简单的介绍过,本章重点不讲解资源,想深入了解可以阅读EOS学习之(RAM,NET,CPU)。发起交易就需要支付手续费,这个是合情合理的,这样整个链才能正常发展,而且节点没有义务免费提供服务。但是EOS
对于CPU机制的设计有问题,并且上线至今仍没有很好的解决,导致CPU价格容易被操控,大部分用户无法正常发起交易,不利于EOS
的发展。
EOSv1.8.0
版本于上一年9月份上线,其中新增了一个可一定程序缓解用户CPU的功能,官方立项名称为:ONLY_BILL_FIRST_AUTHORIZER
。如果翻译过来就是:向第一授权者支付资源费用。这是一个偏技术性的描述,我们还是结合业务解释吧,就是说你(即开发者)可以代替你的用户(即玩家)支付资源手续费(CPU/NET),即是说可以完美解决上面出现CPU不足的问题,让你的玩家可以放心的游戏。这功能是不是很好的解决了你的问题?
我们在实践这个功能之前,先了解一下EOS
内部是怎么做的,分别查看EOS
v1.3x和v1.8x版本 transaction_context.cpp
代码文件最重要的一段:
v1.3x
// Record accounts to be billed for network and CPU usage
for( const auto& act : trx.actions ) {
for( const auto& auth : act.authorization ) {
bill_to_accounts.insert( auth.actor );
}
}
...
v1.8x
// Record accounts to be billed for network and CPU usage
if( control.is_builtin_activated(builtin_protocol_feature_t::only_bill_first_authorizer) ) {
bill_to_accounts.insert( trx.first_authorizer() ); # v1.8x新增
} else {
for( const auto& act : trx.actions ) {
for( const auto& auth : act.authorization ) {
bill_to_accounts.insert( auth.actor );
}
}
}
留意v1.8x多了一个判断分支,如果是ONLY_BILL_FIRST_AUTHORIZER
,只有一个账号进行了支付;trx.first_authorizer()
即通过交易数据获取第一个授权者,放入到支付列表中。
从上面的代码我们可以发现,EOS
的交易是可以多个账号进行授权签名的,这就是为什么EOS
不能像ETH
那样通过类似msg.sender
来到获取当前交易发起人的账号,而是要把交易发起人的账号放在入参。前面我们所讲的交易都是单独一个账号授权的,流程上差异其实并不大,下面我们画了一张流程:
接着我们编写代码实现上图流程功能,这里我还是使用eospy
一个python版本的EOS
库类型实现,功能和JS版本的eosjs
差不多,使用哪种语言看自己喜欢。首先我们有个client.py
文件,代表玩家客户端,一个server.py
文件代表服务端:
#client.py
import json
import eospy
from eospy.cleos import Cleos
from eospy.keys import EOSKey
from eospy.types import EOSEncoder, Transaction
from eospy.utils import sig_digest
import server as SERVER
# 麒麟测试节点
ce = eospy.cleos.Cleos(url='https://api-kylin.eosasia.one')
# 构造交易数据(图1-1 步骤1)
# args是需要调用的`action`的入参,这里是转账所以入参分别是:from、to、quantity和memo
args = {
"from": "sweetsummer1", # 发起交易的账号
"to": "kingofighter", # 接收者账号
"quantity": '100.0000 SJ', # 接收金额
"memo": "action:imrich,us:9ba14079514a,ush:0fa6e90cce77e37b2c5b52311e6ca3383accbb4e88388f316d54a401bdc33929,et:1579777105,sig:SIG_K1_KmKPi2k7zzJhtaspdCVzLdoHuKMDeKZSFHypuJsnoUkSbxFjxSqaZJ8VqdQAxnTGj1Q1P6C9eh3RZXefVT3mU4hzkSbNB3"
}
payload = [
{
"account": "kofgametoken", # 调用的合约账号
"name": "transfer", # action
"authorization": [{
"actor": "kingofighter",# 第一个授权
"permission": "active",
}, {
"actor": "sweetsummer1",# 第二个授权
"permission": "active",
}],
}
]
# 调用区块链,将交易数据转换成16进行字符串(图1-1 步骤2)
# 入参:account、name、args
data = ce.abi_json_to_bin(payload[0]['account'], payload[0]['name'], args)
# 将返回的`data`加入到`payload`
payload[0]['data'] = data['binargs']
# 构造`trx`(图1-1 步骤3)
# 这时的`payload`有:data、account、name和authorization
trx = {"actions": [payload[0]]}
# 对`trx`进行哈希计算(图1-1 步骤4)
chain_info, lib_info = ce.get_chain_lib_info()
trx_chain = Transaction(trx, chain_info, lib_info)
hash_trx = sig_digest(trx_chain.encode(), chain_info['chain_id'])
# 请求服务端 对`trx`哈希进行签名(图1-1 步骤5)
server_sign = SERVER.sign(hash_trx)
# 玩家自己也需要对相同数据进行签名(图1-1 步骤6)
k = EOSKey('sweetsummer1的私钥') # 玩家私钥
player_sign = k.sign(hash_trx)
# 组合签名,顺序要和`payload.authorization`的顺序一致
signatures = []
signatures.append(server_sign) # 服务端签名放在第一个位置
signatures.append(player_sign) # 玩家签名跟在后面
final_trx = {
'compression': 'none',
'transaction': trx_chain.__dict__,
'signatures': signatures
}
data = json.dumps(final_trx, cls=EOSEncoder)
timeout = 30
# 调用区块链`push_transaction`提交交易请求
res = ce.post('chain.push_transaction', params=None, data=data, timeout=timeout)
print(res)
#server.py
from eospy.keys import EOSKey
def sign(digest):
k = EOSKey('kingofighter的`active`私钥') #用于支付CPU的账号私钥
return k.sign(digest)
为了方便,我们打算直接使用小游戏的合约作为支付CPU的账号,你也可以重新创建一个。在执行之前,我们先记录sweetsummer1
(玩家账号)和kingofighter
(需要支付CPU账账号)的资源数据:
#交易执行结果
/usr/local/bin/python3.6 /Users/jan/Documents/github/python/obfa/client.py
{'transaction_id': '7f6b123fd8401f29bf2f2993b7bb32f42b70fba28fd5d3491b2efbef00afcb06'...
由上面两张截图可以看到,虽然依然是玩家发起交易,但是他的CPU和NET没有变化,反而是kingofighter
的可用CPU和NET减少了,表示通过我们的实践,证明了ONLY_BILL_FIRST_AUTHORIZER
的功能。而且更奇妙的是,我们无需修改任何智能合约的代码就实现了。
仔细阅读上面server.py
相关代码:
def sign(digest):
k = EOSKey('kingofighter的`active`私钥') #用于支付CPU的账号私钥
return k.sign(digest)
其实就一句代码,作用就仅是对客户端传过来的数据进行签名。如果你对密码学有一定基础,一定会发现,这里存在很大的安全问题:没有对入参进行校验,客户端给什么就签什么。更好的做法应该是:
#要求客户端给出原数据,服务端也亲自算哈希,如果两个哈希值相等表示没问题。
def sign(digest,data):
s_digest = hash(data)
if digest != s_digest:
return "数据不一致"
k = EOSKey('kingofighter的`active`私钥')
return k.sign(digest)
还有一种实现起来更加优雅的办法,就是在kingofighter
合约新创建一个权限,专门用这个权限来进行支付CPU的签名操作。要使用这种方法首先要对EOS
的账号模型有一定理解,本篇我们只讲涉及到的知识,想深入了解各位可以直接到 EOS官网。在EOS账号模型中,每一个账号可以有多个权限,默认有owner
和active
:
-
owner
可以对所有者权限进行任何更改的操作。 -
active
授权用于交易,投票以及进行其他高级帐户更改。也就是我们之前调用合约是使用这个权限。
目前,我们小游戏合约的账号是这样的:权限和公钥
我们上面是直接使用了active
这个权限进行了签名,有什么问题呢?这个权限级别太高。我们的初衷只是帮玩家支付玩游戏的CPU,但玩家可以通过此权限签名用来做其他交易。这样我们会觉得太不可控了,需要新建一个权限,每一个权限都需要绑定一对密钥,打开 EOS studio 选择Accounts->Manage Keypairs->Create:
创建一对密钥
接着我们使用cleos
客户端来执行命令:
cleos -u https://api-kylin.eosasia.one set account permission kingofighter cpupayer EOS7DaK1J1etMVsHewYVbQC3wuD6ec4GAmeDscwfHXAXQjAxzsMro active -p kingofighter@active
执行后发现在
active
下多了一个cpupayer
权限,注意这个时候cpupayer
权限不能做任何事情,我们还需要授予他可以执行的动作:
cleos -u https://api-kylin.eosasia.one set action permission kingofighter kofgametoken transfer cpupayer -p kingofighter@active
这一句命令的意思是授予kingofighter.cpupayer
权限执行合约kofgametoken.transfer
ACTINO的权限。在麒麟测试网看不到效果,如果是上了EOS
主网那么执行后效果应该是这样的:
最后,我们将上面代码修改一下:
#client.py 文件
...
payload = [
{
"account": "kofgametoken",
"name": "transfer",
"authorization": [{
"actor": "kingofighter",
"permission": "cpupayer", # 由原来的`active`修改为`cpupayer`
}, {
"actor": "sweetsummer1",
"permission": "active",
}],
}
]
...
#server.py 文件
...
def sign(digest):
k = EOSKey('cpupayer权限私钥') # 由原来的`active`修改为`cpupayer`私钥
return k.sign(digest)
...
OK,这样就已经完成了。服务端不需要校验数据的哈希是否一致,只管无脑的签名就可以了,因为我们已经在合约的权限层面做了限制,限制了“只有玩家玩我们合约的游戏时,我们才会帮他支付CPU和NET”。这样使得我们合约的权限变得可控,无论cpupayer
怎么放开签名,都不会响应到别的地方。
到此,这个系列终于赶在年前肝完了,本章的所说的功能虽然没有根本的解决CPU问题,但是却大大降低了用户的门槛,也是EOS
一次非常不错的升级,最后希望各位能开发出爆款DAPP :)。
本章节源代码地址:https://github.com/jan-gogogo/only-bill-first-authorizer