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.
| source_rpc: Box<SourceRpcClient>, | ||
| readiness_threshold: u32, | ||
| validator: Option<Box<ValidatorClient>>, | ||
| validators: Option<Vec<ValidatorClient>>, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| /// 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.
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