Skip to content

fix: verify block signature against parent's validator key#3030

Open
partylikeits1983 wants to merge 1 commit into
nextfrom
ajl-rotate-validator-key
Open

fix: verify block signature against parent's validator key#3030
partylikeits1983 wants to merge 1 commit into
nextfrom
ajl-rotate-validator-key

Conversation

@partylikeits1983

@partylikeits1983 partylikeits1983 commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Summary

A block's validator signature was verified against the validator_key stored in that same block. That is self-certifying: anyone can put their own key in a block and sign with it, so it provides no chain of trust and makes validator key rotation impossible.

This verifies a block's signature against the validator_key committed to by its parent instead. The field now identifies the key authorized to sign the next block, so the current validator commits to the next key in the block it signs and trust chains forward from one key to the next. The signature is checked in Rust by whatever ingests a block (the node's block-ingestion path; in this repo, the mock chain's apply_block), not inside the proof.

Genesis (block 0) has no parent, so its signature is not verified; it is trusted as the starting point and only declares who signs block 1.

Closes #3026.

flowchart LR
    G["Genesis (block 0)<br/>validator_key = pk1"]
    B1["Block 1<br/>signed by sk1<br/>validator_key = pk2"]
    B2["Block 2<br/>signed by sk2<br/>validator_key = pk3"]

    G -- "pk1 verifies block 1's signature" --> B1
    B1 -- "pk2 verifies block 2's signature" --> B2
Loading

Each block's validator_key (pkN+1) is the key that authenticates the next block's signature, and each block is signed by the key its parent committed. To rotate, a block commits a fresh pk; whoever holds the matching sk must sign the following block.

Changes

  • Signature verification moves out of SignedBlock/ProvenBlock new()/validate() and into validate_parent(), which holds the trusted parent header and checks the signature against the parent's validator_key.
  • ProposedBlock gains a next_validator_key input (with_next_validator_key), defaulting to copy-forward (no rotation).
  • MockChain gains validator-key rotation support and now verifies every block against its parent in apply_block.

Breaking changes

  • SignedBlock::new / ProvenBlock::new / ProvenBlock::validate no longer verify the signature; authentication moved to validate_parent.
  • BlockHeader::validator_key now denotes the signer of the next block.
  • ProposedBlock serialization gained a trailing next_validator_key field; older encodings no longer deserialize.

@partylikeits1983 partylikeits1983 changed the title fix: verify block signature against parent's validator key to enable … fix: verify block signature against parent's validator key Jun 2, 2026
@partylikeits1983 partylikeits1983 self-assigned this Jun 2, 2026
A block's validator signature was verified against the validator_key stored in
that same block, which is self-certifying and makes validator key rotation
impossible. Verify a block's signature against the validator_key committed to by
its parent instead: the validator_key field now identifies the key authorized to
sign the next block, so trust chains forward from one key to the next.

- Move signature verification out of SignedBlock/ProvenBlock new()/validate() and
  into validate_parent(), which has the trusted parent header. Genesis is the
  trust root and is never parent-verified. The shared parent-validation logic
  (block-number, prev-commitment and signature checks) lives in one place in
  block::validation; both block types delegate to it and map its error via From.
- Add a next_validator_key input to ProposedBlock (with_next_validator_key),
  defaulting to copy-forward, plus mock-chain rotation support and automatic
  parent verification in MockChain::apply_block.

Closes #3026
@partylikeits1983 partylikeits1983 force-pushed the ajl-rotate-validator-key branch from 1cb439f to fcc22e6 Compare June 10, 2026 02:15
@partylikeits1983 partylikeits1983 marked this pull request as ready for review June 10, 2026 02:25

@bobbinth bobbinth 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.

Looks good! Thank you! I left a few comments inline - but nothing major.

Comment thread CHANGELOG.md

- [BREAKING] Renamed `AccountStorageDelta` to `AccountStoragePatch` ([#3002](https://github.com/0xMiden/protocol/pull/3002)).
- [BREAKING] Replaced the per-tree account and nullifier backend traits with shared `SmtBackend` and `SmtBackendReader` traits, split into read-only and read-write capabilities, enabling read-only `LargeSmt`-backed tree views via `reader()` ([#2755](https://github.com/0xMiden/protocol/pull/2755), [#3009](https://github.com/0xMiden/protocol/pull/3009)).
- [BREAKING] Extracted `NullifierTreeBackendReader` and `AccountTreeBackendReader` traits from existing `NullifierTreeBackend` and `AccountTreeBackend` traits ([#2755](https://github.com/0xMiden/protocol/pull/2755)).

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 entry doesn't seem to be relevant to this PR.

Comment on lines +308 to +310
pub fn next_validator_key(&self) -> &PublicKey {
&self.next_validator_key
}

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.

nit: I'd move this after the timestamp() method (i.e., after line 358).

Comment on lines +165 to +168
/// Validates that the provided parent block precedes and authorizes this block: the parent's
/// number is this block's number minus one, the parent's commitment matches this block's
/// `prev_block_commitment`, and this block's signature verifies against the parent's
/// `validator_key` (the key authorized to sign this block).

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.

nit: I'd probably make this less verbose as most of this info is already in the "Errors" section.

Comment on lines +205 to +208
/// Validates that the provided parent block precedes and authorizes this block: the parent's
/// number is this block's number minus one, the parent's commitment matches this block's
/// `prev_block_commitment`, and this block's signature verifies against the parent's
/// `validator_key` (the key authorized to sign this block).

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.

Same nit as above.

Comment on lines +45 to +49
pub(crate) fn validate_against_parent(
header: &BlockHeader,
signature: &Signature,
parent_block: &BlockHeader,
) -> Result<(), ParentValidationError> {

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 would potentially make this a method on the BlockHeader - i.e.:

impl BlockHeader {

    pub fn validate_parent(
        &self,
        parent: &BlockHeader,
        signature: &Signature,
    ) -> Result<(), BlockHeaderError> {
        ...
    }

}

Comment on lines +178 to 184
/// Returns the public key of the validator authorized to sign the *next* block.
///
/// A block's signature is verified against the `validator_key` committed to by its parent
/// block, not against this field. See the [`BlockHeader`] docs for details.
pub fn validator_key(&self) -> &PublicKey {
&self.validator_key
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

maybe we should call it next_validator_key then?

Comment on lines +33 to +37
/// - `validator_key` is the public key of the validator authorized to sign the *next* block. A
/// block's own signature is verified against the `validator_key` committed to by its parent
/// block, so the current validator commits to the next key by including it in the block it signs
/// and trust chains forward from one key to the next. For the genesis block this field declares
/// the signer of block 1 and is trusted as the root of the chain.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/// - `validator_key` is the public key of the validator authorized to sign the *next* block. A
/// block's own signature is verified against the `validator_key` committed to by its parent
/// block, so the current validator commits to the next key by including it in the block it signs
/// and trust chains forward from one key to the next. For the genesis block this field declares
/// the signer of block 1 and is trusted as the root of the chain.
/// - `validator_key` is the public key of the validator authorized to sign the *next* block.

I think the proposed shortened section is sufficient and self-explanatory

Comment on lines +249 to +250
// By default, the next block keeps the previous block's validator key (no rotation). Use
// `with_next_validator_key` to commit to a different key.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

As described in another PR's review comment, it's enough to just state this once in the method documentation.

Again, a reminder that this should be added as a skill / similar

Comment on lines +492 to +494
/// The returned block header contains the proposed block's `next_validator_key` as its
/// validator public key, which defaults to the previous block's validator key unless a rotation
/// was requested via [`ProposedBlock::with_next_validator_key`].

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/// The returned block header contains the proposed block's `next_validator_key` as its
/// validator public key, which defaults to the previous block's validator key unless a rotation
/// was requested via [`ProposedBlock::with_next_validator_key`].

I don't think this adds any value, taking the next_validator_key from ProposedBlock and into the header is as mechanical as it gets and doesn't need explaining:

let next_validator_key = self.next_validator_key.clone();

Comment on lines +143 to +145
/// Validates that the components of the proven block correspond by checking the transaction
/// commitment and note root. Like [`Self::new`], this does NOT verify the validator signature;
/// call [`Self::validate_parent`] to authenticate the block.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would want to double check this new behavior is as intended.

validate should, well, validate, everything that we can. So I'd maybe pass in another argument parent_block_header or something to allow full verification, if possible?

Otherwise, the type system no longer enforces signature verification, and relies on downstream consumer (node) to perform the verification call.

WDYT?

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.

Bug: unable to rotate validator keys

3 participants