发展历史
从一开始,区块链技术对于普通用户就存在着上手困难的问题,因为它和人们普遍接触的中心化服务很不一样,它没有帐号和密码,取而代之的是私钥(助记词)。对于以太坊来说,Dapps 的上手门槛太高。以太坊的生态系统不足以让一个用户一上来就可以直接使用他需要的功能。因为和链上交互需要使用 Gas,这就有一系列钱包、以太币、Gas 的概念。理想的情况应该是用户一开始只需关心 DApps 提供的功能,等他熟悉之后才引入底层的概念知识。
元交易就是想缓解这个问题的一种技术方案。
最早提出元交易的想法是在 2015 年,Dr. Christian Lundkvist 提出来的。核心观点是,不需要持有 ETH,就能和以太坊区块链进行交互。这样代币的持有人,不需要持有 ETH,就能和 Dapps 交互(正常的话,交互需要向智能合约发送交易,而发送交易就需要消耗 Gas,那就需要事先拥有 ETH)。
这样就会带来很大的好处:对新手很友好,大幅降低代币的使用门槛。App 可以将钱包、助记词这些概念都隐藏掉,新手获得代币后,可以直接相互转账,不需要知道它是否运行在以太坊上。
2017 年 8 月,Nate Rush 在 Medium 上写了 Making uPort Smart Contracts Smarter, Part 3: Fixing User Experience with Meta Transactions。它的工作方式是:用户仍然持有私钥,并且使用私钥签名一些数据,数据发送到「中继」,中继把数据发送到身份验证合约去验证(验证是否是某个 Dapps 的用户),验证通过后,中继把数据发送到用户想去的智能合约,由链下的中继负责支付 Gas 费。
技术方向
目前有很多提案都是这种链下中继的想法,包括:
-
ERC-1776 草稿,叫做 Native Mata Transactions
-
EIP 1077 草稿,它想为所有的这种中继,提供标准化的消息格式
-
EIP 1613 草稿,就是本文的重点 Gas stations network
但实际上就有一些别的思路,这里只是列举一下,并没有仔细考察它们的可行性和进展:
-
新的 Token 标准:
-
智能合约上再包一层
-
改变 EVM
Gas stations netwrok
Gas stations network 技术方案由 OpenZeppelin、Tabookey 公司主导,所以现阶段最成熟。它遵循 EIP 1077。在今年 8 月 6 日释放首个测试版的时候,官方写了一篇很详细的介绍文章。里面有些观点很有意思,值得讨论:
-
交易成本和 Gas 费用应该视作应用的获客成本的一部分,你的商业模型要能负担得起这个获客成本,虽然目前还没定论区块链适合什么样的商业模型,但是 Numeraire、MakerDAO、Instadapp 看起来很有希望。另外随着技术的成熟,这个成本也会下降。
-
用户不关心也不需要关心技术细节,他只需要使用他想使用的功能。
-
应用可以向用户收取订阅的费用,让应用看起来和其它云服务的收费方式差不多,隐藏区块链的技术细节和交易成本概念。或者在应用内创建金币等充值体系。或者建立一个 DAO 资金池。
-
DApps 为了增加新用户,会愿意支付 Gas,或者第三方会愿意补贴 Gas 费用
下面开始介绍它的 EIP 规范文档。
EIP 1613: Gas stations network
整个设计有两个目标:易于被采用、健壮性。
由 4 部分组成:
-
一个单一的公开合约 RelayHub,被所有参与的 DApps 合约(要继承 RelayRecipient 合约)信任,任何人都可以去验证它
-
一层去中心化的中继节点(Relay),被激励去接受不带 Gas 的交易
-
参与的合约,需要继承 RelayRecipient
-
应用
只要有一个中继是诚实的,其它节点就不可能审查交易,假如想破坏这个系统,结果和链上核对被发现了,就会受到惩罚。
中继注册到 RelayHub 里面,会根据合约返回给 DApps 合适的中继(基于信誉),DApp 可以自由切换中继,每个人都可以运行一个中继,可以设置自己的收费。
动机
-
增加智能合约的接受程度
-
用户不需要事先拥有 ETH
-
保持去中心化和抗审查的前提下,不需要和链直接交互
-
-
以太坊节点即使不挖矿,也能有一个新的收入来源,整个网络因此能有更多节点
-
不需要有协议上的变更。gsn 通过智能合约是自组织的,DApps 通过接口和它交互。
RelayHub 的作用
-
维护一个活动状态的 Relay 列表。发送者的每个交易都会从这里面选一个 Relay
-
协调 Relay 和合约的所有通信
-
给合约提供真实的 msg.sender、msg.data
-
维护 Relay 赚到的手续费,Relay 注销再经过一段冻结期后,可以提币
-
维护合约预存的资金池,会拿来补偿给 Relay
-
一旦有地址能提供 Relay 欺骗的证据,Relay 的手续费被打一半给证据提供方,另一半销毁
-
提供一种方法让 Relay 知道接下来的交易会得到多少补偿
Relay 的作用
-
维护一个 ETH 热钱包,它要用来支付 Gas
-
提供一个服务接口,让 App 能够发送不带 Gas 的交易
-
把自己的接口和手续费报价往 RelayHub 里面注册
-
(可选)通过 RelayHub,监控其它 Relay 和撤销的交易,发现其它 Relay 是否欺骗,以此获得它们的手续费。这其实可以由任何人来做,不限于 Relay
实现RelayRecipient
合约
-
已知 RelayHub 的地址
-
往 RelayHub 预存少量的 Eth,用来支付 Gas
-
用 getSender() 和 getMessageData() 代理 msg.sender 和 msg.data
-
实现 acceptRelayCall,返回 0 表示愿意接受转发并且支付手续费,
- 要防止 Relay 创建虚假交易薅羊毛,防止女巫攻击
-
实现 preRelayedCall
-
实现 postRelayedCall
Relay 注册过程
-
Relay 启动,监听服务
-
生成密钥对和地址
-
往地址里面充值 ETH,用来作为 Gas
-
往 RelayHub 调用 RelayHub.stake,提交自己的地址和冻结时间。RelayHub 会存起来
-
往 RelayHub 调用 RelayHub.registerRelay,注册自己的收费系数(最终收入 = 收费系数*实际消耗的 Gas)、服务地址
-
RelayHub 确定 Relay 有足够的 ETH 存款
-
RelayHub 发送事件 RelayAdded,公开 Relay 地址、服务地址、存款情况、手续费、冻结时间
-
Relay 每 6000 块发送 keepalive 交易
-
Relay 等待接受签名的交易
发送交易的过程
-
Sender 通过查看 RelayAdded 事件记录,参考 Relay 的各个参数(手续费、存款额、锁定时间),甚至最近的转发情况,自己决定选哪个 Relay,Sender 可以自己有黑白名单
-
Sender 准备好交易数据:自己的地址、接受方地址、实际的交易数据、愿意给 Relay 的手续费、gasPrice、gasLimit,从 RelayHub.nonce 获取到当前 nonce、RelayHub 的地址、Relay 的地址。然后再签名
-
Sender 确认 RelayHub 中接收的智能合约地址预存了足够的手续费
-
Sender 确认 Relay.balance 中有足够的 Gas 费用
-
Sender 获取 Relay 当前的 nonce,并给出 max_nonce(这是要求 Relay 在这之前要转发出去)
-
发送签名之后的交易和其它元数据给 Relay
-
Relay 收到交易后,把它再用交易包起来,并且签名,上链发给 RelayHub,eth value 为 0,同时这个签名之后包裹交易也会立即发回给 Sender。在这之前,Relay 会验证许多东西,有一个错误就会返回给 Sender 错误信息,确保自己发给链上的交易不会白费 Gas:
-
调用 RelayHub.canRelay(),这个函数里面再调用接收合约的 acceptRelayedCall(),看接收合约是否能接受 Sender 这个交易
-
交易的 nonce 是否符合 RelayHub.nonces[sender]
-
交易给的 Relay 地址是否就是自己
-
交易接受合约在 RelayHub 里面存的 ETH 是否足够手续费
-
Relay 自己是否有足够的手续费
-
max_nonce 的值是否比 Relay 当前的 nonce 大
-
-
Sender 收到 Relay 签名的包裹交易后,会验证一些东西,确保自己的交易真得由那个 Relay 转发了 ,如果有任一项失败,Sender 会重新选个 Relay 重发,并且可以自己降低失败 Relay 的权重:
-
Relay 的地址是否能调用 RelayHub,是否它注册过
-
交易的 nonce 是否符合 Relay 的 nonce,是否小于等于 max_nonce
-
Relay 是否有足够的 Gas 费用
-
包裹在里面的交易否是是自己签名的
-
交易接受合约在 RelayHub 里面存的 ETH 是否足够手续费
-
-
Sender 之后也可以把原生包裹的交易发给以太坊节点,虽然可能没什么作用,因为 mempool 已经有 Relay 发的那个了,但是做这个动作也没有害处,可能能加速
-
Sender 监视 Relay 那个 nonce 的交易是否被打包了。如果 Relay 那个 nonce 打包的是一个其它的交易,Sender 可以通过另一个 Relay,调用 RelayHub.penalizeRepeatedNonce 举报之前的 Relay,获得它存的一半的 ETH(另一半会打到 0 地址销毁,防止 Relay 自己举报自己就没有损失了)。再选其它的 Relay 重发交易
RelayHub 接收交易的流程
-
记录 getleft() 作为 initialGas ,用来作为之后的支付
-
验证注册过的 Relay 发过来的交易
-
验证里面的交易的签名,是否和原始发送者匹配
-
验证里面的交易的 nonce 是否符合 RelayHub.nonces[sender]
-
验证交易里面的 Relay 地址参数,是否和 msg.sender 一致
-
调用接收合约的 acceptRelayedCall(),询问它是否接受交易,如果不能(因为 Relay 没有事先调用 RelayHub.canRealy 就发过来了,或者调用了收到不能接受的响应后还发),发送事件 CanRelayFailed,Relay 不能获得手续费
-
接收合约表示接受后,调用它的 preRelayedCall()
-
转发真正的交易给接收合约,接收合约会处理交易
-
RelayHub 调用接收合约的 postRelayedCall
-
检查调用的返回值,发送 TransactionRelayed 事件,记录转发情况
-
增加 RelayHub.nonce[sender]
-
从接收合约的余额里面转手续费给 Relay.owner,根据交易实际消耗的来(不管接收合约处理交易时是否失败)。只有一种情况,Relay 会亏损,就是调用 RelayHub.canRelay 返回非 0 时,因为必须在转发交易前先查一下
-
Relay 跟踪转发的交易,检查 TransactionRelayed 事件。如果交易最终撤销,手续费没有支付,那说明接收合约对 acceptRelayedCall 请求不一致(Relay 询问时回答接收,RelayHub 询问时回答不接收),Relay 可以将这个接收合约添加到黑名单
Relay 下线流程
-
调用 RelayHub.removeRelayByOwner
-
RelayHub 检查这个 msg.sender 是否就是 Relay 本身,确认后移除 Relay,发送 RelayRemoved 事件
-
RelayHub 开始减它的冻结事件
-
Relay 收到 RelayRemoved 事件
-
Relay 把它预存的 ETH 转出到拥有人的地址
-
Relay 下线
-
等冻结期结束,拥有人调用 RelayHub.unstake() 提取出所有手续费
Demo
最后介绍一下官方给出的一个测试链 Demo 聊天应用。
注意打开浏览器调试工具,跟踪一下它发出去的请求,就会发现它实际会通过 WebSocket 和链节点通讯,往 Realy 发交易请求。然后跟踪 Relay 的以太坊地址,就会找到 RelayHub 等其它信息了。