Engine API: Fix latestValidHash value when invalid timestamp detected

This commit is contained in:
jangko 2023-11-03 21:41:05 +07:00
parent 19f313d891
commit 7de6199ba3
No known key found for this signature in database
GPG Key ID: 31702AE10541E6B9
5 changed files with 234 additions and 254 deletions

View File

@ -438,14 +438,18 @@ proc broadcastNextNewPayload(cl: CLMocker): bool =
cl.executedPayloadHistory[number] = cl.latestPayloadBuilt cl.executedPayloadHistory[number] = cl.latestPayloadBuilt
return true return true
proc broadcastForkchoiceUpdated(cl: CLMocker, eng: EngineEnv, proc broadcastForkchoiceUpdated(cl: CLMocker,
update: ForkchoiceStateV1): Result[ForkchoiceUpdatedResponse, string] = eng: EngineEnv,
let version = cl.latestExecutedPayload.version version: Version,
update: ForkchoiceStateV1):
Result[ForkchoiceUpdatedResponse, string] =
eng.client.forkchoiceUpdated(version, update, none(PayloadAttributes)) eng.client.forkchoiceUpdated(version, update, none(PayloadAttributes))
proc broadcastLatestForkchoice(cl: CLMocker): bool = proc broadcastForkchoiceUpdated*(cl: CLMocker,
version: Version,
update: ForkchoiceStateV1): bool =
for eng in cl.clients: for eng in cl.clients:
let res = cl.broadcastForkchoiceUpdated(eng, cl.latestForkchoice) let res = cl.broadcastForkchoiceUpdated(eng, version, update)
if res.isErr: if res.isErr:
error "CLMocker: broadcastForkchoiceUpdated Error", msg=res.error error "CLMocker: broadcastForkchoiceUpdated Error", msg=res.error
return false return false
@ -474,6 +478,10 @@ proc broadcastLatestForkchoice(cl: CLMocker): bool =
return true return true
proc broadcastLatestForkchoice(cl: CLMocker): bool =
let version = cl.latestExecutedPayload.version
cl.broadcastForkchoiceUpdated(version, cl.latestForkchoice)
func w3Address(x: int): Web3Address = func w3Address(x: int): Web3Address =
var res: array[20, byte] var res: array[20, byte]
res[^1] = x.byte res[^1] = x.byte

View File

@ -39,13 +39,8 @@ method withMainFork(cs: InvalidMissingAncestorReOrgTest, fork: EngineFork): Base
return res return res
method getName(cs: InvalidMissingAncestorReOrgTest): string = method getName(cs: InvalidMissingAncestorReOrgTest): string =
var desc = "Invalid Missing Ancestor ReOrg Invalid" & $cs.invalidField "Invalid Missing Ancestor ReOrg, $1, EmptyTxs=$2, Invalid P$3" % [
$cs.invalidField, $cs.emptyTransactions, $cs.invalidIndex]
if cs.emptyTransactions:
desc.add ", Empty Txs"
desc.add ", Invalid P" & $cs.invalidIndex & "'"
desc
method execute(cs: InvalidMissingAncestorReOrgTest, env: TestEnv): bool = method execute(cs: InvalidMissingAncestorReOrgTest, env: TestEnv): bool =
# Wait until TTD is reached by this client # Wait until TTD is reached by this client
@ -186,25 +181,14 @@ type
# or start chain (if specified). # or start chain (if specified).
reOrgFromCanonical*: bool reOrgFromCanonical*: bool
method withMainFork(cs: InvalidMissingAncestorReOrgSyncTest, fork: EngineFork): BaseSpec = method withMainFork(cs: InvalidMissingAncestorReOrgSyncTest, fork: EngineFork): BaseSpec =
var res = cs.clone() var res = cs.clone()
res.mainFork = fork res.mainFork = fork
return res return res
method getName(cs: InvalidMissingAncestorReOrgSyncTest): string = method getName(cs: InvalidMissingAncestorReOrgSyncTest): string =
var name = "Invalid Missing Ancestor ReOrg" "Invalid Missing Ancestor Syncing ReOrg, $1, EmptyTxs=$2, CanonicalReOrg=$3, Invalid P$4" % [
name.add ", Invalid " & $cs.invalidField $cs.invalidField, $cs.emptyTransactions, $cs.reOrgFromCanonical, $cs.invalidIndex]
if cs.emptyTransactions:
name.add ", Empty Txs"
name.add ", Invalid P" & $cs.invalidIndex & "'"
name.add ", Reveal using sync"
if cs.reOrgFromCanonical:
name.add ", ReOrg from Canonical"
return name
method execute(cs: InvalidMissingAncestorReOrgSyncTest, env: TestEnv): bool = method execute(cs: InvalidMissingAncestorReOrgSyncTest, env: TestEnv): bool =
var sec = env.addEngine(true, cs.reOrgFromCanonical) var sec = env.addEngine(true, cs.reOrgFromCanonical)

View File

@ -12,6 +12,7 @@ import
std/strutils, std/strutils,
eth/common, eth/common,
chronicles, chronicles,
stew/byteutils,
./engine_spec, ./engine_spec,
../cancun/customizer, ../cancun/customizer,
../helper ../helper
@ -115,7 +116,13 @@ type
type type
TransactionReOrgTest* = ref object of EngineSpec TransactionReOrgTest* = ref object of EngineSpec
transactionCount*: int transactionCount*: int
scenario*: TransactionReOrgScenario 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 = method withMainFork(cs: TransactionReOrgTest, fork: EngineFork): BaseSpec =
var res = cs.clone() var res = cs.clone()
@ -128,6 +135,9 @@ method getName(cs: TransactionReOrgTest): string =
name.add ", " & $cs.scenario name.add ", " & $cs.scenario
return name return name
func txHash(shadow: ShadowTx): common.Hash256 =
shadow.tx.get.rlpHash
# Test transaction status after a forkchoiceUpdated re-orgs to an alternative hash where a transaction is not present # 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 = method execute(cs: TransactionReOrgTest, env: TestEnv): bool =
# Wait until this client catches up with latest PoS # Wait until this client catches up with latest PoS
@ -136,259 +146,218 @@ method execute(cs: TransactionReOrgTest, env: TestEnv): bool =
# Produce blocks before starting the test (So we don't try to reorg back to the genesis block) # Produce blocks before starting the test (So we don't try to reorg back to the genesis block)
testCond env.clMock.produceBlocks(5, BlockProcessCallbacks()) testCond env.clMock.produceBlocks(5, BlockProcessCallbacks())
#[
# Create transactions that modify the state in order to check after the reorg.
var (
err error
txCount = cs.transactionCount
sstoreContractAddr = common.HexToAddress("0000000000000000000000000000000000000317")
)
if txCount == 0 ( # Create transactions that modify the state in order to check after the reorg.
# Default is to send 5 transactions var shadow = ShadowTx()
txCount = 5
)
# Send a transaction on each payload of the canonical chain # Send a transaction on each payload of the canonical chain
sendTransaction = func(i int) (Transaction, error) ( shadow.sendTransaction = proc(i: int): Transaction {.gcsafe.} =
data = common.LeftPadBytes([]byte(byte(i)), 32) let sstoreContractAddr = hexToByteArray[20]("0000000000000000000000000000000000000317")
t.Logf("transactionReorg, i=%v, data=%v\n", i, data) var data: array[32, byte]
return t.sendNextTx( data[^1] = i.byte
t.Engine, info "transactionReorg", idx=i
BaseTx( let tc = BaseTx(
recipient: &sstoreContractAddr, recipient: some(sstoreContractAddr),
amount: big0, amount: 0.u256,
payload: data, payload: @data,
txType: cs.txType, txType: cs.txType,
gasLimit: 75000, gasLimit: 75000,
ForkConfig: t.ForkConfig,
),
) )
var tx = env.makeNextTx(tc)
doAssert env.sendTx(tx)
tx
) var txCount = cs.transactionCount
if txCount == 0:
var ( # Default is to send 5 transactions
shadow.payload ExecutableData txCount = 5
nextTx Transaction
tx Transaction
)
for i = 0; i < txCount; i++ (
for i in 0..<txCount:
# Generate two payloads, one with the transaction and the other one without it # Generate two payloads, one with the transaction and the other one without it
env.clMock.produceSingleBlock(BlockProcessCallbacks( let pbRes = env.clMock.produceSingleBlock(BlockProcessCallbacks(
OnPayloadAttributesGenerated: proc(): bool = onPayloadAttributesGenerated: proc(): bool =
# At this point we have not broadcast the transaction. # At this point we have not broadcast the transaction.
if cs.scenario == TransactionReOrgScenarioReOrgOut ( if cs.scenario == TransactionReOrgScenarioReOrgOut:
# Any payload we get should not contain any # Any payload we get should not contain any
payloadAttributes = env.clMock.LatestPayloadAttributes var attr = env.clMock.latestPayloadAttributes
rand.Read(payloadAttributes.Random[:]) attr.prevRandao = Web3Hash.randomBytes()
r = env.engine.client.forkchoiceUpdated(env.clMock.latestForkchoice, payloadAttributes, env.clMock.latestHeader.Time)
var version = env.engine.version(env.clMock.latestHeader.timestamp)
let r = env.engine.client.forkchoiceUpdated(version, env.clMock.latestForkchoice, some(attr))
r.expectNoError() r.expectNoError()
if r.Response.PayloadID == nil ( testCond r.get.payloadID.isSome:
fatal "No payload ID returned by forkchoiceUpdated", t.TestName) fatal "No payload ID returned by forkchoiceUpdated"
)
g = env.engine.client.getPayload(r.Response.PayloadID, payloadAttributes) version = env.engine.version(attr.timestamp)
let g = env.engine.client.getPayload(r.get.payloadID.get, version)
g.expectNoError() g.expectNoError()
shadow.payload = &g.Payload shadow.payload = g.get.executionPayload
if len(shadow.payload.transactions) != 0 ( testCond len(shadow.payload.transactions) == 0:
fatal "Empty payload contains transactions: %v", t.TestName, shadow.payload) fatal "Empty payload contains transactions"
)
)
if cs.scenario != TransactionReOrgScenarioReOrgBackIn ( if cs.scenario != TransactionReOrgScenarioReOrgBackIn:
# At this point we can broadcast the transaction and it will be included in the next payload # 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 # Data is the key where a `1` will be stored
tx, err = sendTransaction(i) let tx = shadow.sendTransaction(i)
testCond ok:
fatal "Error trying to send transaction: %v", t.TestName, err)
)
# Get the receipt # Get the receipt
ctx, cancel = context.WithTimeout(t.TestContext, globals.RPCTimeout) let receipt = env.engine.client.txReceipt(tx.rlpHash)
defer cancel() testCond receipt.isOk:
receipt, _ = t.Eth.TransactionReceipt(ctx, shadow.txHash) fatal "Receipt obtained before tx included in block"
if receipt != nil (
fatal "Receipt obtained before tx included in block: %v", t.TestName, receipt)
)
)
), return true
,
onGetpayload: proc(): bool = onGetpayload: proc(): bool =
# Check that indeed the payload contains the transaction # Check that indeed the payload contains the transaction
if shadow.tx.isSome: if shadow.tx.isSome:
if !TransactionInPayload(env.clMock.latestPayloadBuilt, tx) ( testCond txInPayload(env.clMock.latestPayloadBuilt, shadow.txHash):
fatal "Payload built does not contain the transaction: %v", t.TestName, env.clMock.latestPayloadBuilt) fatal "Payload built does not contain the transaction"
)
)
if cs.scenario == TransactionReOrgScenarioReOrgDifferentBlock || cs.scenario == TransactionReOrgScenarioNewPayloadOnRevert ( if cs.scenario in [TransactionReOrgScenarioReOrgDifferentBlock, TransactionReOrgScenarioNewPayloadOnRevert]:
# Create side payload with different hash # Create side payload with different hash
var err error let customizer = CustomPayloadData(
customizer = &CustomPayloadData( extraData: some(@[0x01.byte])
extraData: &([]byte(0x01)),
)
shadow.payload, err = customizer.customizePayload(env.clMock.latestPayloadBuilt)
testCond ok:
fatal "Error creating reorg payload %v", err)
) )
shadow.payload = customizer.customizePayload(env.clMock.latestExecutableData).basePayload
if shadow.payload.parentHash != env.clMock.latestPayloadBuilt.parentHash ( testCond shadow.payload.parentHash == env.clMock.latestPayloadBuilt.parentHash:
fatal "Incorrect parent hash for payloads: %v != %v", t.TestName, shadow.payload.parentHash, env.clMock.latestPayloadBuilt.parentHash) fatal "Incorrect parent hash for payloads"
)
if shadow.payload.blockHash == env.clMock.latestPayloadBuilt.blockHash ( testCond shadow.payload.blockHash != env.clMock.latestPayloadBuilt.blockHash:
fatal "Incorrect hash for payloads: %v == %v", t.TestName, shadow.payload.blockHash, env.clMock.latestPayloadBuilt.blockHash) fatal "Incorrect hash for payloads"
)
elif cs.scenario == TransactionReOrgScenarioReOrgBackIn ( elif cs.scenario == TransactionReOrgScenarioReOrgBackIn:
# At this point we broadcast the transaction and request a new payload from the client that must # At this point we broadcast the transaction and request a new payload from the client that must
# contain the transaction. # contain the transaction.
# Since we are re-orging out and back in on the next block, the verification of this 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 # being included happens on the next block
shadow.nextTx, err = sendTransaction(i) shadow.nextTx = shadow.sendTransaction(i)
testCond ok:
fatal "Error trying to send transaction: %v", t.TestName, err)
)
if i == 0 ( if i == 0:
# We actually can only do this once because the transaction carries over and we cannot # We actually can only do this once because the transaction carries over and we cannot
# impede it from being included in the next payload # impede it from being included in the next payload
forkchoiceUpdated = env.clMock.latestForkchoice var forkchoiceUpdated = env.clMock.latestForkchoice
payloadAttributes = env.clMock.LatestPayloadAttributes var payloadAttributes = env.clMock.latestPayloadAttributes
rand.Read(payloadAttributes.SuggestedFeeRecipient[:]) payloadAttributes.suggestedFeeRecipient = w3Addr EthAddress.randomBytes()
f = env.engine.client.forkchoiceUpdated(
&forkchoiceUpdated, var version = env.engine.version(env.clMock.latestHeader.timestamp)
&payloadAttributes, let f = env.engine.client.forkchoiceUpdated(version, forkchoiceUpdated, some(payloadAttributes))
env.clMock.latestHeader.Time,
)
f.expectPayloadStatus(PayloadExecutionStatus.valid) f.expectPayloadStatus(PayloadExecutionStatus.valid)
# Wait a second for the client to prepare the payload with the included transaction # Wait a second for the client to prepare the payload with the included transaction
let period = chronos.seconds(env.clMock.payloadProductionClientDelay)
waitFor sleepAsync(period)
await sleepAsync(env.clMock.PayloadProductionClientDelay) version = env.engine.version(env.clMock.latestPayloadAttributes.timestamp)
let g = env.engine.client.getPayload(f.get.payloadID.get, version)
g = env.engine.client.getPayload(f.Response.PayloadID, env.clMock.LatestPayloadAttributes)
g.expectNoError() g.expectNoError()
if !TransactionInPayload(g.Payload, shadow.nextTx) ( let payload = g.get.executionPayload
fatal "Payload built does not contain the transaction: %v", t.TestName, g.Payload) testCond txInPayload(payload, shadow.nextTx.rlpHash):
) fatal "Payload built does not contain the transaction"
# Send the new payload and forkchoiceUpdated to it # Send the new payload and forkchoiceUpdated to it
n = env.engine.client.newPayload(g.Payload) let n = env.engine.client.newPayload(payload)
n.expectStatus(PayloadExecutionStatus.valid) n.expectStatus(PayloadExecutionStatus.valid)
forkchoiceUpdated.headBlockHash = g.Payload.blockHash forkchoiceUpdated.headBlockHash = payload.blockHash
s = env.engine.client.forkchoiceUpdated(forkchoiceUpdated, nil, g.Payload.timestamp) version = env.engine.version(payload.timestamp)
let s = env.engine.client.forkchoiceUpdated(version, forkchoiceUpdated)
s.expectPayloadStatus(PayloadExecutionStatus.valid) s.expectPayloadStatus(PayloadExecutionStatus.valid)
) return true
) ,
),
onNewPayloadBroadcast: proc(): bool = onNewPayloadBroadcast: proc(): bool =
if shadow.tx.isSome: if shadow.tx.isSome:
# Get the receipt # Get the receipt
ctx, cancel = context.WithTimeout(t.TestContext, globals.RPCTimeout) let receipt = env.engine.client.txReceipt(shadow.txHash)
defer cancel() testCond receipt.isOk:
receipt, _ = t.Eth.TransactionReceipt(ctx, shadow.txHash) fatal "Receipt obtained before tx included in block (NewPayload)"
if receipt != nil ( return true
fatal "Receipt obtained before tx included in block (NewPayload): %v", t.TestName, receipt) ,
)
)
),
onForkchoiceBroadcast: proc(): bool = onForkchoiceBroadcast: proc(): bool =
if cs.scenario != TransactionReOrgScenarioReOrgBackIn ( if cs.scenario != TransactionReOrgScenarioReOrgBackIn:
# Transaction is now in the head of the canonical chain, re-org and verify it's removed # Transaction is now in the head of the canonical chain, re-org and verify it's removed
# Get the receipt # Get the receipt
txt = env.engine.client.txReceipt(shadow.txHash) var txt = env.engine.client.txReceipt(shadow.txHash)
txt.expectBlockHash(env.clMock.latestForkchoice.headBlockHash) txt.expectBlockHash(ethHash env.clMock.latestForkchoice.headBlockHash)
if shadow.payload.parentHash != env.clMock.latestPayloadBuilt.parentHash ( testCond shadow.payload.parentHash == env.clMock.latestPayloadBuilt.parentHash:
fatal "Incorrect parent hash for payloads: %v != %v", t.TestName, shadow.payload.parentHash, env.clMock.latestPayloadBuilt.parentHash) fatal "Incorrect parent hash for payloads"
)
if shadow.payload.blockHash == env.clMock.latestPayloadBuilt.blockHash (
fatal "Incorrect hash for payloads: %v == %v", t.TestName, shadow.payload.blockHash, env.clMock.latestPayloadBuilt.blockHash)
)
if shadow.payload == nil ( testCond shadow.payload.blockHash != env.clMock.latestPayloadBuilt.blockHash:
fatal "No payload to re-org to", t.TestName) fatal "Incorrect hash for payloads"
)
r = env.engine.client.newPayload(shadow.payload) #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.expectStatus(PayloadExecutionStatus.valid)
r.expectLatestValidHash(shadow.payload.blockHash) r.expectLatestValidHash(shadow.payload.blockHash)
s = env.engine.client.forkchoiceUpdated(api.ForkchoiceStateV1( let fcu = ForkchoiceStateV1(
headBlockHash: shadow.payload.blockHash, headBlockHash: shadow.payload.blockHash,
safeBlockHash: env.clMock.latestForkchoice.safeBlockHash, safeBlockHash: env.clMock.latestForkchoice.safeBlockHash,
finalizedBlockHash: env.clMock.latestForkchoice.finalizedBlockHash, finalizedBlockHash: env.clMock.latestForkchoice.finalizedBlockHash,
), nil, shadow.payload.timestamp) )
var version = env.engine.version(shadow.payload.timestamp)
let s = env.engine.client.forkchoiceUpdated(version, fcu)
s.expectPayloadStatus(PayloadExecutionStatus.valid) s.expectPayloadStatus(PayloadExecutionStatus.valid)
p = env.engine.client.headerByNumber(Head) let p = env.engine.client.namedHeader(Head)
p.expectHash(shadow.payload.blockHash) p.expectHash(ethHash shadow.payload.blockHash)
txt = env.engine.client.txReceipt(shadow.txHash) txt = env.engine.client.txReceipt(shadow.txHash)
if cs.scenario == TransactionReOrgScenarioReOrgOut ( if cs.scenario == TransactionReOrgScenarioReOrgOut:
if txt.Receipt != nil ( testCond txt.isErr:
receiptJson, _ = json.MarshalIndent(txt.Receipt, "", " ") fatal "Receipt was obtained when the tx had been re-org'd out"
fatal "Receipt was obtained when the tx had been re-org'd out: %s", t.TestName, receiptJson) elif cs.scenario in [TransactionReOrgScenarioReOrgDifferentBlock, TransactionReOrgScenarioNewPayloadOnRevert]:
) txt.expectBlockHash(ethHash shadow.payload.blockHash)
elif cs.scenario == TransactionReOrgScenarioReOrgDifferentBlock || cs.scenario == TransactionReOrgScenarioNewPayloadOnRevert (
txt.expectBlockHash(shadow.payload.blockHash)
)
# Re-org back # Re-org back
if cs.scenario == TransactionReOrgScenarioNewPayloadOnRevert ( if cs.scenario == TransactionReOrgScenarioNewPayloadOnRevert:
r = env.engine.client.newPayload(env.clMock.latestPayloadBuilt) let r = env.engine.client.newPayload(env.clMock.latestPayloadBuilt)
r.expectStatus(PayloadExecutionStatus.valid) r.expectStatus(PayloadExecutionStatus.valid)
r.expectLatestValidHash(env.clMock.latestPayloadBuilt.blockHash) r.expectLatestValidHash(env.clMock.latestPayloadBuilt.blockHash)
)
env.clMock.BroadcastForkchoiceUpdated(env.clMock.latestForkchoice, nil, 1) testCond env.clMock.broadcastForkchoiceUpdated(Version.V1, env.clMock.latestForkchoice)
)
if shadow.tx.isSome: if shadow.tx.isSome:
# Now it should be back with main payload # Now it should be back with main payload
txt = env.engine.client.txReceipt(shadow.txHash) let txt = env.engine.client.txReceipt(shadow.txHash)
txt.expectBlockHash(env.clMock.latestForkchoice.headBlockHash) txt.expectBlockHash(ethHash env.clMock.latestForkchoice.headBlockHash)
if cs.scenario != TransactionReOrgScenarioReOrgBackIn ( if cs.scenario != TransactionReOrgScenarioReOrgBackIn:
tx = nil shadow.tx = none(Transaction)
)
)
if cs.scenario == TransactionReOrgScenarioReOrgBackIn && i > 0 ( if cs.scenario == TransactionReOrgScenarioReOrgBackIn and i > 0:
# Reasoning: Most of the clients do not re-add blob transactions to the pool # 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 # after a re-org, so we need to wait until the next tx is sent to actually
# verify. # verify.
tx = shadow.nextTx shadow.tx = some(shadow.nextTx)
) return true
),
)) ))
testCond pbRes
)
if shadow.tx.isSome: if shadow.tx.isSome:
# Produce one last block and verify that the block contains the transaction # Produce one last block and verify that the block contains the transaction
env.clMock.produceSingleBlock(BlockProcessCallbacks( let pbRes = env.clMock.produceSingleBlock(BlockProcessCallbacks(
onForkchoiceBroadcast: proc(): bool = onForkchoiceBroadcast: proc(): bool =
if !TransactionInPayload(env.clMock.latestPayloadBuilt, tx) ( testCond txInPayload(env.clMock.latestPayloadBuilt, shadow.txHash):
fatal "Payload built does not contain the transaction: %v", t.TestName, env.clMock.latestPayloadBuilt) fatal "Payload built does not contain the transaction"
)
# Get the receipt # Get the receipt
ctx, cancel = context.WithTimeout(t.TestContext, globals.RPCTimeout) let receipt = env.engine.client.txReceipt(shadow.txHash)
defer cancel() testCond receipt.isErr:
receipt, _ = t.Eth.TransactionReceipt(ctx, shadow.txHash) fatal "Receipt not obtained after tx included in block"
if receipt == nil (
fatal "Receipt not obtained after tx included in block: %v", t.TestName, receipt) 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 # 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. # is still capable of progressing.
@ -401,6 +370,13 @@ type
# Whether to execute a sidechain payload on the re-org # Whether to execute a sidechain payload on the re-org
executeSidePayloadOnReOrg*: bool executeSidePayloadOnReOrg*: bool
ShadowCanon = ref object
previousHash: Web3Hash
previousTimestamp: Web3Quantity
payload: ExecutionPayload
parentForkchoice: ForkchoiceStateV1
parentTimestamp: uint64
method withMainFork(cs: ReOrgBackToCanonicalTest, fork: EngineFork): BaseSpec = method withMainFork(cs: ReOrgBackToCanonicalTest, fork: EngineFork): BaseSpec =
var res = cs.clone() var res = cs.clone()
res.mainFork = fork res.mainFork = fork
@ -435,99 +411,99 @@ method execute(cs: ReOrgBackToCanonicalTest, env: TestEnv): bool =
# Produce blocks before starting the test (So we don't try to reorg back to the genesis block) # Produce blocks before starting the test (So we don't try to reorg back to the genesis block)
testCond env.clMock.produceBlocks(5, BlockProcessCallbacks()) testCond env.clMock.produceBlocks(5, BlockProcessCallbacks())
#[
# We are going to reorg back to a previous hash several times # We are going to reorg back to a previous hash several times
previousHash = env.clMock.latestForkchoice.headBlockHash var shadow = ShadowCanon(
previousTimestamp = env.clMock.latestPayloadBuilt.timestamp previousHash: env.clMock.latestForkchoice.headBlockHash,
previousTimestamp: env.clMock.latestPayloadBuilt.timestamp
)
if cs.executeSidePayloadOnReOrg ( if cs.executeSidePayloadOnReOrg:
var ( var pbRes = env.clMock.produceSingleBlock(BlockProcessCallbacks(
shadow.payload ExecutableData onPayloadAttributesGenerated: proc(): bool =
shadow.parentForkchoice api.ForkchoiceStateV1 var attr = env.clMock.latestPayloadAttributes
shadow.parentTimestamp uint64 attr.prevRandao = Web3Hash.randomBytes()
)
env.clMock.produceSingleBlock(BlockProcessCallbacks( var version = env.engine.version(env.clMock.latestHeader.timestamp)
OnPayloadAttributesGenerated: proc(): bool = let r = env.engine.client.forkchoiceUpdated(version, env.clMock.latestForkchoice, some(attr))
payloadAttributes = env.clMock.LatestPayloadAttributes
rand.Read(payloadAttributes.Random[:])
r = env.engine.client.forkchoiceUpdated(env.clMock.latestForkchoice, payloadAttributes, env.clMock.latestHeader.Time)
r.expectNoError() r.expectNoError()
if r.Response.PayloadID == nil ( testcond r.get.payloadID.isSome:
fatal "No payload ID returned by forkchoiceUpdated", t.TestName) fatal "No payload ID returned by forkchoiceUpdated"
)
g = env.engine.client.getPayload(r.Response.PayloadID, payloadAttributes) version = env.engine.version(attr.timestamp)
let g = env.engine.client.getPayload(r.get.payloadID.get, version)
g.expectNoError() g.expectNoError()
shadow.payload = &g.Payload
shadow.payload = g.get.executionPayload
shadow.parentForkchoice = env.clMock.latestForkchoice shadow.parentForkchoice = env.clMock.latestForkchoice
shadow.parentTimestamp = env.clMock.latestHeader.Time shadow.parentTimestamp = env.clMock.latestHeader.timestamp.uint64
), return true
)) ))
testCond pbRes
# Continue producing blocks until we reach the depth of the re-org # Continue producing blocks until we reach the depth of the re-org
testCond env.clMock.produceBlocks(int(cs.GetDepth()-1), BlockProcessCallbacks( pbRes = env.clMock.produceBlocks(cs.getDepth()-1, BlockProcessCallbacks(
onPayloadProducerSelected: proc(): bool = onPayloadProducerSelected: proc(): bool =
# Send a transaction on each payload of the canonical chain # Send a transaction on each payload of the canonical chain
var err error let tc = BaseTx(
_, err = t.sendNextTxs( recipient: some(ZeroAddr),
t.TestContext, amount: 1.u256,
t.Engine, txType: cs.txType,
BaseTx( gasLimit: 75000,
recipient: &ZeroAddr,
amount: big1,
payload: nil,
txType: cs.txType,
gasLimit: 75000,
ForkConfig: t.ForkConfig,
),
cs.transactionPerPayload,
) )
let ok = env.sendNextTxs(env.engine, tc, cs.transactionPerPayload)
testCond ok: testCond ok:
fatal "Error trying to send transactions: %v", t.TestName, err) fatal "Error trying to send transactions"
) return true
),
)) ))
testCond pbRes
# On the last block, before executing the next payload of the canonical chain, # 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 # re-org back to the parent of the side payload and execute the side payload first
env.clMock.produceSingleBlock(BlockProcessCallbacks( pbRes = env.clMock.produceSingleBlock(BlockProcessCallbacks(
onGetpayload: proc(): bool = onGetpayload: proc(): bool =
# We are about to execute the new payload of the canonical chain, re-org back to # We are about to execute the new payload of the canonical chain, re-org back to
# the side payload # the side payload
f = env.engine.client.forkchoiceUpdated(shadow.parentForkchoice, nil, shadow.parentTimestamp) var version = env.engine.version(shadow.parentTimestamp)
let f = env.engine.client.forkchoiceUpdated(version, shadow.parentForkchoice)
f.expectPayloadStatus(PayloadExecutionStatus.valid) f.expectPayloadStatus(PayloadExecutionStatus.valid)
f.expectLatestValidHash(shadow.parentForkchoice.headBlockHash) f.expectLatestValidHash(shadow.parentForkchoice.headBlockHash)
# Execute the side payload # Execute the side payload
n = env.engine.client.newPayload(shadow.payload) let n = env.engine.client.newPayload(shadow.payload)
n.expectStatus(PayloadExecutionStatus.valid) n.expectStatus(PayloadExecutionStatus.valid)
n.expectLatestValidHash(shadow.payload.blockHash) n.expectLatestValidHash(shadow.payload.blockHash)
# At this point the next canonical payload will be executed by the CL mock, so we can # At this point the next canonical payload will be executed by the CL mock, so we can
# continue producing blocks # continue producing blocks
), return true
)) ))
testCond pbRes
else: else:
testCond env.clMock.produceBlocks(int(cs.GetDepth()), BlockProcessCallbacks( let pbRes = env.clMock.produceBlocks(cs.getDepth(), BlockProcessCallbacks(
onForkchoiceBroadcast: proc(): bool = onForkchoiceBroadcast: proc(): bool =
# Send a fcU with the headBlockHash pointing back to the previous block # Send a fcU with the headBlockHash pointing back to the previous block
forkchoiceUpdatedBack = api.ForkchoiceStateV1( let fcu = ForkchoiceStateV1(
headBlockHash: previousHash, headBlockHash: shadow.previousHash,
safeBlockHash: env.clMock.latestForkchoice.safeBlockHash, safeBlockHash: env.clMock.latestForkchoice.safeBlockHash,
finalizedBlockHash: env.clMock.latestForkchoice.finalizedBlockHash, 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 # It is only expected that the client does not produce an error and the CL Mocker is able to progress after the re-org
r = env.engine.client.forkchoiceUpdated(forkchoiceUpdatedBack, nil, previousTimestamp) var version = env.engine.version(shadow.previousTimestamp)
var r = env.engine.client.forkchoiceUpdated(version, fcu)
r.expectNoError() r.expectNoError()
# Re-send the ForkchoiceUpdated that the CLMock had sent # Re-send the ForkchoiceUpdated that the CLMock had sent
r = env.engine.client.forkchoiceUpdated(env.clMock.latestForkchoice, nil, env.clMock.LatestExecutedPayload.timestamp) version = env.engine.version(env.clMock.latestExecutedPayload.timestamp)
r = env.engine.client.forkchoiceUpdated(version, env.clMock.latestForkchoice)
r.expectNoError() r.expectNoError()
), return true
)) ))
) testCond pbRes
# Verify that the client is pointing to the latest payload sent # Verify that the client is pointing to the latest payload sent
r = env.engine.client.headerByNumber(Head) let r = env.engine.client.namedHeader(Head)
r.expectHash(env.clMock.latestPayloadBuilt.blockHash) r.expectHash(ethHash env.clMock.latestPayloadBuilt.blockHash)
) return true
]#
type type
ReOrgBackFromSyncingTest* = ref object of EngineSpec ReOrgBackFromSyncingTest* = ref object of EngineSpec

View File

@ -165,12 +165,16 @@ template expectLatestValidHash*(res: untyped, expectedHash: Web3Hash) =
testCond s.latestValidHash.isSome: testCond s.latestValidHash.isSome:
error "Expect latest valid hash isSome" error "Expect latest valid hash isSome"
testCond s.latestValidHash.get == expectedHash: testCond s.latestValidHash.get == expectedHash:
error "latest valid hash mismatch", expect=expectedHash, get=s.latestValidHash.get error "latest valid hash mismatch",
expect=expectedHash.short,
get=s.latestValidHash.get.short
else: else:
testCond s.payloadStatus.latestValidHash.isSome: testCond s.payloadStatus.latestValidHash.isSome:
error "Expect latest valid hash isSome" error "Expect latest valid hash isSome"
testCond s.payloadStatus.latestValidHash.get == expectedHash: testCond s.payloadStatus.latestValidHash.get == expectedHash:
error "latest valid hash mismatch", expect=expectedHash, get=s.payloadStatus.latestValidHash.get error "latest valid hash mismatch",
expect=expectedHash.short,
get=s.payloadStatus.latestValidHash.get.short
template expectLatestValidHash*(res: untyped) = template expectLatestValidHash*(res: untyped) =
testCond res.isOk: testCond res.isOk:
@ -297,6 +301,13 @@ template expectPayloadParentHash*(res: untyped, expected: Web3Hash) =
testCond rec.executionPayload.parentHash == expected: testCond rec.executionPayload.parentHash == expected:
error "expectPayloadParentHash", expect=expected.short, get=rec.executionPayload.parentHash.short error "expectPayloadParentHash", expect=expected.short, get=rec.executionPayload.parentHash.short
template expectBlockHash*(res: untyped, expected: common.Hash256) =
testCond res.isOk:
error "expectBlockHash", msg=res.error
let rec = res.get
testCond rec.blockHash == expected:
error "expectBlockHash", expect=expected.short, get=rec.blockHash.short
func timestamp*(x: ExecutableData): auto = func timestamp*(x: ExecutableData): auto =
x.basePayload.timestamp x.basePayload.timestamp

View File

@ -126,8 +126,9 @@ proc newPayload*(ben: BeaconEngineRef,
if header.timestamp <= parent.timestamp: if header.timestamp <= parent.timestamp:
warn "Invalid timestamp", warn "Invalid timestamp",
parent = header.timestamp, header = header.timestamp number = header.blockNumber, parentNumber = parent.blockNumber,
return invalidStatus(db.getHeadBlockHash(), "Invalid timestamp") parent = parent.timestamp, header = header.timestamp
return invalidStatus(parent.blockHash, "Invalid timestamp")
if not db.haveBlockAndState(header.parentHash): if not db.haveBlockAndState(header.parentHash):
ben.put(blockHash, header) ben.put(blockHash, header)