fix: verify block signature against parent's validator key#3030
fix: verify block signature against parent's validator key#3030partylikeits1983 wants to merge 1 commit into
Conversation
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
1cb439f to
fcc22e6
Compare
bobbinth
left a comment
There was a problem hiding this comment.
Looks good! Thank you! I left a few comments inline - but nothing major.
|
|
||
| - [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)). |
There was a problem hiding this comment.
This entry doesn't seem to be relevant to this PR.
| pub fn next_validator_key(&self) -> &PublicKey { | ||
| &self.next_validator_key | ||
| } |
There was a problem hiding this comment.
nit: I'd move this after the timestamp() method (i.e., after line 358).
| /// 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). |
There was a problem hiding this comment.
nit: I'd probably make this less verbose as most of this info is already in the "Errors" section.
| /// 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). |
| pub(crate) fn validate_against_parent( | ||
| header: &BlockHeader, | ||
| signature: &Signature, | ||
| parent_block: &BlockHeader, | ||
| ) -> Result<(), ParentValidationError> { |
There was a problem hiding this comment.
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> {
...
}
}| /// 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 | ||
| } |
There was a problem hiding this comment.
maybe we should call it next_validator_key then?
| /// - `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. |
There was a problem hiding this comment.
| /// - `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
| // 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. |
There was a problem hiding this comment.
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
| /// 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`]. |
There was a problem hiding this comment.
| /// 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();| /// 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. |
There was a problem hiding this comment.
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?
Summary
A block's validator signature was verified against the
validator_keystored 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_keycommitted 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'sapply_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" --> B2Each 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 freshpk; whoever holds the matchingskmust sign the following block.Changes
SignedBlock/ProvenBlocknew()/validate()and intovalidate_parent(), which holds the trusted parent header and checks the signature against the parent'svalidator_key.ProposedBlockgains anext_validator_keyinput (with_next_validator_key), defaulting to copy-forward (no rotation).MockChaingains validator-key rotation support and now verifies every block against its parent inapply_block.Breaking changes
SignedBlock::new/ProvenBlock::new/ProvenBlock::validateno longer verify the signature; authentication moved tovalidate_parent.BlockHeader::validator_keynow denotes the signer of the next block.ProposedBlockserialization gained a trailingnext_validator_keyfield; older encodings no longer deserialize.