Skip to content

Commit

Permalink
feat(tee): add support for recoverable signatures
Browse files Browse the repository at this point in the history
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 matter-labs/teepot#228
  • Loading branch information
pbeza committed Dec 30, 2024
1 parent 78af2bf commit 1dc079c
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 7 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

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

10 changes: 9 additions & 1 deletion core/bin/zksync_tee_prover/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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
6 changes: 3 additions & 3 deletions core/bin/zksync_tee_prover/src/api_client.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
Expand Down
84 changes: 81 additions & 3 deletions core/bin/zksync_tee_prover/src/tee_prover.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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();
Expand All @@ -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(),
Expand Down Expand Up @@ -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);
}
}

0 comments on commit 1dc079c

Please sign in to comment.