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 1c239e7
Show file tree
Hide file tree
Showing 4 changed files with 93 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
82 changes: 79 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,6 @@
use std::fmt;

use secp256k1::{ecdsa::Signature, Message, PublicKey, Secp256k1};
use secp256k1::{Message, PublicKey, Secp256k1, SecretKey, SECP256K1};
use zksync_basic_types::H256;
use zksync_node_framework::{
service::StopReceiver,
Expand Down Expand Up @@ -67,10 +67,24 @@ impl fmt::Debug for TeeProver {
}

impl TeeProver {
/// Signs the message in Ethereum-compatible format for on-chain verification.
pub fn sign_message(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 +93,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 = TeeProver::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 +196,65 @@ impl Task for TeeProver {
}
}
}

#[cfg(test)]
mod tests {
use anyhow::Result;
use secp256k1::ecdsa::{RecoverableSignature, RecoveryId};
use sha3::{Digest, Keccak256};

use super::*;

/// 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
}

/// Equivalent to the ecrecover precompile, ensuring that the signatures we produce off-chain
/// can be recovered on-chain.
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() {
// Decode the sample secret key, generate the public key, and derive the Ethereum address
// from the public key
let secp = Secp256k1::new();
let secret_key_bytes =
hex::decode("c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3")
.unwrap();
let secret_key = SecretKey::from_slice(&secret_key_bytes).unwrap();
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
let expected_address = hex::decode("627306090abaB3A6e1400e9345bC60c78a8BEf57").unwrap();
let address = public_key_to_address(&public_key);

assert_eq!(address, expected_address.as_slice());

// Generate a random root hash, create a message from the hash, and sign the message using
// the secret key
let root_hash = H256::random();
let root_hash_bytes = root_hash.as_bytes();
let msg_to_sign = Message::from_slice(root_hash_bytes).unwrap();
let signature = TeeProver::sign_message(&secret_key, msg_to_sign).unwrap();

// Recover the signer's Ethereum address from the signature and the message, and verify it
// matches the expected address
let proof_addr = recover_signer_unchecked(&signature, &msg_to_sign).unwrap();

assert_eq!(proof_addr, expected_address.as_slice());
}
}

0 comments on commit 1c239e7

Please sign in to comment.