Skip to content

Commit 63a7712

Browse files
committed
feat(tee): add support for recoverable signatures
This PR is part of the effort to implement on-chain TEE proof verification. 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 63a7712

File tree

4 files changed

+93
-7
lines changed

4 files changed

+93
-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: 79 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+
pub fn sign_message(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 = TeeProver::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,65 @@ impl Task for TeeProver {
182196
}
183197
}
184198
}
199+
200+
#[cfg(test)]
201+
mod tests {
202+
use anyhow::Result;
203+
use secp256k1::ecdsa::{RecoverableSignature, RecoveryId};
204+
use sha3::{Digest, Keccak256};
205+
206+
use super::*;
207+
208+
/// Converts a public key into an Ethereum address by hashing the encoded public key with Keccak256.
209+
pub fn public_key_to_ethereum_address(public: &PublicKey) -> [u8; 20] {
210+
let public_key_bytes = public.serialize_uncompressed();
211+
212+
// Skip the first byte (0x04) which indicates uncompressed key
213+
let hash: [u8; 32] = Keccak256::digest(&public_key_bytes[1..]).into();
214+
215+
// Take the last 20 bytes of the hash to get the Ethereum address
216+
let mut address = [0u8; 20];
217+
address.copy_from_slice(&hash[12..]);
218+
address
219+
}
220+
221+
/// Equivalent to the ecrecover precompile, ensuring that the signatures we produce off-chain
222+
/// can be recovered on-chain.
223+
pub fn recover_signer(sig: &[u8; 65], msg: &Message) -> Result<[u8; 20]> {
224+
let sig = RecoverableSignature::from_compact(
225+
&sig[0..64],
226+
RecoveryId::from_i32(sig[64] as i32 - 27)?,
227+
)?;
228+
let public = SECP256K1.recover_ecdsa(msg, &sig)?;
229+
Ok(public_key_to_ethereum_address(&public))
230+
}
231+
232+
#[test]
233+
fn recover() {
234+
// Decode the sample secret key, generate the public key, and derive the Ethereum address
235+
// from the public key
236+
let secp = Secp256k1::new();
237+
let secret_key_bytes =
238+
hex::decode("c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3")
239+
.unwrap();
240+
let secret_key = SecretKey::from_slice(&secret_key_bytes).unwrap();
241+
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
242+
let expected_address = hex::decode("627306090abaB3A6e1400e9345bC60c78a8BEf57").unwrap();
243+
let address = public_key_to_ethereum_address(&public_key);
244+
245+
assert_eq!(address, expected_address.as_slice());
246+
247+
// Generate a random root hash, create a message from the hash, and sign the message using
248+
// the secret key
249+
let root_hash = H256::random();
250+
let root_hash_bytes = root_hash.as_bytes();
251+
let msg_to_sign = Message::from_slice(root_hash_bytes).unwrap();
252+
let signature = TeeProver::sign_message(&secret_key, msg_to_sign).unwrap();
253+
254+
// Recover the signer's Ethereum address from the signature and the message, and verify it
255+
// matches the expected address
256+
let proof_addr = recover_signer(&signature, &msg_to_sign).unwrap();
257+
258+
assert_eq!(proof_addr, expected_address.as_slice());
259+
}
260+
}

0 commit comments

Comments
 (0)