mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
One proof implementation, over whichever message the caller has
A node-bound proof is one message among several: a committee entitles a validator ON A CHAIN and signs the chain into its proof, so it has a message of its own. It could only reach the same encoding and possession clauses by restating them, and a second statement of what a canonical key is is a second answer waiting to differ. The general pair is the implementation now and the node-bound pair is it with one message, so a registration and a committee are checked by the same rules or by none. Admission is the same door for the same reason. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
+88
-20
@@ -102,26 +102,54 @@ pub struct Vote {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum CertError {
|
||||
Version { got: u16, want: u16 },
|
||||
Type { got: u8, want: u8 },
|
||||
Version {
|
||||
got: u16,
|
||||
want: u16,
|
||||
},
|
||||
Type {
|
||||
got: u8,
|
||||
want: u8,
|
||||
},
|
||||
UnknownTier(Finality),
|
||||
ThresholdZero,
|
||||
NoVotes,
|
||||
NotStrictlyIncreasing(usize),
|
||||
VoteNotAccept(usize),
|
||||
SigInvalid(usize),
|
||||
BelowThreshold { have: u32, need: u32 },
|
||||
UnresolvedSet { n: i64 },
|
||||
BelowThreshold {
|
||||
have: u32,
|
||||
need: u32,
|
||||
},
|
||||
UnresolvedSet {
|
||||
n: i64,
|
||||
},
|
||||
/// An EXPORT certificate over a signing set too small for a Byzantine
|
||||
/// supermajority to mean anything: f = ⌊(n−1)/3⌋ is zero below
|
||||
/// [`crate::finality::MIN_BFT_COMMITTEE`], so the certificate absorbs no fault
|
||||
/// and one compromised key forges it. A floor on the SET, not on the voters —
|
||||
/// Go folds it into `ErrQCBelowThreshold`.
|
||||
MinCommittee { n: i64, need: i64 },
|
||||
SignerFloor { have: i64, need: i64, n: i64 },
|
||||
StakeZero { epoch_height: u64 },
|
||||
StakeBelowMajority { voted: u64, signer: u64, need_above: u64 },
|
||||
StakeBelowSupermajority { voted: u64, signer: u64, need_above: u64 },
|
||||
MinCommittee {
|
||||
n: i64,
|
||||
need: i64,
|
||||
},
|
||||
SignerFloor {
|
||||
have: i64,
|
||||
need: i64,
|
||||
n: i64,
|
||||
},
|
||||
StakeZero {
|
||||
epoch_height: u64,
|
||||
},
|
||||
StakeBelowMajority {
|
||||
voted: u64,
|
||||
signer: u64,
|
||||
need_above: u64,
|
||||
},
|
||||
StakeBelowSupermajority {
|
||||
voted: u64,
|
||||
signer: u64,
|
||||
need_above: u64,
|
||||
},
|
||||
/// A public key that does not decode to a non-identity point of the right
|
||||
/// subgroup.
|
||||
KeyEncoding,
|
||||
@@ -152,7 +180,11 @@ pub enum CertError {
|
||||
/// not require, and tolerating it would let a certificate redefine the rung
|
||||
/// upward exactly as tolerating an under-claim lets it redefine the rung down.
|
||||
/// Neither direction is the set's number.
|
||||
ThresholdNotDerived { declared: u32, derived: i64, n: i64 },
|
||||
ThresholdNotDerived {
|
||||
declared: u32,
|
||||
derived: i64,
|
||||
n: i64,
|
||||
},
|
||||
/// The set's weights sum past what a `u64` can hold. Go's
|
||||
/// `ErrWeightOverflow`, and refused for the reason Go refuses it: every
|
||||
/// threshold in this crate is read against the total, so a total that
|
||||
@@ -197,11 +229,22 @@ impl std::fmt::Display for CertError {
|
||||
CertError::StakeZero { epoch_height } => {
|
||||
write!(f, "total stake is zero at epoch height {epoch_height}")
|
||||
}
|
||||
CertError::StakeBelowMajority { voted, signer, need_above } => {
|
||||
CertError::StakeBelowMajority {
|
||||
voted,
|
||||
signer,
|
||||
need_above,
|
||||
} => {
|
||||
write!(f, "nova voted={voted} signer={signer}, need > {need_above}")
|
||||
}
|
||||
CertError::StakeBelowSupermajority { voted, signer, need_above } => {
|
||||
write!(f, "quasar voted={voted} signer={signer}, need > {need_above}")
|
||||
CertError::StakeBelowSupermajority {
|
||||
voted,
|
||||
signer,
|
||||
need_above,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"quasar voted={voted} signer={signer}, need > {need_above}"
|
||||
)
|
||||
}
|
||||
CertError::KeyEncoding => write!(f, "public key does not decode to a valid point"),
|
||||
CertError::NoKey => write!(f, "registration carries no public key"),
|
||||
@@ -209,7 +252,11 @@ impl std::fmt::Display for CertError {
|
||||
CertError::DuplicateKey => write!(f, "public key is registered to more than one node"),
|
||||
CertError::DuplicateNode => write!(f, "node is registered more than once"),
|
||||
CertError::PopInvalid => write!(f, "proof of possession does not verify for this key"),
|
||||
CertError::ThresholdNotDerived { declared, derived, n } => write!(
|
||||
CertError::ThresholdNotDerived {
|
||||
declared,
|
||||
derived,
|
||||
n,
|
||||
} => write!(
|
||||
f,
|
||||
"cert declares threshold {declared}, a set of {n} signers derives {derived}"
|
||||
),
|
||||
@@ -358,11 +405,7 @@ impl QuorumCert {
|
||||
/// 5. every vote is an ACCEPT
|
||||
/// 6. every signature verifies over the certificate's own position
|
||||
/// 7. the count of such votes meets the threshold
|
||||
pub fn verify(
|
||||
&self,
|
||||
verifier: &dyn VoteVerifier,
|
||||
epoch_height: u64,
|
||||
) -> Result<(), CertError> {
|
||||
pub fn verify(&self, verifier: &dyn VoteVerifier, epoch_height: u64) -> Result<(), CertError> {
|
||||
if self.version != QUORUM_CERT_VERSION {
|
||||
return Err(CertError::Version {
|
||||
got: self.version,
|
||||
@@ -799,6 +842,31 @@ impl ValidatorSet {
|
||||
weight: u64,
|
||||
public_key: &[u8],
|
||||
proof: &[u8],
|
||||
) -> Result<(), CertError> {
|
||||
self.insert_over(
|
||||
node,
|
||||
weight,
|
||||
public_key,
|
||||
&pop::message(&node, public_key),
|
||||
proof,
|
||||
)
|
||||
}
|
||||
|
||||
/// Admit a member whose proof is over a message of the caller's choosing.
|
||||
///
|
||||
/// [`insert`] is this with the node-bound message, which is the only one a
|
||||
/// registration uses. A committee entitles a validator ON A CHAIN and signs
|
||||
/// the chain into its proof, so it has a message of its own — and every
|
||||
/// other clause of admission, down to which duplicate is refused, must
|
||||
/// still be the same clause. Two admission doors would be two answers to
|
||||
/// who is in the set.
|
||||
pub fn insert_over(
|
||||
&mut self,
|
||||
node: NodeId,
|
||||
weight: u64,
|
||||
public_key: &[u8],
|
||||
message: &[u8],
|
||||
proof: &[u8],
|
||||
) -> Result<(), CertError> {
|
||||
// NO KEY. Not a malformed key — no key. A validator with no key cannot
|
||||
// sign, so it cannot come through the proof path at all.
|
||||
@@ -816,7 +884,7 @@ impl ValidatorSet {
|
||||
// and it is the SAME function the Go oracle's frozen vectors pin. There
|
||||
// is one proof-of-possession implementation in this crate; registration
|
||||
// calls it rather than restating it, so the two cannot drift.
|
||||
pop::verify(&node, public_key, proof).map_err(|e| match e {
|
||||
pop::verify_over(message, public_key, proof).map_err(|e| match e {
|
||||
PopError::Key => CertError::KeyEncoding,
|
||||
PopError::Proof | PopError::Possession => CertError::PopInvalid,
|
||||
})?;
|
||||
|
||||
+77
-6
@@ -71,6 +71,17 @@ pub fn message(node: &NodeId, key: &[u8]) -> Vec<u8> {
|
||||
/// Verify a node-bound proof of possession, in the order the standard fixes:
|
||||
/// encoding, then possession. A port of the Go oracle's `pop.Verify`.
|
||||
pub fn verify(node: &NodeId, key: &[u8], proof: &[u8]) -> Result<(), PopError> {
|
||||
verify_over(&message(node, key), key, proof)
|
||||
}
|
||||
|
||||
/// Verify a proof over ANY message, under the same two clauses in the same
|
||||
/// order: the encodings first, then the possession.
|
||||
///
|
||||
/// A node-bound proof is one message among several. A committee entitles a
|
||||
/// validator ON A CHAIN and signs the chain with it; a caller with a message
|
||||
/// of its own would otherwise restate these clauses, and a second statement of
|
||||
/// what a canonical key is is a second answer waiting to differ from this one.
|
||||
pub fn verify_over(message: &[u8], key: &[u8], proof: &[u8]) -> Result<(), PopError> {
|
||||
if key.len() != KEY_LEN {
|
||||
return Err(PopError::Key);
|
||||
}
|
||||
@@ -91,8 +102,7 @@ pub fn verify(node: &NodeId, key: &[u8], proof: &[u8]) -> Result<(), PopError> {
|
||||
// here so an identity or non-subgroup proof is refused as a bad PROOF — the
|
||||
// encoding clause — exactly as the oracle does, not later as a failed pairing.
|
||||
sig.validate(true).map_err(|_| PopError::Proof)?;
|
||||
let msg = message(node, key);
|
||||
if sig.verify(true, &msg, POP_DST, &[], &pk, true) != BLST_ERROR::BLST_SUCCESS {
|
||||
if sig.verify(true, message, POP_DST, &[], &pk, true) != BLST_ERROR::BLST_SUCCESS {
|
||||
return Err(PopError::Possession);
|
||||
}
|
||||
Ok(())
|
||||
@@ -101,8 +111,69 @@ pub fn verify(node: &NodeId, key: &[u8], proof: &[u8]) -> Result<(), PopError> {
|
||||
/// Produce a node-bound proof for `node` under `secret`. The signing side of
|
||||
/// [`verify`]; used by registrants and by tests.
|
||||
pub fn sign(secret: &blst::min_pk::SecretKey, node: &NodeId, key: &[u8]) -> Vec<u8> {
|
||||
secret
|
||||
.sign(&message(node, key), POP_DST, &[])
|
||||
.compress()
|
||||
.to_vec()
|
||||
sign_over(secret, &message(node, key))
|
||||
}
|
||||
|
||||
/// Produce a proof over ANY message, under the same domain tag. The signing
|
||||
/// side of [`verify_over`], and the one place the tag is applied.
|
||||
pub fn sign_over(secret: &blst::min_pk::SecretKey, message: &[u8]) -> Vec<u8> {
|
||||
secret.sign(message, POP_DST, &[]).compress().to_vec()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod general_message_tests {
|
||||
use super::*;
|
||||
|
||||
fn keypair() -> (blst::min_pk::SecretKey, Vec<u8>) {
|
||||
let sk = blst::min_pk::SecretKey::key_gen(&[7u8; 32], &[]).expect("key");
|
||||
let pk = sk.sk_to_pk().compress().to_vec();
|
||||
(sk, pk)
|
||||
}
|
||||
|
||||
/// The node-bound pair is the general pair with one message. If these ever
|
||||
/// disagree, a registration and a committee are checking different rules.
|
||||
#[test]
|
||||
fn the_node_bound_proof_is_the_general_one_over_its_own_message() {
|
||||
let (sk, pk) = keypair();
|
||||
let node: NodeId = [3u8; NODE_LEN];
|
||||
let bound = sign(&sk, &node, &pk);
|
||||
let general = sign_over(&sk, &message(&node, &pk));
|
||||
assert_eq!(bound, general, "one proof, written two ways");
|
||||
assert!(verify(&node, &pk, &general).is_ok());
|
||||
assert!(verify_over(&message(&node, &pk), &pk, &bound).is_ok());
|
||||
}
|
||||
|
||||
/// A proof for one message does not hold over another — which is the whole
|
||||
/// reason a committee signs the chain it entitles a validator on.
|
||||
#[test]
|
||||
fn a_proof_does_not_carry_to_another_message() {
|
||||
let (sk, pk) = keypair();
|
||||
let node: NodeId = [3u8; NODE_LEN];
|
||||
let here = [b"chain-a".as_slice(), &node, &pk].concat();
|
||||
let elsewhere = [b"chain-b".as_slice(), &node, &pk].concat();
|
||||
let proof = sign_over(&sk, &here);
|
||||
assert!(verify_over(&here, &pk, &proof).is_ok());
|
||||
assert_eq!(
|
||||
verify_over(&elsewhere, &pk, &proof),
|
||||
Err(PopError::Possession),
|
||||
"a proof made for one chain entitles nothing on another"
|
||||
);
|
||||
}
|
||||
|
||||
/// The encoding clauses are the same clauses, in the same order: a bad key
|
||||
/// is a bad key before any pairing runs.
|
||||
#[test]
|
||||
fn the_encoding_clauses_come_first_either_way() {
|
||||
let (sk, pk) = keypair();
|
||||
let msg = b"anything".as_slice();
|
||||
let proof = sign_over(&sk, msg);
|
||||
assert_eq!(
|
||||
verify_over(msg, &pk[..KEY_LEN - 1], &proof),
|
||||
Err(PopError::Key)
|
||||
);
|
||||
assert_eq!(
|
||||
verify_over(msg, &pk, &proof[..PROOF_LEN - 1]),
|
||||
Err(PopError::Proof)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user