Skip to content

Conversation

@dknopik
Copy link
Member

@dknopik dknopik commented Sep 12, 2025

Issue Addressed

Messages used in round change justifications can have justifications themselves. Check those.

Proposed Changes

Decode the justifications and call validate_justifications on them.

@dknopik dknopik added ready-for-review This PR is ready to be reviewed v1.0.0 First Mainnet-release labels Sep 12, 2025
Zacholme7
Zacholme7 previously approved these changes Sep 12, 2025
Copy link
Member

@Zacholme7 Zacholme7 left a 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(
Copy link
Member

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

Copy link
Member

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?

Copy link
Member

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

Copy link
Member

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

Copy link
Member

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

Copy link
Member

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.

Copy link
Member

@diegomrsantos diegomrsantos Sep 13, 2025

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)
}

Copy link
Member

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

Copy link
Member

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

Copy link
Member

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

@diegomrsantos
Copy link
Member

is this supported by the spec?

Copy link
Contributor

Copilot AI left a 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 MalformedJustifications error 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.

Comment on lines 177 to 181
validate_justifications(
&QbftMessage::from_ssz_bytes(signed_message.ssv_message().data())
.map_err(|_| ValidationFailure::MalformedJustifications)?,
operator_pub_keys,
)
Copy link

Copilot AI Sep 12, 2025

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.

Copilot uses AI. Check for mistakes.
Copy link
Member

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?

Copy link
Member

@diegomrsantos diegomrsantos left a 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?

@diegomrsantos
Copy link
Member

@Zacholme7
Copy link
Member

A message with an invalid signature is not something we want to accept

jking-aus
jking-aus previously approved these changes Sep 17, 2025
@jking-aus
Copy link
Member

I'm good for this to go in. Tag it to merge when you're ready @dknopik

@dknopik dknopik dismissed stale reviews from jking-aus and Zacholme7 via 1e88466 September 17, 2025 15:47
diegomrsantos

This comment was marked as outdated.

@diegomrsantos diegomrsantos dismissed their stale review September 17, 2025 15:53

Removing the request

@dknopik dknopik added ready-for-merge and removed ready-for-review This PR is ready to be reviewed waiting-on-author labels Sep 17, 2025
mergify bot added a commit that referenced this pull request Sep 18, 2025
@mergify mergify bot merged commit 3d26b23 into sigp:release-v0.3.0 Sep 18, 2025
17 checks passed
jking-aus pushed a commit to jking-aus/anchor that referenced this pull request Oct 8, 2025
…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]>
petarjuki7 pushed a commit to petarjuki7/anchor that referenced this pull request Oct 16, 2025
…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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-merge v1.0.0 First Mainnet-release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants