以太坊交易中的Nonce详解

nonce在区块链中是一个非常重要的概念,从比特币到以太坊都有nonce的身影。

在比特币中,nonce主要用于调整pow挖矿的难度,而在以太坊中,除了调整挖矿难度外,在外部账户的每笔交易中也都存在一个nonce。这个nonce是一个连续的整数,在每个账户发送交易时所产生,其主要设计目的是为防止双花。

web3中的sendTransaction方法,官方文档是这样写的:

https://web3js.readthedocs.io/en/1.0/web3-eth.html#sendtransaction

image.png

可以看到,在构建交易时,有一个可选的nonce参数,可以覆盖在交易池中的,pending列表中相同nonce的交易。

接下来我会在Geth搭建的私有链来进行以下实验,来看看不同情况下nonce对应的交易会怎样:

  • 相同nonce下的两笔交易
  • 不连续nonce下的交易
  • 不具体指定nonce的交易

说明:使用Geth搭建私有链详见:https://www.jianshu.com/p/4c3efd23a427

首先下方命令看到地址0xfa8d4ded7fe1fec96c1b10443bea261195f233bb的总交易数是2,也就是说,nonce数值已累加到1(nonce值从0开始),新的交易nonce值将是2。

> eth.getTransactionCount("0xfa8d4ded7fe1fec96c1b10443bea261195f233bb")
2

接下来指定nonce为2,创建一笔交易,如下可看到,交易成功,并被打包到了1980区块中。

> web3.eth.sendTransaction({from: "0xfa8d4ded7fe1fec96c1b10443bea261195f233bb", to: "0xf3756e74c9c409fdf4fa6d44c492fbb1edf36f53", value: "1000000000000000000", nonce: "2"})
"0x8a16b9a887928125071469332425896d33a1fa8c4e5a5deb50545f637ea1bf5d"

> eth.getTransaction("0x8a16b9a887928125071469332425896d33a1fa8c4e5a5deb50545f637ea1bf5d")
{
  blockHash: "0x9ba0aad04c9b52ef9bc543d274ce1718e32e9fdbf4bff7f88904f14c03f41855",
  blockNumber: 1980,
  from: "0xfa8d4ded7fe1fec96c1b10443bea261195f233bb",
  gas: 90000,
  gasPrice: 1000000000,
  hash: "0x8a16b9a887928125071469332425896d33a1fa8c4e5a5deb50545f637ea1bf5d",
  input: "0x",
  nonce: 2,
  r: "0xd530dc31288ff48f796763ce398a21cca9e9ab640f80890478b259dc247709e0",
  s: "0x275aa77f51cb157e2088e90cc3859bd2e53dbcc5bf752a5380a8736796a2a383",
  to: "0xf3756e74c9c409fdf4fa6d44c492fbb1edf36f53",
  transactionIndex: 0,
  v: "0x557",
  value: 1000000000000000000
}

如果此时我在发送一笔交易,并指定nonce还是2,运行结果如下所示。

会看到此时交易报错,提示“nonce too low”

> web3.eth.sendTransaction({from: "0xfa8d4ded7fe1fec96c1b10443bea261195f233bb", to: "0xf3756e74c9c409fdf4fa6d44c492fbb1edf36f53", value: "1000000000000000000", nonce: "2"})
Error: nonce too low
    at web3.js:3143:20
    at web3.js:6347:15
    at web3.js:5081:36
    at :1:1

按照nonce的规则,接下来的新交易nonce值应该是3,我现在跳过3,直接指定nonce值为4,会发生什么,如下所示。

> web3.eth.sendTransaction({from: "0xfa8d4ded7fe1fec96c1b10443bea261195f233bb", to: "0xf3756e74c9c409fdf4fa6d44c492fbb1edf36f53", value: "1000000000000000000", nonce:"4"})
"0xcbd473fb1bcc396dfd98e2f1807dea5b78c77f713592c134e7249b3786025d6a"

可以看到,返回了交易hash,我们查看该笔交易,发现blockNumber为null,并没有加入到区块中,如下所示:

> eth.getTransaction("0xcbd473fb1bcc396dfd98e2f1807dea5b78c77f713592c134e7249b3786025d6a")
{
  blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  blockNumber: null,
  from: "0xfa8d4ded7fe1fec96c1b10443bea261195f233bb",
  gas: 90000,
  gasPrice: 1000000000,
  hash: "0xcbd473fb1bcc396dfd98e2f1807dea5b78c77f713592c134e7249b3786025d6a",
  input: "0x",
  nonce: 4,
  r: "0x657ceabd6907d50d1af9046ea58352d0804cc3812b290ff103bc32514bc491c5",
  s: "0x2bc6b80ef7de0ed9a737ba78f404f825d58ab345c446019fbd67644c2f2b2a36",
  to: "0xf3756e74c9c409fdf4fa6d44c492fbb1edf36f53",
  transactionIndex: 0,
  v: "0x558",
  value: 1000000000000000000
}

暂未被加入到区块中的交易,会被放入到交易池中(txpool),交易池里会维护两个列表,一个是待被打包的pending列表,一个是当前无法执行的交易queued列表。

从下方请求可看到0xcbd473fb1bcc396dfd98e2f1807dea5b78c77f713592c134e7249b3786025d6a交易被放到了queued列表中。这是由于交易池中没有找到地址0xfa8d4DEd7fE1feC96c1B10443bEa261195f233Bb为3的nonce。

> web3.txpool.content.queued
{
  0xfa8d4DEd7fE1feC96c1B10443bEa261195f233Bb: {
    4: {
      blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
      blockNumber: null,
      from: "0xfa8d4ded7fe1fec96c1b10443bea261195f233bb",
      gas: "0x15f90",
      gasPrice: "0x3b9aca00",
      hash: "0xcbd473fb1bcc396dfd98e2f1807dea5b78c77f713592c134e7249b3786025d6a",
      input: "0x",
      nonce: "0x4",
      r: "0x657ceabd6907d50d1af9046ea58352d0804cc3812b290ff103bc32514bc491c5",
      s: "0x2bc6b80ef7de0ed9a737ba78f404f825d58ab345c446019fbd67644c2f2b2a36",
      to: "0xf3756e74c9c409fdf4fa6d44c492fbb1edf36f53",
      transactionIndex: "0x0",
      v: "0x558",
      value: "0xde0b6b3a7640000"
    }
}

新建一笔交易,设置nonce为3,如下所示。

会看到提交交易后,queued列表中nonce为4的交易也被移出,同时待打包的pending列表中,有了nonce值为3和4的交易信息。挖矿后,这两笔交易将会被写入到区块中。

> web3.eth.sendTransaction({from: "0xfa8d4ded7fe1fec96c1b10443bea261195f233bb", to: "0xf3756e74c9c409fdf4fa6d44c492fbb1edf36f53", value: "1000000000000000000", nonce:"3"})
"0xf9ed5af220997d8278e075ed87b391e6a28354b0c45726bb41ebae22fe5817b1"

> web3.txpool.content.pending

{
  0xfa8d4DEd7fE1feC96c1B10443bEa261195f233Bb: {
    3: {
      blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
      blockNumber: null,
      from: "0xfa8d4ded7fe1fec96c1b10443bea261195f233bb",
      gas: "0x15f90",
      gasPrice: "0x3b9aca00",
      hash: "0xf9ed5af220997d8278e075ed87b391e6a28354b0c45726bb41ebae22fe5817b1",
      input: "0x",
      nonce: "0x7",
      r: "0x258f9f382c3cd8c595cd610ad7df09cc5b0d6b7a6cd68a67a0d151a1d72d8c72",
      s: "0x7c4fe98cbeb967f8a6d7ebb4c848c264521ddf98fc992f99571d4e2380e50a9a",
      to: "0xf3756e74c9c409fdf4fa6d44c492fbb1edf36f53",
      transactionIndex: "0x0",
      v: "0x557",
      value: "0xde0b6b3a7640000"
    },
    4: {
      blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
      blockNumber: null,
      from: "0xfa8d4ded7fe1fec96c1b10443bea261195f233bb",
      gas: "0x15f90",
      gasPrice: "0x3b9aca00",
      hash: "0xcbd473fb1bcc396dfd98e2f1807dea5b78c77f713592c134e7249b3786025d6a",
      input: "0x",
      nonce: "0x7",
      r: "0x258f9f382c3cd8c595cd610ad7df09cc5b0d6b7a6cd68a67a0d151a1d72d8c72",
      s: "0x7c4fe98cbeb967f8a6d7ebb4c848c264521ddf98fc992f99571d4e2380e50a9a",
      to: "0xf3756e74c9c409fdf4fa6d44c492fbb1edf36f53",
      transactionIndex: "0x0",
      v: "0x557",
      value: "0xde0b6b3a7640000"
    },
  }
}

总结一下:

  1. 以太坊中有两种nonce,一种是在区块中的nonce,主要是调整挖矿难度;一种是每笔交易中nonce。
  2. 每个外部账户(私钥控制的账户)都有一个nonce值,从0开始连续累加,每累加一次,代表一笔交易。
  3. 某一地址的某一交易的nonce值如果大于当前的nonce,该交易会被放到交易池的queued列表中,直到缺失的nonce被提交到交易池中。
  4. 地址的nonce值是一个连续的整数,起设计的主要目的是防止双花。
  5. 在发生一笔交易时,如果不指定nonce值时,节点会根据当前交易池的交易自动计算该笔交易的nonce。有可能会出现节点A和节点B计算的nonce值不一样的情况。

参考代码:

https://github.com/ethereum/go-ethereum/blob/86e77900c53ebce3309099a39cbca38eb4d62fdf/core/tx_pool.go

一笔交易调用add(ctx context.Context, tx *types.Transaction)方法将交易信息加入到交易池的pending列表中,需要对交易信息进行验证,验证方法是validateTx,如下所示:

// validateTx checks whether a transaction is valid according to the consensus
// rules and adheres to some heuristic limits of the local node (price and size).
func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
    // Heuristic limit, reject transactions over 32KB to prevent DOS attacks
    if tx.Size() > 32*1024 {
        return ErrOversizedData
    }
    ...
    // Ensure the transaction adheres to nonce ordering
    if pool.currentState.GetNonce(from) > tx.Nonce() {
        return ErrNonceTooLow
    }
    ...
}

使用GetNonce方法获取当前地址在交易池中的nonce值,如果当前交易的nonce比交易池中的nonce值小,就会报“nonce too low”的错误。

你可能感兴趣的:(以太坊交易中的Nonce详解)