metaflux_client/wallet/
sign.rs1use k256::ecdsa::signature::hazmat::PrehashSigner;
16use k256::ecdsa::{RecoveryId, Signature as K256Sig};
17use tiny_keccak::{Hasher, Keccak};
18
19use crate::error::ClientError;
20use crate::wallet::key::Wallet;
21
22#[derive(Clone, Copy, PartialEq, Eq)]
27pub struct Signature {
28 pub r: [u8; 32],
30 pub s: [u8; 32],
32 pub v: u8,
34}
35
36impl Signature {
37 #[must_use]
39 pub fn to_bytes(&self) -> [u8; 65] {
40 let mut out = [0u8; 65];
41 out[..32].copy_from_slice(&self.r);
42 out[32..64].copy_from_slice(&self.s);
43 out[64] = self.v;
44 out
45 }
46
47 #[must_use]
49 pub fn to_hex(&self) -> String {
50 format!("0x{}", hex::encode(self.to_bytes()))
51 }
52}
53
54impl core::fmt::Debug for Signature {
55 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
56 f.debug_struct("Signature")
57 .field("r", &hex::encode(self.r))
58 .field("s", &hex::encode(self.s))
59 .field("v", &self.v)
60 .finish()
61 }
62}
63
64pub trait Eip712 {
77 fn domain_separator(&self) -> [u8; 32];
79
80 fn struct_hash(&self) -> [u8; 32];
82
83 fn to_digest(&self) -> [u8; 32] {
87 let domain = self.domain_separator();
88 let strukt = self.struct_hash();
89 let mut hasher = Keccak::v256();
90 hasher.update(&[0x19, 0x01]);
91 hasher.update(&domain);
92 hasher.update(&strukt);
93 let mut out = [0u8; 32];
94 hasher.finalize(&mut out);
95 out
96 }
97}
98
99impl Wallet {
100 pub fn sign_eip712<T: Eip712>(&self, msg: &T) -> Result<Signature, ClientError> {
109 let digest = msg.to_digest();
110 self.sign_digest(&digest)
111 }
112
113 pub fn sign_digest(&self, digest: &[u8; 32]) -> Result<Signature, ClientError> {
121 let signing_key = self.signing_key();
122 let (sig, recid): (K256Sig, RecoveryId) = signing_key.sign_prehash(digest)?;
123 let bytes = sig.to_bytes();
125 let mut r = [0u8; 32];
126 let mut s = [0u8; 32];
127 r.copy_from_slice(&bytes[..32]);
128 s.copy_from_slice(&bytes[32..]);
129 let v = 27 + recid.to_byte();
130 Ok(Signature { r, s, v })
131 }
132}
133
134pub(crate) fn recover_address(
141 digest: &[u8; 32],
142 sig: &Signature,
143) -> Result<crate::wallet::key::Address, ClientError> {
144 use k256::ecdsa::VerifyingKey;
145
146 let mut sig_bytes = [0u8; 64];
147 sig_bytes[..32].copy_from_slice(&sig.r);
148 sig_bytes[32..].copy_from_slice(&sig.s);
149 let k_sig = K256Sig::from_slice(&sig_bytes)
150 .map_err(|e| ClientError::Signature(format!("sig decode: {e}")))?;
151 let recid_byte = sig
152 .v
153 .checked_sub(27)
154 .ok_or_else(|| ClientError::Signature(format!("invalid v = {}", sig.v)))?;
155 let recid = RecoveryId::from_byte(recid_byte)
156 .ok_or_else(|| ClientError::Signature(format!("invalid recovery id = {recid_byte}")))?;
157
158 let verifying = VerifyingKey::recover_from_prehash(digest, &k_sig, recid)?;
159 let point = verifying.to_encoded_point(false);
160 let pubkey_bytes = point.as_bytes();
161 let mut hasher = Keccak::v256();
162 hasher.update(&pubkey_bytes[1..]);
163 let mut h = [0u8; 32];
164 hasher.finalize(&mut h);
165 let mut addr = [0u8; 20];
166 addr.copy_from_slice(&h[12..]);
167 Ok(crate::wallet::key::Address::from_bytes(addr))
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 struct ToyMsg {
177 domain: [u8; 32],
178 strukt: [u8; 32],
179 }
180 impl Eip712 for ToyMsg {
181 fn domain_separator(&self) -> [u8; 32] {
182 self.domain
183 }
184 fn struct_hash(&self) -> [u8; 32] {
185 self.strukt
186 }
187 }
188
189 #[test]
190 fn signature_round_trip_recovers_signer() {
191 let wallet = Wallet::random_for_testing();
192 let msg = ToyMsg {
193 domain: [0xAAu8; 32],
194 strukt: [0xBBu8; 32],
195 };
196 let sig = wallet.sign_eip712(&msg).unwrap();
197 let recovered = recover_address(&msg.to_digest(), &sig).unwrap();
198 assert_eq!(recovered, wallet.address());
199 }
200
201 #[test]
202 fn rfc6979_signing_is_deterministic() {
203 let wallet = Wallet::random_for_testing();
204 let msg = ToyMsg {
205 domain: [0x01u8; 32],
206 strukt: [0x02u8; 32],
207 };
208 let sig_a = wallet.sign_eip712(&msg).unwrap();
209 let sig_b = wallet.sign_eip712(&msg).unwrap();
210 assert_eq!(sig_a, sig_b, "RFC-6979 must produce identical signatures");
211 }
212
213 #[test]
214 fn digest_matches_keccak_envelope() {
215 let domain = [0xAAu8; 32];
216 let strukt = [0xBBu8; 32];
217 let msg = ToyMsg { domain, strukt };
218
219 let mut hasher = Keccak::v256();
221 hasher.update(&[0x19, 0x01]);
222 hasher.update(&domain);
223 hasher.update(&strukt);
224 let mut expected = [0u8; 32];
225 hasher.finalize(&mut expected);
226
227 assert_eq!(msg.to_digest(), expected);
228 }
229
230 #[test]
231 fn signature_hex_is_132_chars() {
232 let wallet = Wallet::random_for_testing();
233 let msg = ToyMsg {
234 domain: [0u8; 32],
235 strukt: [0u8; 32],
236 };
237 let sig = wallet.sign_eip712(&msg).unwrap();
238 let h = sig.to_hex();
239 assert!(h.starts_with("0x"));
240 assert_eq!(h.len(), 132); }
242}