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

2006 lines
71 KiB
Nim

import
std/tables,
stew/byteutils,
chronicles,
unittest2,
nimcrypto/sysrand,
chronos,
"."/[test_env, helper, types],
../../../nimbus/transaction,
../../../nimbus/rpc/rpc_types,
../../../nimbus/rpc/merge/mergeutils
const
prevRandaoContractAddr = hexToByteArray[20]("0000000000000000000000000000000000000316")
proc `==`(a: Option[BlockHash], b: Option[Hash256]): bool =
if a.isNone and b.isNone:
return true
if a.isSome and b.isSome:
return a.get() == b.get().data.BlockHash
template testFCU(res, cond: untyped, validHash: Option[Hash256], id = none(PayloadID)) =
testCond res.isOk
let s = res.get()
testCond s.payloadStatus.status == PayloadExecutionStatus.cond:
error "Unexpected FCU status", expect=PayloadExecutionStatus.cond, get=s.payloadStatus.status
testCond s.payloadStatus.latestValidHash == validHash:
error "Unexpected FCU latestValidHash", expect=validHash, get=s.payloadStatus.latestValidHash
testCond s.payloadId == id:
error "Unexpected FCU payloadID", expect=id, get=s.payloadId
template testFCU(res, cond: untyped) =
testCond res.isOk
let s = res.get()
testCond s.payloadStatus.status == PayloadExecutionStatus.cond:
error "Unexpected FCU status", expect=PayloadExecutionStatus.cond, get=s.payloadStatus.status
template testNP(res, cond: untyped, validHash = none(Hash256)) =
testCond res.isOk
let s = res.get()
testCond s.status == PayloadExecutionStatus.cond:
error "Unexpected NewPayload status", expect=PayloadExecutionStatus.cond, get=s.status
testCond s.latestValidHash == validHash:
error "Unexpected NewPayload latestValidHash", expect=validHash, get=s.latestValidHash
template testNPEither(res, cond: untyped, validHash = none(Hash256)) =
testCond res.isOk
let s = res.get()
testCond s.status in cond:
error "Unexpected NewPayload status", expect=cond, get=s.status
testCond s.latestValidHash == validHash:
error "Unexpected NewPayload latestValidHash", expect=validHash, get=s.latestValidHash
template testLatestHeader(client: untyped, expectedHash: BlockHash) =
var lastHeader: EthBlockHeader
var hRes = client.latestHeader(lastHeader)
testCond hRes.isOk:
error "unable to get latest header", msg=hRes.error
let lastHash = BlockHash lastHeader.blockHash.data
# Latest block header available via Eth RPC should not have changed at this point
testCond lastHash == expectedHash:
error "latest block header incorrect",
expect = expectedHash,
get = lastHash
proc sendTx(t: TestEnv, recipient: EthAddress, val: UInt256, data: openArray[byte] = []): bool =
t.tx = t.makeNextTransaction(recipient, val, data)
let rr = t.rpcClient.sendTransaction(t.tx)
if rr.isErr:
error "Unable to send transaction", msg=rr.error
return false
return true
proc sendTx(t: TestEnv, val: UInt256): bool =
t.sendTx(prevRandaoContractAddr, val)
# 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
testFCU(res, invalid, some(Hash256()))
# 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: EMPTY_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, latestValidHash=0x00..00}
# if terminal block conditions are not satisfied
testNP(res, invalid, some(Hash256()))
# 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 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 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 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 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))
testFCU(r, syncing)
else:
let r = client.forkchoiceUpdatedV1(fcu, some(attr))
testCond r.isOk:
error "Unexpected error", msg = r.error
# Check that the forkchoice was applied, regardless of the error
testLatestHeader(client, BlockHash blockHash.data)
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)
testFCU(res, invalid, some(Hash256()))
res = client.forkchoiceUpdatedV1(clMock.latestForkchoice)
testFCU(res, 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 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
# Attempt to re-org to a chain containing an invalid transition payload
proc invalidTransitionPayload(t: TestEnv): TestStatus =
result = TestStatus.OK
# Wait until TTD is reached by main client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
let clMock = t.clMock
let client = t.rpcClient
# Produce two blocks before trying to re-org
t.nonce = 2 # Initial PoW chain already contains 2 transactions
var pbRes = clMock.produceBlocks(2, BlockProcessCallbacks(
onPayloadProducerSelected: proc(): bool =
t.sendTx(1.u256)
))
testCond pbRes
# Introduce the invalid transition payload
pbRes = clMock.produceSingleBlock(BlockProcessCallbacks(
# This is being done in the middle of the block building
# process simply to be able to re-org back.
onGetPayload: proc(): bool =
let basePayload = clMock.executedPayloadHistory[clMock.posBlockNumber]
let alteredPayload = generateInvalidPayload(basePayload, InvalidStateRoot)
let res = client.newPayloadV1(alteredPayload)
let cond = {PayloadExecutionStatus.invalid, PayloadExecutionStatus.accepted}
testNPEither(res, cond, some(Hash256()))
let rr = client.forkchoiceUpdatedV1(
ForkchoiceStateV1(headBlockHash: alteredPayload.blockHash)
)
testFCU(rr, invalid, some(Hash256()))
testLatestHeader(client, clMock.latestExecutedPayload.blockHash)
return true
))
testCond pbRes
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(): bool =
when not emptyTxs:
t.sendTx(0.u256)
else:
true
# 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()
))
testCond pbRes
let invalidPayload = Shadow()
pbRes = clMock.produceSingleBlock(BlockProcessCallbacks(
# Make sure at least one transaction is included in the payload
onPayloadProducerSelected: proc(): bool =
txProc()
,
# 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 alteredPayload = generateInvalidPayload(clMock.latestPayloadBuilt, 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, get = latestValidHash
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: rpc_types.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, toHash=invalidPayload.hash
# 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
testCond t.sendTx(address, 1.u256)
shadow.hash = rlpHash(t.tx)
return true
,
onNewPayloadBroadcast: proc(): bool =
testLatestHeader(client, clMock.latestForkchoice.headBlockHash)
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)
type
MissingAncestorShadow = ref object
cA: ExecutionPayloadV1
n: int
altChainPayloads: seq[ExecutionPayloadV1]
# Attempt to re-org to a chain which at some point contains an unknown payload which is also invalid.
# Then reveal the invalid payload and expect that the client rejects it and rejects forkchoice updated calls to this chain.
# The invalid_index parameter determines how many payloads apart is the common ancestor from the block that invalidates the chain,
# with a value of 1 meaning that the immediate payload after the common ancestor will be invalid.
template invalidMissingAncestorReOrgGen(procName: untyped,
invalid_index: int, payloadField: InvalidPayloadField, p2psync: bool, emptyTxs: 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
testCond clMock.produceBlocks(5, BlockProcessCallbacks())
let shadow = MissingAncestorShadow(
# Save the common ancestor
cA: clMock.latestPayloadBuilt,
# Amount of blocks to deviate starting from the common ancestor
n: 10,
# Slice to save the alternate B chain
altChainPayloads: @[]
)
# Append the common ancestor
shadow.altChainPayloads.add shadow.cA
# Produce blocks but at the same time create an alternate chain which contains an invalid payload at some point (INV_P)
# CommonAncestor◄─▲── P1 ◄─ P2 ◄─ P3 ◄─ ... ◄─ Pn
# │
# └── P1' ◄─ P2' ◄─ ... ◄─ INV_P ◄─ ... ◄─ Pn'
var pbRes = clMock.produceBlocks(shadow.n, BlockProcessCallbacks(
onPayloadProducerSelected: proc(): bool =
# Function to send at least one transaction each block produced.
# Empty Txs Payload with invalid stateRoot discovered an issue in geth sync, hence this is customizable.
when not emptyTxs:
# Send the transaction to the prevRandaoContractAddr
t.sendTx(1.u256)
return true
,
onGetPayload: proc(): bool =
# Insert extraData to ensure we deviate from the main payload, which contains empty extradata
var alternatePayload = customizePayload(clMock.latestPayloadBuilt, CustomPayload(
parentHash: some(shadow.altChainPayloads[^1].blockHash.hash256),
extraData: some(@[1.byte]),
))
if shadow.altChainPayloads.len == invalid_index:
alternatePayload = generateInvalidPayload(alternatePayload, payloadField)
shadow.altChainPayloads.add alternatePayload
return true
))
testCond pbRes
pbRes = clMock.produceSingleBlock(BlockProcessCallbacks(
# Note: We perform the test in the middle of payload creation by the CL Mock, in order to be able to
# re-org back into this chain and use the new payload without issues.
onGetPayload: proc(): bool =
# Now let's send the alternate chain to the client using newPayload/sync
for i in 1..shadow.n:
# Send the payload
var payloadValidStr = "VALID"
if i == invalid_index:
payloadValidStr = "INVALID"
elif i > invalid_index:
payloadValidStr = "VALID with INVALID ancestor"
info "Invalid chain payload",
i = i,
payloadValidStr = payloadValidStr,
hash = shadow.altChainPayloads[i].blockHash
let rr = client.newPayloadV1(shadow.altChainPayloads[i])
testCond rr.isOk
let rs = client.forkchoiceUpdatedV1(ForkchoiceStateV1(
headBlockHash: shadow.altChainPayloads[i].blockHash,
safeBlockHash: shadow.altChainPayloads[i].blockHash
))
if i == invalid_index:
# If this is the first payload after the common ancestor, and this is the payload we invalidated,
# then we have all the information to determine that this payload is invalid.
testNP(rr, invalid, some(shadow.altChainPayloads[i-1].blockHash.hash256))
elif i > invalid_index:
# We have already sent the invalid payload, but the client could've discarded it.
# In reality the CL will not get to this point because it will have already received the `INVALID`
# response from the previous payload.
let cond = {PayloadExecutionStatus.accepted, PayloadExecutionStatus.syncing, PayloadExecutionStatus.invalid}
testNPEither(rr, cond)
else:
# This is one of the payloads before the invalid one, therefore is valid.
let latestValidHash = some(shadow.altChainPayloads[i].blockHash.hash256)
testNP(rr, valid, latestValidHash)
testFCU(rs, valid, latestValidHash)
# Resend the latest correct fcU
let rx = client.forkchoiceUpdatedV1(clMock.latestForkchoice)
testCond rx.isOk:
error "Unexpected error ", msg=rx.error
# After this point, the CL Mock will send the next payload of the canonical chain
return true
))
testCond pbRes
invalidMissingAncestorReOrgGen(invalidMissingAncestor1, 1, InvalidStateRoot, false, true)
invalidMissingAncestorReOrgGen(invalidMissingAncestor2, 9, InvalidStateRoot, false, true)
invalidMissingAncestorReOrgGen(invalidMissingAncestor3, 10, InvalidStateRoot, false, 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
testCond t.sendTx(address, 1.u256)
shadow.hash = rlpHash(t.tx)
return true
,
# Run test after a forkchoice with new HeadBlockHash has been broadcasted
onForkchoiceBroadcast: proc(): bool =
testLatestHeader(client, clMock.latestForkchoice.headBlockHash)
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)
proc blockStatusSafeBlock(t: TestEnv): TestStatus =
result = TestStatus.OK
let clMock = t.clMock
let client = t.rpcClient
# On PoW mode, `safe` tag shall return error.
var header: EthBlockHeader
var rr = client.namedHeader("safe", header)
testCond rr.isErr
# Wait until this client catches up with latest PoS Block
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# First ForkchoiceUpdated sent was equal to 0x00..00, `safe` should return error now
rr = client.namedHeader("safe", header)
testCond rr.isErr
let pbres = clMock.produceBlocks(3, BlockProcessCallbacks(
# Run test after a forkchoice with new SafeBlockHash has been broadcasted
onSafeBlockChange: proc(): bool =
var header: EthBlockHeader
let rr = client.namedHeader("safe", header)
testCond rr.isOk
let safeBlockHash = hash256(clMock.latestForkchoice.safeBlockHash)
header.blockHash == safeBlockHash
))
testCond pbres
proc blockStatusFinalizedBlock(t: TestEnv): TestStatus =
result = TestStatus.OK
let clMock = t.clMock
let client = t.rpcClient
# On PoW mode, `finalized` tag shall return error.
var header: EthBlockHeader
var rr = client.namedHeader("finalized", header)
testCond rr.isErr
# Wait until this client catches up with latest PoS Block
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# First ForkchoiceUpdated sent was equal to 0x00..00, `finalized` should return error now
rr = client.namedHeader("finalized", header)
testCond rr.isErr
let pbres = clMock.produceBlocks(3, BlockProcessCallbacks(
# Run test after a forkchoice with new FinalizedBlockHash has been broadcasted
onFinalizedBlockChange: proc(): bool =
var header: EthBlockHeader
let rr = client.namedHeader("finalized", header)
testCond rr.isOk
let finalizedBlockHash = hash256(clMock.latestForkchoice.finalizedBlockHash)
header.blockHash == finalizedBlockHash
))
testCond pbres
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
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,
get=latestValidHash
return false
# testCond that we reorg to the previous block
testLatestHeader(client, reorgForkchoice.headBlockHash)
# 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,
get=latestValidHash
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 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 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:
testCond t.sendTx(recipient, amountPerTx)
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
testCond clMock.produceSingleBlock(BlockProcessCallbacks())
# 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)
testCond r.isOk:
error "failed to reorg back", msg = r.error
return true
))
testCond r2
# Verify that the client is pointing to the latest payload sent
testLatestHeader(client, clMock.latestPayloadBuilt.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
type
TxReorgShadow = ref object
noTxnPayload: ExecutionPayloadV1
txHash: Hash256
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
testCond t.clMock.produceBlocks(5, BlockProcessCallbacks())
# Create transactions that modify the state in order to testCond after the reorg.
const
txCount = 5
contractAddr = hexToByteArray[20]("0000000000000000000000000000000000000317")
let
client = t.rpcClient
clMock = t.clMock
shadow = TxReorgShadow()
for i in 0..<txCount:
# Generate two payloads, one with the transaction and the other one without it
let pbres = clMock.produceSingleBlock(BlockProcessCallbacks(
onPayloadProducerSelected: proc(): bool =
# At this point we have not broadcast the transaction,
# therefore any payload we get should not contain any transactions
if not clMock.getNextPayloadID(): return false
if not clMock.getNextPayload(): return false
shadow.noTxnPayload = clMock.latestPayloadBuilt
if shadow.noTxnPayload.transactions.len != 0:
error "Empty payload contains transactions"
return false
# At this point we can broadcast the transaction and it will be included in the next payload
# Data is the key where a `1` will be stored
let data = i.u256
testCond t.sendTx(contractAddr, 0.u256, data.toBytesBE)
shadow.txHash = rlpHash(t.tx)
# Get the receipt
let rr = client.txReceipt(shadow.txHash)
if rr.isOk:
error "Receipt obtained before tx included in block"
return false
return true
,
onGetPayload: proc(): bool =
# Check that indeed the payload contains the transaction
if not txInPayload(clMock.latestPayloadBuilt, shadow.txHash):
error "Payload built does not contain the transaction"
return false
return true
,
onForkchoiceBroadcast: proc(): bool =
# Transaction is now in the head of the canonical chain, re-org and verify it's removed
var rr = client.txReceipt(shadow.txHash)
if rr.isErr:
error "Unable to obtain transaction receipt"
return false
if shadow.noTxnPayload.parentHash != clMock.latestPayloadBuilt.parentHash:
error "Incorrect parent hash for payloads",
get = shadow.noTxnPayload.parentHash,
expect = clMock.latestPayloadBuilt.parentHash
return false
if shadow.noTxnPayload.blockHash == clMock.latestPayloadBuilt.blockHash:
error "Incorrect hash for payloads",
get = shadow.noTxnPayload.blockHash,
expect = clMock.latestPayloadBuilt.blockHash
return false
let rz = client.newPayloadV1(shadow.noTxnPayload)
testNP(rz, valid, some(hash256(shadow.noTxnPayload.blockHash)))
let rx = client.forkchoiceUpdatedV1(ForkchoiceStateV1(
headBlockHash: shadow.noTxnPayload.blockHash,
safeBlockHash: clMock.latestForkchoice.safeBlockHash,
finalizedBlockHash: clMock.latestForkchoice.finalizedBlockHash
))
testFCU(rx, valid)
testLatestHeader(client, shadow.noTxnPayload.blockHash)
let rk = client.txReceipt(shadow.txHash)
if rk.isOk:
error "Receipt was obtained when the tx had been re-org'd out"
return false
# Re-org back
let ry = clMock.broadcastForkchoiceUpdated(clMock.latestForkchoice)
ry.isOk
))
testCond pbres
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,
get=opcodeValueAtBlock
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
testCond t.clMock.produceBlocks(5, BlockProcessCallbacks())
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
testCond t.sendTx(0.u256)
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 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 randomBytes(feeRecipient) == 20
let
client = t.rpcClient
clMock = t.clMock
# Send multiple transactions
for i in 0..<txCount:
# Empty self tx
discard t.sendTx(vaultAccountAddr, 0.u256)
# 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,
get=blockIncluded.header.coinbase
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 sendTxAsync(t: TestEnv): Future[void] {.async.} =
let
clMock = t.clMock
period = chronos.milliseconds(500)
while not clMock.ttdReached:
await sleepAsync(period)
discard t.sendTx(0.u256)
proc prevRandaoOpcodeTx(t: TestEnv): TestStatus =
result = TestStatus.OK
let
client = t.rpcClient
clMock = t.clMock
sendTxFuture = sendTxAsync(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 =
testCond t.sendTx(0.u256)
shadow.txs.add t.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
))
testCond produceBlockRes
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, get=storage
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 Transition Payload",
run: invalidTransitionPayload,
ttd: 393504,
chainFile: "blocks_2_td_393504.rlp",
),
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,
),
# Invalid Ancestor Re-Org Tests (Reveal via newPayload)
TestSpec(
name: "Invalid Ancestor Chain Re-Org, Invalid StateRoot, Invalid P1', Reveal using newPayload",
slotsToFinalized: 20,
run: invalidMissingAncestor1,
),
TestSpec(
name: "Invalid Ancestor Chain Re-Org, Invalid StateRoot, Invalid P9', Reveal using newPayload",
slotsToFinalized: 20,
run: invalidMissingAncestor2,
),
TestSpec(
name: "Invalid Ancestor Chain Re-Org, Invalid StateRoot, Invalid P10', Reveal using newPayload",
slotsToFinalized: 20,
run: invalidMissingAncestor3,
),
# Eth RPC Status on ForkchoiceUpdated Events
TestSpec(
name: "Latest Block after NewPayload",
run: blockStatusExecPayload1,
),
TestSpec(
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: "safe Block after New SafeBlockHash",
run: blockStatusSafeBlock,
ttd: 5,
),
TestSpec(
name: "finalized Block after New FinalizedBlockHash",
run: blockStatusFinalizedBlock,
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,
)
]