Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 30 additions & 15 deletions node/src/aggregates/graph_jobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ use crate::error::{FailureClass, SpecialError, classify_failure};
use crate::messages::{
AggNonceConsensus, CommitteePresign, CreateGraph, CutCircuits, EndorseGraph, GOATMessage,
GOATMessageContent, GenCircuits, GraphFinalize, NonceGeneration,
VerifierGraphParamsEndorsement, verify_init_graph_signature,
VerifierGraphParamsEndorsement, sign_gen_circuits, verify_gen_circuits_signature,
verify_init_graph_signature,
};
use crate::soldering_payload_store::{
read_soldering_proof_store_payload, soldering_proof_payload_store_path,
Expand Down Expand Up @@ -324,15 +325,27 @@ async fn operator_init_graph(_deps: &JobDeps, graph_id: Uuid, params: &[u8]) ->

async fn ingest_gen_circuits(deps: &JobDeps, graph_id: Uuid, params: &[u8]) -> Result<Step> {
let (inbound, message) = decode_inbound(params).await?;
let GOATMessageContent::GenCircuits(GenCircuits {
let GOATMessageContent::GenCircuits(gen_circuits) = message.content else {
return invalid("not a GenCircuits message");
};
let verifier_peer = author_peer(&inbound)?;
let verifier_peer_id = verifier_peer.to_bytes();
// Without the key holder's signature for this peer, a registered verifier
// could claim another verifier's key first, and the real holder would be
// refused as "bound to another peer".
if !verify_gen_circuits_signature(&gen_circuits, &verifier_peer_id) {
return Step::ignore(format!(
"GenCircuits from {verifier_peer} is not signed by verifier key {} for that peer",
gen_circuits.verifier_pubkey
));
}
let GenCircuits {
instance_id,
graph_id: message_graph_id,
verifier_pubkey,
setup_package,
}) = message.content
else {
return invalid("not a GenCircuits message");
};
signature: _,
} = gen_circuits;
if message_graph_id != graph_id || instance_id != inbound.instance_id {
return invalid("GenCircuits identity mismatch");
}
Expand All @@ -353,8 +366,6 @@ async fn ingest_gen_circuits(deps: &JobDeps, graph_id: Uuid, params: &[u8]) -> R
setup_package.commits.len()
));
}
let verifier_peer = author_peer(&inbound)?;
let verifier_peer_id = verifier_peer.to_bytes();
if !crate::p2p_admission::peer_registry().is_verifier(&verifier_peer)
&& !deps.goat_client.committee_mana_is_verifier(&verifier_peer_id).await.with_context(
|| format!("validate GenCircuits sender {verifier_peer} against the verifier registry"),
Expand Down Expand Up @@ -1315,7 +1326,16 @@ async fn verifier_setup(deps: &JobDeps, graph_id: Uuid, params: &[u8]) -> Result
soldering_proof_ready: None,
}
};
let setup_package = verifier_state.setup_package.clone();
let local_peer_id = PeerId::from_str(&get_peer_id())
.context("decode local verifier peer id for GenCircuits")?
.to_bytes();
let gen_circuits = sign_gen_circuits(
instance_id,
graph_id,
&verifier_master_key.master_keypair(),
&local_peer_id,
verifier_state.setup_package.clone(),
);
update_babe_setup_state(&deps.local_db, instance_id, graph_id, |state| {
state.verifier = Some(verifier_state);
})?;
Expand All @@ -1326,12 +1346,7 @@ async fn verifier_setup(deps: &JobDeps, graph_id: Uuid, params: &[u8]) -> Result
graph_id,
PHASE_SETUP,
Actor::Operator,
GOATMessageContent::GenCircuits(GenCircuits {
instance_id,
graph_id,
verifier_pubkey,
setup_package,
}),
GOATMessageContent::GenCircuits(gen_circuits),
std::slice::from_ref(&operator_peer),
None,
)
Expand Down
78 changes: 78 additions & 0 deletions node/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,62 @@ pub struct GenCircuits {
pub graph_id: Uuid,
pub verifier_pubkey: PublicKey,
pub setup_package: CACSetupPackage,
/// `verifier_pubkey`'s signature binding it to the sending peer for this
/// graph; see [`sign_gen_circuits`].
pub signature: SchnorrSignature,
}

const GEN_CIRCUITS_SIGNATURE_DOMAIN: &[u8] = b"bitvm2-node/gen-circuits/v1";

/// The setup package is not signed: gossip already authenticates the sending
/// peer. What needs the key holder's consent is the claim that this peer
/// speaks for `verifier_pubkey` in this graph; without it a registered
/// verifier could claim another verifier's key first.
fn gen_circuits_signature_message(
instance_id: Uuid,
graph_id: Uuid,
verifier_pubkey: &PublicKey,
verifier_peer_id: &[u8],
) -> SecpMessage {
let mut hasher = Sha256::new();
hasher.update(GEN_CIRCUITS_SIGNATURE_DOMAIN);
hasher.update(instance_id.as_bytes());
hasher.update(graph_id.as_bytes());
hasher.update(verifier_pubkey.to_bytes());
hasher.update((verifier_peer_id.len() as u32).to_be_bytes());
hasher.update(verifier_peer_id);
SecpMessage::from_digest(hasher.finalize().into())
}

pub fn sign_gen_circuits(
instance_id: Uuid,
graph_id: Uuid,
verifier_keypair: &Keypair,
verifier_peer_id: &[u8],
setup_package: CACSetupPackage,
) -> GenCircuits {
let verifier_pubkey = verifier_keypair.public_key().into();
let signature = SECP256K1.sign_schnorr(
&gen_circuits_signature_message(instance_id, graph_id, &verifier_pubkey, verifier_peer_id),
verifier_keypair,
);
GenCircuits { instance_id, graph_id, verifier_pubkey, setup_package, signature }
}

/// Whether `message.verifier_pubkey` signed its binding to `sender`.
pub fn verify_gen_circuits_signature(message: &GenCircuits, sender: &[u8]) -> bool {
SECP256K1
.verify_schnorr(
&message.signature,
&gen_circuits_signature_message(
message.instance_id,
message.graph_id,
&message.verifier_pubkey,
sender,
),
&XOnlyPublicKey::from(message.verifier_pubkey),
)
.is_ok()
}
#[derive(Serialize, Deserialize, Clone)]
pub struct CutCircuits {
Expand Down Expand Up @@ -449,6 +505,28 @@ mod tests {
assert!(!object.contains_key("verifier_pubkey"));
}

#[test]
fn gen_circuits_binds_the_verifier_key_to_its_sender() {
let keypair = |seed| Keypair::from_seckey_slice(SECP256K1, &[seed; 32]).unwrap();
let (instance_id, graph_id) = (Uuid::new_v4(), Uuid::new_v4());
let (holder, other) = (libp2p::PeerId::random(), libp2p::PeerId::random());
let package = || bitvm_lib::babe_adapter::build_setup_package(1).unwrap();
let message =
sign_gen_circuits(instance_id, graph_id, &keypair(1), &holder.to_bytes(), package());
assert!(verify_gen_circuits_signature(&message, &holder.to_bytes()));
// The holder's message relayed or replayed by another peer.
assert!(!verify_gen_circuits_signature(&message, &other.to_bytes()));
// Another peer claiming the holder's key with its own signature.
let mut squatted =
sign_gen_circuits(instance_id, graph_id, &keypair(2), &other.to_bytes(), package());
squatted.verifier_pubkey = message.verifier_pubkey;
assert!(!verify_gen_circuits_signature(&squatted, &other.to_bytes()));
// The holder's signature reused for another graph.
let mut moved = message.clone();
moved.graph_id = Uuid::new_v4();
assert!(!verify_gen_circuits_signature(&moved, &holder.to_bytes()));
}

#[test]
fn every_wire_kind_is_routed_or_immediate() {
use strum::IntoEnumIterator;
Expand Down
14 changes: 7 additions & 7 deletions node/src/p2p_admission/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,19 +146,19 @@ fn envelope_limits_depend_on_the_encoding_and_the_sender() {
/// Verify GenCircuits fits the binary transport ceiling.
#[tokio::test]
async fn gen_circuits_fits_the_binary_envelope() {
use crate::messages::GenCircuits;
use bitvm_lib::babe_adapter::{BABE_N_CC, build_setup_package};

let secp = bitcoin::secp256k1::Secp256k1::new();
let secret = bitcoin::secp256k1::SecretKey::from_slice(&[7; 32]).unwrap();
let message = GOATMessage::new(
Actor::Operator,
GOATMessageContent::GenCircuits(GenCircuits {
instance_id: Uuid::new_v4(),
graph_id: Uuid::new_v4(),
verifier_pubkey: bitcoin::PublicKey::new(secret.public_key(&secp)),
setup_package: build_setup_package(BABE_N_CC).unwrap(),
}),
GOATMessageContent::GenCircuits(crate::messages::sign_gen_circuits(
Uuid::new_v4(),
Uuid::new_v4(),
&bitcoin::secp256k1::Keypair::from_secret_key(&secp, &secret),
&libp2p::PeerId::random().to_bytes(),
build_setup_package(BABE_N_CC).unwrap(),
)),
);
let bytes = message.serialize_message().await.unwrap();
assert!(GOATMessage::is_binary_envelope(&bytes));
Expand Down
Loading