Permit a null LVH from an INVALID response to newPayload (#4037)

## Issue Addressed

NA

## Proposed Changes

As discovered in #4034, Lighthouse is not accepting `latest_valid_hash == None` in an `INVALID` response to `newPayload`. The `null`/`None` response *was* illegal at one point, however it was added in https://github.com/ethereum/execution-apis/pull/254.

This PR brings Lighthouse in line with the standard and should fix the root cause of what #4034 patched around.

## Additional Info

NA
This commit is contained in:
Paul Hauner 2023-03-03 04:12:50 +00:00
parent 2b6348781a
commit cac3a66be4
5 changed files with 81 additions and 54 deletions

View File

@ -5016,18 +5016,34 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
"status" => ?status "status" => ?status
); );
// This implies that the terminal block was invalid. We are being explicit in match latest_valid_hash {
// invalidating only the head block in this case. // The `latest_valid_hash` is set to `None` when the EE
if latest_valid_hash == ExecutionBlockHash::zero() { // "cannot determine the ancestor of the invalid
// payload". In such a scenario we should only
// invalidate the head block and nothing else.
None => {
self.process_invalid_execution_payload( self.process_invalid_execution_payload(
&InvalidationOperation::InvalidateOne { &InvalidationOperation::InvalidateOne {
block_root: head_block_root, block_root: head_block_root,
}, },
) )
.await?; .await?;
} else { }
// An all-zeros execution block hash implies that
// the terminal block was invalid. We are being
// explicit in invalidating only the head block in
// this case.
Some(hash) if hash == ExecutionBlockHash::zero() => {
self.process_invalid_execution_payload(
&InvalidationOperation::InvalidateOne {
block_root: head_block_root,
},
)
.await?;
}
// The execution engine has stated that all blocks between the // The execution engine has stated that all blocks between the
// `head_execution_block_hash` and `latest_valid_hash` are invalid. // `head_execution_block_hash` and `latest_valid_hash` are invalid.
Some(latest_valid_hash) => {
self.process_invalid_execution_payload( self.process_invalid_execution_payload(
&InvalidationOperation::InvalidateMany { &InvalidationOperation::InvalidateMany {
head_block_root, head_block_root,
@ -5037,6 +5053,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
) )
.await?; .await?;
} }
}
Err(BeaconChainError::ExecutionForkChoiceUpdateInvalid { status }) Err(BeaconChainError::ExecutionForkChoiceUpdateInvalid { status })
} }

View File

@ -172,14 +172,26 @@ async fn notify_new_payload<'a, T: BeaconChainTypes>(
"method" => "new_payload", "method" => "new_payload",
); );
// latest_valid_hash == 0 implies that this was the terminal block // Only trigger payload invalidation in fork choice if the
// Hence, we don't need to run `BeaconChain::process_invalid_execution_payload`. // `latest_valid_hash` is `Some` and non-zero.
if latest_valid_hash == ExecutionBlockHash::zero() { //
return Err(ExecutionPayloadError::RejectedByExecutionEngine { status }.into()); // A `None` latest valid hash indicates that the EE was unable
} // to determine the most recent valid ancestor. Since `block`
// has not yet been applied to fork choice, there's nothing to
// invalidate.
//
// An all-zeros payload indicates that an EIP-3675 check has
// failed regarding the validity of the terminal block. Rather
// than iterating back in the chain to find the terminal block
// and invalidating that, we simply reject this block without
// invalidating anything else.
if let Some(latest_valid_hash) =
latest_valid_hash.filter(|hash| *hash != ExecutionBlockHash::zero())
{
// This block has not yet been applied to fork choice, so the latest block that was // This block has not yet been applied to fork choice, so the latest block that was
// imported to fork choice was the parent. // imported to fork choice was the parent.
let latest_root = block.parent_root(); let latest_root = block.parent_root();
chain chain
.process_invalid_execution_payload(&InvalidationOperation::InvalidateMany { .process_invalid_execution_payload(&InvalidationOperation::InvalidateMany {
head_block_root: latest_root, head_block_root: latest_root,
@ -187,6 +199,7 @@ async fn notify_new_payload<'a, T: BeaconChainTypes>(
latest_valid_ancestor: latest_valid_hash, latest_valid_ancestor: latest_valid_hash,
}) })
.await?; .await?;
}
Err(ExecutionPayloadError::RejectedByExecutionEngine { status }.into()) Err(ExecutionPayloadError::RejectedByExecutionEngine { status }.into())
} }

View File

@ -10,7 +10,9 @@ use types::ExecutionBlockHash;
pub enum PayloadStatus { pub enum PayloadStatus {
Valid, Valid,
Invalid { Invalid {
latest_valid_hash: ExecutionBlockHash, /// The EE will provide a `None` LVH when it is unable to determine the
/// latest valid ancestor.
latest_valid_hash: Option<ExecutionBlockHash>,
validation_error: Option<String>, validation_error: Option<String>,
}, },
Syncing, Syncing,
@ -55,22 +57,10 @@ pub fn process_payload_status(
}) })
} }
} }
PayloadStatusV1Status::Invalid => { PayloadStatusV1Status::Invalid => Ok(PayloadStatus::Invalid {
if let Some(latest_valid_hash) = response.latest_valid_hash { latest_valid_hash: response.latest_valid_hash,
// The response is only valid if `latest_valid_hash` is not `null`. validation_error: response.validation_error,
Ok(PayloadStatus::Invalid { }),
latest_valid_hash,
validation_error: response.validation_error.clone(),
})
} else {
Err(EngineError::Api {
error: ApiError::BadResponse(
"new_payload: response.status = INVALID but null latest_valid_hash"
.to_string(),
),
})
}
}
PayloadStatusV1Status::InvalidBlockHash => { PayloadStatusV1Status::InvalidBlockHash => {
// In the interests of being liberal with what we accept, only raise a // In the interests of being liberal with what we accept, only raise a
// warning here. // warning here.

View File

@ -7,7 +7,7 @@ use std::{env, fs::File};
use tempfile::TempDir; use tempfile::TempDir;
use unused_port::unused_tcp_port; use unused_port::unused_tcp_port;
// const GETH_BRANCH: &str = "master"; const GETH_BRANCH: &str = "master";
const GETH_REPO_URL: &str = "https://github.com/ethereum/go-ethereum"; const GETH_REPO_URL: &str = "https://github.com/ethereum/go-ethereum";
pub fn build_result(repo_dir: &Path) -> Output { pub fn build_result(repo_dir: &Path) -> Output {
@ -27,9 +27,7 @@ pub fn build(execution_clients_dir: &Path) {
} }
// Get the latest tag on the branch // Get the latest tag on the branch
// TODO: Update when version is corrected let last_release = build_utils::get_latest_release(&repo_dir, GETH_BRANCH).unwrap();
// let last_release = build_utils::get_latest_release(&repo_dir, GETH_BRANCH).unwrap();
let last_release = "v1.11.1";
build_utils::checkout(&repo_dir, dbg!(&last_release)).unwrap(); build_utils::checkout(&repo_dir, dbg!(&last_release)).unwrap();
// Build geth // Build geth

View File

@ -427,7 +427,16 @@ impl<E: GenericExecutionEngine> TestRig<E> {
.notify_new_payload(&invalid_payload) .notify_new_payload(&invalid_payload)
.await .await
.unwrap(); .unwrap();
assert!(matches!(status, PayloadStatus::InvalidBlockHash { .. })); assert!(matches!(
status,
PayloadStatus::InvalidBlockHash { .. }
// Geth is returning `INVALID` with a `null` LVH to indicate it
// does not know the invalid ancestor.
| PayloadStatus::Invalid {
latest_valid_hash: None,
..
}
));
/* /*
* Execution Engine A: * Execution Engine A: