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
349 changes: 111 additions & 238 deletions crates/light-client-prover/src/circuit/initial_values.rs

Large diffs are not rendered by default.

174 changes: 124 additions & 50 deletions crates/light-client-prover/src/circuit/method_id_verifier.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,48 @@
use alloy_primitives::eip191_hash_message;
use k256::ecdsa::signature::hazmat::PrehashVerifier;
use k256::ecdsa::{Signature, VerifyingKey};
use alloy_primitives::{eip191_hash_message, keccak256, Address};
use k256::ecdsa::VerifyingKey;
use sov_rollup_interface::da::{
SECURITY_COUNCIL_SIGNATURE_SIZE, SECURITY_COUNCIL_SIGNATURE_THRESHOLD,
};

use crate::circuit::{SECURITY_COUNCIL_COMPRESSED_PUBKEY_SIZE, SECURITY_COUNCIL_MEMBER_COUNT};
use crate::circuit::SECURITY_COUNCIL_MEMBER_COUNT;

/// Error type for public key recovery operations
#[derive(Debug, Clone)]
pub enum PubKeyRecoveryError {
/// Invalid signature length
InvalidSignatureLength,
/// Invalid hash length
InvalidHashLength,
/// Invalid recovery ID (v value)
InvalidRecoveryId(u8),
/// Failed to parse signature bytes
InvalidSignatureBytes(String),
/// Failed to recover the public key
RecoveryFailed(String),
}

impl std::fmt::Display for PubKeyRecoveryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PubKeyRecoveryError::InvalidSignatureLength => write!(f, "Invalid Signature Length"),
PubKeyRecoveryError::InvalidHashLength => write!(f, "Invalid Hash Length"),
PubKeyRecoveryError::InvalidRecoveryId(recovery_id) => {
write!(f, "Invalid Recovery Id: {recovery_id}")
}
PubKeyRecoveryError::InvalidSignatureBytes(bytes_str) => {
write!(f, "Invalid Signature Bytes: {bytes_str}")
}
PubKeyRecoveryError::RecoveryFailed(e) => write!(f, "Recovery Failed with error: {e}"),
}
}
}

/// The three out of 5 signatures should be verified for the method id upgrade to be valid.
/// For each signature, the corresponding public key from the initial values constants is used to verify the signature.
/// If there are less than 3 valid signatures, the verification fails.
/// Note that the pubkey indices of signatures must be in strict ascending order and within bounds [0,(SECURITY_COUNCIL_MEMBER_COUNT - 1)]
pub fn verify_method_id_security_council(
initial_da_pubkeys: [[u8; SECURITY_COUNCIL_COMPRESSED_PUBKEY_SIZE];
SECURITY_COUNCIL_MEMBER_COUNT],
initial_da_addresses: [Address; SECURITY_COUNCIL_MEMBER_COUNT],
msg: &[u8],
signatures_with_idx: &[([u8; SECURITY_COUNCIL_SIGNATURE_SIZE], u8);
SECURITY_COUNCIL_SIGNATURE_THRESHOLD],
Expand Down Expand Up @@ -43,39 +72,96 @@ pub fn verify_method_id_security_council(

for signature_with_idx in signatures_with_idx.iter() {
let signature = signature_with_idx.0;
let pubkey_idx = signature_with_idx.1;
let const_pubkey = initial_da_pubkeys[pubkey_idx as usize];
let address_idx = signature_with_idx.1;
let const_address = initial_da_addresses[address_idx as usize];

let recovered_pubkey = match recover_pub_key_from_signature_and_prehash(
signature.as_slice(),
prehash.as_slice(),
) {
Ok(recovered_pubkey) => recovered_pubkey,
Err(e) => {
log!(
"Failed to recover public key from signature for index {}: {:?}",
address_idx,
e.to_string()
);
return false;
}
};

// ensure the inscription pubkey matches the expected constant (compressed 33B)
let verifying_key = VerifyingKey::from_sec1_bytes(const_pubkey.as_slice())
.expect("Initial DA pubkeys must be parsable to k256 VerifyingKey form sec1 bytes");
let ep = recovered_pubkey.to_encoded_point(false); // uncompressed form

let Ok(parsed_sig) = Signature::from_bytes(&signature.into()) else {
log!("Invalid signature format");
return false; // invalid signature format, fail
};
let bytes = ep.as_bytes();
debug_assert_eq!(bytes[0], 0x04);

// Hash the 64 bytes X||Y (skip the 0x04 prefix)
let hash = keccak256(&bytes[1..]);

// verify prehash with the matching verifying key
if verifying_key
.verify_prehash(prehash.as_slice(), &parsed_sig)
.is_err()
{
log!("Signature verification failed for index: {}", pubkey_idx);
// Take last 20 bytes
let address = Address::from_slice(&hash[12..]);

if address != const_address {
log!(
"Recovered address does not match constant address for index: {}",
address_idx
);
return false;
}
}

true
}

/// Recovers the public key from a signature (65 bytes: r(32) + s(32) + v(1)) and the message prehash.
fn recover_pub_key_from_signature_and_prehash(
signature: &[u8],
message_prehash: &[u8],
) -> Result<VerifyingKey, PubKeyRecoveryError> {
use k256::ecdsa::RecoveryId;

if signature.len() != 65 {
return Err(PubKeyRecoveryError::InvalidSignatureLength);
}
if message_prehash.len() != 32 {
return Err(PubKeyRecoveryError::InvalidHashLength);
}

let v = signature[64];
let recid_u8 = match v {
0..=3 => v,
27..=30 => v - 27,
_ => return Err(PubKeyRecoveryError::InvalidRecoveryId(v)),
};

let mut y_odd = (recid_u8 & 1) == 1;
let x_reduced = (recid_u8 & 2) == 2;

let mut signature = k256::ecdsa::Signature::from_slice(&signature[0..64])
.map_err(|e| PubKeyRecoveryError::InvalidSignatureBytes(format!("{e:?}")))?;

// low-s normalization requires flipping parity
if let Some(s) = signature.normalize_s() {
signature = s;
y_odd = !y_odd;
}

VerifyingKey::recover_from_prehash(
message_prehash,
&signature,
RecoveryId::new(y_odd, x_reduced),
)
.map_err(|e| PubKeyRecoveryError::RecoveryFailed(format!("{e:?}")))
}

#[cfg(test)]
mod tests {
use sov_rollup_interface::da::{BatchProofMethodId, BatchProofMethodIdBody};
use sov_rollup_interface::Network;

use super::*;
use crate::circuit::citrea_network_to_chain_id;
use crate::{create_valid_signatures, generate_initial_pub_keys_with_signers};
use crate::{create_valid_signatures, generate_initial_addresses_with_signers};

#[test]
fn test_valid_signatures() {
Expand All @@ -87,7 +173,7 @@ mod tests {
let msg = body.serialize();
let prehash = eip191_hash_message(msg);

let (initial_pubkeys, signers) = generate_initial_pub_keys_with_signers();
let (initial_addresses, signers) = generate_initial_addresses_with_signers();

let signatures_with_index = create_valid_signatures(&signers, &prehash);

Expand All @@ -101,7 +187,7 @@ mod tests {
};

assert!(verify_method_id_security_council(
initial_pubkeys,
initial_addresses,
batch_proof_method_id.body.serialize().as_slice(),
&batch_proof_method_id.signatures_with_index
));
Expand All @@ -117,7 +203,7 @@ mod tests {
let msg = body.serialize();
let prehash = eip191_hash_message(msg);

let (initial_pubkeys, signers) = generate_initial_pub_keys_with_signers();
let (initial_addresses, signers) = generate_initial_addresses_with_signers();

let mut signatures_with_index = create_valid_signatures(&signers, &prehash);

Expand All @@ -129,7 +215,7 @@ mod tests {
signatures_with_index,
};
assert!(!verify_method_id_security_council(
initial_pubkeys,
initial_addresses,
batch_proof_method_id.body.serialize().as_slice(),
&batch_proof_method_id.signatures_with_index
));
Expand All @@ -145,7 +231,7 @@ mod tests {
let msg = body.serialize();
let prehash = eip191_hash_message(msg);

let (initial_pubkeys, signers) = generate_initial_pub_keys_with_signers();
let (initial_addresses, signers) = generate_initial_addresses_with_signers();

let mut signatures_with_index = create_valid_signatures(&signers, &prehash);

Expand All @@ -157,7 +243,7 @@ mod tests {
signatures_with_index,
};
assert!(!verify_method_id_security_council(
initial_pubkeys,
initial_addresses,
batch_proof_method_id.body.serialize().as_slice(),
&batch_proof_method_id.signatures_with_index
));
Expand All @@ -172,7 +258,7 @@ mod tests {
};
let msg = body.serialize();
let prehash = eip191_hash_message(msg);
let (initial_pubkeys, signers) = generate_initial_pub_keys_with_signers();
let (initial_addresses, signers) = generate_initial_addresses_with_signers();
let mut signatures_with_index = create_valid_signatures(&signers, &prehash);
// Set an out-of-bounds index
signatures_with_index[0].1 = 5; // valid indexes are 0-
Expand All @@ -181,7 +267,7 @@ mod tests {
signatures_with_index,
};
assert!(!verify_method_id_security_council(
initial_pubkeys,
initial_addresses,
batch_proof_method_id.body.serialize().as_slice(),
&batch_proof_method_id.signatures_with_index
));
Expand All @@ -197,7 +283,7 @@ mod tests {
let msg = body.serialize();
let prehash = eip191_hash_message(msg);

let (initial_pubkeys, signers) = generate_initial_pub_keys_with_signers();
let (initial_addresses, signers) = generate_initial_addresses_with_signers();

let mut signatures_with_index = create_valid_signatures(&signers, &prehash);

Expand All @@ -213,7 +299,7 @@ mod tests {

// Should not verify because points to different pubkeys now
assert!(!verify_method_id_security_council(
initial_pubkeys,
initial_addresses,
batch_proof_method_id.body.serialize().as_slice(),
&batch_proof_method_id.signatures_with_index
));
Expand All @@ -225,6 +311,7 @@ mod tests {
fn test_eip191_signature_verification() {
use alloy_signer::SignerSync;
use alloy_signer_local::PrivateKeySigner;
use k256::ecdsa::signature::hazmat::PrehashVerifier;

// signature created with cast: cast wallet sign --private-key d38ba32d6971702225da49b49baac41c5a7ec2f5e3f2bb426976195ccd3266f7 0x48656c6c6f2c20776f726c6421
let msg = b"Hello, world!";
Expand All @@ -243,8 +330,11 @@ fn test_eip191_signature_verification() {
let prehash = eip191_hash_message(msg);

let eip_191_signature = signer.sign_hash_sync(&prehash).unwrap();
let recovered_pub_key =
recover_pub_key_from_cast_sig_and_hash(&eip_191_signature.as_bytes(), prehash.as_slice());
let recovered_pub_key = recover_pub_key_from_signature_and_prehash(
&eip_191_signature.as_bytes(),
prehash.as_slice(),
)
.unwrap();

assert_eq!(pubkey, recovered_pub_key.to_sec1_bytes());

Expand All @@ -262,19 +352,3 @@ fn test_eip191_signature_verification() {
.verify_prehash(prehash.as_slice(), &signature)
.is_ok());
}

/// Recovers the public key from a cast-style signature (65 bytes: r(32) + s(32) + v(1)) and the message hash.
#[cfg(test)]
fn recover_pub_key_from_cast_sig_and_hash(cast_sig: &[u8], hash: &[u8]) -> VerifyingKey {
use k256::ecdsa::RecoveryId;
assert_eq!(cast_sig.len(), 65, "Invalid signature length");
assert_eq!(hash.len(), 32, "Invalid hash length");

let y_odd = cast_sig[64] - 27;
let y_odd = y_odd != 0;

let signature = k256::ecdsa::Signature::from_slice(&cast_sig[0..64]).unwrap();

VerifyingKey::recover_from_prehash(hash, &signature, RecoveryId::new(y_odd, false))
.expect("Failed to recover public key")
}
13 changes: 6 additions & 7 deletions crates/light-client-prover/src/circuit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use accessors::{
BatchProofMethodIdAccessor, BlockHashAccessor, ChunkAccessor, SequencerCommitmentAccessor,
VerifiedStateTransitionForSequencerCommitmentIndexAccessor,
};
use alloy_primitives::Address;
use borsh::BorshDeserialize;
use citrea_primitives::{network_to_dev_mode, MAX_COMPRESSED_BLOB_SIZE};
use initial_values::LCP_JMT_GENESIS_ROOT;
Expand Down Expand Up @@ -366,7 +367,7 @@ impl<S: Storage, DS: DaSpec, Z: Zkvm> LightClientProofCircuit<S, DS, Z> {
/// * `initial_batch_proof_method_ids` - The initial batch proof method IDs that are used to initialize the batch proof method IDs in the JMT state if this is the first light client proof output.
/// * `batch_prover_da_public_key` - The public key of the batch prover to check the sender of the batch proof transactions.
/// * `sequencer_da_public_key` - The public key of the sequencer to check the sender of the sequencer commitment transactions.
/// * `method_id_upgrade_authority_da_public_key` - The public key of the method ID upgrade authority to check the sender of the batch proof method ID transactions.
/// * `method_id_upgrade_authority_da_addresses` - The addresses of the method ID upgrade authority to be verified against the signatures by recovering pubkeys.
///
/// # Logic
/// - The block hash of the header is inserted into the JMT.
Expand All @@ -393,8 +394,7 @@ impl<S: Storage, DS: DaSpec, Z: Zkvm> LightClientProofCircuit<S, DS, Z> {
initial_batch_proof_method_ids: InitialBatchProofMethodIds,
batch_prover_da_public_key: &[u8],
sequencer_da_public_key: &[u8],
method_id_upgrade_authority_da_public_keys: &[[u8; SECURITY_COUNCIL_COMPRESSED_PUBKEY_SIZE];
SECURITY_COUNCIL_MEMBER_COUNT],
method_id_upgrade_authority_da_addresses: &[Address; SECURITY_COUNCIL_MEMBER_COUNT],
) -> RunL1BlockResult<S> {
let mut working_set =
WorkingSet::with_witness(storage.clone(), witness, Default::default());
Expand Down Expand Up @@ -550,7 +550,7 @@ impl<S: Storage, DS: DaSpec, Z: Zkvm> LightClientProofCircuit<S, DS, Z> {
// Verify the signatures only if the activation height is greater than the last one
// This prevents replay attacks of old method IDs
if !verify_method_id_security_council(
*method_id_upgrade_authority_da_public_keys,
*method_id_upgrade_authority_da_addresses,
batch_proof_method_id.body.serialize().as_slice(),
batch_proof_method_id.signatures_with_index(),
) {
Expand Down Expand Up @@ -676,8 +676,7 @@ impl<S: Storage, DS: DaSpec, Z: Zkvm> LightClientProofCircuit<S, DS, Z> {
initial_batch_proof_method_ids: InitialBatchProofMethodIds,
batch_prover_da_public_key: &[u8],
sequencer_da_public_key: &[u8],
method_id_upgrade_authority_da_public_keys: &[[u8; SECURITY_COUNCIL_COMPRESSED_PUBKEY_SIZE];
SECURITY_COUNCIL_MEMBER_COUNT],
method_id_upgrade_authority_da_addresses: &[Address; SECURITY_COUNCIL_MEMBER_COUNT],
) -> Result<LightClientCircuitOutput, LightClientVerificationError<DaV>>
where
DaV: DaVerifier<Spec = DS>,
Expand Down Expand Up @@ -735,7 +734,7 @@ impl<S: Storage, DS: DaSpec, Z: Zkvm> LightClientProofCircuit<S, DS, Z> {
initial_batch_proof_method_ids,
batch_prover_da_public_key,
sequencer_da_public_key,
method_id_upgrade_authority_da_public_keys,
method_id_upgrade_authority_da_addresses,
);

Ok(LightClientCircuitOutput {
Expand Down
2 changes: 1 addition & 1 deletion crates/light-client-prover/src/da_block_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ where
self.network.initial_batch_proof_method_ids().to_vec(),
&self.network.batch_prover_da_public_key(),
&self.network.sequencer_da_public_key(),
&self.network.method_id_upgrade_authority_da_public_keys(),
&self.network.method_id_upgrade_authority_da_addresses(),
);

// This is not exactly right, but works for now because we have a single elf for
Expand Down
Loading
Loading