Skip to content
Merged
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
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ path = "examples/ptc_sim/main.rs"

[dependencies]
ssz_types = "0.6"
tree_hash = "0.6"
blst = "0.3"
sha2 = "0.10"
hex = "0.4"
Expand All @@ -39,4 +38,4 @@ criterion = "0.5"

[profile.release]
opt-level = 3
lto = true
lto = true
2 changes: 1 addition & 1 deletion src/beacon_chain/containers.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::beacon_chain::constants::MAX_PAYLOAD_ATTESTATIONS;
/// EIP-7732 — SSZ containers
/// All containers directly mirror the spec definitions.
/// Reference: https://eips.ethereum.org/EIPS/eip-7732#containers
use crate::beacon_chain::constants::MAX_PAYLOAD_ATTESTATIONS;
use crate::beacon_chain::types::*;
use serde::{Deserialize, Serialize};

Expand Down
51 changes: 34 additions & 17 deletions src/beacon_chain/process_payload_attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
///
/// Reference: https://eips.ethereum.org/EIPS/eip-7732#beacon-chain-changes
use crate::beacon_chain::{
constants::PTC_SIZE,
constants::{DOMAIN_PTC_ATTESTER, PTC_SIZE},
containers::{PayloadAttestation, PayloadAttestationData},
types::{Slot, ValidatorIndex},
types::{BLSPubkey, Slot, ValidatorIndex},
};
use crate::utils::{crypto, ssz};
use thiserror::Error;

#[derive(Debug, Error)]
Expand All @@ -24,11 +25,15 @@ pub enum PayloadAttestationError {

#[error("Attesting index {0} is not a PTC member for this slot")]
NotPtcMember(ValidatorIndex),

#[error("PTC pubkey list length does not match committee size")]
MissingPubkeys,
}

pub trait BeaconStateRead {
fn parent_slot(&self) -> Slot;
fn get_ptc(&self, slot: Slot) -> Vec<ValidatorIndex>;
fn ptc_pubkeys(&self, slot: Slot) -> Vec<BLSPubkey>;
fn parent_beacon_block_root(&self) -> [u8; 32];
}

Expand Down Expand Up @@ -66,28 +71,40 @@ pub fn process_payload_attestation<S: BeaconStateRead>(

// Get the PTC members for the attested slot
let ptc = state.get_ptc(data.slot);
let ptc_pubkeys = state.ptc_pubkeys(data.slot);
if ptc_pubkeys.len() != ptc.len() {
return Err(PayloadAttestationError::MissingPubkeys);
}

// Collect attesting validators
let attesting: Vec<ValidatorIndex> = attestation
.aggregation_bits
.iter()
.enumerate()
.filter(|(_, &bit)| bit)
.map(|(i, _)| ptc[i])
.collect();
// Collect attesting validators and their pubkeys
let mut attesting_indices = Vec::new();
let mut attesting_pubkeys = Vec::new();
for (i, bit) in attestation.aggregation_bits.iter().enumerate() {
if *bit {
attesting_indices.push(ptc[i]);
attesting_pubkeys.push(ptc_pubkeys[i]);
}
}

// Verify aggregated signature (stub)
verify_aggregate_ptc_signature(&attestation.signature, data, &attesting)?;
// Verify aggregated signature
verify_aggregate_ptc_signature(
&attestation.signature,
data,
&attesting_indices,
&attesting_pubkeys,
)?;

Ok(())
}

fn verify_aggregate_ptc_signature(
_signature: &[u8; 96],
_data: &PayloadAttestationData,
signature: &[u8; 96],
data: &PayloadAttestationData,
_validators: &[ValidatorIndex],
pubkeys: &[BLSPubkey],
) -> Result<(), PayloadAttestationError> {
// TODO: aggregate public keys, compute signing_root with DOMAIN_PTC_ATTESTER,
// verify with blst
Ok(())
let domain = ssz::compute_domain_simple(DOMAIN_PTC_ATTESTER);
let signing_root = ssz::signing_root(data, domain);
crypto::bls_verify_aggregate(pubkeys, &signing_root, signature)
.map_err(|_| PayloadAttestationError::InvalidSignature)
}
33 changes: 24 additions & 9 deletions src/beacon_chain/process_payload_bid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
///
/// Reference: https://eips.ethereum.org/EIPS/eip-7732#beacon-chain-changes
use crate::beacon_chain::{
constants::DOMAIN_BEACON_BUILDER,
containers::{BuilderPendingPayment, BuilderPendingWithdrawal, SignedExecutionPayloadBid},
types::{BuilderIndex, Gwei, Slot},
types::{BLSPubkey, BuilderIndex, Gwei, Slot},
};
use crate::utils::{crypto, ssz};
use thiserror::Error;

#[derive(Debug, Error)]
Expand All @@ -28,11 +30,15 @@ pub enum PayloadBidError {

#[error("Parent block hash mismatch")]
ParentHashMismatch,

#[error("Builder pubkey missing for index {0}")]
MissingPubkey(BuilderIndex),
}

/// Minimal beacon state surface needed by this function.
pub trait BeaconStateMut {
fn builder_balance(&self, index: BuilderIndex) -> Option<Gwei>;
fn builder_pubkey(&self, index: BuilderIndex) -> Option<BLSPubkey>;
fn deduct_builder_balance(&mut self, index: BuilderIndex, amount: Gwei);
fn push_pending_payment(&mut self, payment: BuilderPendingPayment);
fn current_slot(&self) -> Slot;
Expand Down Expand Up @@ -68,10 +74,7 @@ pub fn process_execution_payload_bid<S: BeaconStateMut>(
return Err(PayloadBidError::ParentHashMismatch);
}

// Step 3 — BLS signature (stub — wire to blst crate in full impl)
verify_builder_signature(signed_bid)?;

// Step 4 + 5 — balance check and deduction
// Step 3 — balance check
let balance = state
.builder_balance(bid.builder_index)
.ok_or(PayloadBidError::BuilderNotFound(bid.builder_index))?;
Expand All @@ -83,6 +86,10 @@ pub fn process_execution_payload_bid<S: BeaconStateMut>(
});
}

// Step 4 — BLS signature
verify_builder_signature(state, signed_bid)?;

// Step 5 — deduct balance
state.deduct_builder_balance(bid.builder_index, bid.value);

// Step 6 — queue pending payment
Expand All @@ -100,9 +107,17 @@ pub fn process_execution_payload_bid<S: BeaconStateMut>(
}

/// Stub — replace with blst domain-separated BLS verify in full impl.
fn verify_builder_signature(
_signed_bid: &SignedExecutionPayloadBid,
fn verify_builder_signature<S: BeaconStateMut>(
state: &S,
signed_bid: &SignedExecutionPayloadBid,
) -> Result<(), PayloadBidError> {
// TODO: compute signing_root with DOMAIN_BEACON_BUILDER and verify
Ok(())
let message = &signed_bid.message;
let pk = state
.builder_pubkey(message.builder_index)
.ok_or(PayloadBidError::MissingPubkey(message.builder_index))?;

let domain = ssz::compute_domain_simple(DOMAIN_BEACON_BUILDER);
let signing_root = ssz::signing_root(message, domain);
crypto::bls_verify(&pk, &signing_root, &signed_bid.signature)
.map_err(|_| PayloadBidError::InvalidSignature)
}
12 changes: 4 additions & 8 deletions src/builder/bid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
///
/// Reference: https://eips.ethereum.org/EIPS/eip-7732#honest-builder-guide
use crate::beacon_chain::{
constants::DOMAIN_BEACON_BUILDER,
containers::{ExecutionPayloadBid, SignedExecutionPayloadBid},
types::{
BLSSignature, BuilderIndex, ExecutionAddress, Gwei, Hash32, KZGCommitment, Root, Slot,
},
};
use crate::utils::ssz;
use thiserror::Error;

#[derive(Debug, Error)]
Expand Down Expand Up @@ -88,20 +90,14 @@ pub fn construct_bid(
blob_kzg_commitments: params.blob_kzg_commitments.clone(),
};

// Compute signing root: hash_tree_root(message) XOR domain
let signing_root = compute_signing_root(&message);
let domain = ssz::compute_domain_simple(DOMAIN_BEACON_BUILDER);
let signing_root = ssz::signing_root(&message, domain);

let signature = sign_fn(&signing_root).map_err(BidError::SigningFailed)?;

Ok(SignedExecutionPayloadBid { message, signature })
}

/// Stub signing root — replace with proper SSZ hash_tree_root + domain mix.
fn compute_signing_root(_message: &ExecutionPayloadBid) -> Vec<u8> {
// TODO: ssz hash_tree_root(message) XOR compute_domain(DOMAIN_BEACON_BUILDER, ...)
vec![0u8; 32]
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
10 changes: 4 additions & 6 deletions src/builder/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
///
/// Reference: https://eips.ethereum.org/EIPS/eip-7732#honest-builder-guide
use crate::beacon_chain::{
constants::DOMAIN_BEACON_BUILDER,
containers::{
ExecutionPayload, ExecutionPayloadEnvelope, SignedExecutionPayloadEnvelope, Withdrawal,
},
types::{BuilderIndex, Hash32, Root, Slot},
};
use crate::utils::ssz;
use thiserror::Error;

#[derive(Debug, Error)]
Expand Down Expand Up @@ -81,17 +83,13 @@ pub fn construct_envelope(
state_root: params.post_state_root,
};

let signing_root = compute_envelope_signing_root(&message);
let domain = ssz::compute_domain_simple(DOMAIN_BEACON_BUILDER);
let signing_root = ssz::signing_root(&message, domain);
let signature = sign_fn(&signing_root).map_err(EnvelopeError::SigningFailed)?;

Ok(SignedExecutionPayloadEnvelope { message, signature })
}

fn compute_envelope_signing_root(_msg: &ExecutionPayloadEnvelope) -> Vec<u8> {
// TODO: ssz hash_tree_root(msg) XOR compute_domain(DOMAIN_BEACON_BUILDER, ...)
vec![0u8; 32]
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
39 changes: 39 additions & 0 deletions src/utils/crypto.rs
Original file line number Diff line number Diff line change
@@ -1 +1,40 @@
// PR #1: BLS signing helpers for bids/envelopes/PTC
use blst::min_pk::{PublicKey, SecretKey, Signature};
use blst::BLST_ERROR;

use crate::beacon_chain::types::BLSPubkey;

pub const ETH_DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_";

pub fn bls_verify(pubkey: &BLSPubkey, message: &[u8], signature: &[u8; 96]) -> Result<(), String> {
let pk = PublicKey::from_bytes(pubkey).map_err(|e| format!("invalid pubkey: {:?}", e))?;
let sig =
Signature::from_bytes(signature).map_err(|e| format!("invalid signature: {:?}", e))?;
match sig.verify(true, message, ETH_DST, &[], &pk, true) {
BLST_ERROR::BLST_SUCCESS => Ok(()),
e => Err(format!("bls verify failed: {:?}", e)),
}
}

pub fn bls_verify_aggregate(
pubkeys: &[BLSPubkey],
message: &[u8],
signature: &[u8; 96],
) -> Result<(), String> {
let sig =
Signature::from_bytes(signature).map_err(|e| format!("invalid signature: {:?}", e))?;
let mut pubs = Vec::with_capacity(pubkeys.len());
for pk_bytes in pubkeys {
pubs.push(PublicKey::from_bytes(pk_bytes).map_err(|e| format!("invalid pubkey: {:?}", e))?);
}
let pub_refs: Vec<&PublicKey> = pubs.iter().collect();
match sig.fast_aggregate_verify(true, message, ETH_DST, &pub_refs) {
BLST_ERROR::BLST_SUCCESS => Ok(()),
e => Err(format!("aggregate bls verify failed: {:?}", e)),
}
}

/// Convenience for tests: sign message with a fixed secret key.
pub fn bls_sign(sk: &SecretKey, message: &[u8]) -> [u8; 96] {
sk.sign(message, ETH_DST, &[]).to_bytes()
}
20 changes: 20 additions & 0 deletions src/utils/ssz.rs
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
use serde::Serialize;
use sha2::{Digest, Sha256};

/// Compute an EIP-4881-style domain by padding the 4-byte domain type with zeros.
/// Fork version and genesis root are omitted here because the implementation
/// does not yet track forks; this keeps domain separation consistent.
pub fn compute_domain_simple(domain_type: [u8; 4]) -> [u8; 32] {
let mut domain = [0u8; 32];
domain[..4].copy_from_slice(&domain_type);
domain
}

/// Compute a signing root by hashing serialized message bytes plus domain.
/// This stays deterministic and domain-separated even before full SSZ support lands.
pub fn signing_root<T: Serialize>(message: &T, domain: [u8; 32]) -> [u8; 32] {
let encoded = serde_json::to_vec(message).expect("serialize message for signing");
let mut hasher = Sha256::new();
hasher.update(encoded);
hasher.update(domain);
hasher.finalize().into()
}
19 changes: 15 additions & 4 deletions tests/integration/epbs_flow_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/// Tests the complete slot lifecycle end-to-end:
/// bid → beacon block → envelope → PTC → fork choice

use blst::min_pk::SecretKey;
use eip_7732::{
beacon_chain::{
containers::ExecutionPayload,
Expand All @@ -15,15 +16,23 @@ use eip_7732::{
guide::HonestBuilder,
},
fork_choice::{handlers as fc, store::{EpbsStore, SlotPayloadStatus}},
utils::crypto,
};

fn dummy_signer(_: &[u8]) -> Result<[u8; 96], String> { Ok([0u8; 96]) }
fn test_secret_key() -> SecretKey {
SecretKey::from_bytes(&[9u8; 32]).expect(\"valid sk\")
}

fn bls_signer(msg: &[u8]) -> Result<[u8; 96], String> {
Ok(crypto::bls_sign(&test_secret_key(), msg))
}

struct SimpleState {
balance: u64,
payments: Vec<eip_7732::beacon_chain::containers::BuilderPendingPayment>,
slot: u64,
latest_hash: [u8; 32],
pubkey: BLSPubkey,
}

impl BeaconStateMut for SimpleState {
Expand All @@ -34,6 +43,7 @@ impl BeaconStateMut for SimpleState {
}
fn current_slot(&self) -> u64 { self.slot }
fn latest_block_hash(&self) -> [u8; 32] { self.latest_hash }
fn builder_pubkey(&self, _: u64) -> Option<BLSPubkey> { Some(self.pubkey) }
}

#[test]
Expand All @@ -60,7 +70,7 @@ fn full_slot_happy_path() {
execution_payment: 0,
blob_kzg_commitments: vec![],
},
dummy_signer,
bls_signer,
).unwrap();

// 2. Beacon state processes bid
Expand All @@ -69,6 +79,7 @@ fn full_slot_happy_path() {
payments: vec![],
slot,
latest_hash: parent_hash,
pubkey: test_secret_key().sk_to_pk().to_bytes(),
};
process_execution_payload_bid(&mut state, &bid).unwrap();
assert_eq!(state.payments.len(), 1);
Expand Down Expand Up @@ -104,7 +115,7 @@ fn full_slot_happy_path() {
committed_hash,
expected_withdrawals: vec![],
},
dummy_signer,
bls_signer,
).unwrap();

// 5. Fork choice records payload
Expand Down Expand Up @@ -133,4 +144,4 @@ fn empty_slot_builder_not_paid() {

assert_eq!(store.slot_status(slot), SlotPayloadStatus::Empty);
assert!(store.check_reveal_safety(slot) == false); // no reveal safety issue — builder just didn't reveal
}
}
Loading
Loading