交易

交易是由外部所有帐户发起的签名消息,由以太坊网络传输,并在以太坊区块链上进行记录(挖掘)。在这个基本定义背后,有很多令人惊讶和着迷的细节。看待交易的另一种方式是,它们是唯一可触发状态更改或导致合约在EVM中执行的东西。以太坊是一个全球的单实例状态机器,交易是唯一可以让状态机“运动”,改变状态的东西。合约不会自行运行。以太坊不会在后台运行。一切都始于交易。

在本节中,我们将剖析交易,展示它们的工作方式,并了解详细信息。

交易的结构

首先让我们来看看交易的基本结构,因为它是在以太坊网络上进行序列化和传输的。接收序列化交易的每个客户端和应用程序将使用其自己的内部数据结构将其存储在内存中,还会使用网络序列化交易本身中不存在的元数据进行修饰。交易的网络序列化是交易结构的唯一通用标准。

交易是一个序列化的二进制消息,其中包含以下数据:

  • nonce: 由始发EOA(外部所有账户)发出的序列号,用于防止消息重播。
  • gas price: 发起人愿意支付的gas价格(以wei为单位)。
  • start gas: 发起人愿意支付的最大gas量。
  • to: 目标以太坊地址。
  • value: 发送到目标地址的ether数量。
  • data: 变长二进制数据。
  • v,r,s: 始发EOA的ECDSA签名的三个组成部分。

交易消息的结构使用递归长度前缀(RLP)编码方案(参见 [rlp] )进行序列化,该方案是专门为以太坊中准确和字节完美的数据序列化而创建的。以太坊中的所有数字都被编码为大端序整数,其长度为8位的倍数。

请注意,字段的标签(“to”,“start gas”等)在这里是为清楚起见而显示,但不是包含字段值的RLP编码交易序列化数据的一部分。通常,RLP不包含任何字段分隔符或标签。RLP的长度前缀用于标识每个字段的长度。因此,超出定义长度的任何内容都属于结构中的下一个字段。

虽然这是实际传输的交易结构,但大多数内部表示和用户界面可视化都使用来自交易或区块链的附加信息来修饰它。

例如,你可能会注意到没有表示发起人EOA的地址的“from”数据。EOA的公钥可以很容易地从ECDSA签名的+v,r,s+组成部分中派生出来。EOA的地址又可以很容易地从公钥中派生出来。当你看到显示“from”字段的交易时,是该交易所用的软件添加了该字段。客户端软件经常添加到交易中的其他元数据包括块编号(被挖掘之后生成)和交易ID(计算出的哈希)。同样,这些数据来源于交易,但不是交易信息本身的一部分。

交易的随机数(nonce)

nonce是交易中最重要和最少被理解的组成部分之一。黄皮书中的定义(见 [yellow_paper] )写道:

nonce:与此地址发送的交易数量相等的标量值,或者,对于具有关联代码的帐户,表示此帐户创建的合约数量。

严格地说,nonce是始发地址的一个属性(它只在发送地址的上下文中有意义)。但是,该nonce并未作为账户状态的一部分显式存储在区块链中。相反,它是根据来源于此地址的已确认交易的数量动态计算的。

nonce值也用于防止帐户余额的错误计算。例如,假设一个账户有10个以太的余额,并且签署了两个交易,都花费6个ether,分别具有nonce 1和nonce 2。这两笔交易中哪一笔有效?在以太坊这样的分布式系统中,节点可能无序地接收交易。nonce强制任何地址的交易按顺序处理,不管间隔时间如何,无论节点接收到的顺序如何。这样,所有节点都会计算相同的余额。支付6以太币的交易将被成功处理,账户余额减少到4 ether。无论什么时候收到,所有节点都认为与带有nonce 2的交易无效。如果一个节点先收到nonce 2的交易,会持有它,但在收到并处理完nonce 1的交易之前不会验证它。

使用nonce确保所有节点计算相同的余额,并正确地对交易进行排序,相当于比特币中用于防止“双重支付”的机制。但是,因为以太坊跟踪账户余额并且不会单独跟踪独立的币(在比特币中称为UTXO),所以只有在账户余额计算错误时才会发生“双重支付”。nonce机制可以防止这种情况发生。

跟踪nonce

实际上,nonce是源自帐户的 已确认 (已开采)交易数量的最新计数。要找到nonce是什么,你可以询问区块链,例如通过web3界面:

  • Retrieving the transaction count of our example address
    web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f")
    40
    

该nonce是一个基于零的计数器,意味着第一个交易的nonce是0.在Retrieving the transaction count of our example address中,我们有一个交易的计数为40,这意味着从0到39nonce已经被看到。下一个交易的nonce将是40。

你的钱包将跟踪其管理的每个地址的nonce。这很简单,只要你只是从单一点发起交易即可。假设你正在编写自己的钱包软件或其他一些发起交易的应用程序。你如何跟踪nonce?

当你创建新的交易时,你将分配序列中的下一个nonce。但在确认之前,它不会计入 getTransactionCount 的总数。

不幸的是,如果我们连续发送一些交易,getTransactionCount 函数会遇到一些问题。有一个已知的错误,其中 getTransactionCount 不能正确计数待处理(pending)交易。我们来看一个例子:

web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
40
web3.eth.sendTransaction({from: web3.eth.accounts[0], to: "0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.toWei(0.01, "ether")});
web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
41
web3.eth.sendTransaction({from: web3.eth.accounts[0], to: "0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.toWei(0.01, "ether")});
web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
41
web3.eth.sendTransaction({from: web3.eth.accounts[0], to: "0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.toWei(0.01, "ether")});
web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
41

如你所见,我们发送的第一笔交易将交易计数增加到了41,显示了待处理交易。但是当我们连续发送3个更多的交易时,getTransactionCount 调用并没有正确计数。它只计算一个,即使在mempool中有3个待处理交易。如果我们等待几秒钟,一旦块被挖掘,getTransactionCount 调用将返回正确的数字。但在此期间,虽然有多项交易待处理,但对我们无帮助。

当你构建生成交易的应用程序时,无法依赖 getTransactionCount 处理未完成的交易。只有在待处理和已确认相同(所有未完成的交易都已确认)时,才能信任 getTransactionCount 的输出以开始你的nonce计数器。此后,请跟踪你的应用中的nonce,直到每笔交易被确认。

Parity的JSON RPC接口提供 parity_nextNonce 函数,该函数返回应在交易中使用的下一个nonce。parity_nextNonce 函数可以正确地计算nonce,即使你连续快速构建多个交易,但没有确认它们。

  • Parity 有一个用于访问JSON RPC接口的Web控制台,但在这里我们使用命令行HTTP客户端来访问它:
    curl --data '{"method":"parity_nextNonce","params":["0x9e713963a92c02317a681b9bb3065a8249de124f"],"id":1,"jsonrpc":"2.0"}' -H "Content-Type: application/json" -X POST localhost:8545
    {"jsonrpc":"2.0","result":"0x32","id":1}
    
nonce的间隔,重复的nonce和确认

如果你正在以编程方式创建交易,跟踪nonce是十分重要的,特别是如果你同时从多个独立进程执行此操作。

以太坊网络根据nonce顺序处理交易。这意味着如果你使用nonce +0+传输一个交易,然后传输一个具有nonce +2+的交易,则第二个交易将不会被挖掘。它将存储在mempool中,以太坊网络等待丢失的nonce出现。所有节点都会假设缺少的nonce只是延迟了,具有nonce +2+的交易被无序地接收到。

如果你随后发送一个丢失的nonce 1+的交易,则交易(交易+1+和+2)将被开采。一旦你填补了空白,网络可以挖掘它在mempool中的失序交易。

这意味着如果你按顺序创建多个交易,并且其中一个交易未被挖掘,则所有后续交易将“卡住”,等待丢失的事件。交易可以在nonce序列中产生无意的“间隙”,比如因为它无效或gas不足。为了让事情继续进行,你必须传输一个具有丢失的nonce的有效交易。

另一方面,如果你不小心重复一个nonce,例如传输具有相同nonce的两个交易,但收件人或值不同,则其中一个将被确认,另一个将被拒绝。哪一个被确认将取决于它们到达第一个接收它们的验证节点的顺序。

正如你所看到的,跟踪nonce是必要的,如果你的应用程序没有正确地管理这个过程,你会遇到问题。不幸的是,如果你试图并发地做到这一点,事情会变得更加困难,我们将在下一节中看到。

并发,交易的发起和随机数

并发是计算机科学的一个复杂方面,有时候它会突然出现,特别是在像Ethereum这样的去中心化/分布式实时系统中。

简单来说,并发是指多个独立系统同时进行计算。这些可以在相同的程序(例如线程)中,在相同的CPU(例如多进程)上,或在不同的计算机(即分布式系统)上。按照定义,以太坊是一个允许操作(节点,客户端,DApps)并发的系统,但是强制实施一个单一的状态(例如,对于每个开采的区块只有一个公共/共享状态的系统)。

现在,假设我们有多个独立的钱包应用程序正在从同一个地址或同一组地址生成交易。这种情况的一个例子是从热钱包进行提款的交易所。理想情况下,你希望有多台计算机处理提款,以便它不会成为瓶颈或单点故障。然而,这很快就会成为问题,因为有多台计算机生产提款会导致一些棘手的并发问题,其中最重要的是选择nonce。多台电脑如何从同一个热钱包账户协调生成,签署和广播交易?

你可以使用一台计算机根据先到先得的原则为签署交易的计算机分配nonce。但是,这台电脑现在是可能故障的单点。更糟糕的是,如果分配了多个nonce,并且其中一个从没有被使用(因为计算机处理具有该nonce的交易失败),所有后续交易都会卡住。

你可以生成交易,但不为它们签名或为其分配临时值。然后将它们排队到一个签名它们的节点,并跟踪随机数。再次,你有了一个可能故障的单点。nonce的签名和跟踪是你的操作的一部分,可能在负载下变得拥塞,而未签名交易的生成是你并不需要实现并行化的部分。你有并发性,但不是在过程中任何有用的部分。

最后,除了跟踪独立进程中的账户余额和交易确认的难度之外,这些并发问题迫使大多数实现朝着避免并发和创建瓶颈进行,诸如单个进程处理交易所中的所有取款交易。

交易gas

我们在 [gas] 中详细讨论 gas 。但是,让我们介绍有关交易的 gasPrice 和 startGas 字段的一些基本知识。

gas是以太坊的燃料。gas不是ether,它是独立的虚拟货币,有相对于ether的汇率。以太坊使用gas来控制交易可以花费的资源量,因为它将在全球数千台计算机上处理。开放式(图灵完备的)计算模型需要某种形式的计量,以避免拒绝服务攻击或无意中的资源吞噬交易。

gas与ether分离,以保护系统免受随着ether价值快速变化而产生的波动。

交易中的 gasPrice 字段允许交易创建者设置每个单位的gas的汇率。gas价格以每单位gas多少 wei 测量。例如,在我们最近一个例子创建的交易中,我们的钱包已将 gasPrice 设置为 3 Gwei(3千兆,30亿wei)。

网站 ethgasstation.info 提供有关以太坊主网络当前gas价格以及其他相关gas指标的信息:https://ethgasstation.info/

钱包可以在他们发起的交易中调整 gasPrice,以更快地确认(挖掘)交易。gasPrice 越高,交易可能被验证的速度越快。相反,较低优先级的交易可能会降低他们愿意为gas支付的价格,导致确认速度减慢。可以设置的最低+gasPrice+ 为零,这意味着免费的交易。在区块空间需求低的时期,这些交易将被开采。

最低可接受的gasPrice为零。这意味着钱包可以产生完全免费的交易。根据能力的不同,这些可能永远不会被开采,但协议中没有任何禁止免费交易内容。你可以在以太坊区块链中找到几个此类交易成功开采的例子。

  • web3界面通过计算几个区块的中间价格来提供gasPrice建议:
    truffle(mainnet)> web3.eth.getGasPrice(console.log)
    truffle(mainnet)> null BigNumber { s: 1, e: 10, c: [ 10000000000 ] }
    
  • 与gas有关的第二个重要领域是 startGas。这在 [gas] 中有更详细的解释。简单地说,startGas 定义交易发起人愿意花费多少单位完成交易。对于简单付款,意味着将ether从一个EOA转移到另一个EOA的交易,所需的gas量固定为21,000个gas单位。要计算需要花费多少ether,你需要将你愿意支付的 gasPrice 乘以21,000:
    truffle(mainnet)> web3.eth.getGasPrice(function(err, res) {console.log(res*21000)} )
    truffle(mainnet)> 210000000000000
    

如果你的交易的目的地址是合约,则可以估计所需的gas量,但无法准确确定。这是因为合约可以评估不同的条件,导致不同的执行路径和不同的gas成本。这意味着合约可能只执行简单的计算或更复杂的计算,具体取决于你无法控制且无法预测的条件。为了说明这一点,我们使用一个颇为人为的例子:每次调用合约时,它会增加一个计数器,并在第100次(仅)计算一些复杂的事情。如果你调用99次合约,会发生一件事情,但在第100次调用时,会发生完全不同的事情。你要支付的gas数量取决于交易开采前有多少其他交易调用了该功能。也许你的估计是基于第99次交易,并且在你的交易被开采之前,其他人已经调用了99次合约。现在,你是第100个要调用的交易,计算工作量(和gas成本)要高得多。

借用以太坊使用的常见类比,你可以将startGas视为汽车中的油箱(你的汽车是交易)。你认为它需要旅行(验证交易所需的计算),就用尽可能多的gas填满油箱。你可以在某种程度上估算金额,但你的旅程可能会有意想不到的变化,例如分流(更复杂的执行路径),这会增加燃油消耗。

然而,与燃料箱的比较有些误导。这更像是一家加油站公司的信用账户,根据你实际使用的gas量,在旅行完成后支付。当你传输你的交易时,首先验证步骤之一是检查它源自的帐户是否有足够的金额支付 gasPrice * startGas 费用。但是,在交易执行结束之前,金额实际上并未从你的帐户中扣除。只收取你最终交易实际消耗的天然气,但在发送交易之前,你必须有足够的余额用于你愿意支付的最高金额。

交易的接收者

交易的收件人在+to+字段中指定。这包含一个20字节的以太坊地址。地址可以是EOA或合约地址。

以太坊没有进一步验证这个字段。任何20字节的值都被认为是有效的。如果20字节的值对应于没有相应私钥的地址,或没有相应的合约,则该交易仍然有效。以太坊无法知道某个地址是否是从公钥(从私钥导出的)正确导出的。

以太坊不能也不会验证交易中的接收者地址。你可以发送到没有相应私钥或合约的地址,从而“燃烧”ether,使其永远不会被花费。验证应该在用户界面层级完成。

发送一个交易到一个无效的地址会 燃烧 发送的ether,使其永远不可访问(不可花费),因为不能生成用来使用它的签名。假定地址验证发生在用户界面级别(参见 [eip-55] 或 [icap])。事实上,有很多合理的理由来燃烧ether,包括作为游戏理论,来抑制支付通道和其他智能合约作弊。

交易的价值和数据

交易的主要“负载”包含在两个字段中:value 和 data。交易可以同时具有value和data,只有value,只有data,或没有value和data。所有四种组合都是有效的。

只有value的交易是 支付 payment 。只有data的交易是 调用 invocation 。既没有value也没有data的交易,这可能只是浪费gas!但它仍然有可能。

  • 让我们尝试所有上述组合:
  • 首先,我们从我们的钱包中设置源地址和目标地址,以使演示更易于阅读:
  • Set the source and destination addresses
    src = web3.eth.accounts[0];
    dst = web3.eth.accounts[1];
    
有value的交易(支付),没有data
  • Value, no data
    web3.eth.sendTransaction({from: src, to: dst, value: web3.toWei(0.01, "ether"), data: ""});
    

我们的钱包显示确认屏幕,指示要发送的value,并且没有data:

Figure 1. Parity wallet showing a transaction with value, but no dataFigure 1. Parity wallet showing a transaction with value, but no data

有value(支付)data的交易
  • Value and data
    web3.eth.sendTransaction({from: src, to: dst, value: web3.toWei(0.01, "ether"), data: "0x1234"});
    
  • 我们的钱包显示一个确认屏幕,指示要发送的value和data: Figure 2. Parity wallet showing a transaction with value and dataFigure 2. Parity wallet showing a transaction with value and data
0 value 的交易,只有数据
  • No value, only data
    web3.eth.sendTransaction({from: src, to: dst, value: 0, data: "0x1234"});
    
  • 我们的钱包显示一个确认屏幕,指示value为0并显示data: Figure 3. Parity wallet showing a transaction with no value, only dataFigure 3. Parity wallet showing a transaction with no value, only data
既没有value(支付)也没有data的交易
  • No value, no data
    web3.eth.sendTransaction({from: src, to: dst, value: 0, data: ""}));
    
  • 我们的钱包显示确认屏幕,指示0 value并且没有data: Figure 4. Parity wallet showing a transaction with no value, and no dataFigure 4. Parity wallet showing a transaction with no value, and no data

将value传递给EOA和合约

当你构建包含 value 的以太坊交易时,它等同于 payment 。根据目的地址是否为合约,这些交易行为会有所不同。

对于EOA地址,或者更确切地说,对于未在区块链中注册为合约的任何地址,以太坊将记录状态更改,并将你发送的value添加到地址的余额中。如果地址之前没有被查看过,则会创建地址并将其余额初始化为你的付款+value+。

如果目标地址(to)是合约,则EVM将执行合约并尝试调用你的交易的 data 中指定的函数(参见 [invocation] )。如果你的交易中没有 data,那么EVM将调用目标合约的 fallback 函数,如果该函数是payable,则将执行该函数以确定下一步该做什么。

合约可以通过在调用付款功能时立即抛出异常或由付款功能中编码的条件确定来拒绝收款。如果付款功能成功终止(没有意外),则更新合约状态以反映合约的ether余额增加。

将数据传输到EOA或合约

当你的交易包含+data+时,它很可能是发送到合约地址的。这并不意味着你无法向EOA发送+data+。事实上,你可以做到这一点。但是,在这种情况下,data+的解释取决于你用来访问EOA的钱包。大多数钱包会忽略它们控制的EOA交易中收到的任何+data。将来,可能会出现允许钱包以合约的方式解释+data+编码的标准,从而允许交易调用在用户钱包内运行的函数。关键的区别在于,与合约执行不同,EOA对data的任何解释都不受以太坊共识规则的约束。

现在,假设你的交易是向合约地址提供 data。在这种情况下,data 将被EVM解释为 函数调用 function invocation ,调用指定的函数并将任何编码参数传递给该函数。

发送到合约的 data 是一个十六进制序列化的编码:

函数选择器(function selector)函数 prototype 的Keccak256哈希的前4个字节。这使EVM能够明确地识别你希望调用的功能。

函数参数函数的参数,根据EVM定义的各种基本类型的规则进行编码。

我们来看一个简单的例子,它来自我们的[solidity_faucet_example]。在+Faucet.sol+中,我们为取款定义了一个函数:function withdraw(uint withdraw_amount) public {

withdraw函数的 prototype 被定义为包含函数名称的字符串,随后是括号中括起来的每个参数的数据类型,并用单个逗号分隔。函数名称是+withdraw+,它只有一个参数是uint(它是uint256的别名)。所以+withdraw+的原型将是:withdraw(uint256)

我们来计算这个字符串的Keccak256哈希值(我们可以使用truffle控制台或任何JavaScript web3控制台来做到这一点):

web3.sha3("withdraw(uint256)");
'0x2e1a7d4d13322e7b96f9a57413e1525c250fb7a9021cf91d1540d5b69f16a49f'

散列的前4个字节是 0x2e1a7d4d。这是我们的“函数选择器”的值,它会告诉EVM我们想调用哪个函数。

接下来,让我们计算一个值作为参数 withdraw_amount 传递。我们要取款0.01 ether。我们将它编码为一个十六进制序列化的大端序无符号256位整数,以wei为单位:

withdraw_amount = web3.toWei(0.01, "ether");
'10000000000000000'
withdraw_amount_hex = web3.toHex(withdraw_amount);
'0x2386f26fc10000'

现在,我们将函数选择器添加到这个参数上(填充为32字节):2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000

这就是我们的交易的 data,调用 withdraw 函数并请求0.01 ether作为 withdraw_amount。

特殊交易:合约注册

有一种特殊的带有data,没有value的交易。表示注册一个新的合约。合约登记交易被发送到一个特殊的目的地地址,即零地址。简而言之,合约注册交易中的+to+字段包含地址 0x0。该地址既不代表EOA(没有相应的私人/公共密钥对)也不代表合约。它永远不会花费ether或启动交易。它仅用作目的地,具有“注册此合约”的特殊含义。

尽管零地址仅用于合约注册,但它有时会收到来自各个地址的付款。对此有两种解释:无论是偶然的,导致ether的丧失,还是故意的 ether燃烧(见[burning_ether])。如果你想进行有意识的ether燃烧,你应该向网络明确你的意图,并使用专门指定的燃烧地址:0x000000000000000000000000000000000000dEaD

发送至合约注册地址 0x0 或指定燃烧地址 0x0 ... dEaD 的任何ether将变得不可消费并永远丢失。

合约注册交易不应包含ether value,只能包含合约编译字节码的data。此次交易的唯一影响是注册合约。

作为例子,我们可以发布 [intro] 中使用的 Faucet.sol。合约需要编译成二进制十六进制表示。这可以用Solidiy编译器完成。

> solc --bin Faucet.sol
======= Faucet.sol:Faucet =======
Binary:
6060604052341561000f57600080fd5b60e58061001d6000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d146041575b005b3415604b57600080fd5b605f60048080359060200190919050506061565b005b67016345785d8a00008111151515607757600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f19350505050151560b657600080fd5b505600a165627a7a72305820d276ddd56041f7dc2d2eab69f01dd0a0146446562e25236cf4ba5095d2ee802f0029

相同的信息也可以从Remix在线编译器获得。 现在我们可以创建交易。

> src = web3.eth.accounts[0];
> faucet_code = "0x6060604052341561000f57600080fd5b60e58061001d6000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d146041575b005b3415604b57600080fd5b605f60048080359060200190919050506061565b005b67016345785d8a00008111151515607757600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f19350505050151560b657600080fd5b505600a165627a7a72305820d276ddd56041f7dc2d2eab69f01dd0a0146446562e25236cf4ba5095d2ee802f0029"
> web3.eth.sendTransaction({from: src, data: faucet_code, gas: 113558, gasPrice: 200000000000})
"0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b"

不需要指定+to+参数,将使用默认的零地址。你可以指定 gasPrice 和 gas 限制。 一旦合约被开采,我们可以在etherscan区块浏览器上看到它。

Figure 5. Etherscan showing the contract successully mindedFigure 5. Etherscan showing the contract successully minded

你可以查看交易的接收者以获取有关合约的信息。

> eth.getTransactionReceipt("0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b");
{
  blockHash: "0x6fa7d8bf982490de6246875deb2c21e5f3665b4422089c060138fc3907a95bb2",
  blockNumber: 3105256,
  contractAddress: "0xb226270965b43373e98ffc6e2c7693c17e2cf40b",
  cumulativeGasUsed: 113558,
  from: "0x2a966a87db5913c1b22a59b0d8a11cc51c167a89",
  gasUsed: 113558,
  logs: [],
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  status: "0x1",
  to: null,
  transactionHash: "0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b",
  transactionIndex: 0
}

在这里我们可以看到合约的地址。我们可以按照 将数据传输到EOA或合约 所示,从合约发送和接收资金。

> contract_address = "0xb226270965b43373e98ffc6e2c7693c17e2cf40b"
> web3.eth.sendTransaction({from: src, to: contract_address, value: web3.toWei(0.1, "ether"), data: ""});
"0x6ebf2e1fe95cc9c1fe2e1a0dc45678ccd127d374fdf145c5c8e6cd4ea2e6ca9f"
> web3.eth.sendTransaction({from: src, to: contract_address, value: 0, data: "0x2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000"});
"0x59836029e7ce43e92daf84313816ca31420a76a9a571b69e31ec4bf4b37cd16e"

过一段时间,这两个交易都可以在ethescan上看到

Figure 6. Etherscan showing the transactions for sending and receiving fundsFigure 6. Etherscan showing the transactions for sending and receiving funds

数字签名

到目前为止,我们还没有深入探讨“数字签名”的细节。在本节中,我们将探讨数字签名是如何工作的,以及如何在不泄露私钥的情况下提供私钥所有权的证明。

椭圆曲线数字签名算法(ECDSA)

以太坊中使用的数字签名算法是 Elliptic Curve Digital Signature Algorithm,或 ECDSA。ECDSA是用于基于椭圆曲线私钥/公钥对的数字签名的算法,如 [elliptic_curve] 中所述。

数字签名在以太坊中有三种用途(请参阅下面的边栏)。首先,签名证明私钥的所有者,暗示着以太坊账户的所有者,已经授权支付ether或执行合约。其次,授权的证明是 undeniable(不可否认)。第三,签名证明交易数据在交易签名后没有也不能被任何人修改。

Wikipedia对“数字签名”的定义:数字签名是用于证明数字信息或文件真实性的数学方案。有效的数字签名使收件人有理由相信该信息是由已知的发件人(认证)创建的,发件人不能否认已发送的信息(不可否认),并且信息在传输过程中未被更改(完整性) 。 来源: https://en.wikipedia.org/wiki/Digital_signature_

数字签名如何工作

数字签名是一种数学签名,由两部分组成。第一部分是使用私钥(签名密钥)从消息(交易)中创建签名的算法。第二部分是允许任何人仅使用消息和公钥来验证签名的算法。

创建数字签名

以太坊实现的ECDSA中,被签名的“消息”是交易,或者更确切地说,来自交易的RLP编码数据的Keccak256哈希。签名密钥是EOA的私钥。结果是签名:\( ((Sig = F_{sig}(F_{keccak256}(m), k))) \)

其中:

  • k 是签名私钥
  • m 是RLP编码的交易
  • \( F_{keccak256} \) 是Keccak256哈希函数
  • \( F_{sig} \) 是签名算法
  • Sig 是由此产生的签名

更多关于ECDSA数学的细节可以在 ECDSA数学 中找到。函数 $ F_{sig} $ 产生一个由两个值组成的签名+Sig+,通常称为+R+和+S+:Sig = (R, S)

验证签名

要验证签名,必须有签名(R+和+S),序列化交易和公钥(与用于创建签名的私钥对应)。实质上,对签名的验证意味着“只有生成此公钥的私钥的所有者才能在此交易上产生此签名。”

签名验证算法采用消息(交易的散列或其部分),签名者的公钥和签名(+R+和+S+值),如果签名对此消息和公钥有效,则返回TRUE。

ECDSA数学

如前所述,签名由数学函数 \( F_{sig} \) 创建,该函数生成由两个值 RS 组成的签名。在本节中,我们将更详细地讨论函数 \( F_{sig} \) 。

签名算法首先生成 ephemeral(临时的)私钥/公钥对。在涉及签名私钥和交易哈希的转换之后,此临时密钥对用于计算 RS 值。

临时密钥对由两个输入值生成:

1.一个随机数 q ,用作临时私钥 1.和椭圆曲线生成点 G

qG 开始,我们生成相应的临时公钥 Q(以 Q = q * G 计算,与以太坊公钥的派生方式相同,参见[pubkey])。数字签名的 R 值就是临时公钥 Q 的x坐标。

然后,算法计算签名的 S 值,以便: \( S = q^{-1} (Keccak256(m) + k * R)(mod p) \)

  • q 是临时私钥
  • R 是临时公钥的x坐标
  • k 是签名(EOA所有者)的私钥
  • m 是交易数据
  • p 是椭圆曲线的素数阶

验证是签名生成函数的反函数,使用 RS 值和公钥来计算一个值 Q ,它是椭圆曲线上的一个点(签名创建中使用的临时公钥):\( Q = S^{-1} Keccak256(m) * G + S^1 * R * K (mod p) \)

  • RS 是签名值
  • K 是签名者(EOA所有者)的公钥
  • m 是被签名的交易数据
  • G 是椭圆曲线生成点
  • p 是椭圆曲线的素数阶

如果计算的点 Q 的x坐标等于 R ,则验证者可以断定该签名是有效的。请注意,在验证签名时,私钥既不被知道也不会透露。

ECDSA必然是一门相当复杂的数学; 完整的解释超出了本书的范围。许多优秀的在线指南会一步一步地通过它:搜索“ECDSA explained”或尝试这一个:http://bit.ly/2r0HhGB。

实践中的交易签名

为了产生有效的交易,发起者必须使用椭圆曲线数字签名算法将数字签名应用于消息。当我们说“签署交易”时,我们实际上是指“签署RLP序列化交易数据的Keccak256哈希”。签名应用于交易数据的哈希,而不是交易本身。

在#2,675,000块,Ethereum实施了“Spurious Dragon”硬分叉,除其他更改外,还推出了包括交易重播保护的新签名方案。这个新的签名方案在EIP-155中指定(参见[eip155])。此更改会影响签名过程的第一步,在签名之前向交易添加三个字段(v,r,s)。

要在以太坊签署交易,发件人必须:

  1. 创建一个包含九个字段的交易数据结构:nonce,gasPrice,startGas,to,value,data,v,r,s
  2. 生成交易的RLP编码的序列化消息
  3. 计算此序列化消息的Keccak256哈希
  4. 计算ECDSA签名,用发起EOA的私钥签名散列
  5. 在交易中插入ECDSA签名计算出的 r 和 s 值

原始交易创建和签名

让我们创建一个原始交易并使用 ethereumjs-tx 库对其进行签名。此示例的源代码位于GitHub存储库中的 raw_tx_demo.js 中:

  • raw_tx_demo.js: Creating and signing a raw transaction in JavaScript
    link:code/web3js/raw_tx/raw_tx_demo.js
    
  • 在此处下载: https://github.com/ethereumbook/ethereumbook/blob/develop/code/web3js/raw_tx/raw_tx_demo.js
  • 运行示例代码:
    $ node raw_tx_demo.js
    RLP-Encoded Tx: 0xe6808609184e72a0008303000094b0920c523d582040f2bcb1bd7fb1c7c1ecebdb348080
    Tx Hash: 0xaa7f03f9f4e52fcf69f836a6d2bbc7706580adce0a068ff6525ba337218e6992
    Signed Raw Transaction: 0xf866808609184e72a0008303000094b0920c523d582040f2bcb1bd7fb1c7c1ecebdb3480801ca0ae236e42bd8de1be3e62fea2fafac7ec6a0ac3d699c6156ac4f28356a4c034fda0422e3e6466347ef6e9796df8a3b6b05bed913476dc84bbfca90043e3f65d5224
    

用EIP-155创建原始交易

EIP-155“简单重播攻击保护”标准在签名之前指定了重播攻击保护(replay-attack-protected)的交易编码,其中包括交易数据中的 chain identifier。这确保了为一个区块链(例如以太坊主网)创建的交易在另一个区块链(例如Ethereum Classic或Ropsten测试网络)上无效。因此,在一个网络上广播的交易不能在另一个网络上广播,因此得名“重放攻击保护”。

EIP-155向交易数据结构添加了三个字段 v,r+和+s。r+和+s 字段被初始化为零。这三个字段在编码和散列 之前 被添加到交易数据中。因此,三个附加字段会更改交易的散列,稍后将应用签名。通过在被签名的数据中包含链标识符,交易签名可以防止任何更改,因为如果链标识符被修改,签名将失效。因此,EIP-155使交易无法在另一个链上重播,因为签名的有效性取决于链标识符。

签名前缀字段+v+被初始化为链标识符,其值为:

Chain Chain ID
Ethereum main net 1
Morden (obsolete), Expanse 2
Ropsten 3
Rinkeby 4
Rootstock main net 30
Rootstock test net 31
Kovan 42
Ethereum Classic main net 61
Ethereum Classic test net 62
Geth private testnets 1337

由此产生的交易结构被进行RLP编码,哈希和签名。签名算法也稍作修改,以在+v+前缀中对链ID进行编码。

有关更多详细信息,请参阅EIP-155规范: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md

签名前缀值(v)和公钥恢复

如交易的结构所述,交易消息不包含任何“from”字段。这是因为发起者的公钥可以直接从ECDSA签名中计算出来。一旦你有公钥,你可以很容易地计算出地址。恢复签名者公钥的过程称为 公钥恢复

给定 ECDSA数学 中计算的值 r 和 s,我们可以计算两个可能的公钥。

首先,我们根据签名中的x坐标 r 值计算两个椭圆曲线点R和 R^'^ 。有个两点,因为椭圆曲线在x轴上是对称的,所以对于任何值+x+,在x轴的两侧有两个可能的值适合曲线。

从 r 开始,我们也计算 \( r^-1 \) 这是 r 的倒数。

最后我们计算 z,它是消息散列的最低位,其中n是椭圆曲线的阶数。然后两个可能的公钥是: \( K_1 = r^1 (sR-zG) \) 和 \( K_2 = r^1 (sR - zG) \)

  • \( K_1 \) 和 \( K_2 \) 是签名者公钥的两种可能性
  • \( r^{-1} \) 是签名的+r+值的倒数
  • s是签名的+s+值
  • R和R^'^ 是临时公钥 Q 的两种可能性
  • z是消息散列的最低位
  • G是椭圆曲线生成点

为了使事情更有效率,交易签名包括一个前缀值 v,它告诉我们两个可能的R值中哪一个是临时的公钥。如果 v 是偶数,那么R是正确的值。如果 v 是奇数,那么选择R^'^ 。这样,我们只需要计算R的一个值。

分离签名和传输(离线签名)

一旦交易被签署,它就可以传送到以太坊网络。创建,签署和广播交易的三个步骤通常发生在单个函数中,例如使用+web3.eth.sendTransaction+。但是,正如我们在原始交易创建和签名中看到的那样,你可以通过两个单独的步骤创建和签署交易。一旦你签署了交易记录,你就可以使用+web3.eth.sendSignedTransaction+传输该交易记录,该方法采用十六进制编码的签名交易信息并在Ethereum网络上传输。

你为什么要分开交易的签署和传输?最常见的原因是安全:签名交易的计算机必须将解锁的私钥加载到内存中。传输的计算机必须连接到互联网并运行以太坊客户端。如果这两个功能都在一台计算机上,那么你的在线系统上有私钥,这非常危险。分离签名和传输功能称为 离线签名 offline signing ,是一种常见的安全措施。

根据你所需的安全级别,你的“离线签名”计算机可能与在线计算机存在不同程度的分离,从隔离和防火墙子网(在线但隔离)到完全脱机系统,成为 气隙 air-gapped 系统 。在气隙系统中根本没有网络连接 - 计算机与在线环境是“空气”隔离的。使用数据存储介质或(更好)网络摄像头和QR码将交易记录到气隙计算机上,以签署交易。当然,这意味着你必须手动传输你想要签名的每个交易,不能批量化。

尽管没有多少环境可以利用完全气隙系统,但即使是小程度的隔离也具有显着的安全优势。例如,带防火墙的隔离子网只允许通过消息队列协议,可以提供大大降低的攻击面,并且比在线系统上签名的安全性高得多。许多公司使用诸如ZeroMQ(0MQ)的协议,因为它为签名计算机提供了减少的攻击面。有了这样的设置,交易就被序列化并排队等待签名。排队协议以类似于TCP套接字的方式将序列化的消息发送到签名计算机。签名计算机从队列中读取序列化的交易(仔细地),使用适当的密钥应用签名,并将它们放置在传出队列中。传出队列将签名的交易传输到使用Ethereum客户端的计算机上,客户端将这些交易出队并传输。

交易传播

以太坊网络使用“泛洪”路由协议。每个以太坊客户端,在 Peer-to-Peer(P2P)中作为 node ,(理想情况下)构成 mesh 网络。没有网络节点是“特殊的”,它们都作为平等的对等体。我们将使用术语“节点”来指代连接并参与P2P网络的以太坊客户端。

交易传播开始于创建(或从离线接收)签名交易的以太坊节点。交易被验证,然后传送到 直接 连接到始发节点的所有其他以太坊节点。平均而言,每个以太坊节点保持与至少13个其他节点的连接,称为 邻居 。每个邻居节点在收到交易后立即验证交易。如果他们同意这是有效的,他们会保存一份副本并将其传播给所有的邻居(除了它的邻居)。结果,交易从始发节点向外涟漪式地遍历网络,直到网络中的所有节点都拥有交易的副本。

几秒钟内,以太坊交易就会传播到全球所有以太坊节点。从每个节点的角度来看,不可能辨别交易的起源。发送给我们节点的邻居可能是交易的发起者,或者可能从其邻居那里收到它。为了能够跟踪交易的起源或干扰传播,攻击者必须控制所有节点的相当大的百分比。这是P2P网络安全和隐私设计的一部分,尤其适用于区块链

记录到区块链中

虽然以太坊中的所有节点都是相同的对等节点,但其中一些节点由 矿工 操作,并将交易和数据块提供给 挖矿农场 ,这些节点是具有高性能图形处理单元(GPU)的计算机。挖掘计算机将交易添加到候选块,并尝试查找使得候选块有效的 Proof-of-Work。我们将在[consensus]中更详细地讨论这一点。

不深入太多细节,有效的交易最终将被包含在一个交易块中,并记录在以太坊区块链中。一旦开采成块,交易还通过修改账户余额(在简单付款的情况下)或通过调用改变其内部状态的合约来修改以太坊单例的状态。这些更改将与交易一起以交易 收据 receipt 的形式记录,该交易也可能包含 事件 events 。我们将在 [evm] 中更详细地检查所有这些。

我们的交易已经完成了从创建到被EOA签署,传播以及最终采矿的旅程。它改变了单例的状态,并在区块链上留下了不可磨灭的印记。

多重签名(multisig)交易

如果你熟悉比特币的脚本功能,那么你就知道有可能创建一个比特币多重签名账户,该账户只能在多方签署交易时花费资金(例如2个或3个或4个签名)。以太坊的价值交易没有多重签名的规定,尽管可以部署任意条件的任意合约来处理ether和代币的转让。

为了在多重签名情况下保护你的ether,将它们转移到多重签名合约中。无论何时你想将资金转入其他账户,所有必需的用户都需要使用常规钱包软件将交易发送至合约,从而有效授权合约执行最终交易。

这些合约也可以设计为在执行本地代码或触发其他合约之前需要多个签名。该方案的安全性最终由多重签名合约代码确定。

讨论和 Grid+ 参考实现: https://blog.gridplus.io/toward-an-ethereum-multisig-standard-c566c7b7a3f6

下一节:我们在 [intro] 中发现,以太坊有两种不同类型的账户:外部所有账户(EOAs)和合约账户。EOAs由以太坊以外的软件(如钱包应用程序)控制。合约帐户由在以太坊虚拟机(EVM)内运行的软件控制。两种类型的帐户都通过以太坊地址标识。在本节中,我们将讨论第二种类型,合约账户和控制它们的软件:智能合约。