Skip to content

feat: Multiple Validators#2323

Open
sergerad wants to merge 15 commits into
nextfrom
sergerad-multi-validator
Open

feat: Multiple Validators#2323
sergerad wants to merge 15 commits into
nextfrom
sergerad-multi-validator

Conversation

@sergerad

@sergerad sergerad commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Multi-validator signature integration:

    • Proto: SignBlockResponse gains public_key; rpc.proto sync target is repeated.
    • Store genesis: GenesisState.validator_keys: ValidatorKeys; DB signature column stores full BlockSignatures.
    • Validator: fixed a bug — membership checks now test the parent's validator set, not the proposed header's.
    • Block producer: validator_urls: Vec<Url>, concurrent fan-out to all validators, positional signature aggregation.
    • RPC: tx/batch submission fans out to every validator.
  • Single-signature genesis bootstrap: the genesis header commits the full validator set, but the block itself carries exactly one signature from the bootstrapping validator (verified against the committed set). Because a block's signatures are verified against its parent's committed set, the full set is automatically required to sign from block 1 onwards. The verification rule lives in miden_node_utils::genesis::verify_genesis_signatures, shared by the store and the ntx-builder (which previously had its own copy of the all-must-sign check).

    • The validator set is part of the genesis configuration: a top-level validators list of hex-encoded public keys in genesis.toml. When omitted, the set defaults to the bootstrapping validator's key alone; when specified, it must include the bootstrapping validator's public key (rejected with a dedicated error otherwise, since such a genesis could never verify).
    • miden-validator bootstrap takes a single --key.hex/--key.kms-id for the bootstrapping validator's own key; no other key material is passed on the command line.
    • New miden-validator pubkey subcommand prints a key's public key (local or KMS) in the hex encoding the validators config list expects, so operators can hand their pubkey to the bootstrapper without sharing secrets.
  • Added miden-validator bootstrap --file <genesis.dat>: seeds a validator's local DB/block store from an already-signed genesis block without re-signing.

  • Updated scripts/run-node.sh to run two validators end to end: it derives both validators' public keys via miden-validator pubkey, writes them as the validators list of a generated genesis config, and validator 1 bootstraps and signs genesis with only its own key. Validator 2's directory is seeded via --file from that genesis output. Neither validator ever holds the other's secret key.

  • Updated operator docs (bootstrap-and-genesis.md, sequencer.md) to reflect the multi-validator flow: each non-bootstrapping operator sends their public key to the bootstrapping operator, who lists it in the genesis config's validators and signs genesis with their own key alone; every other validator seeds via --file.

Changelog

[[entry]]
scope       = "node"
impact      = "breaking"
description = "Added multiple Validator genesis bootstrap and tx/block validation"

@sergerad sergerad changed the title Initial refactor feat: Multiple Validators Jul 8, 2026
Base automatically changed from jmunoz-update-miden-vm to next July 9, 2026 13:59
@sergerad
sergerad force-pushed the sergerad-multi-validator branch from 7e6e725 to 539d422 Compare July 14, 2026 21:38
@sergerad
sergerad requested review from Mirko-von-Leipzig and kkovaacs and removed request for Mirko-von-Leipzig July 16, 2026 01:34
@sergerad
sergerad marked this pull request as ready for review July 16, 2026 01:34
Comment thread bin/validator/src/commands/bootstrap.rs
@sergerad
sergerad force-pushed the sergerad-multi-validator branch from 0385cfa to 8eebf8a Compare July 17, 2026 01:43

@Mirko-von-Leipzig Mirko-von-Leipzig left a comment

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.

Code lgtm. Just the open question about expected boostrap process.

Comment thread crates/rpc/src/server/mod.rs Outdated
source_rpc: Box<SourceRpcClient>,
readiness_threshold: u32,
validator: Option<Box<ValidatorClient>>,
validators: Option<Vec<ValidatorClient>>,

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 wonder if we shouldn't use Vec<T> directly, dropping the Option.

i.e. the model falls apart a bit since we should also guard against an empty vector?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I replaced the two optional clients with a single optional struct containing both. We decided not to do this in the past because of naming / scope of the struct - in future it might be more than just about pre auth. But I think we should keep it as a pre auth struct now, because it reflects the actual current state of things.

Comment on lines +60 to +69
/// Verifies the signatures of a genesis block against its own header.
///
/// The genesis block has no parent, so it acts as the chain's trust root: it carries exactly one
/// signature, produced by the bootstrapping validator, which must verify against a key in the
/// validator set committed to by its own header. The full committed set is only required to sign
/// from the next block onwards, so bootstrapping needs a single validator key.
pub fn verify_genesis_signatures(
header: &BlockHeader,
signatures: &BlockSignatures,
) -> anyhow::Result<()> {

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 think we may be able to avoid special casing the genesis block by changing the flow a bit.

struct ValidatorSet(Vec<ValidatorKey>);

impl ValidatorSet {
    /// The formal way to advance the key set to the next block.
    pub fn verify_and_update(&mut self, header: &BlockHeader, signatures: &BlockSignatures) 
        -> anyhow::Result<()> {

        // TODO: Verify the signatures and `self.0` match.
        
        self.0 = header.validator_keys().clone();
    }

    /// Only use this if you are certain of the keys providence.
    ///
    /// e.g. for genesis or loading from database on startup.
    pub fn new_unchecked(keys: Vec<ValidatorKey>) -> Self {
        assert!(!keys.is_empty(), "Cannot have an empty validator set");
        Self(keys)
    }
}

Usage then looks something like:

// We instantiate in two ways in the node.
// 
// 1. Bootstrapping from genesis, we assume providence is the genesis keys.
let genesis_keys = genesis_header.validator_keys();
let mut validator_set = ValidatorSet::new_unchecked(genesis_keys);

// 2. On restart, we load from database.
let validator_keys = self.db.get_latest_validator_keys().await.unwrap();
let mut validator_set = ValidatorSet::new_unchecked(genesis_keys);

// And then just advance block by block.
validator_set.verify_and_update(...).unwrap();

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.

That said, I guess this function is also required because we assume only a single validator will bootstrap and sign? We could also build that genesis logic in here i.e. check the block number and branch for the

        // TODO: Verify the signatures and `self.0` match.
        if header.block_num() == BlockNumber::GENESIS {
            // Match on a single key.
        } else {
            // Match on all keys.
        }

I'm unsure if that is expected to be the case, or if actually every validator must actively sign the genesis block. I suspect it is the latter cc @bobbinth

In that case we cannot really automate bootstrap -- and instead must manually hand off the genesis data to every validator. Though I do like the idea of just having a single bootstrapper - unsure if that's acceptable though.

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.

2 participants