mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-02-27 03:10:44 +00:00
* Part of EIP-4895: add withdrawals processing to block processing.
* Refactoring: extracted the engine API handler bodies into procs.
Intending to implement the V2 versions next. (I need the bodies to be
in separate procs so that multiple versions can use them.)
* Working on Engine API changes for Shanghai.
* Updated nim-web3, resolved ambiguity in Hash256 type.
* Updated nim-eth3 to point to master, now that I've merged that.
* I'm confused about what's going on with engine_client.
But let's try resolving this Hash256 ambiguity.
* Still trying to fix this conflict with the Hash256 types.
* Does this work now that nimbus-eth2 has been updated?
* Corrected blockValue in getPayload responses back to UInt256.
c834f67a37
* Working on getting the withdrawals-related tests to pass.
* Fixing more of those Hash256 ambiguities.
(I'm not sure why the nim-web3 library introduced a conflicting type
named Hash256, but right now I just want to get this code to compile again.)
* Bumped a couple of libraries to fix some error messages.
* Needed to get "make fluffy-tools" to pass, too.
* Getting "make nimbus_verified_proxy" to build.
2010 lines
71 KiB
Nim
2010 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
|
|
|
|
import eth/common/eth_types as common_eth_types
|
|
type Hash256 = common_eth_types.Hash256
|
|
|
|
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,
|
|
)
|
|
]
|