第37篇 笔记-以太坊交易池实战分析

本篇文档内容来自网络。

本文中txpool就是交易池。

1. 交易池的主要功能

交易池的主要功能包括四个部分:

1.1 缓存功能

交易池(txpool)作为存放交易的缓冲区,大量交易到来时,先存起来;

txpool由两部分构成,分别是pending和queued:

  1. queued:提交但是当前无法执行的交易,放在queue中等待执行。比如nonce值设置过大的交易。举个例子,当前账户的nonce是10,txpool中有该账户的第100号交易,但txpool中没有第11~99号交易,这些交易的缺失,造成第100号交易无法执行,所以第100号交易就是未来的交易、不可执行的交易,存放在queue中。
  2. pending:等待执行的交易会被放在pending队列中;比如我们把上面的11~99号交易补全了,那么第100号交易都可以进入到pending,因为这些交易都是连续的,都可以打包进区块。

当节点收到交易(本地节点发起的或peer广播来的)时,会先存放到queued,txpool在某些情况下,把queued中可执行的交易,转移到pending中。

1.2 打包区块服务功能

这是txpool最核心的功能,矿工在打包区块的时候,会获取所有的pending交易,但这些交易还存在txpool中,矿工只是读取出来,至于txpool何时删除交易,可以从txpool清理交易的角度看。

1.3 清理交易
txpool清理交易有以下几种条件,符合任意以下1条的,都是无效交易,会被从pending或者queued中移除:

  1. 交易的nonce值已经低于账户在当前高度上的nonce值,代表交易已过期,交易已经上链就属于这种情况;
  2. 交易的GasLimit大于区块的GasLimit,区块容不下交易;
  3. 账户的余额已不足以支持该交易要消耗的gas费用;
  4. 交易的数量超过了queued和pending的缓冲区大小,需要进行清理。

交易清理主要有3个场景:

  1. txpool订阅了ChainHeadEvent事件,该事件代表主链上有新区块产生,txpool会根据最新的区块,检查每个账号的交易,有些无效的会被删除,有些由于区块回滚会从pending移动到queued,然后把queued中可执行的交易移动到pending,为下一轮区块打包做好准备。
  2. queued交易移动到pending被称为“提升”(promote),这个过程中,同样会检查交易,当交易不符合以上条件时,就会被直接从queued中删除。
  3. 删除停留在queued中超过3小时的交易,3小时这个超时时间是可以通过启动参数调整的。txpool记录了某个账户交易进入pending的时间,如果这个时间超过了3小时,代表该账号的交易迟迟不能被主链打包,既然无法被主链接受,就删除掉在queued中本来就无法执行的交易。

1.4 惩罚恶意账号
恶意用户会造成:

  1. 占用txpool空间;
  2. 浪费节点大量内存和CPU;
  3. 降低打包性能。txpool会涉及多个锁,当txpool中的交易很多时,性能会很低,这也会影响到区块的打包。

为了防止恶意账户发起大量垃圾交易。底层有一个处理策略。
只有当交易的总数量超过缓冲区大小时,txpool才会认为有恶意账户发起大量交易。过滤/惩罚发送大量交易的账户(攻击者)。

pending和queued缓冲区大小不同,但处理策略类似:

  1. pending的缓冲区容量是4096,当pending的交易数量多于此时,就会运行检查,每个账号的交易数量是否多于16,把这些账号搜集出来,进行循环依次清理。什么意思呢?就是每轮只删除(移动到queued)这些账号的每个账号1条交易,然后看数量是否降下来了,不满足再进行下一轮,直到满足。
  2. queued的缓冲区容量是1024,超过之后清理策略和pending差不多,但这里是真删除了。
  3. 该部分功能未抽象成单独的函数,而是在promote中,就是在每次把queued交易转移到pending后执行的。
  4. 本地交易有特权,txpool虽然对交易有诸多限制,但如果交易是本节点的账号发起的,以上数量限制等都对他无效。所以,如果用本节点账号不停地发送交易,并不会被认为是攻击者,用txpool.status命令,可以查看到交易的数量,这种情况是可以大于4096的。

2. txpool对应的启动参数

我们先来了解一下,针对txpool有哪些参数项可以设置,然后着重分析。

--txpool.nolocals 为本地提交交易禁用价格豁免 
--txpool.journal value 本地交易的磁盘日志:用于节点重启 (默认: "transactions.rlp") 
--txpool.rejournal value 重新生成本地交易日志的时间间隔 (默认: 1小时) 
--txpool.pricelimit value 加入交易池的最小的gas价格限制(默认: 1) 
--txpool.pricebump value 价格波动百分比(相对之前已有交易) (默认: 10) 
--txpool.accountslots value 每个帐户保证可执行的最少交易槽数量 (默认: 16) 
--txpool.globalslots value 所有帐户可执行的最大交易槽数量 (默认: 4096) 
--txpool.accountqueue value 每个帐户允许的最多非可执行交易槽数量 (默认: 64) 
--txpool.globalqueue value 所有帐户非可执行交易最大槽数量 (默认: 1024) 
--txpool.lifetime value 非可执行交易最大入队时间(默认: 3小时)

 

3. txpool内容的查看

> txpool.content
{ pending: {},
  queued: {}
}

很显然,txpool中由两部分构成pending和queued组成。那么他们两者有什么分别呢。最明显的是一个为待打包状态,一个为队列中。这里我们发起了两笔不同的交易:

> eth.sendTransaction({from:"0xdae19174969a7404e222c24b6726e4d089c12768",to:"0x5929a871f57a1C5F7E4eA304CAe92DACD1C1556b",value:web3.toWei(0.01,"ether"),gasPrice:21000000000,nonce:2});
"0x7db7883bb23a31deb9f01b5e6fb28363b1aee1b9b6797ea8b5706be170a1187c"

> eth.sendTransaction({from:"0xdae19174969a7404e222c24b6726e4d089c12768",to:"0x5929a871f57a1C5F7E4eA304CAe92DACD1C1556b",value:web3.toWei(0.01,"ether")});
"0x2784a79a8c454c72700e7be3b31c1c98ceaea232ca4992a6830b0fc999ebb653"

很显然,第一笔交易指定了nonce为2,第二笔交易未指定nonce值,因为此地址没有发起过交易那么nonce值默认为0。这时我们再看一下txpool中的内容:

> txpool.content
{
  pending: {
    0xdAE19174969A7404e222c24B6726E4D089c12768: {
      0: {
        blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
        blockNumber: null,
        from: "0xdae19174969a7404e222c24b6726e4d089c12768",
        gas: "0x15f90",
        gasPrice: "0x1",
        hash: "0x2784a79a8c454c72700e7be3b31c1c98ceaea232ca4992a6830b0fc999ebb653",
        input: "0x",
        nonce: "0x0",
        r: "0xdabcd46d8d0b61e468d9f10119d544437f89cd094c35a89e5cbed298faf52c4a",
        s: "0x3670f23ecfb0a12e982a60438640fe042eefc50646a077de0244a8d67a84af9e",
        to: "0x5929a871f57a1c5f7e4ea304cae92dacd1c1556b",
        transactionIndex: "0x0",
        v: "0xa95",
        value: "0x2386f26fc10000"
      }
    }
  },
  queued: {
    0xdAE19174969A7404e222c24B6726E4D089c12768: {
      2: {
        blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
        blockNumber: null,
        from: "0xdae19174969a7404e222c24b6726e4d089c12768",
        gas: "0x15f90",
        gasPrice: "0x4e3b29200",
        hash: "0x7db7883bb23a31deb9f01b5e6fb28363b1aee1b9b6797ea8b5706be170a1187c",
        input: "0x",
        nonce: "0x2",
        r: "0xa8953a87c326c02da9d7a712d6c7ac0cd415cbc71ea0c24423f9e01b1fec65bd",
        s: "0x3faefc3a0db585a67f02996a7167890e41ff5fd8fd4be6efff3bea7a797fad29",
        to: "0x5929a871f57a1c5f7e4ea304cae92dacd1c1556b",
        transactionIndex: "0x0",
        v: "0xa96",
        value: "0x2386f26fc10000"
      }
    }
  }
}

现在txpool中有两笔交易,其中nonce为0的在pending中,nonce为2的在queued中。为什么只有nonce不同的两笔交易,在txpool中的位置却不同呢?

 

4. txpool的处理流程

首先,如果不传入nonce值,那么geth节点会默认计算当前地址已经发起了的交易中最大的nonce值为多少,然后将其+1,然后将此交易放置在pending中,等待节点打包。

其次,如果传入的nonce值过大,在进入txpool中检查到它之前的nonce并没有使用过,那么此笔交易不会发送到pending中,而且放置在queued中。只有当前面的nonce补齐之后,才会进入到pending中。那么,我们再发一笔交易把nonce补齐看看:

> eth.sendTransaction({from:"0xdae19174969a7404e222c24b6726e4d089c12768",to:"0x5929a871f57a1C5F7E4eA304CAe92DACD1C1556b",value:web3.toWei(0.01,"ether")});
"0x7ee17d38405c01bab4eec4d9dc62a6bba98283e243a2d9132187706485878ef5"

> txpool.content
{ pending: { 0xdAE19174969A7404e222c24B6726E4D089c12768: { 0: { blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000", blockNumber: null, from: "0xdae19174969a7404e222c24b6726e4d089c12768", gas: "0x15f90", gasPrice: "0x1", hash: "0x2784a79a8c454c72700e7be3b31c1c98ceaea232ca4992a6830b0fc999ebb653", input: "0x", nonce: "0x0", r: "0xdabcd46d8d0b61e468d9f10119d544437f89cd094c35a89e5cbed298faf52c4a", s: "0x3670f23ecfb0a12e982a60438640fe042eefc50646a077de0244a8d67a84af9e", to: "0x5929a871f57a1c5f7e4ea304cae92dacd1c1556b", transactionIndex: "0x0", v: "0xa95", value: "0x2386f26fc10000" },
      1: { blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000", blockNumber: null, from: "0xdae19174969a7404e222c24b6726e4d089c12768", gas: "0x15f90", gasPrice: "0x1", hash: "0x7ee17d38405c01bab4eec4d9dc62a6bba98283e243a2d9132187706485878ef5", input: "0x", nonce: "0x1", r: "0xe03fb4d94b0ff04107c855bfd88a84ecdefb03f4c9b0cea5341591aa69d4751e", s: "0x4d2f60f4045e5492cd4818145cec73c78b00e0cff57026c4528d91a82dee76e1", to: "0x5929a871f57a1c5f7e4ea304cae92dacd1c1556b", transactionIndex: "0x0", v: "0xa96", value: "0x2386f26fc10000" },
      2: { blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000", blockNumber: null, from: "0xdae19174969a7404e222c24b6726e4d089c12768", gas: "0x15f90", gasPrice: "0x4e3b29200", hash: "0x7db7883bb23a31deb9f01b5e6fb28363b1aee1b9b6797ea8b5706be170a1187c", input: "0x", nonce: "0x2", r: "0xa8953a87c326c02da9d7a712d6c7ac0cd415cbc71ea0c24423f9e01b1fec65bd", s: "0x3faefc3a0db585a67f02996a7167890e41ff5fd8fd4be6efff3bea7a797fad29", to: "0x5929a871f57a1c5f7e4ea304cae92dacd1c1556b", transactionIndex: "0x0", v: "0xa96", value: "0x2386f26fc10000" }
    }
  },
  queued: {}
}

很明显,当中间的nonce被补齐之后,原来处于queued当中的交易被放置到了pending中。

 

5. 经验之谈

前文提到的如何处理过期交易中提到了补齐nonce和设置–txpool.lifetime也是基于今天这批文章讲述的基础逻辑。除此之外,我们还要了解一下–txpool.accountqueue参数,它定义了每个账户在本节点queued中存放的最多的交易个数,默认是64个交易。

另外为了避免手续费过低导致交易一直存在于txpool当中占用内存,可以通过console设置手续费的最低值:

>miner.setGasPrice(51000000000)
true

或者在启动参数上添加:

--gasprice "51000000000"

6. nonce覆盖

以太坊中的nonce真是让人又爱又恨,恨它是因为它可以让简单的问题复杂话,在某些情况下需要自己去维护nonce值的递增性,然而它又为大家提供了一些便利的操作,比如因为较低导致交易迟迟未被打包,那么我们可以通过nonce进行覆盖。

解决方案:

当我们发送一笔交易时,支付的手续费较低,导致矿工不愿意打包交易,那么此时我们该怎么办呢?这里就用到了nonce覆盖特性,同样的交易我们把手续费提高再次发送即可。需要注意的前提条件是nonce值由自己维护。

如果nonce值是让geth节点自动生成,那么再次发送时就需要构造之前交易所使用的nonce进行发送,才能达到覆盖的效果。

 

7. 队列失效

问题场景:

如果我们发出一笔交易,当交易迟迟未被打包,此时不想再发次笔交易,或者说想使此笔交易失效,那么该如何操作呢?比如由于程序调用导致nonce错乱,很多交易都处于队列中无法被打包,一个个进行重新发送或许不是最好的方法。

解决方案:

此时如果单纯的重启节点,并不能达到清楚队列的效果,那么我们该如何操作呢?这里geth节点为我们提供了一个解决方案,那就是设置队列失效时间。

--txpool.lifetime value  Maximum amount of time non-executable transaction are queued (default: 3h0m0s)

我们知道队列是存在于txpool里面的,不指定此参数值时,默认为3小时失效。那么,解决问题的方案就显而易见了,我们可以将此参数设置较小,然后重启参数,等待失效,等待txpool中交易失效之后,再改会此参数正常值,再重启项目即可。

源代码:

下面我们看一下默认配置的源代码,其中包含一些其他参数的配置项:

var DefaultTxPoolConfig = TxPoolConfig{
   Journal:   "transactions.rlp",
   Rejournal: time.Hour,

   PriceLimit: 1,
   PriceBump:  10,

   AccountSlots: 16,
   GlobalSlots:  4096,
   AccountQueue: 64,
   GlobalQueue:  1024,

   Lifetime: 3 * time.Hour,

}

最后一行,明确说明了Lifetime为3小时。

猜你喜欢

转载自blog.csdn.net/wonderBlock/article/details/108677787