交易的结构

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

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

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

  1. web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f")
  2. 40
Tip

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

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

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

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

  1. web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
  2. 40
  3. web3.eth.sendTransaction({from: web3.eth.accounts[0], to: "0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.toWei(0.01, "ether")});
  4. web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
  5. 41
  6. web3.eth.sendTransaction({from: web3.eth.accounts[0], to: "0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.toWei(0.01, "ether")});
  7. web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
  8. 41
  9. web3.eth.sendTransaction({from: web3.eth.accounts[0], to: "0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.toWei(0.01, "ether")});
  10. web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
  11. 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客户端来访问它:

  1. curl --data '{"method":"parity_nextNonce","params":["0x9e713963a92c02317a681b9bb3065a8249de124f"],"id":1,"jsonrpc":"2.0"}' -H "Content-Type: application/json" -X POST localhost:8545
  2. {"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的签名和跟踪是你的操作的一部分,可能在负载下变得拥塞,而未签名交易的生成是你并不需要实现并行化的部分。你有并发性,但不是在过程中任何有用的部分。

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