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

799 lines
31 KiB
Nim

# Nimbus
# Copyright (c) 2023-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
# http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.
import
eth/common,
chronicles,
stew/byteutils,
./engine_spec,
../cancun/customizer,
../helper
type
SidechainReOrgTest* = ref object of EngineSpec
method withMainFork(cs: SidechainReOrgTest, fork: EngineFork): BaseSpec =
var res = cs.clone()
res.mainFork = fork
return res
method getName(cs: SidechainReOrgTest): string =
"Sidechain Reorg"
# Reorg to a Sidechain using ForkchoiceUpdated
method execute(cs: SidechainReOrgTest, env: TestEnv): bool =
# Wait until this client catches up with latest PoS
let ok = waitFor env.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test
testCond env.clMock.produceBlocks(5, BlockProcessCallbacks())
# Produce two payloads, send fcU with first payload, check transaction outcome, then reorg, check transaction outcome again
# This single transaction will change its outcome based on the payload
let tc = BaseTx(
recipient: some(prevRandaoContractAddr),
txType: cs.txType,
gasLimit: 75000,
)
let ok2 = env.sendNextTx(env.engine, tc)
testCond ok2:
fatal "Error trying to send transaction"
info "sent tx"
let pbRes = env.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
let alternativePrevRandao = common.Hash256.randomBytes()
let timestamp = w3Qty(env.clMock.latestPayloadBuilt.timestamp, 1)
let customizer = BasePayloadAttributesCustomizer(
timestamp: some(timestamp.uint64),
prevRandao: some(alternativePrevRandao),
)
let attr = customizer.getPayloadAttributes(env.clMock.latestPayloadAttributes)
var version = env.engine.version(env.clMock.latestPayloadBuilt.timestamp)
let r = env.engine.client.forkchoiceUpdated(version, env.clMock.latestForkchoice, some(attr))
r.expectNoError()
let period = chronos.seconds(env.clMock.payloadProductionClientDelay)
waitFor sleepAsync(period)
version = env.engine.version(attr.timestamp)
let g = env.engine.client.getPayload(r.get.payloadID.get, version)
g.expectNoError()
let alternativePayload = g.get.executionPayload
testCond len(alternativePayload.transactions) > 0:
fatal "alternative payload does not contain the prevRandao opcode tx"
let s = env.engine.client.newPayload(alternativePayload)
s.expectStatus(PayloadExecutionStatus.valid)
s.expectLatestValidHash(alternativePayload.blockHash)
# We sent the alternative payload, fcU to it
let fcu = ForkchoiceStateV1(
headBlockHash: alternativePayload.blockHash,
safeBlockHash: env.clMock.latestForkchoice.safeBlockHash,
finalizedBlockHash: env.clMock.latestForkchoice.finalizedBlockHash,
)
version = env.engine.version(alternativePayload.timestamp)
let p = env.engine.client.forkchoiceUpdated(version, fcu)
p.expectPayloadStatus(PayloadExecutionStatus.valid)
# PrevRandao should be the alternative prevRandao we sent
testCond checkPrevRandaoValue(env.engine.client, alternativePrevRandao, alternativePayload.blockNumber.uint64)
return true
))
testCond pbRes
# The reorg actually happens after the CLMocker continues,
# verify here that the reorg was successful
let latestBlockNum = env.clMock.latestHeadNumber.uint64
testCond checkPrevRandaoValue(env.engine.client, env.clMock.prevRandaoHistory[latestBlockNum], latestBlockNum)
return true
# Test performing a re-org that involves removing or modifying a transaction
type
TransactionReOrgScenario = enum
TransactionNoScenario
TransactionReOrgScenarioReOrgOut = "Re-Org Out"
TransactionReOrgScenarioReOrgBackIn = "Re-Org Back In"
TransactionReOrgScenarioReOrgDifferentBlock = "Re-Org to Different Block"
TransactionReOrgScenarioNewPayloadOnRevert = "New Payload on Revert Back"
type
TransactionReOrgTest* = ref object of EngineSpec
transactionCount*: int
scenario*: TransactionReOrgScenario
ShadowTx = ref object
payload: ExecutionPayload
nextTx: Transaction
tx: Option[Transaction]
sendTransaction: proc(i: int): Transaction {.gcsafe.}
method withMainFork(cs: TransactionReOrgTest, fork: EngineFork): BaseSpec =
var res = cs.clone()
res.mainFork = fork
return res
method getName(cs: TransactionReOrgTest): string =
var name = "Transaction Re-Org"
if cs.scenario != TransactionNoScenario:
name.add ", " & $cs.scenario
return name
proc txHash(shadow: ShadowTx): common.Hash256 =
if shadow.tx.isNone:
error "SHADOW TX IS NONE"
return
shadow.tx.get.rlpHash
# Test transaction status after a forkchoiceUpdated re-orgs to an alternative hash where a transaction is not present
method execute(cs: TransactionReOrgTest, env: TestEnv): bool =
# Wait until this client catches up with latest PoS
let ok = waitFor env.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test (So we don't try to reorg back to the genesis block)
testCond env.clMock.produceBlocks(5, BlockProcessCallbacks())
# Create transactions that modify the state in order to check after the reorg.
var shadow = ShadowTx()
# Send a transaction on each payload of the canonical chain
shadow.sendTransaction = proc(i: int): Transaction {.gcsafe.} =
let sstoreContractAddr = hexToByteArray[20]("0000000000000000000000000000000000000317")
var data: array[32, byte]
data[^1] = i.byte
info "transactionReorg", idx=i
let tc = BaseTx(
recipient: some(sstoreContractAddr),
amount: 0.u256,
payload: @data,
txType: cs.txType,
gasLimit: 75000,
)
var tx = env.makeNextTx(tc)
doAssert env.sendTx(tx)
tx
var txCount = cs.transactionCount
if txCount == 0:
# Default is to send 5 transactions
txCount = 5
for i in 0..<txCount:
# Generate two payloads, one with the transaction and the other one without it
let pbRes = env.clMock.produceSingleBlock(BlockProcessCallbacks(
onPayloadAttributesGenerated: proc(): bool =
# At this point we have not broadcast the transaction.
if cs.scenario == TransactionReOrgScenarioReOrgOut:
# Any payload we get should not contain any
var attr = env.clMock.latestPayloadAttributes
attr.prevRandao = Web3Hash.randomBytes()
var version = env.engine.version(env.clMock.latestHeader.timestamp)
let r = env.engine.client.forkchoiceUpdated(version, env.clMock.latestForkchoice, some(attr))
r.expectNoError()
testCond r.get.payloadID.isSome:
fatal "No payload ID returned by forkchoiceUpdated"
version = env.engine.version(attr.timestamp)
let g = env.engine.client.getPayload(r.get.payloadID.get, version)
g.expectNoError()
shadow.payload = g.get.executionPayload
testCond len(shadow.payload.transactions) == 0:
fatal "Empty payload contains transactions"
if cs.scenario != TransactionReOrgScenarioReOrgBackIn:
# 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
shadow.tx = some(shadow.sendTransaction(i))
# Get the receipt
let receipt = env.engine.client.txReceipt(shadow.txHash)
testCond receipt.isErr:
fatal "Receipt obtained before tx included in block"
return true
,
onGetpayload: proc(): bool =
# Check that indeed the payload contains the transaction
if shadow.tx.isSome:
testCond txInPayload(env.clMock.latestPayloadBuilt, shadow.txHash):
fatal "Payload built does not contain the transaction"
if cs.scenario in [TransactionReOrgScenarioReOrgDifferentBlock, TransactionReOrgScenarioNewPayloadOnRevert]:
# Create side payload with different hash
let customizer = CustomPayloadData(
extraData: some(@[0x01.byte])
)
shadow.payload = customizer.customizePayload(env.clMock.latestExecutableData).basePayload
testCond shadow.payload.parentHash == env.clMock.latestPayloadBuilt.parentHash:
fatal "Incorrect parent hash for payloads"
testCond shadow.payload.blockHash != env.clMock.latestPayloadBuilt.blockHash:
fatal "Incorrect hash for payloads"
elif cs.scenario == TransactionReOrgScenarioReOrgBackIn:
# At this point we broadcast the transaction and request a new payload from the client that must
# contain the transaction.
# Since we are re-orging out and back in on the next block, the verification of this transaction
# being included happens on the next block
shadow.nextTx = shadow.sendTransaction(i)
if i == 0:
# We actually can only do this once because the transaction carries over and we cannot
# impede it from being included in the next payload
var forkchoiceUpdated = env.clMock.latestForkchoice
var payloadAttributes = env.clMock.latestPayloadAttributes
payloadAttributes.suggestedFeeRecipient = w3Addr EthAddress.randomBytes()
var version = env.engine.version(env.clMock.latestHeader.timestamp)
let f = env.engine.client.forkchoiceUpdated(version, forkchoiceUpdated, some(payloadAttributes))
f.expectPayloadStatus(PayloadExecutionStatus.valid)
# Wait a second for the client to prepare the payload with the included transaction
let period = chronos.seconds(env.clMock.payloadProductionClientDelay)
waitFor sleepAsync(period)
version = env.engine.version(env.clMock.latestPayloadAttributes.timestamp)
let g = env.engine.client.getPayload(f.get.payloadID.get, version)
g.expectNoError()
let payload = g.get.executionPayload
testCond txInPayload(payload, shadow.nextTx.rlpHash):
fatal "Payload built does not contain the transaction"
# Send the new payload and forkchoiceUpdated to it
let n = env.engine.client.newPayload(payload)
n.expectStatus(PayloadExecutionStatus.valid)
forkchoiceUpdated.headBlockHash = payload.blockHash
version = env.engine.version(payload.timestamp)
let s = env.engine.client.forkchoiceUpdated(version, forkchoiceUpdated)
s.expectPayloadStatus(PayloadExecutionStatus.valid)
return true
,
onNewPayloadBroadcast: proc(): bool =
if shadow.tx.isSome:
# Get the receipt
let receipt = env.engine.client.txReceipt(shadow.txHash)
testCond receipt.isErr:
fatal "Receipt obtained before tx included in block (NewPayload)"
return true
,
onForkchoiceBroadcast: proc(): bool =
if cs.scenario != TransactionReOrgScenarioReOrgBackIn:
# Transaction is now in the head of the canonical chain, re-org and verify it's removed
# Get the receipt
var txt = env.engine.client.txReceipt(shadow.txHash)
txt.expectBlockHash(ethHash env.clMock.latestForkchoice.headBlockHash)
testCond shadow.payload.parentHash == env.clMock.latestPayloadBuilt.parentHash:
fatal "Incorrect parent hash for payloads"
testCond shadow.payload.blockHash != env.clMock.latestPayloadBuilt.blockHash:
fatal "Incorrect hash for payloads"
#if shadow.payload == nil (
# fatal "No payload to re-org to", t.TestName)
let r = env.engine.client.newPayload(shadow.payload)
r.expectStatus(PayloadExecutionStatus.valid)
r.expectLatestValidHash(shadow.payload.blockHash)
let fcu = ForkchoiceStateV1(
headBlockHash: shadow.payload.blockHash,
safeBlockHash: env.clMock.latestForkchoice.safeBlockHash,
finalizedBlockHash: env.clMock.latestForkchoice.finalizedBlockHash,
)
var version = env.engine.version(shadow.payload.timestamp)
let s = env.engine.client.forkchoiceUpdated(version, fcu)
s.expectPayloadStatus(PayloadExecutionStatus.valid)
let p = env.engine.client.namedHeader(Head)
p.expectHash(ethHash shadow.payload.blockHash)
txt = env.engine.client.txReceipt(shadow.txHash)
if cs.scenario == TransactionReOrgScenarioReOrgOut:
testCond txt.isErr:
fatal "Receipt was obtained when the tx had been re-org'd out"
elif cs.scenario in [TransactionReOrgScenarioReOrgDifferentBlock, TransactionReOrgScenarioNewPayloadOnRevert]:
txt.expectBlockHash(ethHash shadow.payload.blockHash)
# Re-org back
if cs.scenario == TransactionReOrgScenarioNewPayloadOnRevert:
let r = env.engine.client.newPayload(env.clMock.latestPayloadBuilt)
r.expectStatus(PayloadExecutionStatus.valid)
r.expectLatestValidHash(env.clMock.latestPayloadBuilt.blockHash)
testCond env.clMock.broadcastForkchoiceUpdated(Version.V1, env.clMock.latestForkchoice)
if shadow.tx.isSome:
# Now it should be back with main payload
let txt = env.engine.client.txReceipt(shadow.txHash)
txt.expectBlockHash(ethHash env.clMock.latestForkchoice.headBlockHash)
if cs.scenario != TransactionReOrgScenarioReOrgBackIn:
shadow.tx = none(Transaction)
if cs.scenario == TransactionReOrgScenarioReOrgBackIn and i > 0:
# Reasoning: Most of the clients do not re-add blob transactions to the pool
# after a re-org, so we need to wait until the next tx is sent to actually
# verify.
shadow.tx = some(shadow.nextTx)
return true
))
testCond pbRes
if shadow.tx.isSome:
# Produce one last block and verify that the block contains the transaction
let pbRes = env.clMock.produceSingleBlock(BlockProcessCallbacks(
onForkchoiceBroadcast: proc(): bool =
testCond txInPayload(env.clMock.latestPayloadBuilt, shadow.txHash):
fatal "Payload built does not contain the transaction"
# Get the receipt
let receipt = env.engine.client.txReceipt(shadow.txHash)
testCond receipt.isOk:
fatal "Receipt not obtained after tx included in block"
return true
))
testCond pbRes
return true
# 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.
type
ReOrgBackToCanonicalTest* = ref object of EngineSpec
# Depth of the re-org to back in the canonical chain
reOrgDepth*: int
# Number of transactions to send on each payload
transactionPerPayload*: int
# Whether to execute a sidechain payload on the re-org
executeSidePayloadOnReOrg*: bool
ShadowCanon = ref object
previousHash: Web3Hash
previousTimestamp: Web3Quantity
payload: ExecutionPayload
parentForkchoice: ForkchoiceStateV1
parentTimestamp: uint64
method withMainFork(cs: ReOrgBackToCanonicalTest, fork: EngineFork): BaseSpec =
var res = cs.clone()
res.mainFork = fork
return res
method getName(cs: ReOrgBackToCanonicalTest): string =
var name = "Re-Org Back into Canonical Chain, Depth=" & $cs.reOrgDepth
if cs.executeSidePayloadOnReOrg:
name.add ", Execute Side Payload on Re-Org"
return name
proc getDepth(cs: ReOrgBackToCanonicalTest): int =
if cs.reOrgDepth == 0:
return 3
return cs.reOrgDepth
# 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.
method execute(cs: ReOrgBackToCanonicalTest, env: TestEnv): bool =
# Wait until this client catches up with latest PoS
let ok = waitFor env.clMock.waitForTTD()
testCond ok
# Check the CLMock configured safe and finalized
testCond env.clMock.slotsToSafe > cs.reOrgDepth:
fatal "[TEST ISSUE] CLMock configured slots to safe less than re-org depth"
testCond env.clMock.slotsToFinalized > cs.reOrgDepth:
fatal "[TEST ISSUE] CLMock configured slots to finalized less than re-org depth"
# Produce blocks before starting the test (So we don't try to reorg back to the genesis block)
testCond env.clMock.produceBlocks(5, BlockProcessCallbacks())
# We are going to reorg back to a previous hash several times
var shadow = ShadowCanon(
previousHash: env.clMock.latestForkchoice.headBlockHash,
previousTimestamp: env.clMock.latestPayloadBuilt.timestamp
)
if cs.executeSidePayloadOnReOrg:
var pbRes = env.clMock.produceSingleBlock(BlockProcessCallbacks(
onPayloadAttributesGenerated: proc(): bool =
var attr = env.clMock.latestPayloadAttributes
attr.prevRandao = Web3Hash.randomBytes()
var version = env.engine.version(env.clMock.latestHeader.timestamp)
let r = env.engine.client.forkchoiceUpdated(version, env.clMock.latestForkchoice, some(attr))
r.expectNoError()
testCond r.get.payloadID.isSome:
fatal "No payload ID returned by forkchoiceUpdated"
version = env.engine.version(attr.timestamp)
let g = env.engine.client.getPayload(r.get.payloadID.get, version)
g.expectNoError()
shadow.payload = g.get.executionPayload
shadow.parentForkchoice = env.clMock.latestForkchoice
shadow.parentTimestamp = env.clMock.latestHeader.timestamp.uint64
return true
))
testCond pbRes
# Continue producing blocks until we reach the depth of the re-org
pbRes = env.clMock.produceBlocks(cs.getDepth()-1, BlockProcessCallbacks(
onPayloadProducerSelected: proc(): bool =
# Send a transaction on each payload of the canonical chain
let tc = BaseTx(
recipient: some(ZeroAddr),
amount: 1.u256,
txType: cs.txType,
gasLimit: 75000,
)
let ok = env.sendNextTxs(env.engine, tc, cs.transactionPerPayload)
testCond ok:
fatal "Error trying to send transactions"
return true
))
testCond pbRes
# On the last block, before executing the next payload of the canonical chain,
# re-org back to the parent of the side payload and execute the side payload first
pbRes = env.clMock.produceSingleBlock(BlockProcessCallbacks(
onGetpayload: proc(): bool =
# We are about to execute the new payload of the canonical chain, re-org back to
# the side payload
var version = env.engine.version(shadow.parentTimestamp)
let f = env.engine.client.forkchoiceUpdated(version, shadow.parentForkchoice)
f.expectPayloadStatus(PayloadExecutionStatus.valid)
f.expectLatestValidHash(shadow.parentForkchoice.headBlockHash)
# Execute the side payload
let n = env.engine.client.newPayload(shadow.payload)
n.expectStatus(PayloadExecutionStatus.valid)
n.expectLatestValidHash(shadow.payload.blockHash)
# At this point the next canonical payload will be executed by the CL mock, so we can
# continue producing blocks
return true
))
testCond pbRes
else:
let pbRes = env.clMock.produceBlocks(cs.getDepth(), BlockProcessCallbacks(
onForkchoiceBroadcast: proc(): bool =
# Send a fcU with the headBlockHash pointing back to the previous block
let fcu = ForkchoiceStateV1(
headBlockHash: shadow.previousHash,
safeBlockHash: env.clMock.latestForkchoice.safeBlockHash,
finalizedBlockHash: env.clMock.latestForkchoice.finalizedBlockHash,
)
# It is only expected that the client does not produce an error and the CL Mocker is able to progress after the re-org
var version = env.engine.version(shadow.previousTimestamp)
var r = env.engine.client.forkchoiceUpdated(version, fcu)
r.expectNoError()
# Re-send the ForkchoiceUpdated that the CLMock had sent
version = env.engine.version(env.clMock.latestExecutedPayload.timestamp)
r = env.engine.client.forkchoiceUpdated(version, env.clMock.latestForkchoice)
r.expectNoError()
return true
))
testCond pbRes
# Verify that the client is pointing to the latest payload sent
let r = env.engine.client.namedHeader(Head)
r.expectHash(ethHash env.clMock.latestPayloadBuilt.blockHash)
return true
type
ReOrgBackFromSyncingTest* = ref object of EngineSpec
Shadow = ref object
payloads: seq[ExecutableData]
method withMainFork(cs: ReOrgBackFromSyncingTest, fork: EngineFork): BaseSpec =
var res = cs.clone()
res.mainFork = fork
return res
method getName(cs: ReOrgBackFromSyncingTest): string =
"Re-Org Back to Canonical Chain From Syncing Chain"
# Test that performs a re-org back to the canonical chain after re-org to syncing/unavailable chain.
method execute(cs: ReOrgBackFromSyncingTest, env: TestEnv): bool =
# Wait until this client catches up with latest PoS
let ok = waitFor env.clMock.waitForTTD()
testCond ok
# Produce an alternative chain
var shadow = Shadow()
var pbRes = env.clMock.produceBlocks(10, BlockProcessCallbacks(
onPayloadProducerSelected: proc(): bool =
# Send a transaction on each payload of the canonical chain
let tc = BaseTx(
recipient: some(ZeroAddr),
amount: 1.u256,
txType: cs.txType,
gasLimit: 75000,
)
let ok = env.sendNextTx(env.engine, tc)
testCond ok:
fatal "Error trying to send transactions"
return true
,
onGetpayload: proc(): bool =
# Check that at least one transaction made it into the payload
testCond len(env.clMock.latestPayloadBuilt.transactions) > 0:
fatal "No transactions in payload"
# Generate an alternative payload by simply adding extraData to the block
var altParentHash = env.clMock.latestPayloadBuilt.parentHash
if len(shadow.payloads) > 0:
altParentHash = shadow.payloads[^1].blockHash
let customizer = CustomPayloadData(
parentHash: some(ethHash altParentHash),
extraData: some(@[0x01.byte]),
)
let payload = customizer.customizePayload(env.clMock.latestExecutableData)
shadow.payloads.add payload
return true
))
testCond pbRes
pbRes = env.clMock.produceSingleBlock(BlockProcessCallbacks(
onGetpayload: proc(): bool =
# Re-org to the unavailable sidechain in the middle of block production
# to be able to re-org back to the canonical chain
var version = env.engine.version(shadow.payloads[^1].timestamp)
let r = env.engine.client.newPayload(version, shadow.payloads[^1])
r.expectStatusEither([PayloadExecutionStatus.syncing, PayloadExecutionStatus.accepted])
r.expectLatestValidHash()
# We are going to send one of the alternative payloads and fcU to it
let fcu = ForkchoiceStateV1(
headBlockHash: shadow.payloads[^1].blockHash,
safeBlockHash: env.clMock.latestForkchoice.safeBlockHash,
finalizedBlockHash: env.clMock.latestForkchoice.finalizedBlockHash,
)
# It is only expected that the client does not produce an error and the CL Mocker is able to progress after the re-org
version = env.engine.version(shadow.payloads[^1].timestamp)
let s = env.engine.client.forkchoiceUpdated(version, fcu)
s.expectLatestValidHash()
s.expectPayloadStatus(PayloadExecutionStatus.syncing)
# 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.
return true
))
testCond pbRes
return true
type
ReOrgPrevValidatedPayloadOnSideChainTest* = ref object of EngineSpec
method withMainFork(cs: ReOrgPrevValidatedPayloadOnSideChainTest, fork: EngineFork): BaseSpec =
var res = cs.clone()
res.mainFork = fork
return res
method getName(cs: ReOrgPrevValidatedPayloadOnSideChainTest): string =
"Re-org to Previously Validated Sidechain Payload"
func toSeq(x: string): seq[byte] =
for z in x:
result.add z.byte
func ethAddress(a, b: int): EthAddress =
result[0] = a.byte
result[1] = b.byte
# Test that performs a re-org to a previously validated payload on a side chain.
method execute(cs: ReOrgPrevValidatedPayloadOnSideChainTest, env: TestEnv): bool =
# Wait until this client catches up with latest PoS
let ok = waitFor env.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test
testCond env.clMock.produceBlocks(5, BlockProcessCallbacks())
var shadow = Shadow()
# Produce a canonical chain while at the same time generate a side chain to which we will re-org.
var pbRes = env.clMock.produceBlocks(5, BlockProcessCallbacks(
onPayloadProducerSelected: proc(): bool =
# Send a transaction on each payload of the canonical chain
let tc = BaseTx(
recipient: some(ZeroAddr),
amount: 1.u256,
txType: cs.txType,
gasLimit: 75000,
)
let ok = env.sendNextTx(env.engine, tc)
testCond ok:
fatal "Error trying to send transactions"
return true
,
onGetpayload: proc(): bool =
# Check that at least one transaction made it into the payload
testCond len(env.clMock.latestPayloadBuilt.transactions) > 0:
fatal "No transactions in payload"
# The side chain will consist simply of the same payloads with extra data appended
var customData = CustomPayloadData(
extraData: some(toSeq("side")),
)
if len(shadow.payloads) > 0:
customData.parentHash = some(ethHash shadow.payloads[^1].blockHash)
let payload = customData.customizePayload(env.clMock.latestExecutableData)
shadow.payloads.add payload
let version = env.engine.version(payload.timestamp)
let r = env.engine.client.newPayload(version, payload)
r.expectStatus(PayloadExecutionStatus.valid)
r.expectLatestValidHash(payload.blockHash)
return true
))
testCond pbRes
# Attempt to re-org to one of the sidechain payloads, but not the leaf,
# and also build a new payload from this sidechain.
pbRes = env.clMock.produceSingleBlock(BlockProcessCallbacks(
onGetpayload: proc(): bool =
var
prevRandao = common.Hash256.randomBytes()
suggestedFeeRecipient = ethAddress(0x12, 0x34)
let payloadAttributesCustomizer = BasePayloadAttributesCustomizer(
prevRandao: some(prevRandao),
suggestedFeerecipient: some(suggestedFeeRecipient),
)
let reOrgPayload = shadow.payloads[^2]
let reOrgPayloadAttributes = shadow.payloads[^1].attr
let newPayloadAttributes = payloadAttributesCustomizer.getPayloadAttributes(reOrgPayloadAttributes)
let fcu = ForkchoiceStateV1(
headBlockHash: reOrgPayload.blockHash,
safeBlockHash: env.clMock.latestForkchoice.safeBlockHash,
finalizedBlockHash: env.clMock.latestForkchoice.finalizedBlockHash,
)
var version = env.engine.version(reOrgPayload.timestamp)
let r = env.engine.client.forkchoiceUpdated(version, fcu, some(newPayloadAttributes))
r.expectPayloadStatus(PayloadExecutionStatus.valid)
r.expectLatestValidHash(reOrgPayload.blockHash)
version = env.engine.version(newPayloadAttributes.timestamp)
let p = env.engine.client.getPayload(r.get.payloadID.get, version)
p.expectPayloadParentHash(reOrgPayload.blockHash)
let payload = p.get.executionPayload
let s = env.engine.client.newPayload(payload)
s.expectStatus(PayloadExecutionStatus.valid)
s.expectLatestValidHash(payload.blockHash)
# 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.
return true
))
testCond pbRes
return true
type
SafeReOrgToSideChainTest* = ref object of EngineSpec
method withMainFork(cs: SafeReOrgToSideChainTest, fork: EngineFork): BaseSpec =
var res = cs.clone()
res.mainFork = fork
return res
method getName(cs: SafeReOrgToSideChainTest): string =
"Safe Re-Org to Side Chain"
# Test that performs a re-org of the safe block to a side chain.
method execute(cs: SafeReOrgToSideChainTest, env: TestEnv): bool =
# Wait until this client catches up with latest PoS
let ok = waitFor env.clMock.waitForTTD()
testCond ok
# Produce an alternative chain
var shadow = Shadow()
testCond cs.slotsToSafe == 1:
fatal "[TEST ISSUE] CLMock configured slots to safe not equal to 1"
testCond cs.slotsToFinalized == 2:
fatal "[TEST ISSUE] CLMock configured slots to finalized not equal to 2"
# Produce three payloads `P1`, `P2`, `P3`, along with the side chain payloads `P2'`, `P3'`
# First payload is finalized so no alternative payload
testCond env.clMock.produceSingleBlock(BlockProcessCallbacks())
testCond env.clMock.produceBlocks(2, BlockProcessCallbacks(
onGetpayload: proc(): bool =
# Generate an alternative payload by simply adding extraData to the block
var altParentHash = env.clMock.latestPayloadBuilt.parentHash
if len(shadow.payloads) > 0:
altParentHash = shadow.payloads[^1].blockHash
let customizer = CustomPayloadData(
parentHash: some(ethHash altParentHash),
extraData: some(@[0x01.byte]),
)
let payload = customizer.customizePayload(env.clMock.latestExecutableData)
shadow.payloads.add payload
return true
))
# Verify current state of labels
let head = env.engine.client.namedHeader(Head)
head.expectHash(ethHash env.clMock.latestPayloadBuilt.blockHash)
let safe = env.engine.client.namedHeader(Safe)
safe.expectHash(ethHash env.clMock.executedPayloadHistory[2].blockHash)
let finalized = env.engine.client.namedHeader(Finalized)
finalized.expectHash(ethHash env.clMock.executedPayloadHistory[1].blockHash)
# Re-org the safe/head blocks to point to the alternative side chain
let pbRes = env.clMock.produceSingleBlock(BlockProcessCallbacks(
onGetpayload: proc(): bool =
for p in shadow.payloads:
let version = env.engine.version(p.timestamp)
let r = env.engine.client.newPayload(version, p)
r.expectStatusEither([PayloadExecutionStatus.valid, PayloadExecutionStatus.accepted])
let fcu = ForkchoiceStateV1(
headBlockHash: shadow.payloads[1].blockHash,
safeBlockHash: shadow.payloads[0].blockHash,
finalizedBlockHash: env.clMock.executedPayloadHistory[1].blockHash,
)
let version = env.engine.version(shadow.payloads[1].timestamp)
let r = env.engine.client.forkchoiceUpdated(version, fcu)
r.expectPayloadStatus(PayloadExecutionStatus.valid)
let head = env.engine.client.namedHeader(Head)
head.expectHash(ethHash shadow.payloads[1].blockHash)
let safe = env.engine.client.namedHeader(Safe)
safe.expectHash(ethHash shadow.payloads[0].blockHash)
let finalized = env.engine.client.namedHeader(Finalized)
finalized.expectHash(ethHash env.clMock.executedPayloadHistory[1].blockHash)
return true
))
testCond pbRes
return true