Skip to content
Open
351 changes: 113 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")
}
11 changes: 5 additions & 6 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 @@ -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