Skip to main content

defuse_erc191/
lib.rs

1use defuse_crypto::{Curve, Secp256k1};
2use impl_tools::autoimpl;
3
4/// See [ERC-191](https://github.com/ethereum/ercs/blob/master/ERCS/erc-191.md)
5#[cfg_attr(
6    feature = "serde",
7    derive(::serde::Serialize, ::serde::Deserialize),
8    cfg_attr(feature = "abi", derive(::schemars::JsonSchema))
9)]
10#[derive(Debug, Clone)]
11pub struct Erc191Payload(pub String);
12
13impl defuse_crypto::Payload for Erc191Payload {
14    #[inline]
15    fn hash(&self) -> defuse_crypto::CryptoHash {
16        use defuse_digest::{Digest, sha3::Keccak256};
17
18        Keccak256::new_with_prefix(b"\x19Ethereum Signed Message:\n")
19            .chain_update(self.0.len().to_string())
20            .chain_update(self.0.as_bytes())
21            .finalize()
22            .into()
23    }
24}
25
26#[cfg_attr(
27    feature = "serde",
28    ::cfg_eval::cfg_eval,
29    ::serde_with::serde_as,
30    derive(::serde::Serialize, ::serde::Deserialize),
31    cfg_attr(feature = "abi", derive(::schemars::JsonSchema))
32)]
33#[autoimpl(Deref using self.payload)]
34#[derive(Debug, Clone)]
35pub struct SignedErc191Payload {
36    pub payload: Erc191Payload,
37
38    /// There is no public key member because the public key can be recovered
39    /// via `ecrecover()` knowing the data and the signature
40    #[cfg_attr(
41        feature = "serde",
42        serde_as(as = "defuse_crypto::serde::AsCurve<Secp256k1>")
43    )]
44    pub signature: <Secp256k1 as Curve>::Signature,
45}
46
47impl defuse_crypto::Payload for SignedErc191Payload {
48    #[inline]
49    fn hash(&self) -> defuse_crypto::CryptoHash {
50        self.payload.hash()
51    }
52}
53
54#[cfg(any(test, feature = "near-contract"))]
55const _: () = {
56    use defuse_crypto::{Payload, SignedPayload, VerifiableCurve};
57    impl SignedPayload for SignedErc191Payload {
58        type PublicKey = <Secp256k1 as Curve>::PublicKey;
59
60        #[inline]
61        fn verify(&self) -> Option<Self::PublicKey> {
62            Secp256k1::verify(&self.signature, &self.payload.hash(), &())
63        }
64    }
65};
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use defuse_crypto::SignedPayload;
71    use hex_literal::hex;
72
73    const fn fix_v_in_signature(mut sig: [u8; 65]) -> [u8; 65] {
74        if *sig.last().unwrap() >= 27 {
75            // Ethereum only uses uncompressed keys, with corresponding value v=27/28
76            // https://bitcoin.stackexchange.com/a/38909/58790
77            *sig.last_mut().unwrap() -= 27;
78        }
79        sig
80    }
81
82    // Signature constructed in Metamask, using private key: a4b319a82adfc43584e4537fec97a80516e16673db382cd91eba97abbab8ca56
83    const REFERENCE_SIGNATURE: [u8; 65] = hex!(
84        "7800a70d05cde2c49ed546a6ce887ce6027c2c268c0285f6efef0cdfc4366b23643790f67a86468ee8301ed12cfffcb07c6530f90a9327ec057800fabd332e471c"
85    );
86    const INVALID_REFERENCE_SIGNATURE: [u8; 65] = hex!(
87        "7900a70d05cde2c49ed546a6ce887ce6027c2c268c0285f6efef0cdfc4366b23643790f67a86468ee8301ed12cfffcb07c6530f90a9327ec057800fabd332e471c"
88    );
89    const REFERENCE_MESSAGE: &str = "Hello world!";
90    const INVALID_REFERENCE_MESSAGE: &str = "Hello, NEAR!";
91
92    // Public key can be derived using `ethers_signers` crate:
93    // let wallet = LocalWallet::from_str(
94    //     "a4b319a82adfc43584e4537fec97a80516e16673db382cd91eba97abbab8ca56",
95    // )?;
96    // let signing_key = wallet.signer();
97    // let verifying_key = signing_key.verifying_key();
98    // let public_key = verifying_key.to_encoded_point(false);
99    // // Notice that we skip the first byte, 0x04
100    // println!("Public key: 0x{}", hex::encode(public_key.as_bytes()[1..]));
101    const REFERENCE_PUBKEY: [u8; 64] = hex!(
102        "85a66984273f338ce4ef7b85e5430b008307e8591bb7c1b980852cf6423770b801f41e9438155eb53a5e20f748640093bb42ae3aeca035f7b7fd7a1a21f22f68"
103    );
104
105    #[test]
106    fn test_reference_signature_verification_works() {
107        assert_eq!(
108            SignedErc191Payload {
109                payload: Erc191Payload(REFERENCE_MESSAGE.to_string()),
110                signature: fix_v_in_signature(REFERENCE_SIGNATURE),
111            }
112            .verify(),
113            Some(REFERENCE_PUBKEY)
114        );
115    }
116
117    #[test]
118    fn test_invalid_reference_message_verification_fails() {
119        assert_ne!(
120            SignedErc191Payload {
121                payload: Erc191Payload(INVALID_REFERENCE_MESSAGE.to_string()),
122                signature: fix_v_in_signature(REFERENCE_SIGNATURE),
123            }
124            .verify(),
125            Some(REFERENCE_PUBKEY)
126        );
127    }
128
129    #[test]
130    fn test_invalid_reference_signature_verification_fails() {
131        assert_ne!(
132            SignedErc191Payload {
133                payload: Erc191Payload(REFERENCE_MESSAGE.to_string()),
134                signature: fix_v_in_signature(INVALID_REFERENCE_SIGNATURE),
135            }
136            .verify(),
137            Some(REFERENCE_PUBKEY)
138        );
139    }
140}