本文的目的是为在以太坊生态中发送交易使用的各类技术,模式和机制提供一个指南。由于新技术层出不穷,本文也会随之更新,所以可以认为是未完待续的一个状态。
针对这个公认的大课题,本文将包含以下内容:
- Brief introduction to Ethereum transactions 以太坊交易的简明介绍
- Gas usage and gasToken Gas 的用途和 gas 代币
- Meta transactions 元交易
- Submarine sends 潜艇交易
- Counterfactual instantiation 反事实合约实例化
- Zero confirmation transactions 零确认交易
- Batched transfers 批量转账
- SMS based payments 基于短信的付款
- Repeated subscription payments 重复订阅付款
- Gas reduction via oracle contract for batched transactions 通过预言机合约为批量交易节省 Gas
- Multisends with one-time addresses 使用一次性地址进行多笔付款
背景知识
以太坊是一个基于账户的系统,目前有两种账户:普通账户和合约账户。这两种账户都有自己的以太坊地址,交易计数器 Nonce 和余额。合约账户还额外拥有不可变的代码以及相应的存储空间。这里有一篇介绍这些基本概念的好文章。
一个以太坊交易包含以下关键字段:
- Nonce,或者说交易计数器,即该账户主动发起的交易数量,从0开始计数
- gas Price,决定了该笔交易需要支付的 Ether 数量
- gas Limit,即处理该交易所允许的最大 gas 数量
- 目标地址,即接受该笔交易的对象,如为空,则该交易会创建一个新的合约
- 交易金额,即发送的 Ether 数量
- 数据,即可以是任意的一条文本消息,也可以是某合约的一次调用或者创建新合约的一段代码
请注意,以上关键点在于没有“交易发起”地址,因为该地址可以从生成该笔交易哈希值签名的公钥-私钥对推导出来,其中交易字段采用了适当的 RLP(递归长度前缀)编码。
Gas 用途和 Gas 代币
站在一个很通俗的角度,区块链可以看成是一个共享数据库。每次从该数据库读取或者写入数据都需要花费 gas 以防止类似垃圾邮件的恶意攻击。具体来说,以太坊上执行的每个计算步骤都需要花费 gas,以避免可能导致以太坊停摆的恶意攻击。每个操作码的 gas 开销都在以太坊黄皮书中有说明。但操作码的 gas 开销仍是一个热烈讨论的活跃话题,以太坊的社区成员们正在研究引入存储租金机制的可能性,甚至是 gas 和操作码的动态定价方案。
在以太坊区块链中写入数据很贵,比如创建一个非空的存储单元需要花费 20000 gas,几乎与一次简单的 Ether 转账交易(即当交易结构中的数据字段为空时)花费相当(21000 gas)。作为缓解区块链数据存储暴涨的一种激励方案,以太坊协议会为清空不再使用的旧存储单元退还 10000 个 gas。
这个 Ether 的退还机制最多可以返还合约交易花费的一半 gas(普通转账交易是无法获得退款的,因为它们已经使用了 gas 的最低消费;但是针对合约的批量调用是可以享受这个退款机制的)。Gas Token 允许开发者简单而高效地利用这个退款机制,即通过 gas 的代币化,在 gas 价格低的时候囤货,然后在 gas 价格高的时候花掉之前储存的 gas 代币。
最近确实有在一些交易所发现了一个没有正确设定交易
gas上限的漏洞。攻击方法很简单:在交易所申请提现,然后将提现交易目标地址设置成一个攻击者部署的恶意合约,其默认 fallback 函数(发送 Ether 到该合约会触发该函数的调用)就会趁机铸造新的 gas Token(囤货 gas)。(校对注:详情可见文末超链接《深处的蚁穴》)
元交易
元交易是这样一种发送交易的模式:发送方先对一个合法的以太坊交易签名,然后把该交易和签名通过链下传递的方式转交给一个中继方,该中继方愿意承担该笔交易的 gas 开销并最终发送交易到以太坊网络中。
元交易最终的目标地址一般都是某个以太坊合约,且在某种程度上,该合约知道,交易的签名方并不是交易实际的发送者。以太坊交易 API 的 msg.sender 字段会返回中继方的地址,但其很可能并没有代表签名方操作的权利,所以在这个场景下(仅仅查看 msg.sender 字段)并没有太大意义。因此,许多元交易依赖链上的签名校验(通过以太坊 API 的 ecRecover 函数)来保证签名方账户的确是在一份合适的白名单里面(有权限操作该交易想要执行的指令)。
潜艇交易(Submarine Send)
(别跟这个潜艇交易网站 https://submarineswaps.org/ 混淆了)
矿工抢跑(frontruning)现象在基于区块链经济的交易市场中是一个很难杜绝的老问题,即矿工可以对交易重新排序,随意裁剪或者让他们自己的交易插队来获利。潜艇交易试图通过极强的保密特性来提供矿工抢跑问题的一个解决方案。不仅仅是隐藏交易金额,潜艇交易会尝试完全隐藏该笔交易的存在。当然,如果一笔交易永远都被隐藏着,那也就没啥意思了。潜艇交易允许发送方在未来的某个时刻公开该笔交易,这也是称其为“潜艇”交易的缘由。
反事实合约实例化
Counterfactual(反事实)一词源于哲学和思辩中的一个概念。一条反事实陈述是一连串有理有据的推理以及相应的结论,但是该陈述的前提是有意与事实相反的。除开这个与事实不符的前提,整个推理链条是合理的,所以如果前提正确,最终结论也会是正确的。应用到区块链交易场景,Conterfactual 的逻辑不光会考虑区块链当前的状态,还会考虑如果某合约部署完成后,区块链的状态。
更具体来说,在合约部署之前就获取它的地址,这种模式被称作反事实合约实例化,这个理论由 L4 发表在他们的“反事实状态通道”论文中,并受到以太坊社区的广泛欢迎。
目前,新的合约地址由以太坊操作码 CREATE 生成,并可通过合约的创建者账户地址(sender字段)以及创建者已经发出去的交易数量(nonce字段)来明确决定,即 sender 和 nonce 字段会通过 RLP 编码然后经过 Keccak256 哈希算法生成新的合约地址。
EIP1014 引入的 Skinny CREATE2 操作码更进一步,允许用户与链上尚不存在的地址进行交互;虽然该地址上还没有代码,但可以保证它最终只可能包含通过一段特定的初始代码生成的合约逻辑。与 CREATE 操作码一般使用 sender-nonce 然后哈希得到合约地址不同,CREATE2 操作码使用的是如下地址生成公式: keccak256( 0xff ++ address ++ salt ++ keccak256(init_code)))[12:]
。
这种模式,对于涉及与尚不存在的合约进行交互的状态通道场景,尤其重要。它让以太坊主链可以成为争议(解决)层,并且不需要考虑合约部署的真实开销。类似地,这种模式在已知功能将创建新地址的场景也可以使用,比如这里的借贷还款地址。
零确认交易
零确认交易源于 Bitcoin Cash 社区,目前仍是一个有趣但尚未经过证明的研究领域,在这样的一个区块链网络中,出块时间实质上可能更加不利于用户体验(UX-inhibiting)。零确认交易的发送方需要提交一个保证金,如果有双花行为,发送方就会损失掉该保证金。在比特币现金中,双花行为可以通过 UTXO 的输入项重用被检测到。任何人(一般假设是矿工)都可以提交找到的两笔双花交易,然后得到保证金的奖励。
在以太坊的账户网络中,不同于使用类似比特币的UTXO,我们可以检查同一发送方是否重用了同一个 nonce。比如一个已部署合约提供一个 reportDoubleSpend 方法,该方法接受两个待完成的已签名交易,然后合约会检查其发送方和 nonce,如果相等,就会把保证金奖励给方法调用者。原理很简单:如果保证金数额足够大,这对于交易发送方而言,就是防止其作弊(双花)的一个有力武器,因为他们有可能损失缴纳的保证金。这种交易类型被认为最适合用于小额一次性的单笔支付场景,因为有一系列针对该场景的潜在攻击模式存在。
批量转账
跟 ERC20 代币交互的一个主要问题在于,一般需要两次不同的交易:一次是调用代币合约的 approve 方法,另一次才是真正调用目标合约(该目标合约内部会调用 transferFrom 方法)使用代币完成特定逻辑(doSomethingForTokens 方法)。这种模式就会产生非原子性交易的一系列问题。最简单的情况就是,如果 doSomethingForTokens 调用交易失败了,之前的 approve 调用不会回滚,即 approve 方法允许合约支配的代币额度(allowance)仍然成立。
基于短信的付款
CoinText 可能是最有名的基于短信的密码学货币支付服务商,目前专注提供比特币现金的交易。这种付款机制对于发展中国家和地区的移动设备尤其有用。Eth2 也已经在以太坊上部署了类似的技术,它可以通过传统的基于移动应用的以太坊钱包(比如 Trust)来工作。
中心化服务器用来对手机号做验证并传递秘钥,但是 Eth2 的服务器无法控制锁定在托管合约中的 Ether。如果中心化服务器被攻击了,付款交易会失败,但是 Ether 仍在托管合约中。如果此时想拿回锁定的 Ether,发送方可以通过调用托管合约取消该笔付款。
订阅付款
基于可选择退出的订阅服务付款是 Web2.0 时代互联网服务的主流变现方式,比如 Spotify,Netflix,Headspace 和 Tinder 都是基于订阅付款构建其商业收入模型。
密码学货币中的订阅付款概念也不是新东西,在比特币中,nLocktime 字段就可以用来保证一笔已签名的交易在指定的区块高度之前不会被打包(即消费掉)。但在以太坊上,用于未来支付的预签名交易意义不大,因为账户的 nonce 会随着该账户不停发出交易而增长,会导致预签名时所用 nonce 偏小,进而导致交易无效。
幸好,以太坊的图灵完备性提供了一个解决办法:有一些针对重复订阅类型交易的架构方案。这些架构在保证金(流动性),用户体验复杂性,可选功能,gas 开销和可延展性方面有不同的权衡取舍。
基于预言机的方法调用
另一种更加特殊的交易发送方式是使用预言机服务,比如 Oraclize,以期通过适当的中心化来换取gas使用量的减少,可以参见此文。
使用一次性地址进行多笔付款
如背景知识中介绍的,交易字段中并没有“发起地址”。这个址可以通过 ecRecover 函数计算得出。那么问题来了:我们能不能在交易签名中任意填入我们想要的数据?事实表明,有一半的签名是正确的,即 ecRecover 仍然返回一个合法的公钥(因此也对应着以太坊地址)。由于我们无法控制生成的地址,那么我们通过设置字段值,其实是在构建这样一个交易:该交易可以花费看上去是一个随机生成的地址中的余额。
如果我们创建了这样一笔交易(发送方是我们想要生成的地址),并给生成的地址充值了若干Ether,那么该笔交易就可以像一般交易一样执行。这样我们实质上创建了一个一次性的地址,因为其中的余额只能被一笔交易使用。如果我们以某种可预测的方式选择交易签名中的字段值并公布该笔交易,我们就可以向任何人证明,发给交易发送方地址的金额,只能被该笔交易使用,而不能被其它任何交易使用。
如上图所述,该场景尝试发送交易至11140个目标地址,由一系列发送 Ether 至多个地址的交易组成,每个交易发送到 110 个地址,其中发送方的地址通过上述方法生成。对于签名字段,我们填入‘0xDA0DA0DA0…’ ,这是一个可预测的值,这样我们确定,没有人能拿到这些签名所对应地址的私钥。
这就创建了一批拥有“一次性地址”的交易,这些地址可以用来给相应交易提供所需交易金额。但 104 个要签名的交易对于受托自然人而言还是太多了,所以我们重复一次上述过程,形成一个级联结构:我们先构造 104 笔交易,每笔交易都有其对应的唯一地址,然后再构造一笔发送 Ether到对应的 104 个地址的转账。通过验证,代码确实可以按照预期运行,那么任何人就可以这些构建好的交易发送到以太坊网络中,整个过程就像多米诺骨牌一样自动进行了:名单上的 11400 个地址都会收到 Ether,但我们仅仅用了一次人工签名。