nimbus-eth1/hive_integration/nodocker/engine/engine_tests.nim

1921 lines
67 KiB
Nim

import
std/tables,
test_env,
stew/byteutils,
chronicles,
unittest2,
nimcrypto,
chronos,
./helper,
../../../nimbus/transaction,
../../../nimbus/rpc/rpc_types,
../../../nimbus/merge/mergeutils
type
TestSpec* = object
name*: string
run*: proc(t: TestEnv): TestStatus
ttd*: int64
const
prevRandaoContractAddr = hexToByteArray[20]("0000000000000000000000000000000000000316")
template testCond(expr: untyped) =
if not (expr):
return TestStatus.Failed
template testCond(expr, body: untyped) =
if not (expr):
body
return TestStatus.Failed
# Invalid Terminal Block in ForkchoiceUpdated:
# Client must reject ForkchoiceUpdated directives if the referenced HeadBlockHash does not meet the TTD requirement.
proc invalidTerminalBlockForkchoiceUpdated(t: TestEnv): TestStatus =
result = TestStatus.OK
let
gHash = Web3BlockHash t.gHeader.blockHash.data
forkchoiceState = ForkchoiceStateV1(
headBlockHash: gHash,
safeBlockHash: gHash,
finalizedBlockHash: gHash,
)
let res = t.rpcClient.forkchoiceUpdatedV1(forkchoiceState)
# Execution specification:
# {payloadStatus: {status: INVALID, latestValidHash=0x00..00}, payloadId: null}
# either obtained from the Payload validation process or as a result of validating a PoW block referenced by forkchoiceState.headBlockHash
testCond res.isOk
let s = res.get()
testCond s.payloadStatus.status == PayloadExecutionStatus.invalid
testCond s.payloadStatus.latestValidHash.isNone
testCond s.payloadId.isNone
# ValidationError is not validated since it can be either null or a string message
# Check that PoW chain progresses
testCond t.verifyPoWProgress(t.gHeader.blockHash)
# Invalid GetPayload Under PoW: Client must reject GetPayload directives under PoW.
proc invalidGetPayloadUnderPoW(t: TestEnv): TestStatus =
result = TestStatus.OK
# We start in PoW and try to get an invalid Payload, which should produce an error but nothing should be disrupted.
let id = PayloadID [1.byte, 2,3,4,5,6,7,8]
let res = t.rpcClient.getPayloadV1(id)
testCond res.isErr
# Check that PoW chain progresses
testCond t.verifyPoWProgress(t.gHeader.blockHash)
# Invalid Terminal Block in NewPayload:
# Client must reject NewPayload directives if the referenced ParentHash does not meet the TTD requirement.
proc invalidTerminalBlockNewPayload(t: TestEnv): TestStatus =
result = TestStatus.OK
let gBlock = t.gHeader
let payload = ExecutableData(
parentHash: gBlock.blockHash,
stateRoot: gBlock.stateRoot,
receiptsRoot: BLANK_ROOT_HASH,
number: 1,
gasLimit: gBlock.gasLimit,
gasUsed: 0,
timestamp: gBlock.timestamp + 1.seconds,
baseFeePerGas:gBlock.baseFee
)
let hashedPayload = customizePayload(payload, CustomPayload())
let res = t.rpcClient.newPayloadV1(hashedPayload)
# Execution specification:
# {status: INVALID_TERMINAL_BLOCK, latestValidHash: null, validationError: errorMessage | null}
# if terminal block conditions are not satisfied
testCond res.isOk
let s = res.get()
testCond s.status == PayloadExecutionStatus.invalid
testCond s.latestValidHash.isNone
# Check that PoW chain progresses
testCond t.verifyPoWProgress(t.gHeader.blockHash)
proc unknownHeadBlockHash(t: TestEnv): TestStatus =
result = TestStatus.OK
let ok = waitFor t.clMock.waitForTTD()
testCond ok
var randomHash: Hash256
testCond nimcrypto.randomBytes(randomHash.data) == 32
let clMock = t.clMock
let forkchoiceStateUnknownHeadHash = ForkchoiceStateV1(
headBlockHash: BlockHash randomHash.data,
safeBlockHash: clMock.latestForkchoice.finalizedBlockHash,
finalizedBlockHash: clMock.latestForkchoice.finalizedBlockHash,
)
var res = t.rpcClient.forkchoiceUpdatedV1(forkchoiceStateUnknownHeadHash)
testCond res.isOk
let s = res.get()
# Execution specification::
# - {payloadStatus: {status: SYNCING, latestValidHash: null, validationError: null}, payloadId: null}
# if forkchoiceState.headBlockHash references an unknown payload or a payload that can't be validated
# because requisite data for the validation is missing
testCond s.payloadStatus.status == PayloadExecutionStatus.syncing
# Test again using PayloadAttributes, should also return SYNCING and no PayloadID
let timestamp = uint64 clMock.latestExecutedPayload.timestamp
let payloadAttr = PayloadAttributesV1(
timestamp: Quantity(timestamp + 1)
)
res = t.rpcClient.forkchoiceUpdatedV1(forkchoiceStateUnknownHeadHash, some(payloadAttr))
testCond res.isOk
testCond s.payloadStatus.status == PayloadExecutionStatus.syncing
testCond s.payloadId.isNone
proc unknownSafeBlockHash(t: TestEnv): TestStatus =
result = TestStatus.OK
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test
let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produce5BlockRes
let clMock = t.clMock
let client = t.rpcClient
let produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks(
# Run test after a new payload has been broadcast
onNewPayloadBroadcast: proc(): bool =
# Generate a random SafeBlock hash
var randomSafeBlockHash: Hash256
doAssert nimcrypto.randomBytes(randomSafeBlockHash.data) == 32
# Send forkchoiceUpdated with random SafeBlockHash
let forkchoiceStateUnknownSafeHash = ForkchoiceStateV1(
headBlockHash: clMock.latestExecutedPayload.blockHash,
safeBlockHash: BlockHash randomSafeBlockHash.data,
finalizedBlockHash: clMock.latestForkchoice.finalizedBlockHash,
)
# Execution specification:
# - This value MUST be either equal to or an ancestor of headBlockHash
let res = client.forkchoiceUpdatedV1(forkchoiceStateUnknownSafeHash)
return res.isErr
))
testCond produceSingleBlockRes
proc unknownFinalizedBlockHash(t: TestEnv): TestStatus =
result = TestStatus.OK
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test
let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produce5BlockRes
let clMock = t.clMock
let client = t.rpcClient
let produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks(
# Run test after a new payload has been broadcast
onNewPayloadBroadcast: proc(): bool =
# Generate a random SafeBlock hash
var randomFinalBlockHash: Hash256
doAssert nimcrypto.randomBytes(randomFinalBlockHash.data) == 32
# Send forkchoiceUpdated with random SafeBlockHash
let forkchoiceStateUnknownFinalizedHash = ForkchoiceStateV1(
headBlockHash: clMock.latestExecutedPayload.blockHash,
safeBlockHash: clMock.latestForkchoice.safeBlockHash,
finalizedBlockHash: BlockHash randomFinalBlockHash.data,
)
# Execution specification:
# - This value MUST be either equal to or an ancestor of headBlockHash
var res = client.forkchoiceUpdatedV1(forkchoiceStateUnknownFinalizedHash)
if res.isOk:
return false
# Test again using PayloadAttributes, should also return INVALID and no PayloadID
let timestamp = uint64 clMock.latestExecutedPayload.timestamp
let payloadAttr = PayloadAttributesV1(
timestamp: Quantity(timestamp + 1)
)
res = client.forkchoiceUpdatedV1(forkchoiceStateUnknownFinalizedHash, some(payloadAttr))
return res.isErr
))
testCond produceSingleBlockRes
# Send an inconsistent ForkchoiceState with a known payload that belongs to a side chain as head, safe or finalized.
type
Inconsistency {.pure.} = enum
Head
Safe
Finalized
PayloadList = ref object
canonicalPayloads : seq[ExecutableData]
alternativePayloads: seq[ExecutableData]
template inconsistentForkchoiceStateGen(procname: untyped, inconsistency: Inconsistency) =
proc procName(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
var pList = PayloadList()
let clMock = t.clMock
let client = t.rpcClient
# Produce blocks before starting the test
let produceBlockRes = clMock.produceBlocks(3, BlockProcessCallbacks(
onGetPayload: proc(): bool =
# Generate and send an alternative side chain
var customData = CustomPayload(
extraData: some(@[0x01.byte])
)
if pList.alternativePayloads.len > 0:
customData.parentHash = some(pList.alternativePayloads[^1].blockHash)
let executableData = toExecutableData(clMock.latestPayloadBuilt)
let alternativePayload = customizePayload(executableData, customData)
pList.alternativePayloads.add(alternativePayload.toExecutableData)
let latestCanonicalPayload = toExecutableData(clMock.latestPayloadBuilt)
pList.canonicalPayloads.add(latestCanonicalPayload)
# Send the alternative payload
let res = client.newPayloadV1(alternativePayload)
if res.isErr:
return false
let s = res.get()
s.status == PayloadExecutionStatus.valid or s.status == PayloadExecutionStatus.accepted
))
testCond produceBlockRes
# Send the invalid ForkchoiceStates
let len = pList.alternativePayloads.len
var inconsistentFcU = ForkchoiceStateV1(
headBlockHash: Web3BlockHash pList.canonicalPayloads[len-1].blockHash.data,
safeBlockHash: Web3BlockHash pList.canonicalPayloads[len-2].blockHash.data,
finalizedBlockHash: Web3BlockHash pList.canonicalPayloads[len-3].blockHash.data,
)
when inconsistency == Inconsistency.Head:
inconsistentFcU.headBlockHash = Web3BlockHash pList.alternativePayloads[len-1].blockHash.data
elif inconsistency == Inconsistency.Safe:
inconsistentFcU.safeBlockHash = Web3BlockHash pList.alternativePayloads[len-2].blockHash.data
else:
inconsistentFcU.finalizedBlockHash = Web3BlockHash pList.alternativePayloads[len-3].blockHash.data
var r = client.forkchoiceUpdatedV1(inconsistentFcU)
testCond r.isErr
# Return to the canonical chain
r = client.forkchoiceUpdatedV1(clMock.latestForkchoice)
testCond r.isOk
let s = r.get()
testCond s.payloadStatus.status == PayloadExecutionStatus.valid
inconsistentForkchoiceStateGen(inconsistentForkchoiceState1, Inconsistency.Head)
inconsistentForkchoiceStateGen(inconsistentForkchoiceState2, Inconsistency.Safe)
inconsistentForkchoiceStateGen(inconsistentForkchoiceState3, Inconsistency.Finalized)
# Verify behavior on a forkchoiceUpdated with invalid payload attributes
template invalidPayloadAttributesGen(procname: untyped, syncingCond: bool) =
proc procName(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
let clMock = t.clMock
let client = t.rpcClient
# Produce blocks before starting the test
var produceBlockRes = clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produceBlockRes
# Send a forkchoiceUpdated with invalid PayloadAttributes
produceBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks(
onNewPayloadBroadcast: proc(): bool =
# Try to apply the new payload with invalid attributes
var blockHash: Hash256
when syncingCond:
# Setting a random hash will put the client into `SYNCING`
doAssert nimcrypto.randomBytes(blockHash.data) == 32
else:
# Set the block hash to the next payload that was broadcasted
blockHash = hash256(clMock.latestPayloadBuilt.blockHash)
let fcu = ForkchoiceStateV1(
headBlockHash: Web3BlockHash blockHash.data,
safeBlockHash: Web3BlockHash blockHash.data,
finalizedBlockHash: Web3BlockHash blockHash.data,
)
let attr = PayloadAttributesV1()
# 0) Check headBlock is known and there is no missing data, if not respond with SYNCING
# 1) Check headBlock is VALID, if not respond with INVALID
# 2) Apply forkchoiceState
# 3) Check payloadAttributes, if invalid respond with error: code: Invalid payload attributes
# 4) Start payload build process and respond with VALID
when syncingCond:
# If we are SYNCING, the outcome should be SYNCING regardless of the validity of the payload atttributes
let r = client.forkchoiceUpdatedV1(fcu, some(attr))
let s = r.get()
if s.payloadStatus.status != PayloadExecutionStatus.syncing:
return false
if s.payloadId.isSome:
return false
else:
let r = client.forkchoiceUpdatedV1(fcu, some(attr))
if r.isOk:
debugEcho "EEEE"
return false
# Check that the forkchoice was applied, regardless of the error
var header: EthBlockHeader
let s = client.latestHeader(header)
if s.isErr:
return false
if header.blockHash != blockHash:
return false
return true
))
testCond produceBlockRes
invalidPayloadAttributesGen(invalidPayloadAttributes1, false)
invalidPayloadAttributesGen(invalidPayloadAttributes2, true)
proc preTTDFinalizedBlockHash(t: TestEnv): TestStatus =
result = TestStatus.OK
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test
let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produce5BlockRes
let
gHash = Web3BlockHash t.gHeader.blockHash.data
forkchoiceState = ForkchoiceStateV1(
headBlockHash: gHash,
safeBlockHash: gHash,
finalizedBlockHash: gHash,
)
client = t.rpcClient
clMock = t.clMock
var res = client.forkchoiceUpdatedV1(forkchoiceState)
# TBD: Behavior on this edge-case is undecided, as behavior of the Execution client
# if not defined on re-orgs to a point before the latest finalized block.
res = client.forkchoiceUpdatedV1(clMock.latestForkchoice)
testCond res.isOk
let s = res.get()
testCond s.payloadStatus.status == PayloadExecutionStatus.valid
# Corrupt the hash of a valid payload, client should reject the payload.
# All possible scenarios:
# (fcU)
# ┌────────┐ ┌────────────────────────┐
# │ HEAD │◄───────┤ Bad Hash (!Sync,!Side) │
# └────┬───┘ └────────────────────────┘
# │
# │
# ┌────▼───┐ ┌────────────────────────┐
# │ HEAD-1 │◄───────┤ Bad Hash (!Sync, Side) │
# └────┬───┘ └────────────────────────┘
# │
#
#
# (fcU)
# ******************** ┌───────────────────────┐
# * (Unknown) HEAD *◄─┤ Bad Hash (Sync,!Side) │
# ******************** └───────────────────────┘
# │
# │
# ┌────▼───┐ ┌───────────────────────┐
# │ HEAD-1 │◄───────────┤ Bad Hash (Sync, Side) │
# └────┬───┘ └───────────────────────┘
# │
#
type
Shadow = ref object
hash: Hash256
template badHashOnNewPayloadGen(procname: untyped, syncingCond: bool, sideChain: bool) =
proc procName(t: TestEnv): TestStatus =
result = TestStatus.OK
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test
let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produce5BlockRes
let clMock = t.clMock
let client = t.rpcClient
let shadow = Shadow()
var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks(
# Run test after the new payload has been obtained
onGetPayload: proc(): bool =
# Alter hash on the payload and send it to client, should produce an error
var alteredPayload = clMock.latestPayloadBuilt
var invalidPayloadHash = hash256(alteredPayload.blockHash)
let lastByte = int invalidPayloadHash.data[^1]
invalidPayloadHash.data[^1] = byte(not lastByte)
shadow.hash = invalidPayloadHash
alteredPayload.blockHash = BlockHash invalidPayloadHash.data
when not syncingCond and sideChain:
# We alter the payload by setting the parent to a known past block in the
# canonical chain, which makes this payload a side chain payload, and also an invalid block hash
# (because we did not update the block hash appropriately)
alteredPayload.parentHash = Web3BlockHash clMock.latestHeader.parentHash.data
elif syncingCond:
# We need to send an fcU to put the client in SYNCING state.
var randomHeadBlock: Hash256
doAssert nimcrypto.randomBytes(randomHeadBlock.data) == 32
let latestHeaderHash = clMock.latestHeader.blockHash
let fcU = ForkchoiceStateV1(
headBlockHash: Web3BlockHash randomHeadBlock.data,
safeBlockHash: Web3BlockHash latestHeaderHash.data,
finalizedBlockHash: Web3BlockHash latestHeaderHash.data
)
let r = client.forkchoiceUpdatedV1(fcU)
if r.isErr:
return false
let z = r.get()
if z.payloadStatus.status != PayloadExecutionStatus.syncing:
return false
when sidechain:
# Syncing and sidechain, the caonincal head is an unknown payload to us,
# but this specific bad hash payload is in theory part of a side chain.
# Therefore the parent we use is the head hash.
alteredPayload.parentHash = Web3BlockHash latestHeaderHash.data
else:
# The invalid bad-hash payload points to the unknown head, but we know it is
# indeed canonical because the head was set using forkchoiceUpdated.
alteredPayload.parentHash = Web3BlockHash randomHeadBlock.data
let res = client.newPayloadV1(alteredPayload)
# Execution specification::
# - {status: INVALID_BLOCK_HASH, latestValidHash: null, validationError: null} if the blockHash validation has failed
if res.isErr:
return false
let s = res.get()
if s.status != PayloadExecutionStatus.invalid_block_hash:
return false
s.latestValidHash.isNone
))
testCond produceSingleBlockRes
# Lastly, attempt to build on top of the invalid payload
produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks(
# Run test after the new payload has been obtained
onGetPayload: proc(): bool =
let payload = toExecutableData(clMock.latestPayloadBuilt)
let alteredPayload = customizePayload(payload, CustomPayload(
parentHash: some(shadow.hash),
))
let res = client.newPayloadV1(alteredPayload)
if res.isErr:
return false
# Response status can be ACCEPTED (since parent payload could have been thrown out by the client)
# or INVALID (client still has the payload and can verify that this payload is incorrectly building on top of it),
# but a VALID response is incorrect.
let s = res.get()
s.status != PayloadExecutionStatus.valid
))
testCond produceSingleBlockRes
badHashOnNewPayloadGen(badHashOnNewPayload1, false, false)
badHashOnNewPayloadGen(badHashOnNewPayload2, true, false)
badHashOnNewPayloadGen(badHashOnNewPayload3, false, true)
badHashOnNewPayloadGen(badHashOnNewPayload4, true, true)
proc parentHashOnExecPayload(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test
let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produce5BlockRes
let clMock = t.clMock
let client = t.rpcClient
var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks(
# Run test after the new payload has been obtained
onGetPayload: proc(): bool =
# Alter hash on the payload and send it to client, should produce an error
var alteredPayload = clMock.latestPayloadBuilt
alteredPayload.blockHash = alteredPayload.parentHash
let res = client.newPayloadV1(alteredPayload)
if res.isErr:
return false
# Execution specification::
# - {status: INVALID_BLOCK_HASH, latestValidHash: null, validationError: null} if the blockHash validation has failed
let s = res.get()
s.status == PayloadExecutionStatus.invalid_block_hash
))
testCond produceSingleBlockRes
template invalidPayloadTestCaseGen(procName: untyped, payloadField: InvalidPayloadField, emptyTxs: bool = false) =
proc procName(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
let clMock = t.clMock
let client = t.rpcClient
template txProc() =
when not emptyTxs:
let
tx = t.makeNextTransaction(prevRandaoContractAddr, 0.u256)
rr = client.sendTransaction(tx)
if rr.isErr:
error "Unable to send transaction", msg=rr.error
return false
# Produce blocks before starting the test
var pbRes = clMock.produceBlocks(5, BlockProcessCallbacks(
# Make sure at least one transaction is included in each block
onPayloadProducerSelected: proc(): bool =
txProc()
return true
))
testCond pbRes
let invalidPayload = Shadow()
pbRes = clMock.produceSingleBlock(BlockProcessCallbacks(
# Make sure at least one transaction is included in the payload
onPayloadProducerSelected: proc(): bool =
txProc()
return true
,
# Run test after the new payload has been obtained
onGetPayload: proc(): bool =
# Alter the payload while maintaining a valid hash and send it to the client, should produce an error
# We need at least one transaction for most test cases to work
when not emptyTxs:
if clMock.latestPayloadBuilt.transactions.len == 0:
# But if the payload has no transactions, the test is invalid
error "No transactions in the base payload"
return false
let execData = clMock.latestPayloadBuilt.toExecutableData
let alteredPayload = generateInvalidPayload(execData, payloadField, t.vaultKey)
invalidPayload.hash = hash256(alteredPayload.blockHash)
# Depending on the field we modified, we expect a different status
let rr = client.newPayloadV1(alteredPayload)
if rr.isErr:
error "unable to send altered payload", msg=rr.error
return false
let s = rr.get()
when payloadField == InvalidParentHash:
# Execution specification::
# {status: ACCEPTED, latestValidHash: null, validationError: null} if the following conditions are met:
# - the blockHash of the payload is valid
# - the payload doesn't extend the canonical chain
# - the payload hasn't been fully validated
# {status: SYNCING, latestValidHash: null, validationError: null}
# if the payload extends the canonical chain and requisite data for its validation is missing
# (the client can assume the payload extends the canonical because the linking payload could be missing)
if s.status notin {PayloadExecutionStatus.syncing, PayloadExecutionStatus.accepted}:
error "newPayloadV1 status expect syncing or accepted", get=s.status
return false
if s.latestValidHash.isSome:
error "newPayloadV1 latestValidHash not empty"
return false
else:
if s.status != PayloadExecutionStatus.invalid:
error "newPayloadV1 status expect invalid", get=s.status
return false
if s.latestValidHash.isNone:
return false
let latestValidHash = s.latestValidHash.get
if latestValidHash != alteredPayload.parentHash:
error "latestValidHash is not the same with parentHash",
expected = alteredPayload.parentHash.toHex, get = latestValidHash.toHex
return false
# Send the forkchoiceUpdated with a reference to the invalid payload.
let fcState = ForkchoiceStateV1(
headBlockHash: alteredPayload.blockHash,
safeBlockHash: alteredPayload.blockHash,
finalizedBlockHash: alteredPayload.blockHash,
)
let timestamp = Quantity(alteredPayload.timestamp.int64 + 1)
let payloadAttr = PayloadAttributesV1(timestamp: timestamp)
# Execution specification:
# {payloadStatus: {status: INVALID, latestValidHash: null, validationError: errorMessage | null}, payloadId: null}
# obtained from the Payload validation process if the payload is deemed INVALID
let rs = client.forkchoiceUpdatedV1(fcState, some(payloadAttr))
# Execution specification:
# {payloadStatus: {status: INVALID, latestValidHash: null, validationError: errorMessage | null}, payloadId: null}
# obtained from the Payload validation process if the payload is deemed INVALID
# Note: SYNCING/ACCEPTED is acceptable here as long as the block produced after this test is produced successfully
if rs.isErr:
error "unable to send altered payload", msg=rs.error
return false
let z = rs.get()
if z.payloadStatus.status notin {PayloadExecutionStatus.syncing, PayloadExecutionStatus.accepted, PayloadExecutionStatus.invalid}:
return false
# Finally, attempt to fetch the invalid payload using the JSON-RPC endpoint
var header: BlockHeader
let rp = client.headerByHash(alteredPayload.blockHash.hash256, header)
rp.isErr
))
testCond pbRes
# Lastly, attempt to build on top of the invalid payload
let psb = clMock.produceSingleBlock(BlockProcessCallbacks(
# Run test after the new payload has been obtained
onGetPayload: proc(): bool =
let alteredPayload = customizePayload(clMock.latestPayloadBuilt.toExecutableData, CustomPayload(
parentHash: some(invalidPayload.hash),
))
info "Sending customized NewPayload: ParentHash",
fromHash=clMock.latestPayloadBuilt.parentHash.toHex, toHash=invalidPayload.hash.toHex
# Response status can be ACCEPTED (since parent payload could have been thrown out by the client)
# or SYNCING (parent payload is thrown out and also client assumes that the parent is part of canonical chain)
# or INVALID (client still has the payload and can verify that this payload is incorrectly building on top of it),
# but a VALID response is incorrect.
let rr = client.newPayloadV1(alteredPayload)
if rr.isErr:
error "unable to send altered payload", msg=rr.error
return false
let z = rr.get()
z.status in {PayloadExecutionStatus.syncing, PayloadExecutionStatus.accepted, PayloadExecutionStatus.invalid}
))
testCond psb
invalidPayloadTestCaseGen(invalidPayload1, InvalidParentHash)
invalidPayloadTestCaseGen(invalidPayload2, InvalidStateRoot)
invalidPayloadTestCaseGen(invalidPayload3, InvalidStateRoot, true)
invalidPayloadTestCaseGen(invalidPayload4, InvalidReceiptsRoot)
invalidPayloadTestCaseGen(invalidPayload5, InvalidNumber)
invalidPayloadTestCaseGen(invalidPayload6, InvalidGasLimit)
invalidPayloadTestCaseGen(invalidPayload7, InvalidGasUsed)
invalidPayloadTestCaseGen(invalidPayload8, InvalidTimestamp)
invalidPayloadTestCaseGen(invalidPayload9, InvalidPrevRandao)
invalidPayloadTestCaseGen(invalidPayload10, RemoveTransaction)
invalidPayloadTestCaseGen(invalidPayload11, InvalidTransactionSignature)
invalidPayloadTestCaseGen(invalidPayload12, InvalidTransactionNonce)
invalidPayloadTestCaseGen(invalidPayload13, InvalidTransactionGasPrice)
invalidPayloadTestCaseGen(invalidPayload14, InvalidTransactionGas)
invalidPayloadTestCaseGen(invalidPayload15, InvalidTransactionValue)
# Test to verify Block information available at the Eth RPC after NewPayload
template blockStatusExecPayloadGen(procname: untyped, transitionBlock: bool) =
proc procName(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test, only if we are not testing the transition block
when not transitionBlock:
let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produce5BlockRes
let clMock = t.clMock
let client = t.rpcClient
let shadow = Shadow()
var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks(
onPayloadProducerSelected: proc(): bool =
var address: EthAddress
let tx = t.makeNextTransaction(address, 1.u256)
let res = client.sendTransaction(tx)
if res.isErr:
error "Unable to send transaction"
return false
shadow.hash = rlpHash(tx)
return true
,
onNewPayloadBroadcast: proc(): bool =
# TODO: Ideally, we would need to testCond that the newPayload returned VALID
var lastHeader: EthBlockHeader
var hRes = client.latestHeader(lastHeader)
if hRes.isErr:
error "unable to get latest header", msg=hRes.error
return false
let lastHash = BlockHash lastHeader.blockHash.data
# Latest block header available via Eth RPC should not have changed at this point
if lastHash!= clMock.latestForkchoice.headBlockHash:
error "latest block header incorrect after newPayload", hash=lastHash.toHex
return false
let nRes = client.blockNumber()
if nRes.isErr:
error "Unable to get latest block number", msg=nRes.error
return false
# Latest block number available via Eth RPC should not have changed at this point
let latestNumber = nRes.get
if latestNumber != clMock.latestHeadNumber:
error "latest block number incorrect after newPayload",
expected=clMock.latestHeadNumber,
get=latestNumber
return false
# Check that the receipt for the transaction we just sent is still not available
let rr = client.txReceipt(shadow.hash)
if rr.isOk:
error "not expecting receipt"
return false
return true
))
testCond produceSingleBlockRes
blockStatusExecPayloadGen(blockStatusExecPayload1, false)
blockStatusExecPayloadGen(blockStatusExecPayload2, true)
template blockStatusHeadBlockGen(procname: untyped, transitionBlock: bool) =
proc procName(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test, only if we are not testing the transition block
when not transitionBlock:
let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produce5BlockRes
let clMock = t.clMock
let client = t.rpcClient
let shadow = Shadow()
var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks(
onPayloadProducerSelected: proc(): bool =
var address: EthAddress
let tx = t.makeNextTransaction(address, 1.u256)
let res = client.sendTransaction(tx)
if res.isErr:
error "Unable to send transaction"
return false
shadow.hash = rlpHash(tx)
return true
,
# Run test after a forkchoice with new HeadBlockHash has been broadcasted
onForkchoiceBroadcast: proc(): bool =
var lastHeader: EthBlockHeader
var hRes = client.latestHeader(lastHeader)
if hRes.isErr:
error "unable to get latest header", msg=hRes.error
return false
let lastHash = BlockHash lastHeader.blockHash.data
if lastHash != clMock.latestForkchoice.headBlockHash:
error "latest block header doesn't match HeadBlock hash", hash=lastHash.toHex
return false
let rr = client.txReceipt(shadow.hash)
if rr.isErr:
error "unable to get transaction receipt"
return false
return true
))
testCond produceSingleBlockRes
blockStatusHeadBlockGen(blockStatusHeadBlock1, false)
blockStatusHeadBlockGen(blockStatusHeadBlock2, true)
template blockStatusSafeBlockGen(procname: untyped, transitionBlock: bool) =
proc procName(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test, only if we are not testing the transition block
when not transitionBlock:
let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produce5BlockRes
let clMock = t.clMock
let client = t.rpcClient
let shadow = Shadow()
var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks(
onPayloadProducerSelected: proc(): bool =
var address: EthAddress
let tx = t.makeNextTransaction(address, 1.u256)
let res = client.sendTransaction(tx)
if res.isErr:
error "Unable to send transaction"
return false
shadow.hash = rlpHash(tx)
return true
,
# Run test after a forkchoice with new HeadBlockHash has been broadcasted
onSafeBlockChange: proc(): bool =
var lastHeader: EthBlockHeader
var hRes = client.latestHeader(lastHeader)
if hRes.isErr:
error "unable to get latest header", msg=hRes.error
return false
let lastHash = BlockHash lastHeader.blockHash.data
if lastHash != clMock.latestForkchoice.headBlockHash:
error "latest block header doesn't match SafeBlock hash", hash=lastHash.toHex
return false
let rr = client.txReceipt(shadow.hash)
if rr.isErr:
error "unable to get transaction receipt"
return false
return true
))
testCond produceSingleBlockRes
blockStatusSafeBlockGen(blockStatusSafeBlock1, false)
blockStatusSafeBlockGen(blockStatusSafeBlock2, true)
template blockStatusFinalizedBlockGen(procname: untyped, transitionBlock: bool) =
proc procName(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test, only if we are not testing the transition block
when not transitionBlock:
let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produce5BlockRes
let clMock = t.clMock
let client = t.rpcClient
let shadow = Shadow()
var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks(
onPayloadProducerSelected: proc(): bool =
var address: EthAddress
let tx = t.makeNextTransaction(address, 1.u256)
let res = client.sendTransaction(tx)
if res.isErr:
error "Unable to send transaction"
return false
shadow.hash = rlpHash(tx)
return true
,
# Run test after a forkchoice with new HeadBlockHash has been broadcasted
onFinalizedBlockChange: proc(): bool =
var lastHeader: EthBlockHeader
var hRes = client.latestHeader(lastHeader)
if hRes.isErr:
error "unable to get latest header", msg=hRes.error
return false
let lastHash = BlockHash lastHeader.blockHash.data
if lastHash != clMock.latestForkchoice.headBlockHash:
error "latest block header doesn't match FinalizedBlock hash", hash=lastHash.toHex
return false
let rr = client.txReceipt(shadow.hash)
if rr.isErr:
error "unable to get transaction receipt"
return false
return true
))
testCond produceSingleBlockRes
blockStatusFinalizedBlockGen(blockStatusFinalizedBlock1, false)
blockStatusFinalizedBlockGen(blockStatusFinalizedBlock2, true)
proc blockStatusReorg(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test
let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produce5BlockRes
let clMock = t.clMock
let client = t.rpcClient
var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks(
# Run test after a forkchoice with new HeadBlockHash has been broadcasted
onForkchoiceBroadcast: proc(): bool =
# Verify the client is serving the latest HeadBlock
var currHeader: EthBlockHeader
var hRes = client.latestHeader(currHeader)
if hRes.isErr:
error "unable to get latest header", msg=hRes.error
return false
var currHash = BlockHash currHeader.blockHash.data
if currHash != clMock.latestForkchoice.headBlockHash or
currHash == clMock.latestForkchoice.safeBlockHash or
currHash == clMock.latestForkchoice.finalizedBlockHash:
error "latest block header doesn't match HeadBlock hash", hash=currHash.toHex
return false
# Reorg back to the previous block (FinalizedBlock)
let reorgForkchoice = ForkchoiceStateV1(
headBlockHash: clMock.latestForkchoice.finalizedBlockHash,
safeBlockHash: clMock.latestForkchoice.finalizedBlockHash,
finalizedBlockHash: clMock.latestForkchoice.finalizedBlockHash
)
var res = client.forkchoiceUpdatedV1(reorgForkchoice)
if res.isErr:
error "Could not send forkchoiceUpdatedV1", msg=res.error
return false
var s = res.get()
if s.payloadStatus.status != PayloadExecutionStatus.valid:
error "Incorrect status returned after a HeadBlockHash reorg", status=s.payloadStatus.status
return false
if s.payloadStatus.latestValidHash.isNone:
error "Cannot get latestValidHash from payloadStatus"
return false
var latestValidHash = s.payloadStatus.latestValidHash.get
if latestValidHash != reorgForkchoice.headBlockHash:
error "Incorrect latestValidHash returned after a HeadBlockHash reorg",
expected=reorgForkchoice.headBlockHash.toHex,
get=latestValidHash.toHex
return false
# testCond that we reorg to the previous block
hRes = client.latestHeader(currHeader)
if hRes.isErr:
error "unable to get latest header", msg=hRes.error
return false
currHash = BlockHash currHeader.blockHash.data
if currHash != reorgForkchoice.headBlockHash:
error "`latest` block hash doesn't match reorg hash",
expected=reorgForkchoice.headBlockHash.toHex,
get=currHash.toHex
return false
# Send the HeadBlock again to leave everything back the way it was
res = client.forkchoiceUpdatedV1(clMock.latestForkchoice)
if res.isErr:
error "Could not send forkchoiceUpdatedV1", msg=res.error
return false
s = res.get()
if s.payloadStatus.status != PayloadExecutionStatus.valid:
error "Incorrect status returned after a HeadBlockHash reorg",
status=s.payloadStatus.status
return false
if s.payloadStatus.latestValidHash.isNone:
error "Cannot get latestValidHash from payloadStatus"
return false
latestValidHash = s.payloadStatus.latestValidHash.get
if latestValidHash != clMock.latestForkchoice.headBlockHash:
error "Incorrect latestValidHash returned after a HeadBlockHash reorg",
expected=clMock.latestForkchoice.headBlockHash.toHex,
get=latestValidHash.toHex
return false
return true
))
testCond produceSingleBlockRes
proc reExecPayloads(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until this client catches up with latest PoS
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# How many Payloads we are going to re-execute
var payloadReExecCount = 10
# Create those blocks
let produceBlockRes = t.clMock.produceBlocks(payloadReExecCount, BlockProcessCallbacks())
testCond produceBlockRes
# Re-execute the payloads
let client = t.rpcClient
var hRes = client.blockNumber()
testCond hRes.isOk:
error "unable to get blockNumber", msg=hRes.error
let lastBlock = int(hRes.get)
info "Started re-executing payloads at block", number=lastBlock
let
clMock = t.clMock
start = lastBlock - payloadReExecCount + 1
for i in start..lastBlock:
if clMock.executedPayloadHistory.hasKey(uint64 i):
let payload = clMock.executedPayloadHistory[uint64 i]
let res = client.newPayloadV1(payload)
testCond res.isOk:
error "FAIL (%s): Unable to re-execute valid payload", msg=res.error
let s = res.get()
testCond s.status == PayloadExecutionStatus.valid:
error "Unexpected status after re-execute valid payload", status=s.status
else:
testCond true:
error "(test issue) Payload does not exist", index=i
proc multipleNewCanonicalPayloads(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test
let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produce5BlockRes
let clMock = t.clMock
let client = t.rpcClient
var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks(
# Run test after a new payload has been obtained
onGetPayload: proc(): bool =
let payloadCount = 80
let basePayload = toExecutableData(clMock.latestPayloadBuilt)
var newPrevRandao: Hash256
# Fabricate and send multiple new payloads by changing the PrevRandao field
for i in 0..<payloadCount:
doAssert nimcrypto.randomBytes(newPrevRandao.data) == 32
let newPayload = customizePayload(basePayload, CustomPayload(
prevRandao: some(newPrevRandao)
))
let res = client.newPayloadV1(newPayload)
if res.isErr:
error "Unable to send new valid payload extending canonical chain", msg=res.error
return false
let s = res.get()
if s.status != PayloadExecutionStatus.valid:
error "Unexpected status after trying to send new valid payload extending canonical chain",
status=s.status
return false
return true
))
# At the end the clMocker continues to try to execute fcU with the original payload, which should not fail
testCond produceSingleBlockRes
proc outOfOrderPayloads(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# First prepare payloads on a first client, which will also contain multiple transactions
# We will be also verifying that the transactions are correctly interpreted in the canonical chain,
# prepare a random account to receive funds.
const
amountPerTx = 1000.u256
txPerPayload = 20
payloadCount = 10
var recipient: EthAddress
doAssert nimcrypto.randomBytes(recipient) == 20
let clMock = t.clMock
let client = t.rpcClient
var produceBlockRes = clMock.produceBlocks(payloadCount, BlockProcessCallbacks(
# We send the transactions after we got the Payload ID, before the clMocker gets the prepared Payload
onPayloadProducerSelected: proc(): bool =
for i in 0..<txPerPayload:
let tx = t.makeNextTransaction(recipient, amountPerTx)
let res = client.sendTransaction(tx)
if res.isErr:
error "Unable to send transaction"
return false
return true
))
testCond produceBlockRes
let expectedBalance = amountPerTx * u256(payloadCount*txPerPayload)
# testCond balance on this first client
let balRes = client.balanceAt(recipient)
testCond balRes.isOk:
error "Error while getting balance of funded account"
let bal = balRes.get()
testCond expectedBalance == bal
# TODO: this section need multiple client
# Test that performing a re-org back into a previous block of the canonical chain does not produce errors and the chain
# is still capable of progressing.
proc reorgBack(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
let clMock = t.clMock
let client = t.rpcClient
let r1 = clMock.produceSingleBlock(BlockProcessCallbacks())
testCond r1
# We are going to reorg back to this previous hash several times
let previousHash = clMock.latestForkchoice.headBlockHash
# Produce blocks before starting the test (So we don't try to reorg back to the genesis block)
let r2 = clMock.produceBlocks(5, BlockProcessCallbacks(
onForkchoiceBroadcast: proc(): bool =
# Send a fcU with the HeadBlockHash pointing back to the previous block
let forkchoiceUpdatedBack = ForkchoiceStateV1(
headBlockHash: previousHash,
safeBlockHash: previousHash,
finalizedBlockHash: previousHash,
)
# It is only expected that the client does not produce an error and the CL Mocker is able to progress after the re-org
let r = client.forkchoiceUpdatedV1(forkchoiceUpdatedBack)
r.isOk
))
testCond r2
# Verify that the client is pointing to the latest payload sent
var header: EthBlockHeader
let r = client.latestHeader(header)
testCond r.isOk
let blockHash = hash256(clMock.latestPayloadBuilt.blockHash)
testCond blockHash == header.blockHash
# Test that performs a re-org back to the canonical chain after re-org to syncing/unavailable chain.
type
SideChainList = ref object
sidechainPayloads: seq[ExecutionPayloadV1]
proc reorgBackFromSyncing(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce an alternative chain
let pList = SideChainList()
let clMock = t.clMock
let client = t.rpcClient
let r1 = clMock.produceBlocks(10, BlockProcessCallbacks(
onGetPayload: proc(): bool =
# Generate an alternative payload by simply adding extraData to the block
var altParentHash = clMock.latestPayloadBuilt.parentHash
if pList.sidechainPayloads.len > 0:
altParentHash = pList.sidechainPayloads[^1].blockHash
let executableData = toExecutableData(clMock.latestPayloadBuilt)
let altPayload = customizePayload(executableData,
CustomPayload(
parentHash: some(altParentHash.hash256),
extraData: some(@[0x01.byte]),
))
pList.sidechainPayloads.add(altPayload)
return true
))
testCond r1
# Produce blocks before starting the test (So we don't try to reorg back to the genesis block)
let r2= clMock.produceSingleBlock(BlockProcessCallbacks(
onGetPayload: proc(): bool =
let r = client.newPayloadV1(pList.sidechainPayloads[^1])
if r.isErr:
return false
let s = r.get()
if s.status notin {PayloadExecutionStatus.syncing, PayloadExecutionStatus.accepted}:
return false
# We are going to send one of the alternative payloads and fcU to it
let len = pList.sidechainPayloads.len
let forkchoiceUpdatedBack = ForkchoiceStateV1(
headBlockHash: pList.sidechainPayloads[len-1].blockHash,
safeBlockHash: pList.sidechainPayloads[len-2].blockHash,
finalizedBlockHash: pList.sidechainPayloads[len-3].blockHash,
)
# It is only expected that the client does not produce an error and the CL Mocker is able to progress after the re-org
let res = client.forkchoiceUpdatedV1(forkchoiceUpdatedBack)
if res.isErr:
return false
let rs = res.get()
if rs.payloadStatus.status != PayloadExecutionStatus.syncing:
return false
rs.payloadStatus.latestValidHash.isNone
# After this, the clMocker will continue and try to re-org to canonical chain once again
# clMocker will fail the test if this is not possible, so nothing left to do.
))
testCond r2
proc transactionReorg(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test
let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produce5BlockRes
# Create transactions that modify the state in order to testCond after the reorg.
const
txCount = 5
contractAddr = hexToByteArray[20]("0000000000000000000000000000000000000317")
var
receipts: array[txCount, rpc_types.ReceiptObject]
txs: array[txCount, Transaction]
let
client = t.rpcClient
clMock = t.clMock
for i in 0..<txCount:
# Data is the key where a `1` will be stored
let data = i.u256
let tx = t.makeNextTransaction(contractAddr, 0.u256, data.toBytesBE)
txs[i] = tx
# Send the transaction
let res = client.sendTransaction(tx)
testCond res.isOk:
error "Unable to send transaction", msg=res.error
# Produce the block containing the transaction
var blockRes = clMock.produceSingleBlock(BlockProcessCallbacks())
testCond blockRes
# Get the receipt
let rr = client.txReceipt(rlpHash(tx))
testCond rr.isOk:
error "Unable to obtain transaction receipt", msg=rr.error
receipts[i] = rr.get()
for i in 0..<txCount:
# The sstore contract stores a `1` to key specified in data
let storageKey = i.u256
var rr = client.storageAt(contractAddr, storageKey)
testCond rr.isOk:
error "Could not get storage", msg=rr.error
let valueWithTxApplied = rr.get()
testCond valueWithTxApplied == 1.u256
if valueWithTxApplied != 1.u256:
error "Expected storage not set after transaction", valueWithTxApplied
return
# Get value at a block before the tx was included
let number = UInt256.fromHex(receipts[i].blockNumber.string).truncate(uint64)
var reorgBlock: EthBlockHeader
let blockRes = client.headerByNumber(number - 1, reorgBlock)
rr = client.storageAt(contractAddr, storageKey, reorgBlock.blockNumber)
testCond rr.isOk:
error "could not get storage", msg= rr.error
let valueWithoutTxApplied = rr.get()
testCond valueWithoutTxApplied == 0.u256:
error "Storage not unset before transaction!", valueWithoutTxApplied
# Re-org back to a previous block where the tx is not included using forkchoiceUpdated
let rHash = Web3BlockHash reorgBlock.blockHash.data
let reorgForkchoice = ForkchoiceStateV1(
headBlockHash: rHash,
safeBlockHash: rHash,
finalizedBlockHash: rHash,
)
var res = client.forkchoiceUpdatedV1(reorgForkchoice)
testCond res.isOk:
error "Could not send forkchoiceUpdatedV1", msg=res.error
var s = res.get()
testCond s.payloadStatus.status == PayloadExecutionStatus.valid:
error "Could not send forkchoiceUpdatedV1", status=s.payloadStatus.status
# testCond storage again using `latest`, should be unset
rr = client.storageAt( contractAddr, storageKey)
testCond rr.isOk:
error "could not get storage", msg= rr.error
let valueAfterReOrgBeforeTxApplied = rr.get()
testCond valueAfterReOrgBeforeTxApplied == 0.u256:
error "Storage not unset after re-org", valueAfterReOrgBeforeTxApplied
# Re-send latest forkchoice to test next transaction
res = client.forkchoiceUpdatedV1(clMock.latestForkchoice)
testCond res.isOk:
error "Could not send forkchoiceUpdatedV1", msg=res.error
s = res.get()
testCond s.payloadStatus.status == PayloadExecutionStatus.valid:
error "Could not send forkchoiceUpdatedV1", status=s.payloadStatus.status
proc testCondPrevRandaoValue(t: TestEnv, expectedPrevRandao: Hash256, blockNumber: uint64): bool =
let storageKey = blockNumber.u256
let client = t.rpcClient
let res = client.storageAt(prevRandaoContractAddr, storageKey)
if res.isErr:
error "Unable to get storage", msg=res.error
return false
let opcodeValueAtBlock = Hash256(data: res.get().toBytesBE)
if opcodeValueAtBlock != expectedPrevRandao:
error "Storage does not match prevRandao",
expected=expectedPrevRandao.data.toHex,
get=opcodeValueAtBlock.data.toHex
return false
true
proc sidechainReorg(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test
let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produce5BlockRes
let
client = t.rpcClient
clMock = t.clMock
# Produce two payloads, send fcU with first payload, testCond transaction outcome, then reorg, testCond transaction outcome again
# This single transaction will change its outcome based on the payload
let tx = t.makeNextTransaction(prevRandaoContractAddr, 0.u256)
let rr = client.sendTransaction(tx)
testCond rr.isOk:
error "Unable to send transaction", msg=rr.error
let singleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks(
onNewPayloadBroadcast: proc(): bool =
# At this point the clMocker has a payload that will result in a specific outcome,
# we can produce an alternative payload, send it, fcU to it, and verify the changes
var alternativePrevRandao: Hash256
doAssert nimcrypto.randomBytes(alternativePrevRandao.data) == 32
let timestamp = Quantity toUnix(clMock.latestHeader.timestamp + 1.seconds)
let payloadAttributes = PayloadAttributesV1(
timestamp: timestamp,
prevRandao: FixedBytes[32] alternativePrevRandao.data,
suggestedFeeRecipient: Address clMock.nextFeeRecipient,
)
var res = client.forkchoiceUpdatedV1(clMock.latestForkchoice, some(payloadAttributes))
if res.isErr:
error "Could not send forkchoiceUpdatedV1", msg=res.error
return false
let s = res.get()
let rr = client.getPayloadV1(s.payloadID.get())
if rr.isErr:
error "Could not get alternative payload", msg=rr.error
return false
let alternativePayload = rr.get()
if alternativePayload.transactions.len == 0:
error "alternative payload does not contain the prevRandao opcode tx"
return false
let rx = client.newPayloadV1(alternativePayload)
if rx.isErr:
error "Could not send alternative payload", msg=rx.error
return false
let alternativePayloadStatus = rx.get()
if alternativePayloadStatus.status != PayloadExecutionStatus.valid:
error "Alternative payload response returned Status!=VALID",
status=alternativePayloadStatus.status
return false
# We sent the alternative payload, fcU to it
let alternativeHeader = toBlockHeader(alternativePayload)
let rHash = BlockHash alternativeHeader.blockHash.data
let alternativeFcU = ForkchoiceStateV1(
headBlockHash: rHash,
safeBlockHash: clMock.latestForkchoice.safeBlockHash,
finalizedBlockHash: clMock.latestForkchoice.finalizedBlockHash
)
res = client.forkchoiceUpdatedV1(alternativeFcU)
if res.isErr:
error "Could not send alternative fcU", msg=res.error
return false
let alternativeFcUResp = res.get()
if alternativeFcUResp.payloadStatus.status != PayloadExecutionStatus.valid:
error "Alternative fcU response returned Status!=VALID",
status=alternativeFcUResp.payloadStatus.status
return false
# PrevRandao should be the alternative prevRandao we sent
return testCondPrevRandaoValue(t, alternativePrevRandao, uint64 alternativePayload.blockNumber)
))
testCond singleBlockRes
# The reorg actually happens after the clMocker continues,
# verify here that the reorg was successful
let latestBlockNum = clMock.latestHeadNumber.uint64
testCond testCondPrevRandaoValue(t, clMock.prevRandaoHistory[latestBlockNum], latestBlockNum)
proc suggestedFeeRecipient(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Amount of transactions to send
const
txCount = 20
# Verify that, in a block with transactions, fees are accrued by the suggestedFeeRecipient
var feeRecipient: EthAddress
testCond nimcrypto.randomBytes(feeRecipient) == 20
let
client = t.rpcClient
clMock = t.clMock
# Send multiple transactions
for i in 0..<txCount:
# Empty self tx
let tx = t.makeNextTransaction(vaultAccountAddr, 0.u256)
let res = client.sendTransaction(tx)
testCond res.isOk:
error "unable to send transaction", msg=res.error
# Produce the next block with the fee recipient set
clMock.nextFeeRecipient = feeRecipient
testCond clMock.produceSingleBlock(BlockProcessCallbacks())
# Calculate the fees and testCond that they match the balance of the fee recipient
var blockIncluded: EthBlock
var rr = client.latestBlock(blockIncluded)
testCond rr.isOk:
error "unable to get latest block", msg=rr.error
testCond blockIncluded.txs.len == txCount:
error "not all transactions were included in block",
expected=txCount,
get=blockIncluded.txs.len
testCond blockIncluded.header.coinbase == feeRecipient:
error "feeRecipient was not set as coinbase",
expected=feeRecipient.toHex,
get=blockIncluded.header.coinbase.toHex
var feeRecipientFees = 0.u256
for tx in blockIncluded.txs:
let effGasTip = tx.effectiveGasTip(blockIncluded.header.fee)
let tr = client.txReceipt(rlpHash(tx))
testCond tr.isOk:
error "unable to obtain receipt", msg=tr.error
let rec = tr.get()
let gasUsed = UInt256.fromHex(rec.gasUsed.string)
feeRecipientFees = feeRecipientFees + effGasTip.u256 * gasUsed
var br = client.balanceAt(feeRecipient)
testCond br.isOk
var feeRecipientBalance = br.get()
testCond feeRecipientBalance == feeRecipientFees:
error "balance does not match fees",
feeRecipientBalance, feeRecipientFees
# Produce another block without txns and get the balance again
clMock.nextFeeRecipient = feeRecipient
testCond clMock.produceSingleBlock(BlockProcessCallbacks())
br = client.balanceAt(feeRecipient)
testCond br.isOk
feeRecipientBalance = br.get()
testCond feeRecipientBalance == feeRecipientFees:
error "balance does not match fees",
feeRecipientBalance, feeRecipientFees
proc sendTx(t: TestEnv): Future[void] {.async.} =
let
client = t.rpcClient
clMock = t.clMock
period = chronos.milliseconds(500)
while not clMock.ttdReached:
await sleepAsync(period)
let
tx = t.makeNextTransaction(prevRandaoContractAddr, 0.u256)
rr = client.sendTransaction(tx)
if rr.isErr:
error "Unable to send transaction", msg=rr.error
proc prevRandaoOpcodeTx(t: TestEnv): TestStatus =
result = TestStatus.OK
let
client = t.rpcClient
clMock = t.clMock
sendTxFuture = sendTx(t)
# Wait until TTD is reached by this client
let ok = waitFor clMock.waitForTTD()
testCond ok
# Ideally all blocks up until TTD must have a DIFFICULTY opcode tx in it
let nr = client.blockNumber()
testCond nr.isOk:
error "Unable to get latest block number", msg=nr.error
let ttdBlockNumber = nr.get()
# Start
for i in ttdBlockNumber..ttdBlockNumber:
# First testCond that the block actually contained the transaction
var blk: EthBlock
let res = client.blockByNumber(i, blk)
testCond res.isOk:
error "Unable to get block", msg=res.error
testCond blk.txs.len > 0:
error "(Test issue) no transactions went in block"
let storageKey = i.u256
let rr = client.storageAt(prevRandaoContractAddr, storageKey)
testCond rr.isOk:
error "Unable to get storage", msg=rr.error
let opcodeValueAtBlock = rr.get()
testCond opcodeValueAtBlock == 2.u256:
error "Incorrect difficulty value in block",
expect=2,
get=opcodeValueAtBlock
# Send transactions now past TTD, the value of the storage in these blocks must match the prevRandao value
type
ShadowTx = ref object
currentTxIndex: int
txs: seq[Transaction]
let shadow = ShadowTx(currentTxIndex: 0)
let produceBlockRes = clMock.produceBlocks(10, BlockProcessCallbacks(
onPayloadProducerSelected: proc(): bool =
let
tx = t.makeNextTransaction(prevRandaoContractAddr, 0.u256)
rr = client.sendTransaction(tx)
if rr.isErr:
error "Unable to send transaction", msg=rr.error
return false
shadow.txs.add tx
inc shadow.currentTxIndex
return true
,
onForkchoiceBroadcast: proc(): bool =
# Check the transaction tracing, which is client specific
let expectedPrevRandao = clMock.prevRandaoHistory[clMock.latestHeadNumber + 1'u64]
let res = debugPrevRandaoTransaction(client, shadow.txs[shadow.currentTxIndex-1], expectedPrevRandao)
if res.isErr:
error "unable to debug prev randao", msg=res.error
return false
return true
))
let rr = client.blockNumber()
testCond rr.isOk:
error "Unable to get latest block number"
let lastBlockNumber = rr.get()
for i in ttdBlockNumber + 1 ..< lastBlockNumber:
let expectedPrevRandao = UInt256.fromBytesBE(clMock.prevRandaoHistory[i].data)
let storageKey = i.u256
let rz = client.storageAt(prevRandaoContractAddr, storageKey)
testCond rz.isOk:
error "Unable to get storage", msg=rz.error
let storage = rz.get()
testCond storage == expectedPrevRandao:
error "Unexpected storage", expected=expectedPrevRandao.toHex, get=storage.toHex
proc postMergeSync(t: TestEnv): TestStatus =
result = TestStatus.SKIPPED
# TODO: need multiple client
const engineTestList* = [
# Engine API Negative Test Cases
TestSpec(
name: "Invalid Terminal Block in ForkchoiceUpdated",
run: invalidTerminalBlockForkchoiceUpdated,
ttd: 1000000
),
TestSpec(
name: "Invalid GetPayload Under PoW",
run: invalidGetPayloadUnderPoW,
ttd: 1000000
),
TestSpec(
name: "Invalid Terminal Block in NewPayload",
run: invalidTerminalBlockNewPayload,
ttd: 1000000,
),
TestSpec(
name: "Inconsistent Head in ForkchoiceState",
run: inconsistentForkchoiceState1,
),
TestSpec(
name: "Inconsistent Safe in ForkchoiceState",
run: inconsistentForkchoiceState2,
),
TestSpec(
name: "Inconsistent Finalized in ForkchoiceState",
run: inconsistentForkchoiceState3,
),
TestSpec(
name: "Unknown HeadBlockHash",
run: unknownHeadBlockHash,
),
TestSpec(
name: "Unknown SafeBlockHash",
run: unknownSafeBlockHash,
),
TestSpec(
name: "Unknown FinalizedBlockHash",
run: unknownFinalizedBlockHash,
),
TestSpec(
name: "ForkchoiceUpdated Invalid Payload Attributes",
run: invalidPayloadAttributes1,
),
TestSpec(
name: "ForkchoiceUpdated Invalid Payload Attributes (Syncing)",
run: invalidPayloadAttributes2,
),
TestSpec(
name: "Pre-TTD ForkchoiceUpdated After PoS Switch",
run: preTTDFinalizedBlockHash,
ttd: 2,
),
# Invalid Payload Tests
TestSpec(
name: "Bad Hash on NewPayload",
run: badHashOnNewPayload1,
),
TestSpec(
name: "Bad Hash on NewPayload Syncing",
run: badHashOnNewPayload2,
),
TestSpec(
name: "Bad Hash on NewPayload Side Chain",
run: badHashOnNewPayload3,
),
TestSpec(
name: "Bad Hash on NewPayload Side Chain Syncing",
run: badHashOnNewPayload4,
),
TestSpec(
name: "ParentHash==BlockHash on NewPayload",
run: parentHashOnExecPayload,
),
TestSpec(
name: "Invalid ParentHash NewPayload",
run: invalidPayload1,
),
TestSpec(
name: "Invalid StateRoot NewPayload",
run: invalidPayload2,
),
TestSpec(
name: "Invalid StateRoot NewPayload, Empty Transactions",
run: invalidPayload3,
),
TestSpec(
name: "Invalid ReceiptsRoot NewPayload",
run: invalidPayload4,
),
TestSpec(
name: "Invalid Number NewPayload",
run: invalidPayload5,
),
TestSpec(
name: "Invalid GasLimit NewPayload",
run: invalidPayload6,
),
TestSpec(
name: "Invalid GasUsed NewPayload",
run: invalidPayload7,
),
TestSpec(
name: "Invalid Timestamp NewPayload",
run: invalidPayload8,
),
TestSpec(
name: "Invalid PrevRandao NewPayload",
run: invalidPayload9,
),
TestSpec(
name: "Invalid Incomplete Transactions NewPayload",
run: invalidPayload10,
),
TestSpec(
name: "Invalid Transaction Signature NewPayload",
run: invalidPayload11,
),
TestSpec(
name: "Invalid Transaction Nonce NewPayload",
run: invalidPayload12,
),
TestSpec(
name: "Invalid Transaction GasPrice NewPayload",
run: invalidPayload13,
),
TestSpec(
name: "Invalid Transaction Gas NewPayload",
run: invalidPayload14,
),
TestSpec(
name: "Invalid Transaction Value NewPayload",
run: invalidPayload15,
),
# Eth RPC Status on ForkchoiceUpdated Events
TestSpec( # TODO: fix/debug
name: "Latest Block after NewPayload",
run: blockStatusExecPayload1,
),
TestSpec( # TODO: fix/debug
name: "Latest Block after NewPayload (Transition Block)",
run: blockStatusExecPayload2,
ttd: 5,
),
TestSpec(
name: "Latest Block after New HeadBlock",
run: blockStatusHeadBlock1,
),
TestSpec(
name: "Latest Block after New HeadBlock (Transition Block)",
run: blockStatusHeadBlock2,
ttd: 5,
),
TestSpec(
name: "Latest Block after New SafeBlock",
run: blockStatusSafeBlock1,
),
TestSpec(
name: "Latest Block after New SafeBlock (Transition Block)",
run: blockStatusSafeBlock2,
ttd: 5,
),
TestSpec(
name: "Latest Block after New FinalizedBlock",
run: blockStatusFinalizedBlock1,
),
TestSpec(
name: "Latest Block after New FinalizedBlock (Transition Block)",
run: blockStatusFinalizedBlock2,
ttd: 5,
),
TestSpec(
name: "Latest Block after Reorg",
run: blockStatusReorg,
),
# Payload Tests
TestSpec(
name: "Re-Execute Payload",
run: reExecPayloads,
),
TestSpec(
name: "Multiple New Payloads Extending Canonical Chain",
run: multipleNewCanonicalPayloads,
),
TestSpec(
name: "Out of Order Payload Execution",
run: outOfOrderPayloads,
),
# Transaction Reorg using Engine API
TestSpec(
name: "Transaction Reorg",
run: transactionReorg,
),
TestSpec(
name: "Sidechain Reorg",
run: sidechainReorg,
),
TestSpec(
name: "Re-Org Back into Canonical Chain",
run: reorgBack,
),
TestSpec(
name: "Re-Org Back to Canonical Chain From Syncing Chain",
run: reorgBackFromSyncing,
),
# Suggested Fee Recipient in Payload creation
TestSpec(
name: "Suggested Fee Recipient Test",
run: suggestedFeeRecipient,
),
# PrevRandao opcode tests
TestSpec(
name: "PrevRandao Opcode Transactions",
run: prevRandaoOpcodeTx,
ttd: 10,
),
# Multi-Client Sync tests
TestSpec(
name: "Sync Client Post Merge",
run: postMergeSync,
ttd: 10,
)
]