use k256::ecdsa::signature::hazmat::PrehashSigner;
use k256::ecdsa::{RecoveryId, Signature as K256Sig};
use tiny_keccak::{Hasher, Keccak};
use crate::error::ClientError;
use crate::wallet::key::Wallet;
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Signature {
pub r: [u8; 32],
pub s: [u8; 32],
pub v: u8,
}
impl Signature {
#[must_use]
pub fn to_bytes(&self) -> [u8; 65] {
let mut out = [0u8; 65];
out[..32].copy_from_slice(&self.r);
out[32..64].copy_from_slice(&self.s);
out[64] = self.v;
out
}
#[must_use]
pub fn to_hex(&self) -> String {
format!("0x{}", hex::encode(self.to_bytes()))
}
}
impl core::fmt::Debug for Signature {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Signature")
.field("r", &hex::encode(self.r))
.field("s", &hex::encode(self.s))
.field("v", &self.v)
.finish()
}
}
pub trait Eip712 {
fn domain_separator(&self) -> [u8; 32];
fn struct_hash(&self) -> [u8; 32];
fn to_digest(&self) -> [u8; 32] {
let domain = self.domain_separator();
let strukt = self.struct_hash();
let mut hasher = Keccak::v256();
hasher.update(&[0x19, 0x01]);
hasher.update(&domain);
hasher.update(&strukt);
let mut out = [0u8; 32];
hasher.finalize(&mut out);
out
}
}
impl Wallet {
pub fn sign_eip712<T: Eip712>(&self, msg: &T) -> Result<Signature, ClientError> {
let digest = msg.to_digest();
self.sign_digest(&digest)
}
pub fn sign_digest(&self, digest: &[u8; 32]) -> Result<Signature, ClientError> {
let signing_key = self.signing_key();
let (sig, recid): (K256Sig, RecoveryId) = signing_key.sign_prehash(digest)?;
let bytes = sig.to_bytes();
let mut r = [0u8; 32];
let mut s = [0u8; 32];
r.copy_from_slice(&bytes[..32]);
s.copy_from_slice(&bytes[32..]);
let v = 27 + recid.to_byte();
Ok(Signature { r, s, v })
}
}
pub(crate) fn recover_address(
digest: &[u8; 32],
sig: &Signature,
) -> Result<crate::wallet::key::Address, ClientError> {
use k256::ecdsa::VerifyingKey;
let mut sig_bytes = [0u8; 64];
sig_bytes[..32].copy_from_slice(&sig.r);
sig_bytes[32..].copy_from_slice(&sig.s);
let k_sig = K256Sig::from_slice(&sig_bytes)
.map_err(|e| ClientError::Signature(format!("sig decode: {e}")))?;
let recid_byte = sig
.v
.checked_sub(27)
.ok_or_else(|| ClientError::Signature(format!("invalid v = {}", sig.v)))?;
let recid = RecoveryId::from_byte(recid_byte)
.ok_or_else(|| ClientError::Signature(format!("invalid recovery id = {recid_byte}")))?;
let verifying = VerifyingKey::recover_from_prehash(digest, &k_sig, recid)?;
let point = verifying.to_encoded_point(false);
let pubkey_bytes = point.as_bytes();
let mut hasher = Keccak::v256();
hasher.update(&pubkey_bytes[1..]);
let mut h = [0u8; 32];
hasher.finalize(&mut h);
let mut addr = [0u8; 20];
addr.copy_from_slice(&h[12..]);
Ok(crate::wallet::key::Address::from_bytes(addr))
}
#[cfg(test)]
mod tests {
use super::*;
struct ToyMsg {
domain: [u8; 32],
strukt: [u8; 32],
}
impl Eip712 for ToyMsg {
fn domain_separator(&self) -> [u8; 32] {
self.domain
}
fn struct_hash(&self) -> [u8; 32] {
self.strukt
}
}
#[test]
fn signature_round_trip_recovers_signer() {
let wallet = Wallet::random_for_testing();
let msg = ToyMsg {
domain: [0xAAu8; 32],
strukt: [0xBBu8; 32],
};
let sig = wallet.sign_eip712(&msg).unwrap();
let recovered = recover_address(&msg.to_digest(), &sig).unwrap();
assert_eq!(recovered, wallet.address());
}
#[test]
fn rfc6979_signing_is_deterministic() {
let wallet = Wallet::random_for_testing();
let msg = ToyMsg {
domain: [0x01u8; 32],
strukt: [0x02u8; 32],
};
let sig_a = wallet.sign_eip712(&msg).unwrap();
let sig_b = wallet.sign_eip712(&msg).unwrap();
assert_eq!(sig_a, sig_b, "RFC-6979 must produce identical signatures");
}
#[test]
fn digest_matches_keccak_envelope() {
let domain = [0xAAu8; 32];
let strukt = [0xBBu8; 32];
let msg = ToyMsg { domain, strukt };
let mut hasher = Keccak::v256();
hasher.update(&[0x19, 0x01]);
hasher.update(&domain);
hasher.update(&strukt);
let mut expected = [0u8; 32];
hasher.finalize(&mut expected);
assert_eq!(msg.to_digest(), expected);
}
#[test]
fn signature_hex_is_132_chars() {
let wallet = Wallet::random_for_testing();
let msg = ToyMsg {
domain: [0u8; 32],
strukt: [0u8; 32],
};
let sig = wallet.sign_eip712(&msg).unwrap();
let h = sig.to_hex();
assert!(h.starts_with("0x"));
assert_eq!(h.len(), 132); }
}