Skip to content

Commit 9c15a1b

Browse files
committed
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 matter-labs/teepot#228
1 parent 78af2bf commit 9c15a1b

File tree

4 files changed

+83
-7
lines changed

4 files changed

+83
-7
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/bin/zksync_tee_prover/Cargo.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ anyhow.workspace = true
1616
async-trait.workspace = true
1717
envy.workspace = true
1818
reqwest = { workspace = true, features = ["zstd"] }
19-
secp256k1 = { workspace = true, features = ["serde"] }
19+
secp256k1 = { workspace = true, features = [
20+
"global-context",
21+
"recovery",
22+
"serde",
23+
] }
2024
serde = { workspace = true, features = ["derive"] }
2125
thiserror.workspace = true
2226
tokio = { workspace = true, features = ["full"] }
@@ -31,3 +35,7 @@ zksync_prover_interface.workspace = true
3135
zksync_tee_verifier.workspace = true
3236
zksync_types.workspace = true
3337
zksync_vlog.workspace = true
38+
39+
[dev-dependencies]
40+
hex.workspace = true
41+
sha3.workspace = true

core/bin/zksync_tee_prover/src/api_client.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use reqwest::{Client, Response, StatusCode};
2-
use secp256k1::{ecdsa::Signature, PublicKey};
2+
use secp256k1::PublicKey;
33
use serde::Serialize;
44
use url::Url;
55
use zksync_basic_types::H256;
@@ -87,13 +87,13 @@ impl TeeApiClient {
8787
pub async fn submit_proof(
8888
&self,
8989
batch_number: L1BatchNumber,
90-
signature: Signature,
90+
signature: [u8; 65],
9191
pubkey: &PublicKey,
9292
root_hash: H256,
9393
tee_type: TeeType,
9494
) -> Result<(), TeeProverError> {
9595
let request = SubmitTeeProofRequest(Box::new(L1BatchTeeProofForL1 {
96-
signature: signature.serialize_compact().into(),
96+
signature: signature.into(),
9797
pubkey: pubkey.serialize().into(),
9898
proof: root_hash.as_bytes().into(),
9999
tee_type,

core/bin/zksync_tee_prover/src/tee_prover.rs

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::fmt;
22

3-
use secp256k1::{ecdsa::Signature, Message, PublicKey, Secp256k1};
3+
use secp256k1::{Message, PublicKey, Secp256k1, SecretKey, SECP256K1};
44
use zksync_basic_types::H256;
55
use zksync_node_framework::{
66
service::StopReceiver,
@@ -67,10 +67,24 @@ impl fmt::Debug for TeeProver {
6767
}
6868

6969
impl TeeProver {
70+
/// Signs the message in Ethereum-compatible format for on-chain verification.
71+
fn sign_message(&self, sec: &SecretKey, message: Message) -> Result<[u8; 65], TeeProverError> {
72+
let s = SECP256K1.sign_ecdsa_recoverable(&message, sec);
73+
let (rec_id, data) = s.serialize_compact();
74+
75+
let mut signature = [0u8; 65];
76+
signature[..64].copy_from_slice(&data);
77+
// as defined in the Ethereum Yellow Paper (Appendix F)
78+
// https://ethereum.github.io/yellowpaper/paper.pdf
79+
signature[64] = 27 + rec_id.to_i32() as u8;
80+
81+
Ok(signature)
82+
}
83+
7084
fn verify(
7185
&self,
7286
tvi: TeeVerifierInput,
73-
) -> Result<(Signature, L1BatchNumber, H256), TeeProverError> {
87+
) -> Result<([u8; 65], L1BatchNumber, H256), TeeProverError> {
7488
match tvi {
7589
TeeVerifierInput::V1(tvi) => {
7690
let observer = METRICS.proof_generation_time.start();
@@ -79,7 +93,7 @@ impl TeeProver {
7993
let batch_number = verification_result.batch_number;
8094
let msg_to_sign = Message::from_slice(root_hash_bytes)
8195
.map_err(|e| TeeProverError::Verification(e.into()))?;
82-
let signature = self.config.signing_key.sign_ecdsa(msg_to_sign);
96+
let signature = self.sign_message(&self.config.signing_key, msg_to_sign)?;
8397
let duration = observer.observe();
8498
tracing::info!(
8599
proof_generation_time = duration.as_secs_f64(),
@@ -182,3 +196,55 @@ impl Task for TeeProver {
182196
}
183197
}
184198
}
199+
200+
#[cfg(test)]
201+
mod tests {
202+
use super::*;
203+
use anyhow::Result;
204+
use secp256k1::ecdsa::{RecoverableSignature, RecoveryId};
205+
use sha3::{Digest, Keccak256};
206+
207+
/// Converts a public key into an Ethereum address by hashing the encoded public key with Keccak256.
208+
pub fn public_key_to_address(public: &PublicKey) -> [u8; 20] {
209+
let public_key_bytes = public.serialize_uncompressed();
210+
211+
// Skip the first byte (0x04) which indicates uncompressed key
212+
let hash: [u8; 32] = Keccak256::digest(&public_key_bytes[1..]).into();
213+
214+
// Take the last 20 bytes of the hash to get the Ethereum address
215+
let mut address = [0u8; 20];
216+
address.copy_from_slice(&hash[12..]);
217+
address
218+
}
219+
220+
/// Equivalent to the ecrecover precompile.
221+
pub fn recover_signer_unchecked(sig: &[u8; 65], msg: &Message) -> Result<[u8; 20]> {
222+
let sig = RecoverableSignature::from_compact(
223+
&sig[0..64],
224+
RecoveryId::from_i32(sig[64] as i32 - 27)?,
225+
)?;
226+
let public = SECP256K1.recover_ecdsa(msg, &sig)?;
227+
Ok(public_key_to_address(&public))
228+
}
229+
230+
#[test]
231+
/// Credit to:
232+
/// https://github.com/taikoxyz/raiko/blob/ddba6b0add73f05778617e3950ffafc371b9293d/provers/sgx/guest/src/signature.rs#L67
233+
fn recover() {
234+
let proof = "01000000c13bd882edb37ffbabc9f9e34a0d9789633b850fe55e625b768cc8e5feed7d9f7ab536cbc210c2fcc1385aaf88d8a91d8adc2740245f9deee5fd3d61dd2a71662fb6639515f1e2f3354361a82d86c1952352c1a81b";
235+
let proof_bytes = hex::decode(proof).unwrap();
236+
let msg = "216ac5cd5a5e13b0c9a81efb1ad04526b9f4ddd2fe6ebc02819c5097dfb0958c";
237+
let msg_bytes = hex::decode(msg).unwrap();
238+
let proof_addr = recover_signer_unchecked(
239+
&proof_bytes[24..].try_into().unwrap(),
240+
&Message::from_slice(&msg_bytes).unwrap(),
241+
)
242+
.unwrap();
243+
let priv_key = "324b5d1744ec27d6ac458350ce6a6248680bb0209521b2c730c1fe82a433eb54";
244+
let priv_key_bytes = hex::decode(priv_key).unwrap();
245+
let priv_key = SecretKey::from_slice(&priv_key_bytes).unwrap();
246+
let pubkey = PublicKey::from_secret_key_global(&priv_key);
247+
let pub_addr = public_key_to_address(&pubkey);
248+
assert_eq!(pub_addr, proof_addr);
249+
}
250+
}

0 commit comments

Comments
 (0)