From 1dc079cfd08965b316c41bf612110f64ca4fabae Mon Sep 17 00:00:00 2001 From: Patrick Beza Date: Mon, 30 Dec 2024 15:19:15 +0100 Subject: [PATCH] feat(tee): add support for recoverable signatures Signatures produced by the TEE Prover are now compatible with the on-chain verifier that uses the `ecrecover` precompile. Until now, we've been using _non-recoverable_ signatures in the TEE prover with a compressed ECDSA public key in each attestation -- it was compressed because there are only 64 bytes available in the report attestation quote. That worked fine for off-chain proof verification, but for on-chain verification, it's better to use the Ethereum address derived from the public key so we can call ecrecover in Solidity to verify the signature. This PR goes hand in hand with https://github.com/matter-labs/teepot/pull/228 --- Cargo.lock | 2 + core/bin/zksync_tee_prover/Cargo.toml | 10 ++- core/bin/zksync_tee_prover/src/api_client.rs | 6 +- core/bin/zksync_tee_prover/src/tee_prover.rs | 84 +++++++++++++++++++- 4 files changed, 95 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a98fb5ccd23..195e6a7ae707 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12903,9 +12903,11 @@ dependencies = [ "anyhow", "async-trait", "envy", + "hex", "reqwest 0.12.9", "secp256k1", "serde", + "sha3 0.10.8", "thiserror 1.0.69", "tokio", "tracing", diff --git a/core/bin/zksync_tee_prover/Cargo.toml b/core/bin/zksync_tee_prover/Cargo.toml index b853da348ee0..2628673845c9 100644 --- a/core/bin/zksync_tee_prover/Cargo.toml +++ b/core/bin/zksync_tee_prover/Cargo.toml @@ -16,7 +16,11 @@ anyhow.workspace = true async-trait.workspace = true envy.workspace = true reqwest = { workspace = true, features = ["zstd"] } -secp256k1 = { workspace = true, features = ["serde"] } +secp256k1 = { workspace = true, features = [ + "global-context", + "recovery", + "serde", +] } serde = { workspace = true, features = ["derive"] } thiserror.workspace = true tokio = { workspace = true, features = ["full"] } @@ -31,3 +35,7 @@ zksync_prover_interface.workspace = true zksync_tee_verifier.workspace = true zksync_types.workspace = true zksync_vlog.workspace = true + +[dev-dependencies] +hex.workspace = true +sha3.workspace = true diff --git a/core/bin/zksync_tee_prover/src/api_client.rs b/core/bin/zksync_tee_prover/src/api_client.rs index ffc2839b8d3b..186acc9f952a 100644 --- a/core/bin/zksync_tee_prover/src/api_client.rs +++ b/core/bin/zksync_tee_prover/src/api_client.rs @@ -1,5 +1,5 @@ use reqwest::{Client, Response, StatusCode}; -use secp256k1::{ecdsa::Signature, PublicKey}; +use secp256k1::PublicKey; use serde::Serialize; use url::Url; use zksync_basic_types::H256; @@ -87,13 +87,13 @@ impl TeeApiClient { pub async fn submit_proof( &self, batch_number: L1BatchNumber, - signature: Signature, + signature: [u8; 65], pubkey: &PublicKey, root_hash: H256, tee_type: TeeType, ) -> Result<(), TeeProverError> { let request = SubmitTeeProofRequest(Box::new(L1BatchTeeProofForL1 { - signature: signature.serialize_compact().into(), + signature: signature.into(), pubkey: pubkey.serialize().into(), proof: root_hash.as_bytes().into(), tee_type, diff --git a/core/bin/zksync_tee_prover/src/tee_prover.rs b/core/bin/zksync_tee_prover/src/tee_prover.rs index 58f3d45969ca..7ec604ae2373 100644 --- a/core/bin/zksync_tee_prover/src/tee_prover.rs +++ b/core/bin/zksync_tee_prover/src/tee_prover.rs @@ -1,6 +1,9 @@ use std::fmt; -use secp256k1::{ecdsa::Signature, Message, PublicKey, Secp256k1}; +use secp256k1::{ + ecdsa::{RecoverableSignature, RecoveryId}, + Message, PublicKey, Secp256k1, SecretKey, SECP256K1, +}; use zksync_basic_types::H256; use zksync_node_framework::{ service::StopReceiver, @@ -67,10 +70,25 @@ impl fmt::Debug for TeeProver { } impl TeeProver { + // TODO TODO TODO add tests for this function + /// Signs the message in Ethereum-compatible format for on-chain verification. + fn sign_message(&self, sec: &SecretKey, message: Message) -> Result<[u8; 65], TeeProverError> { + let s = SECP256K1.sign_ecdsa_recoverable(&message, sec); + let (rec_id, data) = s.serialize_compact(); + + let mut signature = [0u8; 65]; + signature[..64].copy_from_slice(&data); + // as defined in the Ethereum Yellow Paper (Appendix F) + // https://ethereum.github.io/yellowpaper/paper.pdf + signature[64] = 27 + rec_id.to_i32() as u8; + + Ok(signature) + } + fn verify( &self, tvi: TeeVerifierInput, - ) -> Result<(Signature, L1BatchNumber, H256), TeeProverError> { + ) -> Result<([u8; 65], L1BatchNumber, H256), TeeProverError> { match tvi { TeeVerifierInput::V1(tvi) => { let observer = METRICS.proof_generation_time.start(); @@ -79,7 +97,7 @@ impl TeeProver { let batch_number = verification_result.batch_number; let msg_to_sign = Message::from_slice(root_hash_bytes) .map_err(|e| TeeProverError::Verification(e.into()))?; - let signature = self.config.signing_key.sign_ecdsa(msg_to_sign); + let signature = self.sign_message(&self.config.signing_key, msg_to_sign)?; let duration = observer.observe(); tracing::info!( proof_generation_time = duration.as_secs_f64(), @@ -182,3 +200,63 @@ impl Task for TeeProver { } } } + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::Result; + use sha3::{Digest, Keccak256}; + + /// Converts a public key into an Ethereum address by hashing the encoded public key with Keccak256. + pub fn public_key_to_address(public: &PublicKey) -> [u8; 20] { + let public_key_bytes = public.serialize_uncompressed(); + + // Skip the first byte (0x04) which indicates uncompressed key + let hash: [u8; 32] = Keccak256::digest(&public_key_bytes[1..]).into(); + + // Take the last 20 bytes of the hash to get the Ethereum address + let mut address = [0u8; 20]; + address.copy_from_slice(&hash[12..]); + address + } + + /// Recovers the address of the sender using secp256k1 pubkey recovery. + /// + /// Converts the public key into an ethereum address by hashing the public key with + /// keccak256. + /// + /// This does not ensure that the `s` value in the signature is low, and _just_ wraps the + /// underlying secp256k1 library. + /// &Message::from_digest_slice(&msg[..32]) + // #[allow(dead_code)] + pub fn recover_signer_unchecked(sig: &[u8; 65], msg: &Message) -> Result<[u8; 20]> { + let sig = RecoverableSignature::from_compact( + &sig[0..64], + RecoveryId::from_i32(sig[64] as i32 - 27)?, + )?; + + let public = SECP256K1.recover_ecdsa(msg, &sig)?; + Ok(public_key_to_address(&public)) + } + + #[test] + fn recover() { + let proof = "01000000c13bd882edb37ffbabc9f9e34a0d9789633b850fe55e625b768cc8e5feed7d9f7ab536cbc210c2fcc1385aaf88d8a91d8adc2740245f9deee5fd3d61dd2a71662fb6639515f1e2f3354361a82d86c1952352c1a81b"; + let proof_bytes = hex::decode(proof).unwrap(); + let msg = "216ac5cd5a5e13b0c9a81efb1ad04526b9f4ddd2fe6ebc02819c5097dfb0958c"; + let msg_bytes = hex::decode(msg).unwrap(); + let proof_addr = recover_signer_unchecked( + &proof_bytes[24..].try_into().unwrap(), + &Message::from_slice(&msg_bytes).unwrap(), + ) + .unwrap(); + let priv_key = "324b5d1744ec27d6ac458350ce6a6248680bb0209521b2c730c1fe82a433eb54"; + let priv_key_bytes = hex::decode(priv_key).unwrap(); + let priv_key = SecretKey::from_slice(&priv_key_bytes).unwrap(); + let pubkey = PublicKey::from_secret_key_global(&priv_key); + let pub_addr = public_key_to_address(&pubkey); + assert_eq!(pub_addr, proof_addr); + println!("Public address: {:?}", pub_addr); + println!("Proof public address: {:?}", proof_addr); + } +}