pink_web3/
signing.rs

1//! Signing capabilities and utilities.
2use crate::prelude::*;
3use crate::types::H256;
4
5/// Error during signing.
6#[derive(Debug, derive_more::Display, PartialEq, Clone)]
7pub enum SigningError {
8    /// A message to sign is invalid. Has to be a non-zero 32-bytes slice.
9    #[display(fmt = "Message has to be a non-zero 32-bytes slice.")]
10    InvalidMessage,
11}
12#[cfg(feature = "std")]
13impl std::error::Error for SigningError {}
14
15/// Error during sender recovery.
16#[derive(Debug, derive_more::Display, PartialEq, Clone)]
17pub enum RecoveryError {
18    /// A message to recover is invalid. Has to be a non-zero 32-bytes slice.
19    #[display(fmt = "Message has to be a non-zero 32-bytes slice.")]
20    InvalidMessage,
21    /// A signature is invalid and the sender could not be recovered.
22    #[display(fmt = "Signature is invalid (check recovery id).")]
23    InvalidSignature,
24}
25#[cfg(feature = "std")]
26impl std::error::Error for RecoveryError {}
27
28#[cfg(feature = "signing")]
29pub use feature_gated::*;
30
31#[cfg(feature = "signing")]
32mod feature_gated {
33    use super::*;
34    use crate::types::Address;
35    /// A trait representing ethereum-compatible key with signing capabilities.
36    ///
37    /// The purpose of this trait is to prevent leaking `secp256k1::SecretKey` struct
38    /// in stack or memory.
39    /// To use secret keys securely, they should be wrapped in a struct that prevents
40    /// leaving copies in memory (both when it's moved or dropped). Please take a look
41    /// at:
42    /// - https://github.com/graphprotocol/solidity-bindgen/blob/master/solidity-bindgen/src/secrets.rs
43    /// - or https://crates.io/crates/zeroize
44    /// if you care enough about your secrets to be used securely.
45    ///
46    /// If it's enough to pass a reference to `SecretKey` (lifetimes) than you can use `SecretKeyRef`
47    /// wrapper.
48    pub trait Key {
49        /// Sign given message and include chain-id replay protection.
50        ///
51        /// When a chain ID is provided, the `Signature`'s V-value will have chain replay
52        /// protection added (as per EIP-155). Otherwise, the V-value will be in
53        /// 'Electrum' notation.
54        fn sign(&self, message: &[u8; 32], chain_id: Option<u64>) -> Result<Signature, SigningError>;
55
56        /// Sign given message without manipulating V-value; used for typed transactions
57        /// (AccessList and EIP-1559)
58        fn sign_message(&self, message: &[u8; 32]) -> Result<Signature, SigningError>;
59
60        /// Get public address that this key represents.
61        fn address(&self) -> Address;
62    }
63}
64
65/// A struct that represents the components of a secp256k1 signature.
66pub struct Signature {
67    /// V component in electrum format with chain-id replay protection.
68    pub v: u64,
69    /// R component of the signature.
70    pub r: H256,
71    /// S component of the signature.
72    pub s: H256,
73}
74
75/// Compute the Keccak-256 hash of input bytes.
76pub fn keccak256(bytes: &[u8]) -> [u8; 32] {
77    use tiny_keccak::{Hasher, Keccak};
78    let mut output = [0u8; 32];
79    let mut hasher = Keccak::v256();
80    hasher.update(bytes);
81    hasher.finalize(&mut output);
82    output
83}
84
85/// Result of the name hash algotithm.
86pub type NameHash = [u8; 32];
87
88/// Compute the hash of a domain name using the namehash algorithm.
89///
90/// [Specification](https://docs.ens.domains/contract-api-reference/name-processing#hashing-names)
91pub fn namehash(name: &str) -> NameHash {
92    let mut node = [0u8; 32];
93
94    if name.is_empty() {
95        return node;
96    }
97
98    let mut labels: Vec<&str> = name.split('.').collect();
99
100    labels.reverse();
101
102    for label in labels.iter() {
103        let label_hash = keccak256(label.as_bytes());
104
105        node = keccak256(&[node, label_hash].concat());
106    }
107
108    node
109}
110
111/// Hash a message according to EIP-191.
112///
113/// The data is a UTF-8 encoded string and will enveloped as follows:
114/// `"\x19Ethereum Signed Message:\n" + message.length + message` and hashed
115/// using keccak256.
116pub fn hash_message<S>(message: S) -> H256
117where
118    S: AsRef<[u8]>,
119{
120    let message = message.as_ref();
121
122    let mut eth_message = format!("\x19Ethereum Signed Message:\n{}", message.len()).into_bytes();
123    eth_message.extend_from_slice(message);
124
125    keccak256(&eth_message).into()
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    //See -> https://eips.ethereum.org/EIPS/eip-137 for test cases
133
134    #[test]
135    fn name_hash_empty() {
136        let input = "";
137
138        let result = namehash(input);
139
140        let expected = [0u8; 32];
141
142        assert_eq!(expected, result);
143    }
144
145    #[test]
146    fn name_hash_eth() {
147        let input = "eth";
148
149        let result = namehash(input);
150
151        let expected = [
152            0x93, 0xcd, 0xeb, 0x70, 0x8b, 0x75, 0x45, 0xdc, 0x66, 0x8e, 0xb9, 0x28, 0x01, 0x76, 0x16, 0x9d, 0x1c, 0x33,
153            0xcf, 0xd8, 0xed, 0x6f, 0x04, 0x69, 0x0a, 0x0b, 0xcc, 0x88, 0xa9, 0x3f, 0xc4, 0xae,
154        ];
155
156        assert_eq!(expected, result);
157    }
158
159    #[test]
160    fn name_hash_foo_eth() {
161        let input = "foo.eth";
162
163        let result = namehash(input);
164
165        let expected = [
166            0xde, 0x9b, 0x09, 0xfd, 0x7c, 0x5f, 0x90, 0x1e, 0x23, 0xa3, 0xf1, 0x9f, 0xec, 0xc5, 0x48, 0x28, 0xe9, 0xc8,
167            0x48, 0x53, 0x98, 0x01, 0xe8, 0x65, 0x91, 0xbd, 0x98, 0x01, 0xb0, 0x19, 0xf8, 0x4f,
168        ];
169
170        assert_eq!(expected, result);
171    }
172}