Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tee): add support for recoverable signatures #3414

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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
pbeza marked this conversation as resolved.
Show resolved Hide resolved
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the zksync_crypto_primitives library; it has this functionality (and one used in the unit tests below) implemented.

Copy link
Collaborator Author

@pbeza pbeza Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was excited to hear we had all the crypto primitives ready to reuse, but when I dove in, it turned out they were kinda inconvenient or even impossible to use because:

  1. Some primitives are defined as pub(super), pub(crate), or totally private. I don't really get why, TBH.
  2. Some of our crypto wrappers are way less convenient than the primitives they actually wrap.

I reused what I could, but I still had to almost copy-paste some of the existing primitives. :( Overall it seems the number of LOC increased. :P Lemme know if I'm missing something.

/// Recovers the public key from the signature for the message
fn recover(signature: &Signature, message: &Message) -> Result<Public> {
let rsig = RecoverableSignature::from_compact(
&signature[0..64],
RecoveryId::from_i32(signature[64] as i32 - 27)?,
)?;
let pubkey = &SECP256K1.recover_ecdsa(&Message::from_slice(&message[..])?, &rsig)?;
let serialized = pubkey.serialize_uncompressed();
let mut public = Public::default();
public.as_bytes_mut().copy_from_slice(&serialized[1..65]);
Ok(public)
}
/// Convert public key into the address
fn public_to_address(public: &Public) -> Address {
let hash = keccak256(public.as_bytes());
let mut result = Address::zero();
result.as_bytes_mut().copy_from_slice(&hash[12..]);
result

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@slowli, kindly ping.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing primitives are private because so far, there was no use case to make them public, and keeping stuff private by default is a good practice. Now that such a use case has appeared, I'd suggest to make the necessary primitives public and export them from crypto_primitives.

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)?;
pbeza marked this conversation as resolved.
Show resolved Hide resolved
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_ethereum_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(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_ethereum_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_ethereum_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(&signature, &msg_to_sign).unwrap();

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