区块链学习笔记Day5:交易
学区块链做的笔记Day5,大部分内容来自《精通以太坊》。
交易是由外部账户发出的经过签名的消息,通过以太坊的网络传播,由矿工记录在区块链上。从另一个角度来看,交易是唯一能够触发区块链状态改变,或触发EVM上的合约执行的东西。以太坊是一个全局的单体状态机,交易是唯一能够让这台状态机向前推进并改变状态的东西。合约并不会自动运行。以太坊也不会在“后台”运行。所有这一切,都是由交易触发的。
交易的结构
交易是一串打包在一起的二进制数据,包括如下内容:
- nonce
一个序列编号,由创建这个交易的外部账号提供,用于防止交易的重放攻击。 - gas price
交易发起方愿意支付的gas(单位:wei)价格。 - gas limit
交易发起方愿意为这个交易支付的最大gas数量。 - recipient
目标以太坊地址。 - value
发送给目标地址的以太币数量。 - data
附在交易中的可变长度的数据。 - v,r,s
由构建交易的外部账户提供的椭圆曲线签名的三个组成部分。
交易内容的结构采用递归长度前缀(RLP)编码标准
。这个标准是以太坊专门创建的,主要是为了打包准确且字节合适的数据。以太坊交易中所有的数字都采用大端模式编码,长度都是8比特的倍数。
请注意,上面这些结构的名字(to,gaslimit等)都是为了清楚表达而列出的,但这些并不是以太坊交易数据包的内容,数据包中的RLP编码已经包含了字段的定义信息。总体来说,RLP编码中不会包含任何字段标识符或者标签。RLP的长度偏移量用来表示每一个字段的长度。任何超过定义的长度,就自然属于结构体中下一个字段的内容了。(大概就类似TCP包这种感觉)
交易的随机数
nonce
是交易中最重要确实最难理解的一个概念。以太坊黄皮书对nonce的定义如下:
- nonce:一个数值,等于这个地址发出的交易数量,当这个地址与合约关联时,是这个地址所创建的合约数量。
严格来说,nonce
是发起方地址的一个属性,也就是说它只在发送地址的上下文中才有意义。而且nonce
也不会作为账户状态的一部分显式地保存在区块链上。相反,它是通过计算发送方地址已经确认的交易数量而动态计算的。
在两种情况下,交易数量随机数的存在是非常重要的:包含在交易创建顺序中的可用性特征。以及交易重复保护的重要特征。让我们看一下每个场景的示例:
- 你想进行两笔交易,其中一笔是需要支付6 ether的重要付款,另一笔交易需要支付8ether。首先,你签名并广播了那笔6ether的交易。因为它比较重要。然后,你又签名并广播了第二笔8ether的交易。遗憾的是,你忽略了一个事实,即你的账户只有10ether。所以(以太坊)网络不可能同时接受这两笔交易:其中一笔会失败。因为你首先发送了那笔更加重要的6ether的交易,于是你理所应当的认为它会被接受。而8ether的交易会被拒绝。但是,在像以太坊这样的去中心化系统中,节点会以任意顺序接收交易;没有任何方法能保证某个节点在另外一个节点之前接收到某笔交易。因此,几乎可以肯定的是,某些节点会先接收到6ether的交易,而一些节点会先接收到8ether的交易。在没有随机数的情况下,接收与否完全是随机的。相反,如果包含了随机数,那么你发送的第一笔交易将具有一个随机数,假设是3,而8ether 的交易将具有下一个随机数(如图 4)。因此,这笔交易将被忽略,直到 nonce值为0到3的交易已经被处理,即便它是先接收到的。
(感觉这个随机数并不算是真正的随机数,它反而像是对交易的编号) - 你有一个包含100ether的账户,太好了。你在网上找到了出售你非常想要的mcguffin挂件的卖家,并且他们接受以太币支付。你付给他们2ether。他们把mcguffin挂件发给了你。为了完成这笔2ether的支付,你签名了一笔交易,将账户里的2ether发送到他们的账户,然后将这笔交易广播到以太坊网络,使其能够被验证并包含到区块链中。现在,在交易没有nonce的情况下,再次将2ether发送给同一地址的交易与第一次交易看起来完全相同。这就意味着任何能在以太坊网络上看见这笔交易的人(包括收件人和你的敌人),都能简单地通过复制粘贴你的原始交易一次又一次的重放该交易,直到你的以太币消耗殆尽。但是,如果加以数据中包含nonce值,那么每一笔交易都是唯一的,即便是多次向同一个收件人地址发送相同数量的以太币交易也是如此。因此,通过将递增的随机数作为交易的一部分,任何人都没有办法“复制”你已经完成的付款。
总之,值得注意的是,与比特币协议使用的 “未花费输出”(UTXO)机制
相比,使用随机数对于基于账户的区块链协议实际上是至关重要的。
保持对随机数的追踪
可以在安装过MetaMask插件的浏览器内打开控制台,输入
1 | window.ethereum |
也可以直接通过RPC API来获取,返回值如下:
1 | { |
nonce是从0开始计数的。当上面的代码返回的值是9,意味着区块链上已经发现了这个地址从0到8的交易,下一个交易中应该写入的nonce值就是⑨。
创建一个新的交易时,你会按照记录把下一个nonce值分配给新交易。但是要知道这笔交易被以太坊网络确认之前,交易中的nonce值都不会被计入getTransactionCount的返回值中。
当我们一次提交几个交易时,getTransactionCount函数可能会发生一些问题。
1 | > web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", \ |
当你的应用程序需要构建和发布交易时,不能依赖getTranscationCount这个函数来计算nonce值,只有当待确认交易和确认交易相等(所有的待确认交易都确认了)的时候,你才可以把getTranscationCount作为nonce计数,在此之后,在应用程序中保持对nonce的追踪,直到每一笔交易都得到确认。
Parity
的JSONRPC接口提供了一个parity_nextNonce
函数,它返回了可供下一个交易使用的nonce值。parity_nextNonce
这个函数的计算方式是正确的。即使同时构建多个交易并且这些交易都没有得到确认,nonce的值也不会被计算错误。
1 | $ curl --data '{"method":"parity_nextNonce", \ |
随机数的差额、重复和确认
以太坊网络是根据nonce数值的顺序处理交易的。
如果你发起一个nonce值为0的交易,接着又发出了一个nonce值为2的交易,那么第二个交易不会被包含在任何区块中。它会被保存在待确认交易内存池中,以太坊网络会一直等待nonce值为1的交易出现。
如果这时再创建一个nonce值为1的交易,那么这两个交易(nonce值为1和2)都会被确认并写入区块链。
这意味着如果你按顺序创建了一系列交易,但其中一个没有得到确认,那么之后的所有交易都会“堵”住,等待这个缺失的交易。如果某个交易的nonce值不对,或者没有足够得gas,就很可能会导致这样的堵车现象,为了疏通堵车,你必须创建一个正确的交易,并使用缺失得那个nonce值。同样值得注意的是,一旦网络验证了“缺失”的交易,那么具有后续随机数的所有交易都会被广播,并逐渐变得有效;而且无法撤回任何交易。
另一方面,如果你不小心创建了重复的交易。例如:发出了两个具有相同nonce值的交易,但是收款地址或交易金额不同,那么其中一个会被确认,而另外一个会被拒绝。最先到达以太坊网络中确认节点的那个交易会被确认(这会相当随机)。
如你所见,追踪nonce的值是非常有必要的,如果你的应用程序没有正确的管理nonce,那么你就会遇到麻烦。糟糕的是,在并发交易的情况下,这个问题会变得更加困难。
并发情况下的随机数
处理并发是计算机科学中的一个难题,有时会在意想不到的情况下出现并发问题,尤其是去中心化的,分布式的,实时处理的系统中。如以太坊。
简单的说,并发就是指多个独立的系统同时进行一项计算任务,可以是在同一个程序中(例如多线程),在同一颗CPU上(例如多进程),或者在不同的计算节点上(分布式系统),以太坊是一个允许并发操作(包含节点,客户端,去中心化的应用)。但通过共识强制维持单体状态的系统。
假设我们有多个独立的钱包软件,正在使用相同的外部账户地址生成交易。这种情况的例子可以是交易所处理从热钱包中提币的操作(热钱包中的密钥是在线存储的,而冷钱包则不联网)。理想情况下,你一般会有不止一台计算机用于处理这些提币申请,这样不至于出现处理瓶颈或者单点故障。然而,这很快会引发另一个问题。使用多于一台计算机处理提币申请时,会导致一些令人头疼的并发问题,不仅仅是如何设定nonce的问题。如何使用多台计算机,针对同一个热钱包进行地址生成签名和广播交易呢?
你应该使用一台计算机来分配nonce,以先到先得的方式给那些需要对交易进行签名的计算机。然而,这台计算机就会成为单点故障。更糟糕的是,如果分配了多个nonce,但是由于某些原因,有一个nonce没有被使用(例如:处理哪个Nonce的计算机正好出故障),那么随后所有的交易都会堵住
你也可先生成交易,但是不签名,或分配nonce(由于nonce是交易数据的一部分,因此必须包含在用于认证交易的数字签名中)。然后把这些交易传送到一台计算机上排队处理,逐一给他们分配nonce,并对签名交易。不过,这也会导致单点故障的存在。当处理量比较大的时候,进行签名和分配nonce的操作就会成为一个拥堵的节点。尽管这些没有被签名的交易并非一定需要并发处理。我们虽然名义上使用了并发,但是在关键环节上并没有带来任何用处。
最终,这些并发问题引发的在独立进程中追踪账户余额和交易确认的难题,迫使多数软件实现过程放弃使用并发,而不得不使用可能导致瓶颈的单一进程来处理所有的交易所提币操作,或者使用完全独立处理提币操作只需间歇性重新计算余额的热钱包。
交易的gas
gas
就是以太坊的燃料。gas
并不是以太币,它是一种独立的虚拟货币,跟以太币之间存在汇率的关系。以太坊使用gas来控制交易对资源的使用,因为交易会在数以千计的以太坊节点上被处理。开放型(图灵完备)的计算模式需要一种计量单位,以确保避免拒绝服务式攻击或过度消耗资源的交易。
gas
独立于以太币,是为了在以太币价格大幅度波动的情况下,仍旧保护系统的灵活性。同时,对于各种消耗gas
的资源(比如,计算、内存和存储),gas
能够管理他们之间重要而敏感的汇率关系。
交易中的gasPrice
字段允许交易的发起方设定针对gas
的汇率。gas
价格的计量单位是 wei/gas
,例如,我们刚才在本书中创建的交易示例,钱包软件把gasPrice
设定为3gwei(3gigawei,即30亿wei)。
钱包软件可以调整它们所发起的交易中的gasPrice值,这样就可以让交易更快得到区块链的确认(更快被矿工打包)。gasPrice越高,交易被确认的速度也越来越快。与之对应的是,相对低优先级的交易可以支付比平均值低的gas费用,这样它们被确认的时间也会较长。gasPrice最低可以为零,这意味着没有手续费的交易。如果区块中尚有多余的空间,那么这样的交易也会被矿工打包并提交到区块链上。
与gas相关的第二个重要字段是gasLimit
。简单地说,gasLimit
表示这个交易的发起方为了完成交易所愿意支付的最大gas数量。对于简单的以太币转账来说,从一个外部账户转账到另一个外部账户,所需要的gas数量是固定的21000单位个gas。为了算出这些 gas对应的以太币数量,只需要使用 21000乘以你所指定的gasPrice。
如果你的交易目标地址是一个合约,那么需要gas的数量可以估计,但是很难精确算出,因为合约可以根据不同的初始条件选择不同的执行路径,这样会导致不同的gas开销。这意味着合约可能只执行一个简单的计算,也可能是更复杂的计算。这些可能取决于一些你无法控制或预判的外部因素。为了解释清楚这个概念,我们举个例子:
假设有这样一个智能合约,每次被调用时,它的计数器都会加一,并且执行一个特殊的循环,执行次数等于调用次数。有可能在第100次调用是,会出现一个特殊的奖励(像中彩票一样)。但是需要额外的计算量才能算出奖励,如果你调用了合约99次,都只会发生一件事情,但是第100次调用却完全不同。你所支付的gas数量取决于在你的交易被包含进区块之前,这个合约已经被调用了多少次。可能你所预估的gas是基于99次调用的。但是恰好在你的交易被矿工确认之前,其他人发起了第99次调用。现在你变成了发起第100次调用的那个人,这次调用所对应的计算量,也就是你需要支付的gas开销相比之前会大很多。
借助以太坊常用的比喻,你可以把gasLimit字段想象为汽车的油箱(这里把交易比喻为汽车)。你会在启程前加够你认为这一路所需要的汽油(完成交易所需要的计算量)。你可以适当地估计油量,但是由于路上的突发情况,或者临时的线路更改(合约转入一个更复杂的计算分支),这些都可能增加汽车的油耗。
也许油耗的比喻有一些误导,这更像一个用于加油站的信用账户·。你可以在旅程结束后支付,基于实际的gas开销。当你发出交易时,验证交易的第一个步骤就是确保发起交易的账户中有足够的以太币来支付对应的gas费用(即gasPrice*gas).但是这时并不会从账户中直接扣除gas,而是直到交易执行结束后才会扣除。你只会被扣除交易所执行实际发生的gas.但是账户中必须有高于你在交易中指定的最高的gas费用对应的以太币,这个交易才能通过验证。
(gasLimit没咋懂)
交易的接收方
交易的接收方在to
字段中指定。这个字段包含了一个20字节的以太坊地址。这个地址既可以是外部账户,也可以是合约的地址。
以太坊不会进一步验证这个地址。任何一个20字节的值都被认为是正确的。如果这20字节是一个没有私钥的地址,或者没有对应的合约。这个交易依然合法。以太坊无法判断这个地址是否是从已知的公钥(因而这个地址会有一个可以解锁的私钥)衍生而来的。
以太坊协议也不会验证接收方地址的正确性,你可以向一个没有对应私钥的“地址’’发送以太币。这就相当于是销毁以太币。接收方地址的验证工作需要在用户应用这一层完成。
实际上,也存在一些合理的销毁以太币的场景,如支付通道和其他合约存在欺诈行为时,由于以太币是有限的,销毁以太币能有效地将价值传到所有以太币持有者手上。
交易中的以太币和数据
交易数据包的核心是这两个字段:value
和data
。交易可以同时包括value和data,只有value只有data或既没有value也没有data这几种情况都是正确的且合法的。
只包含value的交易是支付操作。只包含data的交易是针对合约的调用。既没有value也没data的交易也许只是为了浪费gas(笑死),但是也是允许的。
向外部账户和合约转账的交易
当你构建的以太坊交易中包含value,也就是转账支付的以太币数量时,交易的行为模式取决于你的目的地址是另一个外部账户,还是一个合约。
对于外部账户,或者任何在区块上没有注册为合约的地址,以太坊会记录这一次的状态变化,在目的账户余额中增加你所指定的以太币。如果你这个地址之前不存在,那么以太坊会在区块链上创建这个地址,把它的余额初始化你在支付交易中指定的value值。
如果目标地址(to)是一个合约,那么EVM会执行这个合约,并尝试调用在交易的data字段中指定的函数。如果交易中的data字段是空的,那么EVM会指定目标合约的回退函数,如果这个会退函数是可支付的,那么根据函数的代码来决定下一步的执行动作。如果没有回退函数,那么交易的效果就是增加合约的余额,如同向钱包支付一样。
合约可以在可支付函数被调用时,立刻通过抛出异常的方式拒绝转入的支付,也可以根据可支付回退函数的逻辑做出决定。如果可支付回退函数正常执行(没有异常),那么合约的状态就会被更新,以此反映出合约账户余额的变化。
向外部账户和合约传送数据的交易
当交易的data字段含有内容时,多数情况下这个交易的目标是一个合约。这并不意味着你不能通过交易向外部账户发送data字段。实际上,你完全可以这么做。然而,在这种情况下,对data字段内容的解读取决于目标外部账户的主人所使用的钱包软件。以太坊协议并未对此作出规定。大部分钱包软件会忽略针对外部账户交易中包含在 data字段中的内容。在将来,也许会有一些标准来规范化钱包软件如何使用交易中包含在data字段中的内容,这样也许可能允许用户调用运行在用户钱包内部的函数,非常重要的一点区别在于,外部账户对交易中的data内容的解读和使用,都不属于以太坊区块链共识的一部分,这是完全不同于合约执行的。
现在我们假设你的交易包含data字段的内容,目标是一个合约地址,在这样的情况下,data字段的内容会被EVM解读为针对合约的函数调用,调用data中指定的函数,并把需要的参数传递给这个函数。
发送给合约的data字段内容是通过十六进制编码的:
- 函数选择器
被调函数原型的Keccak-256
哈希值的前4个字节。这允许EVM准确无误的识别被调函数。 - 函数参数
函数的参数,根据EVM多种实现规则定义的编码结构。
在之前的Faucet的代码中,我们定义了一个用于提币的函数:
1 | function withdraw(uint withdraw_amount) public { |
这个方法的原型定义为包含函数名称和圆括号内每一个参数类型的字符串。函数的名称withdraw
,它接收一个参数,类型是 unit(也就是unit256)。所以这个函数的定义就是:
1 | withdraw(uint256) |
这个哈希值的前4个字节是0x2e1a7d4d。这是“函数选择器”的值,用于告诉EVM调用的是哪一个函数。
接着我们来计算作为参数传递给withdraw-amount的那个值。我们希望提取0.01ether。因此把这个值编码为十六进制大端字节序 无符号256位整数,采用wei为单位:
1 | > withdraw_amount = web3.utils.toWei(0.01, "ether"); |
现在,增加函数选择器之后的调用内容如下(填满了32字节)。
1 | 2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000 |
这就是我们这个交易中data字段的内容,调用withdraw方法并通过withdraw-amount参数请求提取0.01ether。
特殊交易:合约创建
有一种特殊的交易值得我们关注,即在区块链上创建新合约的交易,这些合约用于未来有需求的场合。合约注册交易的目的地是一个特殊的地址——零地址。简单地说,合约注册交易的to
字段包含的是0x0
。这个地址既不是一个外部账户地址(没有与之对应的私钥或者公钥),也不是一个合约地址。这个地址永远不能用来支付以太币或者触发交易。它只是作为一个目标,一个带有特殊的“注册合约”含义的目标地址。
尽管全零地址仅用来进行合约注册,但有时候这个地址也会收到来自其他账户的以太币转账。对这个情况有两种解释:一种情况是由于误操作,这就导致了以太币的丢失;另一种情况是有意的销毁以太币(故意把以太币发到永远不会花费他的地址)。如果你希望有意的销毁一些以太币,那么需要向网络明确表示你的意图,使用这个特殊的销毁地址:
1 | #因为在网上没有找到关于这个地址的其他信息所以就不贴了,销毁的话可以发送以太币到黑洞地址 |
合约注册交易中唯一需要的就是在data
字段中包含经过编译的合约字节码
。这个交易的唯一用处就是把合约注册到以太坊区块链上。你可以在value
字段中包含以太币,从而为新的合约设置起始余额
,但这完全是可选的。
可以通过eth_getTransactionReceipt
查看创建协议的信息。
1 | { |
1 | { |
数字签名
以太坊中数字签名起到了三个作用:
- 数字签名用来证明签名方是私钥的持有人,因此也就是对应以太坊账户的主人,用于兽圈以太坊的转账或合约的执行。
- 用于证明这个“授权”是不可否认的。
- 用于确保交易数据在经过签名之后没有也不能被任何人修改。
维基百科上关于数字签名的定义
数字签名是一种数学标准,用于证明某个数字消息或文档的真实性。一个合法的数字签名可以人接收方相信消息来自于已知的发送方(身份认证),发送方不能否认发送过这个消息(不可否认),同时这个消息在传输过程中是无法被修改的(完整性)。
数字签名的工作原理
数字签名的数学标准中包含两个部分。
- 第一部分适用于创建签名的算法,针对一个消息(列如我们的以太坊交易数据包)使用私钥(签名密钥)进行签名。
- 第二部分是一个允许任何人使用消息和公钥进行签名验证的算法。
创建数字签名
在以太坊的ECDSA
实现中,被签名的“消息”是交易的数据包,或者更准确地说,是经过RLP编码
的交易数据包的Keccak-256
哈希值。签名密钥是外部账户的私钥,这个算法的输出结果就是一个数字签名:
其中:
- 是用于签名的私钥
- 是经过
RLP编码
的交易数据包 - 是
Keccak-256
哈希函数 - 是签名算法
- 是输出的数字签名
函数生成了一个签名,这个签名包含两部分内容,通常称为和:
验证数字签名
为了验证签名,我们必须有签名本身(即和)、交易数据包和公钥(对应着这个签名的私钥)。
验证签名意味着:只有私钥的持有者才能够针对交易生成这样的签名。
签名验证算法的输入包括交易数据包(其实是交易的哈希值的一部分)、签名方的公钥和签名(和值),如果针对消息和公钥的签名验证成功,算法会返回true
。
ECDSA
背后的数学过程
签名算法首先以密码学安全的方式生成一个*临时的私钥。这个临时的密钥用于计算和的值,以确保攻击者无法通过在以太坊上查看已签名的交易来计算发送者的真实密钥*。
这个临时私钥是由以下两个输入决定的:
- 一个密码学安全的随机数q,作为临时密钥。
- 从q生成的临时公钥Q,以及椭圆曲线上的生成点G。
数字签名中的r值就是临时公钥Q的x轴
的值。
在此基础上,算法计算数字签名的s值,通过如下公式:
其中:
- q是临时私钥
- r是临时公钥对应的x轴坐标
- k是用于签名的私钥(外部账户持有人私钥)
- m是被签名的交易数据包(的哈希值)
- p是椭圆曲线上的素数阶
p就是前面椭圆曲线公式定义里的p,为一个很大的素数。
secp256k1曲线的定义为:
签名生成流程图:
验证签名是一个相反的操作,使用r,s和公钥的值来计算Q,Q是椭圆曲线上的一个点(用于本次签名创建的临时公钥),具体的步骤如下:
- 检查所有的输入都是正确的形式
- 计算
- 计算
- 计算
- 计算椭圆曲线上的点
其中:
- r和s是数字签名的值
- K是签名方(外部账户持有人)的公钥
- m是被签名的交易数据包
- G是椭圆曲线上的生成点
- p是椭圆曲线上的素数阶
如果计算得到的Q点对应的x轴坐标等于r,那么就可以认定这个签名是合法的。
注意在整个签名验证的过程中,并不需要对外提供私钥(零知识证明?)。
ECDSA的数学实现是非常复杂的,远远超出了本书的范围,网上有一些非常详细的关于椭圆曲线的介绍,请搜索“ECDSA Explained”,或访问Understanding How ECDSA Protects Your Data. : 15 Steps - Instructables
验证签名流程图:
这里应该使用到了零知识证明来验证签名是否合法。
因为网上的教程都有些难理解,这里我拿RSA算法举例,附上我的理解,如果有不对的地方还请大佬来指正。
RSA算法生成了私钥和公钥
正常传输数据时,我们将数据用公钥加密,接收方用私钥解密,即可做到信息的加密传输。
如果我们想将RSA用作签名,我们就需要在发出我们信息时,证明这条信息是由我来发出的。
这时候我们就可以用私钥对信息进行一次加密,加密后的信息则只能被公钥所解密。因为公私钥是成对生成的,因此如果只要信息能被我的公钥解密,就说明消息的发出方是我(除非我的私钥已经被泄露)。
交易签名实践
为了生成一个正确的交易,交易的发起方必须在交易的数据包中附带上使用椭圆曲线数字签名算法生成的数字签名。当我们说“对某个交易签名”时,实际上是指对“采用RLP编码的交易数据包的Keccak-256哈希值”进行签名。签名是针对交易数据包的哈希值进行的,而不是针对交易数据包本身。
在以太坊中签名一个交易,发起方需要:
生成一个交易数据包,包括九个字段:
nonce
、gasPrice
、gasLimit
、to
、value
、data
、chainID
、0
、0
。这里这个0,0我确实没看懂啊,我去GitHub找了英文版也写的是0,0。
但是我在网上找的盗版中文版又写的是另外的。
把交易数据包进行
RLP格式编码
。计算这个交易的
Keccak-256哈希值
。计算
ECDSA签名
,使用交易方发起方的私钥进行签名。在交易中插入经过
ECDSA签名
获得的v、r和s的值。
特殊签名变量v代表两件事情:链ID,以及帮助ECDSArecover
函数检查签名的恢复标识符。它被计算为27或28,或者将链ID加倍后再加上35或36。关于链ID更加详细的介绍请参考本章“使用EIP-155创建原生交易”一节的内容。恢复标识符(在“老式”签名中是27或者28,在完整的SpuriousDragon
式签名中是35或者36)用来表明公钥的y分量奇偶性,更多细节同样在“使用EIP-155创建原生交易”一节中有介绍。
原生交易创建和签名
这一节我们来创建一个原生交易并对它进行签名,使用ethereumjs-tx
这个程序库。这个例子展示了钱包中的常用功能,以及应用程序如何为用户签署交易。例子源代码位于:ethereumbook/code/web3js/raw_tx at develop · ethereumbook/ethereumbook (github.com)
运行前要先安装环境:
1 | npm init |
这里直接运行可能会有些问题
盲猜应该是ethereumjs-tx版本导致的,因为不清楚具体要哪个版本,然后在搜索的时候发现好像有人也遇到了这个问题,然后回答提供了ethereumjs-tx的GitHub的地址,上面有一个实例代码。ethereumjs/ethereumjs-tx: Project is in active development and has been moved to the EthereumJS VM monorepo. (github.com)
1 | const EthereumTx = require('ethereumjs-tx').Transaction |
对比了下,发现问题可能出现在data这里
书上的代码改成:
1 | const txData = { |
使用EIP-155创建原生交易
名为“简单重放攻击保护”的EIP-155
标准制定了带有重放攻击保护能力的交易的编码格式,这个标准在交易签名之前包含了一个链标识符。这确保在一个以太坊区块链(如以太坊主网)上创建的交易在其他以太坊区块链(如以太坊经典)上是不合法的。因此,在一个网络上广播的交易不能被重放到另一个网络,这就是重放攻击保护名字的来源。
EIP-155
在包含六个字段的交易的数据结构中添加了三个字段,分别是链标识符、0和0。这三个值在交易被编码和哈希之前加入数据结构中。因此这三个值会改变交易的哈希值,这个哈希值之后是用来被签名的。
链标识符会根据交易处在的以太坊网络取不同的值:
Chain | Chain ID |
---|---|
Ethereum mainnet | 1 |
Morden (obsolete), Expanse | 2 |
Ropsten | 3 |
Rinkeby | 4 |
Rootstock mainnet | 30 |
Rootstock testnet | 31 |
Kovan | 42 |
Ethereum Classic mainnet | 61 |
Ethereum Classic testnet | 62 |
Geth private testnets | 1337 |
产生的交易数据经过RLP编码、哈希并签名。这个签名算法经过少量的修改,以对v前缀中的链标识符进行编码。
更多细节请参考EIP-155规范。
签名的前缀值(v)和公钥恢复
获得了公钥之后,就很容易计算出交易发起方的以太坊地址。整个过程称为公钥恢复。
对于给定的供椭圆曲线进行计算的r和s,我们可以算出两个可能的公钥。
首先,我们通过签名中r值的x坐标,计算椭圆曲线上的点R和R’。因为椭圆曲线是关于x轴对称的,所以对于x值,在椭圆曲线上有两个满足的点,分列在x轴的两侧。
通过,我们也可以计算出,这是的模乘法逆运算。
最终我们可以计算得到z,这是交易哈希值低位的第n位,其中n是椭圆曲线的阶。
因此两个可能的公钥是:
和
其中:
- 和是交易发起方的两个可能的公钥
- 是签名中值的模乘法逆运算
- 是签名中的值
- 和是临时公钥的两个可能的值
- 是交易哈希值低位的第位
- 是椭圆曲线的生成点
为了让事情变得更高效,交易签名中包含了一个v字段。如果v是偶数,那么就是正确的值。如果v是奇数,那么就是临时公钥。通过这样的方式,我们只需要计算R就可以得到K的值。
离线签名
当交易被签名后,它就可以被广播到以太坊网络。通常交易的创建签名和广播都是在一个操作中完成的。例如:web3.eth.sendTranscation
。然而,如我们在本章“原生交易创建和签名”一节中所见,你可以把创建和签名交易的动作分成两步来执行。签名完成后,你可以使用 web3.eth.sendSignedTransaction
广播交易,这个方法接受十六进制编码和经过签名的交易数据包,并把这个数据包广播到以太坊网络上。
为什么我们想要把交易的签名和广播分开处理呢?主要原因是安全。用于签名的计算机必须保存在外部账户的私钥,用于广播交易的计算机必须连接在互联网上,并且运行在以太坊客户端。如果这两个操作在用一台计算机上完成,那么就相当于把私钥放置在了一台联网的计算机上,这是非常危险的。把交易的签名和广播分开处理的做法称为离线签名(联网的设备和未联网的设备分别处理),这是一种非常常见的安全实践。
- 为当前状态的账户通过在线的计算机创建一个未签名的交易,可以检索该账户当前的
nonce
和余额。 - 将未签名的交易转移到“空气隔离”的离线设备以便进行签名,如通过QR码或USB闪存设备。
- 将已签名的交易转移回在线设备,以便在以太坊区块链上进行广播,如通过QR码或USB闪存设备。
取决于我们所需要的安全等级(等保是吧),用于“离线签名’’的计算机可以有不同等级的安全防护,可以是处在防火墙之后的专用子网中的计算机,也可以是一台完全离线的计算机(我们称之为空气隔离)在”空气隔离“的系统中根本就没有网络连接,计算机完全隔离于任何网络环境。在隔离环境完成交易签名之后,你需要通过存储介质,或者网络摄像头和QR编码的方式把签名后的数据包转移到在线计算机。这也意味着每一笔交易都需要这样的方式才能转移到在线计算机,这太不方便了。
很多情况下完全空气隔离的系统是不可用的,但是即使是一定程度的隔离,也会带来极大的安全优势。例如,一个在防火墙之后的专用子网,只允许消息队列协议通过,这相比在网上系统处理签名而言,攻击面更小,更安全。很多公司在交易签名计算机使用ZeroMQ(0MQ)消息队列
,它的攻击面很小。使用这样的设置,交易通过消息队列完成签名。消息队列协议采用类似TCP数据包的方式把交易数据包发送给签名计算机。签名计算机从消息队列中读取交易数据包,使用恰当的密钥对交易进行签名,然后把签名之后的交易数据包发回到回复消息队列。这个回复消息队列接着把交易发送给带有以太坊客户端的联网计算机,进行交易广播。
交易的传播
以太坊网络使用“洪泛”路由协议。每一个以太坊客户端都是P2P网络上的一个节点,这些节点构成了一个网状的结构。没有任何节点是特殊的,它们之间的身份都是对等的。我们会使用“节点”来代表连接在P2P网络上的以太坊客户端。
交易的广播从一个发起以太坊节点的交易创建(或从离线设备接收)和签名开始。交易通过验证后,会传送给所有跟这个发起节点直接相连的以太坊节点。平均而言,每一个以太坊节点大约维护了至少13个跟他直接相连的其他节点,称为邻居。每一个邻居节点都会在收到交易数据包后立刻进行验证。如果他们确认交易数据包是合法的,这些节点就会保留一份交易副本,然后把交易数据包广播给与他们相连的邻居(除去消息来源的那个上游节点)。因此交易就像水中的波纹一样,在整个网络中迅速传播开,直到所有节点都收到了一份交易的副本。节点可以对所广播的消息进行过滤,但默认情况下,节点会广播收到所有的验证消息。
在短短的几秒钟时间内,一个以太坊交易就能到达全球范围内所有的以太坊节点。从单个节点的角度而言,它无从知晓这个消息的来源。向这个节点发送交易的邻居,可能只是扮演了“二传手’’的角色,但也可能是交易的真正发起方。为了追踪到交易的最初发起方,或者影响交易在以太坊中传播的形式,攻击者必须要控制网络中的大部分节点。这是P2P网络安全和隐私性的设计,对于区块链而言尤为重要。
记录在区块链上
尽管以太坊网络上的所有节点都是平等的,但有些节点会扮演矿工的角色,这些*矿工把交易打包成区块链,投入到具备高性能GPU的矿场中,这些挖矿用的节点把一些交易打包成候选区块,使用“*工作量证明’’算法完成挖矿并达成全网共识。
不需要深入细节,合法验证过的交易最终会被包含在一个区块中,并且记录在以太坊区块链上。计入区块链之后,交易就会更改以太坊网络的单体,修改账户余额(对于转账类交易而言),或者调用改变内部状态的合约。这些变化在交易进行的同时被记录,称之为交易的收据,也可能会包含一些事件。
一笔交易完成了他从创建、由外部账户私钥签名,广播最终写入区块链的旅程。它会触发以太坊单体状态的修改,并在区块链上留下自己的印记。
多签名交易
如果你对比特币脚本的功能很熟悉的话,就会知道创建多重账户是可能的,该账户只有在多方共同签名的情况下才能花费其中的资金(比如2或者3或者4个签名中的2个)以太坊基本的外部账户值交易并没有提供多重签名的功能,但是,任何有关签名的限制都可以通过智能合约来强制执行,包括你能想到的关于交易以太币和其他通证的任何条件。
为了利用这种能力,以太币必须被转移到“钱包合约”,该合约对支出规则进行了编程,例如:多重签名或者要求支出限额(或者两者的结合)。一旦满足支出条件,钱包合约将在外部账户的授权提示下进行支出。所以,要想在多重条件下保护你的以太币,请将以太币转移到多重签名合约。每当你想将资金发送到另外一个账户时,所有必要的用户都需要通过常规的钱包应用向合约发送交易,从而有效授权合约执行最终的交易。
这些合约还可以被设计为在执行本地代码之前需要多个签名或者触发其他合约。这个方案的安全性最终由多重签名合约的代码所决定
能够为只能合约实现多重签名的能力证明了以太坊的灵活性。但是,这是一把双刃剑,额外的灵活性可能会导致bug,从而破坏多重签名方案的安全性。实际上,有许多提案建议在EVM中添加多重签名命令,以消除对智能合约的要求,至少对于简单的M-of-N多重签名方案
来说是这样的。这就相当于比特币的多重签名系统,它是核心共识规则的一部分,并且已被证明是健壮和安全的。