feat: Multiple Validators#2323
Conversation
7e6e725 to
539d422
Compare
0385cfa to
8eebf8a
Compare
Mirko-von-Leipzig
left a comment
There was a problem hiding this comment.
Code lgtm. Just the open question about expected boostrap process.
| /// 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<()> { |
There was a problem hiding this comment.
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();There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I haven't thought too much about it, but do we need to sign the genesis block at all? Maybe the way to think about this is to consider two scenarios:
- The genesis block includes a validator that didn't want to sign the block. If that's the case, the chain won't move to the next block because the validator would just not sign the next block.
- The genesis block should have been signed by some validator - but the validator key was missing from it. This is really easy to detect as whoever is launching the network would see that some validator's key is not in the block. This would be equivalent to launching a network with a smaller set of validators - which could be done even if all validators had to sign the block.
So, I don't really see a strong reason for why the validators need to sign the genesis block. The only argument I can come up with is that they want to sign of on the contents of the genesis block (e.g., the initial set of accounts) - but this can be done via subsequent signatures (e.g., if you don't like what was in the genesis block, stop signing subsequent blocks).
There was a problem hiding this comment.
I agree; I think not signing genesis makes sense.
Summary
Multi-validator signature integration:
SignBlockResponsegainspublic_key;rpc.protosync target isrepeated.GenesisState.validator_keys: ValidatorKeys; DBsignaturecolumn stores fullBlockSignatures.validator_urls: Vec<Url>, concurrent fan-out to all validators, positional signature aggregation.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).validatorslist of hex-encoded public keys ingenesis.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 bootstraptakes a single--key.hex/--key.kms-idfor the bootstrapping validator's own key; no other key material is passed on the command line.miden-validator pubkeysubcommand prints a key's public key (local or KMS) in the hex encoding thevalidatorsconfig 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.shto run two validators end to end: it derives both validators' public keys viamiden-validator pubkey, writes them as thevalidatorslist of a generated genesis config, and validator 1 bootstraps and signs genesis with only its own key. Validator 2's directory is seeded via--filefrom 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'svalidatorsand signs genesis with their own key alone; every other validator seeds via--file.Changelog