Skip to content

feat: forward and validate beacon block - #618

Merged
KolbyML merged 5 commits into
ReamLabs:masterfrom
varun-doshi:varun/gossip-block
Jul 28, 2025
Merged

feat: forward and validate beacon block#618
KolbyML merged 5 commits into
ReamLabs:masterfrom
varun-doshi:varun/gossip-block

Conversation

@varun-doshi varun-doshi self-assigned this Jun 25, 2025
@varun-doshi
varun-doshi requested a review from unnawut June 25, 2025 10:33
Comment thread crates/common/beacon_chain/src/beacon_chain.rs Outdated
Comment thread crates/common/beacon_chain/src/beacon_chain.rs Outdated
Comment thread crates/networking/manager/src/gossipsub.rs Outdated
Comment thread crates/networking/manager/src/gossipsub.rs Outdated
Comment thread crates/networking/manager/src/service.rs Outdated
@KolbyML

KolbyML commented Jun 25, 2025

Copy link
Copy Markdown
Contributor

I will wait for @unnawut comments to be resolved before reviewing this

@varun-doshi
varun-doshi marked this pull request as ready for review June 25, 2025 19:53
@varun-doshi
varun-doshi requested a review from Kayden-ML as a code owner June 25, 2025 19:53
@varun-doshi
varun-doshi requested review from KolbyML and unnawut June 25, 2025 19:54
Comment on lines +29 to +30
pub cached_proposer_signature: RwLock<HashMap<(PublicKey, u64), BLSSignature>>,
pub cached_bls_to_execution_signature: RwLock<HashMap<(PublicKey, u64), BLSToExecutionChange>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be moved out of the BeaconChain struct? as they aren't used here

@@ -0,0 +1,123 @@
use anyhow::{Ok, anyhow};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
use anyhow::{Ok, anyhow};
use anyhow::{anyhow};

we shouldn't be importing OK

Comment on lines +1 to +13
use anyhow::{Ok, anyhow};
use ream_beacon_chain::beacon_chain::BeaconChain;
use ream_consensus::{
constants::MAX_BLOBS_PER_BLOCK_ELECTRA, electra::beacon_block::SignedBeaconBlock,
misc::compute_start_slot_at_epoch,
};
use ream_storage::tables::{Field, Table};

#[derive(Debug)]
pub enum ValidationResult {
Accept,
Ignore,
Reject,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make gossipsub.rs into a folder then move this file their, but then make a folder called validate, and make a file called beacon_block and put validate_beacon_block there

Comment on lines +22 to +25
let latest_block_in_db = store.db.get_latest_block()?;
let latest_state_in_db = store.db.get_latest_state()?;

if block.message.slot < latest_block_in_db.message.slot {

@KolbyML KolbyML Jun 26, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IGNORE] The block is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- i.e. validate that signed_beacon_block.message.slot <= current_slot (a client MAY queue future blocks for processing at the appropriate slot).

Are you sure that it is the latest slot we have or it it the actual latest slot? because you can do math to calculate what the current_slot is as there is 1 slot every 12 seconds. Because it mentions MAXIMUM_GOSSIP_CLOCK_DISPARITY, I am assuming we are calcualting the actual current slot, but you should double check

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm good question

Here's my thought process on this:
if a node has latest block =10
actual latest block = 15

the node can never now that the actual latest block number(or by extension, slot) is 15...i dont think there's a way to calculate. Of course slot=time / 12. But in the context of the node, the time is that of block=10. i.e. it does not the actual chain has progresses till 15. Please correct me if im wrong or the explanation is not clear.

Now,

  • case 1: node receives block from gossip network for block=11.
    it passes all validation and the node is updated.

  • case 2: node receives block = 13
    currently it will ignore it because we are not queuing it. I think this should be done in a separate issue since there could possibly require discussions on how to implement it and then use the queued block later at the correct slot. Also, wrapping this PR will help others working on other validate and forward gossip issues to make progress.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will create an issue for this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the node can never now that the actual latest block number(or by extension, slot) is 15...i dont think there's a way to calculate.

I think it's this one: https://ethereum.github.io/consensus-specs/specs/phase0/fork-choice/#get_current_slot. So:

  1. I think we can base off the global slot number. Another reason why I think we should base on the global slot number is because if our node is stalling (stuck processing a block), latest_block_in_db.message.slot will get stuck. Then, more and more messages will not be ignored by block.message.slot < latest_block_in_db.message.slot and spamming the gossip network. A block.message.slot < store.get_current_slot() will solve this.

  2. But the function above also doesn't take into account MAXIMUM_GOSSIP_CLOCK_DISPARITY so I agree we can take this MAXIMUM_GOSSIP_CLOCK_DISPARITY part in another PR so please create a new issue!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like i missed this comment...will push a fix

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also on rechecking this comparison was wrong;
it should be

if block.message.slot > current_global_slot {
        return Ok(ValidationResult::Ignore(
            "Block is from a future slot".to_string(),
        ));
    }

Taking the example above:
if our node has latest block =10
actual latest block = 15

Cases:

@KolbyML KolbyML left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to read validate_beacon_block more deeply, but here is a quick review to get started before I go to bed

Comment on lines +29 to +34
let start_slot_at_epoch =
compute_start_slot_at_epoch(store.db.finalized_checkpoint_provider().get()?.epoch);

if block.message.slot < start_slot_at_epoch {
return Ok(ValidationResult::Ignore);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might as well inline this, as the variable isn't reused

Comment on lines +29 to +30
pub cached_proposer_signature: RwLock<HashMap<(PublicKey, u64), BLSSignature>>,
pub cached_bls_to_execution_signature: RwLock<HashMap<(PublicKey, u64), BLSToExecutionChange>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is the logic which clears this cache shouldn't we use an LRU here?

Comment on lines +94 to +126
if let Ok(validation_result) =
validate_beacon_block(beacon_chain, &signed_block).await
{
match validation_result {
ValidationResult::Accept => {
if let Err(err) =
beacon_chain.process_block(*signed_block.clone()).await
{
error!("Failed to process gossipsub beacon block: {err}");
}
p2psender.send_gossip(GossipMessage {
topic: GossipTopic::from_topic_hash(&message.topic)
.expect("invalid topic hash"),
data: signed_block.as_ssz_bytes(),
});
}
ValidationResult::Ignore => warn!("Ignoring gossipsub beacon block"),
ValidationResult::Reject => {
warn!("Rejecting gossipsub beacon block. Peer should be penalized")
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if let Ok(validation_result) =
validate_beacon_block(beacon_chain, &signed_block).await
{
match validation_result {
ValidationResult::Accept => {
if let Err(err) =
beacon_chain.process_block(*signed_block.clone()).await
{
error!("Failed to process gossipsub beacon block: {err}");
}
p2psender.send_gossip(GossipMessage {
topic: GossipTopic::from_topic_hash(&message.topic)
.expect("invalid topic hash"),
data: signed_block.as_ssz_bytes(),
});
}
ValidationResult::Ignore => warn!("Ignoring gossipsub beacon block"),
ValidationResult::Reject => {
warn!("Rejecting gossipsub beacon block. Peer should be penalized")
}
}
let Ok(validation_result) = validate_beacon_block(beacon_chain, &signed_block).await else {
return;
}
match validation_result {
ValidationResult::Accept => {
if let Err(err) =
beacon_chain.process_block(*signed_block.clone()).await
{
error!("Failed to process gossipsub beacon block: {err}");
}
p2psender.send_gossip(GossipMessage {
topic: GossipTopic::from_topic_hash(&message.topic)
.expect("invalid topic hash"),
data: signed_block.as_ssz_bytes(),
});
}
ValidationResult::Ignore => warn!("Ignoring gossipsub beacon block"),
ValidationResult::Reject => {
warn!("Rejecting gossipsub beacon block. Peer should be penalized")
}
}

Comment on lines +9 to +19
#[derive(Debug)]
pub enum ValidationResult {
Accept,
Ignore,
Reject,
}

pub async fn validate_beacon_block(
beacon_chain: &BeaconChain,
block: &SignedBeaconBlock,
) -> anyhow::Result<ValidationResult> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we are defining ValidationResult, shouldn't we have Error as a part of the result, instead of having to unwrap 2 results

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually reutrning a result in validate_beacon_block is helpful since there are many calls to other functions that return a Result.
if validate_beacon_block only returns ValidationResult, this will mean there are many if..let or match operations in this function.

I dont think its an issue if we are checking once for error and then once again

Comment thread crates/storage/src/db.rs Outdated
Comment on lines +199 to +201
}
pub fn get_latest_block(&self) -> anyhow::Result<SignedBeaconBlock> {
let highest_root = self

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a new line here

@KolbyML

KolbyML commented Jun 26, 2025

Copy link
Copy Markdown
Contributor

Could you add a comment with the first sentence of every check being done, just so it is easier to distinguish

@varun-doshi
varun-doshi requested a review from KolbyML June 30, 2025 12:07

@KolbyML KolbyML left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving feedback quick, I need to find some time to more thoroughly review the validate beacon block function

Comment thread crates/networking/manager/src/service.rs Outdated
Comment thread crates/storage/src/cache.rs
Comment thread crates/storage/src/cache.rs Outdated
Comment thread crates/networking/manager/src/gossipsub/handle_gossipsub.rs Outdated
Comment thread crates/networking/manager/src/gossipsub/handle_gossipsub.rs Outdated
@varun-doshi
varun-doshi requested review from KolbyML and unnawut July 4, 2025 12:39

@unnawut unnawut left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some initial comments below.

Unless I'm totally mistaken, I think we really need to be careful getting the validation rules right, both in terms of implementation and testing.

Comment thread crates/networking/manager/src/gossipsub/validate/beacon_block.rs Outdated
Comment thread crates/storage/src/db.rs Outdated
Comment thread crates/networking/manager/src/gossipsub/validate/beacon_block.rs
}

// [REJECT] The block's execution payload timestamp is correct with respect to the slot.
if !block.message.body.execution_payload.timestamp

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the intended way to compare timestamps (u64) using the bitwise negation? For example:

println!("{:?}", !1u64 == 1u64); // false
println!("{:?}", !1u64 == 2u64); // false

Also applies to other similar uses with other return types as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh yes good catch

@KolbyML KolbyML left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is some feedback

Comment thread crates/networking/manager/src/gossipsub/validate/beacon_block.rs Outdated
Comment thread crates/networking/manager/src/gossipsub/validate/beacon_block.rs Outdated
Comment thread crates/networking/manager/src/gossipsub/validate/beacon_block.rs
Comment on lines +67 to +152
// [IGNORE] The block's parent (defined by block.parent_root) has been seen.
if let Some(parent) = store
.db
.beacon_block_provider()
.get(block.message.parent_root)?
{
// [REJECT] The block is from a higher slot than its parent.
if block.message.slot > parent.message.slot + 1 {
return Ok(ValidationResult::Reject(
"Block is from a higher slot than expected".to_string(),
));
}
} else {
return Ok(ValidationResult::Ignore(
"Parent block not found".to_string(),
));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// [IGNORE] The block's parent (defined by block.parent_root) has been seen.
if let Some(parent) = store
.db
.beacon_block_provider()
.get(block.message.parent_root)?
{
// [REJECT] The block is from a higher slot than its parent.
if block.message.slot > parent.message.slot + 1 {
return Ok(ValidationResult::Reject(
"Block is from a higher slot than expected".to_string(),
));
}
} else {
return Ok(ValidationResult::Ignore(
"Parent block not found".to_string(),
));
}
match store
.db
.beacon_block_provider()
.get(block.message.parent_root)? {
Some(parent_block) => {
// [REJECT] The block is from a higher slot than its parent.
if block.message.slot > parent.message.slot + 1 {
return Ok(ValidationResult::Reject(
"Block is from a higher slot than expected".to_string(),
));
}
}
None => {
// [IGNORE] The block's parent (defined by block.parent_root) has been seen.
return Ok(ValidationResult::Ignore(
"Parent block not found".to_string(),
));
}
}

I think a match case would be a lot more readable here, also moving // [IGNORE] The block's parent (defined by block.parent_root) has been seen. down

Comment on lines +84 to +187

let finalized_checkpoint = store.db.finalized_checkpoint_provider().get()?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't we missing the

[REJECT] The block's parent (defined by block.parent_root) passes validation. check? I can't see it

Comment on lines +86 to +88
// [REJECT] The current finalized_checkpoint is an ancestor of block.
if !store.get_checkpoint_block(block.message.parent_root, finalized_checkpoint.epoch)?
== finalized_checkpoint.root

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// [REJECT] The current finalized_checkpoint is an ancestor of block.
if !store.get_checkpoint_block(block.message.parent_root, finalized_checkpoint.epoch)?
== finalized_checkpoint.root
// [REJECT] The current finalized_checkpoint is an ancestor of block.
if store.get_checkpoint_block(block.message.parent_root, finalized_checkpoint.epoch)?
!= finalized_checkpoint.root

shouldn't it be like this?

Comment on lines +95 to +97
// [REJECT] The block is proposed by the expected proposer_index for the block's slot.
if !latest_state_in_db.get_beacon_proposer_index(Some(block.message.slot))?
== block.message.proposer_index

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// [REJECT] The block is proposed by the expected proposer_index for the block's slot.
if !latest_state_in_db.get_beacon_proposer_index(Some(block.message.slot))?
== block.message.proposer_index
// [REJECT] The block is proposed by the expected proposer_index for the block's slot.
if latest_state_in_db.get_beacon_proposer_index(Some(block.message.slot))?
!= block.message.proposer_index

same comment, having ! at the start looks weird without (), but also it isn't needed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this was fixed in latest commit...just saw your review now will implement the suggestions next

"Execution payload timestamp is incorrect".to_string(),
));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

I think we are missing the highlighted cases

Comment on lines +130 to +224
})
{
return Ok(ValidationResult::Ignore(
"Signature already received".to_string(),
));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IGNORE] current_epoch >= CAPELLA_FORK_EPOCH, where current_epoch is defined by the current wall-clock time.
[IGNORE] The signed_bls_to_execution_change is the first valid signed bls to execution change received for the validator with index signed_bls_to_execution_change.message.validator_index.
[REJECT] All of the conditions within process_bls_to_execution_change pass validation.

aren't we missing the last case here?

@varun-doshi

varun-doshi commented Jul 7, 2025

Copy link
Copy Markdown
Contributor Author

Latest changes does the following things:

  • Makes the assertions in process_bls_to_execution_change into a separate function called validate_bls_to_execution_change since we need to use it in gossip block validation and the former function executes a state change(which should not happen in the validation pipeline) Not required after rebase
  • creates a new function validate_parent_beacon_block since we need to check the parent block and to avoid recursively calling the same function till genesis
  • Adds other missing checks

@varun-doshi
varun-doshi force-pushed the varun/gossip-block branch from 459e3eb to f5dcc6a Compare July 7, 2025 08:09
@varun-doshi
varun-doshi requested review from KolbyML and unnawut July 8, 2025 06:54
Comment thread crates/networking/manager/src/gossipsub/validate/beacon_block.rs Outdated
Comment thread crates/networking/manager/src/gossipsub/validate/beacon_block.rs
@varun-doshi
varun-doshi requested a review from unnawut July 8, 2025 10:22
@varun-doshi
varun-doshi force-pushed the varun/gossip-block branch from 2a369c5 to 295b524 Compare July 8, 2025 15:35
Comment thread crates/networking/manager/src/gossipsub/validate/beacon_block.rs Outdated
@varun-doshi
varun-doshi force-pushed the varun/gossip-block branch from 295b524 to 116249e Compare July 9, 2025 09:57
@varun-doshi
varun-doshi force-pushed the varun/gossip-block branch from 116249e to 31cb932 Compare July 9, 2025 14:00
@varun-doshi
varun-doshi requested a review from unnawut July 9, 2025 14:11
@unnawut

unnawut commented Jul 10, 2025

Copy link
Copy Markdown
Contributor

lgtm now. Please recheck formatting & @KolbyML's 👍 before merging

@KolbyML KolbyML left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR is looking better, but a I have a few concerns that should be resolved

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gossipsub/handle_gossipsub.rs

this should just be gossipsub/handle.rs then, having gossipsub written twice is redundent

Comment on lines +111 to +118
ValidationResult::Accept => {
if let Err(err) = beacon_chain.process_block(*signed_block.clone()).await {
error!("Failed to process gossipsub beacon block: {err}");
}
p2psender.send_gossip(GossipMessage {
topic: GossipTopic::from_topic_hash(&message.topic)
.expect("invalid topic hash"),
data: signed_block.as_ssz_bytes(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ValidationResult::Accept => {
if let Err(err) = beacon_chain.process_block(*signed_block.clone()).await {
error!("Failed to process gossipsub beacon block: {err}");
}
p2psender.send_gossip(GossipMessage {
topic: GossipTopic::from_topic_hash(&message.topic)
.expect("invalid topic hash"),
data: signed_block.as_ssz_bytes(),
ValidationResult::Accept => {
let signed_block_bytes = signed_block.as_ssz_bytes();
if let Err(err) = beacon_chain.process_block(signed_block).await {
error!("Failed to process gossipsub beacon block: {err}");
}
p2psender.send_gossip(GossipMessage {
topic: GossipTopic::from_topic_hash(&message.topic)
.expect("invalid topic hash"),
data: signed_block_bytes,

we can avoid the clone right?

Comment on lines +26 to +67
let Some(parent_block) = store
.db
.beacon_block_provider()
.get(block.message.parent_root)?
else {
return Err(anyhow!("failed to get parent block"));
};

let Some(parent_state) = store
.db
.beacon_state_provider()
.get(block.message.parent_root)?
else {
return Err(anyhow!("failed to get parent state"));
};

// Validate incoming block
match validate_beacon_block(beacon_chain, cached_db, block, &latest_state_in_db, false).await? {
ValidationResult::Accept => {}
ValidationResult::Ignore(reason) => {
return Ok(ValidationResult::Ignore(reason));
}
ValidationResult::Reject(reason) => {
return Ok(ValidationResult::Reject(reason));
}
}
// Validate parent block [block.message.parent_root]
match validate_beacon_block(beacon_chain, cached_db, &parent_block, &parent_state, true).await?
{
ValidationResult::Accept => {}
ValidationResult::Ignore(reason) => {
return Ok(ValidationResult::Ignore(reason));
}
ValidationResult::Reject(reason) => {
return Ok(ValidationResult::Reject(reason));
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let Some(parent_block) = store
.db
.beacon_block_provider()
.get(block.message.parent_root)?
else {
return Err(anyhow!("failed to get parent block"));
};
let Some(parent_state) = store
.db
.beacon_state_provider()
.get(block.message.parent_root)?
else {
return Err(anyhow!("failed to get parent state"));
};
// Validate incoming block
match validate_beacon_block(beacon_chain, cached_db, block, &latest_state_in_db, false).await? {
ValidationResult::Accept => {}
ValidationResult::Ignore(reason) => {
return Ok(ValidationResult::Ignore(reason));
}
ValidationResult::Reject(reason) => {
return Ok(ValidationResult::Reject(reason));
}
}
// Validate parent block [block.message.parent_root]
match validate_beacon_block(beacon_chain, cached_db, &parent_block, &parent_state, true).await?
{
ValidationResult::Accept => {}
ValidationResult::Ignore(reason) => {
return Ok(ValidationResult::Ignore(reason));
}
ValidationResult::Reject(reason) => {
return Ok(ValidationResult::Reject(reason));
}
};
// Validate incoming block
match validate_beacon_block(beacon_chain, cached_db, block, &latest_state_in_db, false).await? {
ValidationResult::Accept => {}
ValidationResult::Ignore(reason) => {
return Ok(ValidationResult::Ignore(reason));
}
ValidationResult::Reject(reason) => {
return Ok(ValidationResult::Reject(reason));
}
}
let Some(parent_block) = store
.db
.beacon_block_provider()
.get(block.message.parent_root)?
else {
bail!("failed to get parent block"));
};
let Some(parent_state) = store
.db
.beacon_state_provider()
.get(block.message.parent_root)?
else {
bail!("failed to get parent state"));
};
// Validate parent block [block.message.parent_root]
match validate_beacon_block(beacon_chain, cached_db, &parent_block, &parent_state, true).await?
{
ValidationResult::Accept => {}
ValidationResult::Ignore(reason) => {
return Ok(ValidationResult::Ignore(reason));
}
ValidationResult::Reject(reason) => {
return Ok(ValidationResult::Reject(reason));
}
};

we should probably move those 2 getter calls down as it isn't needed above

Comment on lines +81 to +86
let signed_proposer_bls_execution_change = block
.message
.body
.bls_to_execution_changes
.get(block.message.proposer_index as usize)
.ok_or(anyhow!("Invalid index for signed bls to execution change"))?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should choose one style and be consistent either do let some or ok_or in this function, but not both

Comment on lines +102 to +107
pub async fn validate_beacon_block(
beacon_chain: &BeaconChain,
cached_db: &CachedDB,
block: &SignedBeaconBlock,
state: &BeaconState,
is_parent: bool,
) -> anyhow::Result<ValidationResult> {
let store = beacon_chain.store.lock().await;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code will not work, as it will deadlock no?

We call let store = beacon_chain.store.lock().await; in the function above. Have you tested if this code works?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right about the deadlock here...will be fixed in latest

As discussed with O, testing this will be top priority after this PR. Issue here: #635

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't be submitting broken code, you need to make sure it works. If that expectation can't be met maybe tests should be done in this PR.

Even if tests aren't added in a PR you should test if the code works.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course edge cases can be missed and that is fine, it is just the deadlock problem could be avoided fairly easily, also deadlocks could cost the team a week or two, depending on when we find them.

Comment on lines +111 to +114
let current_global_slot = store.get_current_slot()?;

// [IGNORE] The block is not from a future slot.
if block.message.slot > current_global_slot {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let current_global_slot = store.get_current_slot()?;
// [IGNORE] The block is not from a future slot.
if block.message.slot > current_global_slot {
// [IGNORE] The block is not from a future slot.
if block.message.slot > store.get_current_slot()? {

we might as well inline this, as we don't reuse the variable

@varun-doshi
varun-doshi requested a review from KolbyML July 11, 2025 06:30

@KolbyML KolbyML left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@varun-doshi could you make a PR with just the scaffolding everything but, validate_gossip_beacon_block and validate_beacon_block. Then rebase this PR ontop of the scaffolding PR. We should be able to merge the scaffolding in quick, then would you be able to add some tests for the validation logic being added in the PR which adds validate_gossip_beacon_block and validate_beacon_block. Just because a few tests can save us time in the future, also I think they are high value in this case, as this code path is fairly critical and a hot path.

The tests should create dummy states/blocks, mainnet blocks would be to big of course.

Comment on lines +22 to +27
let latest_state_in_db = {
let store = beacon_chain.store.lock().await;

store.db.get_latest_state()?
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let latest_state_in_db = {
let store = beacon_chain.store.lock().await;
store.db.get_latest_state()?
};
let latest_state = beacon_chain.store.lock().await.db.get_latest_state()?;

this can be inlined

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feedback wasn't addressed

@varun-doshi
varun-doshi requested a review from syjn99 as a code owner July 17, 2025 19:51
@varun-doshi

Copy link
Copy Markdown
Contributor Author

Tests added for block validation.
To run:

cargo test --package beacon-api-tests --test validate_block -- tests::test_validate_beacon_block --exact --show-output

There's 1 catch here regarding the test though. Specifically the following validation rule:
[REJECT] The current finalized_checkpoint is an ancestor of block -- i.e. get_checkpoint_block(store, block.parent_root, store.finalized_checkpoint.epoch) == store.finalized_checkpoint.root

The issue is this goes back recursively backwords to the last finalized checkpoint so which means we need atleast the last 64 blocks (which means we need to save 64 blocks as json). Now I've tested it going backwards 3 blocks and it words as expected.
This is why you will see this specific validation commented out.
Even on general case, this validation should not always run. I think it should only run when we know there is sufficient backfill. Which would mean there needs to be a flag indicating backfill status and execute this validation only then.

Thoughts @unnawut @KolbyML

@KolbyML

KolbyML commented Jul 17, 2025

Copy link
Copy Markdown
Contributor
image

Going forward we shouldn't include test data in json files like this, they should be encoded into ssz_snappy

Like was done in this PR https://github.com/ReamLabs/ream/pull/609/files and in ef-tests

How big in terms of MB is the testdata?

@KolbyML

KolbyML commented Jul 17, 2025

Copy link
Copy Markdown
Contributor

Tests added for block validation. To run:

cargo test --package beacon-api-tests --test validate_block -- tests::test_validate_beacon_block --exact --show-output

There's 1 catch here regarding the test though. Specifically the following validation rule: [REJECT] The current finalized_checkpoint is an ancestor of block -- i.e. get_checkpoint_block(store, block.parent_root, store.finalized_checkpoint.epoch) == store.finalized_checkpoint.root

we shouldn't be uncommenting validation cases, maybe mark this section with a cfg to not compile it in test code

The issue is this goes back recursively backwords to the last finalized checkpoint so which means we need atleast the last 64 blocks (which means we need to save 64 blocks as json). Now I've tested it going backwards 3 blocks and it words as expected. This is why you will see this specific validation commented out. Even on general case, this validation should not always run. I think it should only run when we know there is sufficient backfill. Which would mean there needs to be a flag indicating backfill status and execute this validation only then.

Thoughts @unnawut @KolbyML

I need to read more into the case before I can fully comment on this. I left some early feedback above

@varun-doshi
varun-doshi requested a review from KolbyML July 19, 2025 04:38
@KolbyML

KolbyML commented Jul 23, 2025

Copy link
Copy Markdown
Contributor

@varun-doshi can you rebase?

@KolbyML KolbyML left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the PR is looking very good now, just a few little issues

Comment on lines +22 to +27
let latest_state_in_db = {
let store = beacon_chain.store.lock().await;

store.db.get_latest_state()?
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feedback wasn't addressed

Comment on lines +131 to +136
let validator =
if let Some(validator) = state.validators.get(block.message.proposer_index as usize) {
validator
} else {
return Ok(ValidationResult::Reject("Validator not found".to_string()));
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let validator =
if let Some(validator) = state.validators.get(block.message.proposer_index as usize) {
validator
} else {
return Ok(ValidationResult::Reject("Validator not found".to_string()));
};
let Some(validator) = state.validators.get(block.message.proposer_index as usize) else {
return Ok(ValidationResult::Reject("Validator not found".to_string()));
};

^ we can simplify the code to this

Comment on lines +73 to +74
let validator = if let Some(validator) = latest_state_in_db
.validators
.get(block.message.proposer_index as usize)
{
validator
} else {
return Ok(ValidationResult::Reject("Validator not found".to_string()));
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here as well

Comment on lines +168 to +169
match store
.db
.beacon_block_provider()
.get(block.message.parent_root)?
{
Some(parent_block) => {
// [REJECT] The block is from a higher slot than its parent.
if block.message.slot > parent_block.message.slot + 1 {
return Ok(ValidationResult::Reject(
"Block is from a higher slot than expected".to_string(),
));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
match store
.db
.beacon_block_provider()
.get(block.message.parent_root)?
{
Some(parent_block) => {
// [REJECT] The block is from a higher slot than its parent.
if block.message.slot > parent_block.message.slot + 1 {
return Ok(ValidationResult::Reject(
"Block is from a higher slot than expected".to_string(),
));
match store
.db
.beacon_block_provider()
.get(block.message.parent_root)?
{
Some(parent_block) => {
// [REJECT] The block is from a higher slot than its parent.
if block.message.slot <= parent_block.message.slot {
return Ok(ValidationResult::Reject(
"Block is from a higher slot than expected".to_string(),
));

^ shouldn't the condition be this?

Comment on lines +189 to +191
#[cfg(feature = "allow_ancestor_validation")]
{
let finalized_checkpoint = store.db.finalized_checkpoint_provider().get()?;
// [REJECT] The current finalized_checkpoint is an ancestor of block.
if store.get_checkpoint_block(block.message.parent_root, finalized_checkpoint.epoch)?
!= finalized_checkpoint.root
{
return Ok(ValidationResult::Reject(
"Finalized checkpoint is not an ancestor".to_string(),
));
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you negate this so it is on by default, then disable it just for the test

@varun-doshi
varun-doshi requested a review from KolbyML July 28, 2025 11:52

@KolbyML KolbyML left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit: looks good. I will merge the PR now as I want it in 😤

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the PR is pretty much ready to merge, could you move these tests to the crate they are for, as this test isn't related to the beacon-api

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or make a new testing crate

@KolbyML
KolbyML added this pull request to the merge queue Jul 28, 2025
Merged via the queue into ReamLabs:master with commit b69bcf9 Jul 28, 2025
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gossipsub validate and forward: beacon_block

4 participants