- 
                Notifications
    
You must be signed in to change notification settings  - Fork 25
 
fix(validation): Check the signatures of the justifications' justifications #604
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice!
| verify_message_signatures(signed_message, operator_pub_keys) | ||
| verify_message_signatures(signed_message, operator_pub_keys)?; | ||
| // Also check the justifications' justifications | ||
| validate_justifications( | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love recursion in the wild
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is validate_justifications validating only signatures?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prepare messages never carry justifications. So recursing “inside a Prepare” is a dead end by spec design
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it will only recurse if it is non-empty, and since prepare messages never carry justifications it will not recurse
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imo this isn't a good idea
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example, if someone adds, for some reason, invalid justifications inside a prepare message, our code will reject it, but it's "valid". If we want to be very strict, imo it'd be better to assert that a prepare message doesn't have any justification instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about something like
pub(crate) fn validate_justifications(
    consensus_message: &QbftMessage,
    operator_pub_keys: &HashMap<OperatorId, Rsa<Public>>,
) -> Result<(), ValidationFailure> {
    // ----- Presence rules (where each justification set is allowed) -----
    // Prepare justifications MUST appear only on Proposals.
    if !consensus_message.prepare_justification.is_empty()
        && consensus_message.qbft_message_type != QbftMessageType::Proposal
    {
        return Err(ValidationFailure::UnexpectedPrepareJustifications);
    }
    // "round_change_justification" is:
    // - on a Proposal: the RC set (each item must be a RoundChange)
    // - on a RoundChange: the prepares for the last prepared (each item must be a Prepare)
    // - otherwise: must be empty
    if !consensus_message.round_change_justification.is_empty()
        && consensus_message.qbft_message_type != QbftMessageType::Proposal
        && consensus_message.qbft_message_type != QbftMessageType::RoundChange
    {
        return Err(ValidationFailure::UnexpectedRoundChangeJustifications);
    }
    // ----- Type + signature checks for each set -----
    match consensus_message.qbft_message_type {
        QbftMessageType::Proposal => {
            // On Proposal:
            //   - round_change_justification := set of RoundChange messages
            //   - prepare_justification      := set of Prepare messages (the single prepared cert)
            for signed in &consensus_message.round_change_justification {
                let inner = decode_inner_qbft(signed)?;
                if inner.qbft_message_type != QbftMessageType::RoundChange {
                    return Err(ValidationFailure::JustificationWrongType {
                        expected: QbftMessageType::RoundChange,
                        got: inner.qbft_message_type,
                    });
                }
                verify_message_signatures(signed, operator_pub_keys)?;
                // IMPORTANT: do NOT recurse into inner.justifications here.
                // The spec does not require per-RC nested-prepare validation on proposal validation.
            }
            for signed in &consensus_message.prepare_justification {
                let inner = decode_inner_qbft(signed)?;
                if inner.qbft_message_type != QbftMessageType::Prepare {
                    return Err(ValidationFailure::JustificationWrongType {
                        expected: QbftMessageType::Prepare,
                        got: inner.qbft_message_type,
                    });
                }
                verify_message_signatures(signed, operator_pub_keys)?;
                // No recursion; prepares never carry justifications per spec model.
            }
        }
        QbftMessageType::RoundChange => {
            // On RoundChange:
            //   - prepare_justification MUST be empty (already enforced above)
            //   - round_change_justification := set of Prepare messages proving "prepared" (if prepared)
            for signed in &consensus_message.round_change_justification {
                let inner = decode_inner_qbft(signed)?;
                if inner.qbft_message_type != QbftMessageType::Prepare {
                    return Err(ValidationFailure::JustificationWrongType {
                        expected: QbftMessageType::Prepare,
                        got: inner.qbft_message_type,
                    });
                }
                verify_message_signatures(signed, operator_pub_keys)?;
            }
        }
        // For Prepare / Commit / others, both sets must be empty (already enforced above).
        _ => { /* nothing else to validate */ }
    }
    Ok(())
}
/// Decode the embedded QbftMessage out of a SignedMessage’s SSZ bytes.
fn decode_inner_qbft(signed: &SignedMessage) -> Result<QbftMessage, ValidationFailure> {
    QbftMessage::from_ssz_bytes(signed.ssv_message().data())
        .map_err(|_| ValidationFailure::MalformedJustifications)
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are confident on not checking the inner justifications I think that looks nice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a proposal, we don't need to check the prepares inside the round change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But for a standalone round change, we actually need to
| 
           is this supported by the spec?  | 
    
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements recursive validation of justifications within consensus messages by checking the signatures of nested justifications. The change ensures that when messages contain justifications (such as in round change scenarios), those justifications' own justifications are also validated recursively.
- Consolidates validation error types by removing duplicate error variants
 - Adds recursive validation of nested justifications in consensus messages
 - Updates error handling to use the consolidated 
MalformedJustificationserror type 
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description | 
|---|---|
| anchor/message_validator/src/lib.rs | Consolidates error types by removing duplicate justification validation errors | 
| anchor/message_validator/src/consensus_message.rs | Implements recursive validation of justifications and updates error handling | 
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| validate_justifications( | ||
| &QbftMessage::from_ssz_bytes(signed_message.ssv_message().data()) | ||
| .map_err(|_| ValidationFailure::MalformedJustifications)?, | ||
| operator_pub_keys, | ||
| ) | 
    
      
    
      Copilot
AI
    
    
    
      Sep 12, 2025 
    
  
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This recursive call creates potential for infinite recursion if justifications contain circular references. Consider adding a recursion depth limit or cycle detection to prevent stack overflow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Im not sure this is possible. We cap the data lengths and I think circular references are hard to create in rust without some fancy RefCell stuff?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's confirm first this is supported by the spec? Is go doing it?
| 
           A message with an invalid signature is not something we want to accept  | 
    
| 
           I'm good for this to go in. Tag it to merge when you're ready @dknopik  | 
    
…ations (sigp#604) Messages used in round change justifications can have justifications themselves. Check those. Decode the justifications and call `validate_justifications` on them. Co-Authored-By: Daniel Knopik <[email protected]>
…ations (sigp#604) Messages used in round change justifications can have justifications themselves. Check those. Decode the justifications and call `validate_justifications` on them. Co-Authored-By: Daniel Knopik <[email protected]>
Issue Addressed
Messages used in round change justifications can have justifications themselves. Check those.
Proposed Changes
Decode the justifications and call
validate_justificationson them.