mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-01-11 21:04:11 +00:00
103656dbb5
details: For documentation, see comments in the file tx_pool.nim. For prettified manual pages run 'make docs' in the nimbus directory and point your web browser to the newly created 'docs' directory.
911 lines
31 KiB
Nim
911 lines
31 KiB
Nim
# Nimbus
|
|
# Copyright (c) 2018-2019 Status Research & Development GmbH
|
|
# Licensed under either of
|
|
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
|
# http://www.apache.org/licenses/LICENSE-2.0)
|
|
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
|
# http://opensource.org/licenses/MIT)
|
|
# at your option. This file may not be copied, modified, or distributed except
|
|
# according to those terms.
|
|
|
|
import
|
|
std/[algorithm, os, random, sequtils, strformat, strutils, tables, times],
|
|
../nimbus/[chain_config, config, db/db_chain, vm_state, vm_types],
|
|
../nimbus/p2p/[chain, clique, executor],
|
|
../nimbus/utils/[tx_pool, tx_pool/tx_item],
|
|
./test_txpool/[helpers, setup, sign_helper],
|
|
chronos,
|
|
eth/[common, keys, p2p],
|
|
stew/[keyed_queue, sorted_set],
|
|
stint,
|
|
unittest2
|
|
|
|
type
|
|
CaptureSpecs = tuple
|
|
network: NetworkID
|
|
file: string
|
|
numBlocks, minBlockTxs, numTxs: int
|
|
|
|
const
|
|
prngSeed = 42
|
|
|
|
baseDir = [".", "tests", ".." / "tests", $DirSep] # path containg repo
|
|
repoDir = ["replay", "status"] # alternative repos
|
|
|
|
goerliCapture: CaptureSpecs = (
|
|
network: GoerliNet,
|
|
file: "goerli68161.txt.gz",
|
|
numBlocks: 22000, # block chain prequel
|
|
minBlockTxs: 300, # minimum txs in imported blocks
|
|
numTxs: 840) # txs following (not in block chain)
|
|
|
|
loadSpecs = goerliCapture
|
|
|
|
# 75% <= #local/#remote <= 1/75%
|
|
# note: by law of big numbers, the ratio will exceed any upper or lower
|
|
# on a +1/-1 random walk if running long enough (with expectation
|
|
# value 0)
|
|
randInitRatioBandPC = 75
|
|
|
|
# 95% <= #remote-deleted/#remote-present <= 1/95%
|
|
deletedItemsRatioBandPC = 95
|
|
|
|
# 70% <= #addr-local/#addr-remote <= 1/70%
|
|
# note: this ratio might vary due to timing race conditions
|
|
addrGroupLocalRemotePC = 70
|
|
|
|
# With a large enough block size, decreasing it should not decrease the
|
|
# profitability (very much) as the the number of blocks availabe increases
|
|
# (and a better choice might be available?) A good value for the next
|
|
# parameter should be above 100%.
|
|
decreasingBlockProfitRatioPC = 92
|
|
|
|
# test block chain
|
|
networkId = GoerliNet # MainNet
|
|
|
|
var
|
|
minGasPrice = GasPrice.high
|
|
maxGasPrice = GasPrice.low
|
|
|
|
prng = prngSeed.initRand
|
|
|
|
# to be set up in runTxLoader()
|
|
statCount: array[TxItemStatus,int] # per status bucket
|
|
|
|
txList: seq[TxItemRef]
|
|
effGasTips: seq[GasPriceEx]
|
|
|
|
# running block chain
|
|
bcDB: BaseChainDB
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Helpers
|
|
# ------------------------------------------------------------------------------
|
|
|
|
proc randStatusRatios: seq[int] =
|
|
for n in 1 .. statCount.len:
|
|
let
|
|
inx = (n mod statCount.len).TxItemStatus
|
|
prv = (n - 1).TxItemStatus
|
|
if statCount[inx] == 0:
|
|
result.add int.high
|
|
else:
|
|
result.add (statCount[prv] * 100 / statCount[inx]).int
|
|
|
|
proc randStatus: TxItemStatus =
|
|
result = prng.rand(TxItemStatus.high.ord).TxItemStatus
|
|
statCount[result].inc
|
|
|
|
template wrapException(info: string; action: untyped) =
|
|
try:
|
|
action
|
|
except CatchableError:
|
|
raiseAssert info & " has problems: " & getCurrentExceptionMsg()
|
|
|
|
proc addOrFlushGroupwise(xp: TxPoolRef;
|
|
grpLen: int; seen: var seq[TxItemRef]; w: TxItemRef;
|
|
noisy = true): bool =
|
|
# to be run as call back inside `itemsApply()`
|
|
wrapException("addOrFlushGroupwise()"):
|
|
seen.add w
|
|
if grpLen <= seen.len:
|
|
# clear waste basket
|
|
discard xp.txDB.flushRejects
|
|
|
|
# flush group-wise
|
|
let xpLen = xp.nItems.total
|
|
noisy.say "*** updateSeen: deleting ", seen.mapIt($it.itemID).join(" ")
|
|
for item in seen:
|
|
doAssert xp.txDB.dispose(item,txInfoErrUnspecified)
|
|
doAssert xpLen == seen.len + xp.nItems.total
|
|
doAssert seen.len == xp.nItems.disposed
|
|
seen.setLen(0)
|
|
|
|
# clear waste basket
|
|
discard xp.txDB.flushRejects
|
|
|
|
return true
|
|
|
|
proc findFilePath(file: string): string =
|
|
result = "?unknown?" / file
|
|
for dir in baseDir:
|
|
for repo in repoDir:
|
|
let path = dir / repo / file
|
|
if path.fileExists:
|
|
return path
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Test Runners
|
|
# ------------------------------------------------------------------------------
|
|
|
|
proc runTxLoader(noisy = true; capture = loadSpecs) =
|
|
let
|
|
elapNoisy = noisy
|
|
veryNoisy = false # noisy
|
|
fileInfo = capture.file.splitFile.name.split(".")[0]
|
|
filePath = capture.file.findFilePath
|
|
|
|
# Reset/initialise
|
|
statCount.reset
|
|
txList.reset
|
|
effGasTips.reset
|
|
bcDB = capture.network.blockChainForTesting
|
|
|
|
suite &"TxPool: Transactions from {fileInfo} capture":
|
|
var
|
|
xp: TxPoolRef
|
|
nTxs: int
|
|
|
|
test &"Import {capture.numBlocks.toKMG} blocks + {capture.minBlockTxs} txs"&
|
|
&" and collect {capture.numTxs} txs for pooling":
|
|
|
|
elapNoisy.showElapsed("Total collection time"):
|
|
(xp, nTxs) = bcDB.toTxPool(file = filePath,
|
|
getStatus = randStatus,
|
|
loadBlocks = capture.numBlocks,
|
|
minBlockTxs = capture.minBlockTxs,
|
|
loadTxs = capture.numTxs,
|
|
noisy = veryNoisy)
|
|
|
|
# Make sure that sample extraction from file was ok
|
|
check capture.minBlockTxs <= nTxs
|
|
check capture.numTxs == xp.nItems.total
|
|
|
|
# Set txs to pseudo random status
|
|
check xp.verify.isOK
|
|
xp.setItemStatusFromInfo
|
|
|
|
# Boundary conditions regarding nonces might be violated by running
|
|
# setItemStatusFromInfo() => xp.txDB.verify() rather than xp.verify()
|
|
check xp.txDB.verify.isOK
|
|
|
|
check txList.len == 0
|
|
check xp.nItems.disposed == 0
|
|
|
|
noisy.say "***",
|
|
"Latest item: <", xp.txDB.byItemID.last.value.data.info, ">"
|
|
|
|
# make sure that the block chain was initialised
|
|
check capture.numBlocks.u256 <= bcDB.getCanonicalHead.blockNumber
|
|
|
|
check xp.nItems.total == foldl(@[0]&statCount.toSeq, a+b)
|
|
# ^^^ sum up statCount[] values
|
|
|
|
# make sure that PRNG did not go bonkers
|
|
for statusRatio in randStatusRatios():
|
|
check randInitRatioBandPC < statusRatio
|
|
check statusRatio < (10000 div randInitRatioBandPC)
|
|
|
|
# Load txList[]
|
|
txList = xp.toItems
|
|
check txList.len == xp.nItems.total
|
|
|
|
elapNoisy.showElapsed("Load min/max gas prices"):
|
|
for item in txList:
|
|
if item.tx.gasPrice < minGasPrice and 0 < item.tx.gasPrice:
|
|
minGasPrice = item.tx.gasPrice.GasPrice
|
|
if maxGasPrice < item.tx.gasPrice.GasPrice:
|
|
maxGasPrice = item.tx.gasPrice.GasPrice
|
|
|
|
check 0.GasPrice <= minGasPrice
|
|
check minGasPrice <= maxGasPrice
|
|
|
|
test &"Concurrent job processing example":
|
|
var log = ""
|
|
|
|
# This test does not verify anything but rather shows how the pool
|
|
# primitives could be used in an async context.
|
|
|
|
proc delayJob(xp: TxPoolRef; waitMs: int) {.async.} =
|
|
let n = xp.nJobs
|
|
xp.job(TxJobDataRef(kind: txJobNone))
|
|
xp.job(TxJobDataRef(kind: txJobNone))
|
|
xp.job(TxJobDataRef(kind: txJobNone))
|
|
log &= " wait-" & $waitMs & "-" & $(xp.nJobs - n)
|
|
await chronos.milliseconds(waitMs).sleepAsync
|
|
xp.jobCommit
|
|
log &= " done-" & $waitMs
|
|
|
|
# run async jobs, completion should be sorted by timeout argument
|
|
proc runJobs(xp: TxPoolRef) {.async.} =
|
|
let
|
|
p1 = xp.delayJob(900)
|
|
p2 = xp.delayJob(1)
|
|
p3 = xp.delayJob(700)
|
|
await p3
|
|
await p2
|
|
await p1
|
|
|
|
waitFor xp.runJobs
|
|
check xp.nJobs == 0
|
|
check log == " wait-900-3 wait-1-3 wait-700-3 done-1 done-700 done-900"
|
|
|
|
# Cannot rely on boundary conditions regarding nonces. So xp.verify()
|
|
# will not work here => xp.txDB.verify()
|
|
check xp.txDB.verify.isOK
|
|
|
|
|
|
proc runTxPoolTests(noisy = true) =
|
|
let elapNoisy = false
|
|
|
|
suite &"TxPool: Play with pool functions and primitives":
|
|
|
|
block:
|
|
const groupLen = 13
|
|
let veryNoisy = noisy and false
|
|
|
|
test &"Load/forward walk ID queue, " &
|
|
&"deleting groups of at most {groupLen}":
|
|
var
|
|
xq = bcDB.toTxPool(txList, noisy = noisy)
|
|
seen: seq[TxItemRef]
|
|
|
|
# Set txs to pseudo random status
|
|
xq.setItemStatusFromInfo
|
|
|
|
check xq.txDB.verify.isOK
|
|
elapNoisy.showElapsed("Forward delete-walk ID queue"):
|
|
for item in xq.txDB.byItemID.nextValues:
|
|
if not xq.addOrFlushGroupwise(groupLen, seen, item, veryNoisy):
|
|
break
|
|
check xq.txDB.verify.isOK
|
|
check seen.len == xq.nItems.total
|
|
check seen.len < groupLen
|
|
|
|
test &"Load/reverse walk ID queue, " &
|
|
&"deleting in groups of at most {groupLen}":
|
|
var
|
|
xq = bcDB.toTxPool(txList, noisy = noisy)
|
|
seen: seq[TxItemRef]
|
|
|
|
# Set txs to pseudo random status
|
|
xq.setItemStatusFromInfo
|
|
|
|
check xq.txDB.verify.isOK
|
|
elapNoisy.showElapsed("Revese delete-walk ID queue"):
|
|
for item in xq.txDB.byItemID.nextValues:
|
|
if not xq.addOrFlushGroupwise(groupLen, seen, item, veryNoisy):
|
|
break
|
|
check xq.txDB.verify.isOK
|
|
check seen.len == xq.nItems.total
|
|
check seen.len < groupLen
|
|
|
|
block:
|
|
var
|
|
xq = TxPoolRef.new(bcDB,testAddress)
|
|
testTxs: array[5,(TxItemRef,Transaction,Transaction)]
|
|
|
|
test &"Superseding txs with sender and nonce variants":
|
|
var
|
|
testInx = 0
|
|
let
|
|
testBump = xq.priceBump
|
|
lastBump = testBump - 1 # implies underpriced item
|
|
|
|
# load a set of suitable txs into testTxs[]
|
|
for n in 0 ..< txList.len:
|
|
let
|
|
item = txList[n]
|
|
bump = if testInx < testTxs.high: testBump else: lastBump
|
|
rc = item.txModPair(testInx,bump.int)
|
|
if not rc[0].isNil:
|
|
testTxs[testInx] = rc
|
|
testInx.inc
|
|
if testTxs.high < testInx:
|
|
break
|
|
|
|
# verify that test does not degenerate
|
|
check testInx == testTxs.len
|
|
check 0 < lastBump # => 0 < testBump
|
|
|
|
# insert some txs
|
|
for triple in testTxs:
|
|
xq.jobAddTx(triple[1], triple[0].info)
|
|
xq.jobCommit
|
|
|
|
check xq.nItems.total == testTxs.len
|
|
check xq.nItems.disposed == 0
|
|
let infoLst = testTxs.toSeq.mapIt(it[0].info).sorted
|
|
check infoLst == xq.toItems.toSeq.mapIt(it.info).sorted
|
|
|
|
# re-insert modified transactions
|
|
for triple in testTxs:
|
|
xq.jobAddTx(triple[2], "alt " & triple[0].info)
|
|
xq.jobCommit
|
|
|
|
check xq.nItems.total == testTxs.len
|
|
check xq.nItems.disposed == testTxs.len
|
|
|
|
# last update item was underpriced, so it must not have been
|
|
# replaced
|
|
var altLst = testTxs.toSeq.mapIt("alt " & it[0].info)
|
|
altLst[^1] = testTxs[^1][0].info
|
|
check altLst.sorted == xq.toItems.toSeq.mapIt(it.info).sorted
|
|
|
|
test &"Deleting tx => also delete higher nonces":
|
|
|
|
let
|
|
# From the data base, get the one before last item. This was
|
|
# replaced earlier by the second transaction in the triple, i.e.
|
|
# testTxs[^2][2]. FYI, the last transaction is testTxs[^1][1] as
|
|
# it could not be replaced earlier by testTxs[^1][2].
|
|
item = xq.getItem(testTxs[^2][2].itemID).value
|
|
|
|
nWasteBasket = xq.nItems.disposed
|
|
|
|
# make sure the test makes sense, nonces were 0 ..< testTxs.len
|
|
check (item.tx.nonce + 2).int == testTxs.len
|
|
|
|
xq.disposeItems(item)
|
|
|
|
check xq.nItems.total + 2 == testTxs.len
|
|
check nWasteBasket + 2 == xq.nItems.disposed
|
|
|
|
# --------------------------
|
|
|
|
block:
|
|
var
|
|
gap: Time
|
|
nItems: int
|
|
xq = bcDB.toTxPool(timeGap = gap,
|
|
nGapItems = nItems,
|
|
itList = txList,
|
|
itemsPC = 35, # arbitrary
|
|
delayMSecs = 100, # large enough to process
|
|
noisy = noisy)
|
|
|
|
# Set txs to pseudo random status. Note that this functon will cause
|
|
# a violation of boundary conditions regarding nonces. So database
|
|
# integrily check needs xq.txDB.verify() rather than xq.verify().
|
|
xq.setItemStatusFromInfo
|
|
|
|
test &"Auto delete about {nItems} expired txs out of {xq.nItems.total}":
|
|
|
|
check 0 < nItems
|
|
xq.lifeTime = getTime() - gap
|
|
xq.flags = xq.flags + {autoZombifyPacked}
|
|
|
|
# evict and pick items from the wastbasket
|
|
let
|
|
disposedBase = xq.nItems.disposed
|
|
evictedBase = evictionMeter.value
|
|
impliedBase = impliedEvictionMeter.value
|
|
xq.jobCommit(true)
|
|
let
|
|
disposedItems = xq.nItems.disposed - disposedBase
|
|
evictedItems = (evictionMeter.value - evictedBase).int
|
|
impliedItems = (impliedEvictionMeter.value - impliedBase).int
|
|
|
|
check xq.txDB.verify.isOK
|
|
check disposedItems + disposedBase + xq.nItems.total == txList.len
|
|
check 0 < evictedItems
|
|
check evictedItems <= disposedItems
|
|
check disposedItems == evictedItems + impliedItems
|
|
|
|
# make sure that deletion was sort of expected
|
|
let deleteExpextRatio = (evictedItems * 100 / nItems).int
|
|
check deletedItemsRatioBandPC < deleteExpextRatio
|
|
check deleteExpextRatio < (10000 div deletedItemsRatioBandPC)
|
|
|
|
# --------------------
|
|
|
|
block:
|
|
var
|
|
xq = bcDB.toTxPool(txList, noisy = noisy)
|
|
maxAddr: EthAddress
|
|
nAddrItems = 0
|
|
|
|
nAddrPendingItems = 0
|
|
nAddrStagedItems = 0
|
|
nAddrPackedItems = 0
|
|
|
|
fromNumItems = nAddrPendingItems
|
|
fromBucketInfo = "pending"
|
|
fromBucket = txItemPending
|
|
toBucketInfo = "staged"
|
|
toBucket = txItemStaged
|
|
|
|
# Set txs to pseudo random status
|
|
xq.setItemStatusFromInfo
|
|
|
|
# find address with max number of transactions
|
|
for (address,nonceList) in xq.txDB.incAccount:
|
|
if nAddrItems < nonceList.nItems:
|
|
maxAddr = address
|
|
nAddrItems = nonceList.nItems
|
|
|
|
# count items
|
|
nAddrPendingItems = xq.txDB.bySender.eq(maxAddr).eq(txItemPending).nItems
|
|
nAddrStagedItems = xq.txDB.bySender.eq(maxAddr).eq(txItemStaged).nItems
|
|
nAddrPackedItems = xq.txDB.bySender.eq(maxAddr).eq(txItemPacked).nItems
|
|
|
|
# find the largest from-bucket
|
|
if fromNumItems < nAddrStagedItems:
|
|
fromNumItems = nAddrStagedItems
|
|
fromBucketInfo = "staged"
|
|
fromBucket = txItemStaged
|
|
toBucketInfo = "packed"
|
|
toBucket = txItemPacked
|
|
if fromNumItems < nAddrPackedItems:
|
|
fromNumItems = nAddrPackedItems
|
|
fromBucketInfo = "packed"
|
|
fromBucket = txItemPacked
|
|
toBucketInfo = "pending"
|
|
toBucket = txItemPending
|
|
|
|
let moveNumItems = fromNumItems div 2
|
|
|
|
test &"Reassign {moveNumItems} of {fromNumItems} items "&
|
|
&"from \"{fromBucketInfo}\" to \"{toBucketInfo}\"":
|
|
|
|
# requite mimimum => there is a status queue with at least 2 entries
|
|
check 3 < nAddrItems
|
|
|
|
check nAddrPendingItems +
|
|
nAddrStagedItems +
|
|
nAddrPackedItems == nAddrItems
|
|
|
|
check 0 < moveNumItems
|
|
check 1 < fromNumItems
|
|
|
|
var count = 0
|
|
let nonceList = xq.txDB.bySender.eq(maxAddr).eq(fromBucket).value.data
|
|
block collect:
|
|
for item in nonceList.incNonce:
|
|
count.inc
|
|
check xq.txDB.reassign(item, toBucket)
|
|
if moveNumItems <= count:
|
|
break collect
|
|
check xq.txDB.verify.isOK
|
|
|
|
case fromBucket
|
|
of txItemPending:
|
|
check nAddrPendingItems - moveNumItems ==
|
|
xq.txDB.bySender.eq(maxAddr).eq(txItemPending).nItems
|
|
check nAddrStagedItems + moveNumItems ==
|
|
xq.txDB.bySender.eq(maxAddr).eq(txItemStaged).nItems
|
|
check nAddrPackedItems ==
|
|
xq.txDB.bySender.eq(maxAddr).eq(txItemPacked).nItems
|
|
of txItemStaged:
|
|
check nAddrStagedItems - moveNumItems ==
|
|
xq.txDB.bySender.eq(maxAddr).eq(txItemStaged).nItems
|
|
check nAddrPackedItems + moveNumItems ==
|
|
xq.txDB.bySender.eq(maxAddr).eq(txItemPacked).nItems
|
|
check nAddrPendingItems ==
|
|
xq.txDB.bySender.eq(maxAddr).eq(txItemPending).nItems
|
|
else:
|
|
check nAddrPackedItems - moveNumItems ==
|
|
xq.txDB.bySender.eq(maxAddr).eq(txItemPacked).nItems
|
|
check nAddrPendingItems + moveNumItems ==
|
|
xq.txDB.bySender.eq(maxAddr).eq(txItemPending).nItems
|
|
check nAddrPackedItems ==
|
|
xq.txDB.bySender.eq(maxAddr).eq(txItemPacked).nItems
|
|
|
|
# --------------------
|
|
|
|
let expect = (
|
|
xq.txDB.byStatus.eq(txItemPending).nItems,
|
|
xq.txDB.byStatus.eq(txItemStaged).nItems,
|
|
xq.txDB.byStatus.eq(txItemPacked).nItems)
|
|
|
|
test &"Verify #items per bucket ({expect[0]},{expect[1]},{expect[2]})":
|
|
let status = xq.nItems
|
|
check expect == (status.pending,status.staged,status.packed)
|
|
|
|
test "Recycling from waste basket":
|
|
|
|
let
|
|
basketPrefill = xq.nItems.disposed
|
|
numDisposed = min(50,txList.len)
|
|
|
|
# make sure to work on a copy of the pivot item (to see changes)
|
|
thisItem = xq.getItem(txList[^numDisposed].itemID).value.dup
|
|
|
|
# move to wastebasket
|
|
xq.maxRejects = txList.len
|
|
for n in 1 .. numDisposed:
|
|
# use from top avoiding extra deletes (higer nonces per account)
|
|
xq.disposeItems(txList[^n])
|
|
|
|
# make sure that the pivot item is in the waste basket
|
|
check xq.getItem(thisItem.itemID).isErr
|
|
check xq.txDB.byRejects.hasKey(thisItem.itemID)
|
|
check basketPrefill + numDisposed == xq.nItems.disposed
|
|
check txList.len == xq.nItems.total + xq.nItems.disposed
|
|
|
|
# re-add item
|
|
xq.jobAddTx(thisItem.tx)
|
|
xq.jobCommit
|
|
|
|
# verify that the pivot item was moved out from the waste basket
|
|
check not xq.txDB.byRejects.hasKey(thisItem.itemID)
|
|
check basketPrefill + numDisposed == xq.nItems.disposed + 1
|
|
check txList.len == xq.nItems.total + xq.nItems.disposed
|
|
|
|
# verify that a new item was derived from the waste basket pivot item
|
|
let wbItem = xq.getItem(thisItem.itemID).value
|
|
check thisItem.info == wbItem.info
|
|
check thisItem.timestamp < wbItem.timestamp
|
|
|
|
|
|
proc runTxPackerTests(noisy = true) =
|
|
let
|
|
elapNoisy = true # noisy
|
|
|
|
suite &"TxPool: Block packer tests":
|
|
var
|
|
ntBaseFee = 0.GasPrice
|
|
ntNextFee = 0.GasPrice
|
|
|
|
test &"Calculate some non-trivial base fee":
|
|
var
|
|
xq = bcDB.toTxPool(txList, noisy = noisy)
|
|
feesList = SortedSet[GasPriceEx,bool].init()
|
|
|
|
# provide a sorted list of gas fees
|
|
for item in txList:
|
|
discard feesList.insert(item.tx.effectiveGasTip(0.GasPrice))
|
|
|
|
let
|
|
minKey = max(0, feesList.ge(GasPriceEx.low).value.key.int64)
|
|
lowKey = feesList.gt(minKey.GasPriceEx).value.key.uint64
|
|
highKey = feesList.le(GasPriceEx.high).value.key.uint64
|
|
keyRange = highKey - lowKey
|
|
keyStep = max(1u64, keyRange div 500_000)
|
|
|
|
# what follows is a rather crude partitioning so that
|
|
# * ntBaseFee partititions non-zero numbers of pending and staged txs
|
|
# * ntNextFee decreases the number of staged txs
|
|
ntBaseFee = (lowKey + keyStep).GasPrice
|
|
|
|
# the following might throw an exception if the table is de-generated
|
|
var nextKey = ntBaseFee
|
|
for _ in [1, 2, 3]:
|
|
let rcNextKey = feesList.gt(nextKey.GasPriceEx)
|
|
check rcNextKey.isOK
|
|
nextKey = rcNextKey.value.key.uint64.GasPrice
|
|
|
|
ntNextFee = nextKey + keyStep.GasPrice
|
|
|
|
# of course ...
|
|
check ntBaseFee < ntNextFee
|
|
|
|
block:
|
|
var
|
|
xq = bcDB.toTxPool(txList, ntBaseFee, noisy)
|
|
xr = bcDB.toTxPool(txList, ntNextFee, noisy)
|
|
block:
|
|
let
|
|
pending = xq.nItems.pending
|
|
staged = xq.nItems.staged
|
|
packed = xq.nItems.packed
|
|
|
|
test &"Load txs with baseFee={ntBaseFee}, "&
|
|
&"buckets={pending}/{staged}/{packed}":
|
|
|
|
check 0 < pending
|
|
check 0 < staged
|
|
check xq.nItems.total == txList.len
|
|
check xq.nItems.disposed == 0
|
|
|
|
block:
|
|
let
|
|
pending = xr.nItems.pending
|
|
staged = xr.nItems.staged
|
|
packed = xr.nItems.packed
|
|
|
|
test &"Re-org txs previous buckets setting baseFee={ntNextFee}, "&
|
|
&"buckets={pending}/{staged}/{packed}":
|
|
|
|
check 0 < pending
|
|
check 0 < staged
|
|
check xr.nItems.total == txList.len
|
|
check xr.nItems.disposed == 0
|
|
|
|
# having the same set of txs, setting the xq database to the same
|
|
# base fee as the xr one, the bucket fills of both database must
|
|
# be the same after re-org
|
|
xq.baseFee = ntNextFee
|
|
xq.triggerReorg
|
|
xq.jobCommit(forceMaintenance = true)
|
|
|
|
# now, xq should look like xr
|
|
check xq.verify.isOK
|
|
check xq.nItems == xr.nItems
|
|
|
|
block:
|
|
# get some value below the middle
|
|
let
|
|
packPrice = ((minGasPrice + maxGasPrice).uint64 div 3).GasPrice
|
|
lowerPrice = minGasPrice + 1.GasPrice
|
|
|
|
test &"Packing txs, baseFee=0 minPrice={packPrice} "&
|
|
&"targetBlockSize={xq.trgGasLimit}":
|
|
|
|
# verify that the test does not degenerate
|
|
check 0 < minGasPrice
|
|
check minGasPrice < maxGasPrice
|
|
|
|
# ignore base limit so that the `packPrice` below becomes effective
|
|
xq.baseFee = 0.GasPrice
|
|
check xq.nItems.disposed == 0
|
|
|
|
# set minimum target price
|
|
xq.minPreLondonGasPrice = packPrice
|
|
check xq.minPreLondonGasPrice == packPrice
|
|
|
|
# employ packer
|
|
xq.jobCommit(forceMaintenance = true)
|
|
xq.packerVmExec
|
|
check xq.verify.isOK
|
|
|
|
# verify that the test did not degenerate
|
|
check 0 < xq.gasTotals.packed
|
|
check xq.nItems.disposed == 0
|
|
|
|
# assemble block from `packed` bucket
|
|
let
|
|
items = xq.toItems(txItemPacked)
|
|
total = foldl(@[0.GasInt] & items.mapIt(it.tx.gasLimit), a+b)
|
|
check xq.gasTotals.packed == total
|
|
|
|
noisy.say "***", "1st bLock size=", total, " stats=", xq.nItems.pp
|
|
|
|
test &"Clear and re-pack bucket":
|
|
let
|
|
items0 = xq.toItems(txItemPacked)
|
|
saveState0 = foldl(@[0.GasInt] & items0.mapIt(it.tx.gasLimit), a+b)
|
|
check 0 < xq.nItems.packed
|
|
|
|
# re-pack bucket
|
|
xq.jobCommit(forceMaintenance = true)
|
|
xq.packerVmExec
|
|
check xq.verify.isOK
|
|
|
|
let
|
|
items1 = xq.toItems(txItemPacked)
|
|
saveState1 = foldl(@[0.GasInt] & items1.mapIt(it.tx.gasLimit), a+b)
|
|
check items0 == items1
|
|
check saveState0 == saveState1
|
|
|
|
test &"Delete item and re-pack bucket/w lower minPrice={lowerPrice}":
|
|
# verify that the test does not degenerate
|
|
check 0 < lowerPrice
|
|
check lowerPrice < packPrice
|
|
check 0 < xq.nItems.packed
|
|
|
|
let
|
|
saveStats = xq.nItems
|
|
lastItem = xq.toItems(txItemPacked)[^1]
|
|
|
|
# delete last item from packed bucket
|
|
xq.disposeItems(lastItem)
|
|
check xq.verify.isOK
|
|
|
|
# set new minimum target price
|
|
xq.minPreLondonGasPrice = lowerPrice
|
|
check xq.minPreLondonGasPrice == lowerPrice
|
|
|
|
# re-pack bucket, packer needs extra trigger because there is
|
|
# not necessarily a buckets re-org resulting in a change
|
|
xq.jobCommit(forceMaintenance = true)
|
|
xq.packerVmExec
|
|
check xq.verify.isOK
|
|
|
|
let
|
|
items = xq.toItems(txItemPacked)
|
|
newTotal = foldl(@[0.GasInt] & items.mapIt(it.tx.gasLimit), a+b)
|
|
newStats = xq.nItems
|
|
newItem = xq.toItems(txItemPacked)[^1]
|
|
|
|
# for sanity assert the obvoius
|
|
check 0 < xq.gasTotals.packed
|
|
check xq.gasTotals.packed == newTotal
|
|
|
|
# verify incremental packing
|
|
check lastItem.info != newItem.info
|
|
check saveStats.packed <= newStats.packed
|
|
|
|
noisy.say "***", "2st bLock size=", newTotal, " stats=", newStats.pp
|
|
|
|
# -------------------------------------------------
|
|
|
|
block:
|
|
var
|
|
xq = bcDB.toTxPool(txList, ntBaseFee, noisy)
|
|
let
|
|
(nMinTxs, nTrgTxs) = (15, 15)
|
|
(nMinAccounts, nTrgAccounts) = (1, 8)
|
|
canonicalHead = xq.chain.db.getCanonicalHead
|
|
|
|
test &"Back track block chain head (at least "&
|
|
&"{nMinTxs} txs, {nMinAccounts} known accounts)":
|
|
|
|
# get the environment of a state back in the block chain, preferably
|
|
# at least `nTrgTxs` txs and `nTrgAccounts` known accounts
|
|
let
|
|
(backHeader,backTxs,accLst) = xq.getBackHeader(nTrgTxs,nTrgAccounts)
|
|
nBackBlocks = xq.head.blockNumber - backHeader.blockNumber
|
|
stats = xq.nItems
|
|
|
|
# verify that the test would not degenerate
|
|
check nMinAccounts <= accLst.len
|
|
check nMinTxs <= backTxs.len
|
|
|
|
noisy.say "***",
|
|
&"back tracked block chain:" &
|
|
&" {backTxs.len} txs, {nBackBlocks} blocks," &
|
|
&" {accLst.len} known accounts"
|
|
|
|
check xq.nJobs == 0 # want cleared job queue
|
|
check xq.jobDeltaTxsHead(backHeader) # set up tx diff jobs
|
|
xq.head = backHeader # move insertion point
|
|
xq.jobCommit # apply job diffs
|
|
|
|
# make sure that all txs have been added to the pool
|
|
let nFailed = xq.nItems.disposed - stats.disposed
|
|
check stats.disposed == 0
|
|
check stats.total + backTxs.len == xq.nItems.total
|
|
|
|
test &"Run packer, profitability will not increase with block size":
|
|
|
|
xq.flags = xq.flags - {packItemsMaxGasLimit}
|
|
xq.packerVmExec
|
|
let
|
|
smallerBlockProfitability = xq.profitability
|
|
smallerBlockSize = xq.gasCumulative
|
|
|
|
noisy.say "***", "trg-packing",
|
|
" profitability=", xq.profitability,
|
|
" used=", xq.gasCumulative,
|
|
" trg=", xq.trgGasLimit,
|
|
" slack=", xq.trgGasLimit - xq.gasCumulative
|
|
|
|
xq.flags = xq.flags + {packItemsMaxGasLimit}
|
|
xq.packerVmExec
|
|
|
|
noisy.say "***", "max-packing",
|
|
" profitability=", xq.profitability,
|
|
" used=", xq.gasCumulative,
|
|
" max=", xq.maxGasLimit,
|
|
" slack=", xq.maxGasLimit - xq.gasCumulative
|
|
|
|
check smallerBlockSize < xq.gasCumulative
|
|
check 0 < xq.profitability
|
|
|
|
# Well, this ratio should be above 100 but might be slightly less
|
|
# with small data samples (pathological case.)
|
|
let blockProfitRatio =
|
|
(((smallerBlockProfitability.uint64 * 1000) div
|
|
(max(1u64,xq.profitability.uint64))) + 5) div 10
|
|
check decreasingBlockProfitRatioPC <= blockProfitRatio
|
|
|
|
noisy.say "***", "cmp",
|
|
" increase=", xq.gasCumulative - smallerBlockSize,
|
|
" trg/max=", blockProfitRatio, "%"
|
|
|
|
# if true: return
|
|
test "Store generated block in block chain database":
|
|
|
|
# Force maximal block size. Accidentally, the latest tx should have
|
|
# a `gasLimit` exceeding the available space on the block `gasLimit`
|
|
# which will be checked below.
|
|
xq.flags = xq.flags + {packItemsMaxGasLimit}
|
|
|
|
# Invoke packer
|
|
let blk = xq.ethBlock
|
|
|
|
# Make sure that there are at least two txs on the packed block so
|
|
# this test does not degenerate.
|
|
check 1 < xq.chain.receipts.len
|
|
|
|
var overlap = -1
|
|
for n in countDown(blk.txs.len - 1, 0):
|
|
let total = xq.chain.receipts[n].cumulativeGasUsed
|
|
if blk.header.gasUsed < total + blk.txs[n].gasLimit:
|
|
overlap = n
|
|
break
|
|
|
|
noisy.say "***",
|
|
"overlap=#", overlap,
|
|
" tx=#", blk.txs.len,
|
|
" gasUsed=", blk.header.gasUsed,
|
|
" gasLimit=", blk.header.gasLimit
|
|
|
|
if 0 <= overlap:
|
|
let
|
|
n = overlap
|
|
mostlySize = xq.chain.receipts[n].cumulativeGasUsed
|
|
noisy.say "***", "overlap",
|
|
" size=", mostlySize + blk.txs[n].gasLimit - blk.header.gasUsed
|
|
|
|
let
|
|
poa = bcDB.newClique
|
|
bdy = BlockBody(transactions: blk.txs)
|
|
hdr = block:
|
|
var rc = blk.header
|
|
rc.gasLimit = blk.header.gasUsed
|
|
rc.testKeySign
|
|
|
|
# Make certain that some tx was set up so that its gasLimit overlaps
|
|
# with the total block size. Of course, running it in the VM will burn
|
|
# much less than permitted so this block will be accepted.
|
|
check 0 < overlap
|
|
|
|
# Test low-level function for adding the new block to the database
|
|
xq.chain.maxMode = (packItemsMaxGasLimit in xq.flags)
|
|
xq.chain.clearAccounts
|
|
check xq.chain.vmState.processBlock(poa, hdr, bdy).isOK
|
|
|
|
# Re-allocate using VM environment from `persistBlocks()`
|
|
check BaseVMState.new(hdr, bcDB).processBlock(poa, hdr, bdy).isOK
|
|
|
|
# This should not have changed
|
|
check canonicalHead == xq.chain.db.getCanonicalHead
|
|
|
|
# Using the high-level library function, re-append the block while
|
|
# turning off header verification.
|
|
let c = bcDB.newChain(extraValidation = false)
|
|
check c.persistBlocks(@[hdr], @[bdy]).isOK
|
|
|
|
# The canonical head will be set to hdr if it scores high enough
|
|
# (see implementation of db_chain.persistHeaderToDb()).
|
|
let
|
|
canonScore = xq.chain.db.getScore(canonicalHead.blockHash)
|
|
headerScore = xq.chain.db.getScore(hdr.blockHash)
|
|
|
|
if canonScore < headerScore:
|
|
# Note that the updated canonical head is equivalent to hdr but not
|
|
# necessarily binary equal.
|
|
check hdr.blockHash == xq.chain.db.getCanonicalHead.blockHash
|
|
else:
|
|
check canonicalHead == xq.chain.db.getCanonicalHead
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Main function(s)
|
|
# ------------------------------------------------------------------------------
|
|
|
|
proc txPoolMain*(noisy = defined(debug)) =
|
|
noisy.runTxLoader
|
|
noisy.runTxPoolTests
|
|
noisy.runTxPackerTests
|
|
|
|
when isMainModule:
|
|
const
|
|
noisy = defined(debug)
|
|
capts0: CaptureSpecs = goerliCapture
|
|
capts1: CaptureSpecs = (GoerliNet, "goerli504192.txt.gz", 30000, 500, 1500)
|
|
# Note: mainnet has the leading 45k blocks without any transactions
|
|
capts2: CaptureSpecs = (MainNet, "mainnet843841.txt.gz", 30000, 500, 1500)
|
|
|
|
noisy.runTxLoader(capture = capts2)
|
|
noisy.runTxPoolTests
|
|
true.runTxPackerTests
|
|
|
|
#noisy.runTxLoader(dir = ".")
|
|
#noisy.runTxPoolTests
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# End
|
|
# ------------------------------------------------------------------------------
|