alloy_primitives/signature/
utils.rs

1use crate::ChainId;
2
3/// Applies [EIP-155](https://eips.ethereum.org/EIPS/eip-155).
4#[inline]
5pub const fn to_eip155_v(v: u8, chain_id: ChainId) -> ChainId {
6    (v as u64) + 35 + chain_id * 2
7}
8
9/// Attempts to normalize the v value to a boolean parity value.
10///
11/// Returns `None` if the value is invalid for any of the known Ethereum parity encodings.
12#[inline]
13pub const fn normalize_v(v: u64) -> Option<bool> {
14    if !is_valid_v(v) {
15        return None;
16    }
17
18    // Simplifying:
19    //  0| 1 => v % 2 == 0
20    // 27|28 => (v - 27) % 2 == 0
21    //  35.. => (v - 35) % 2 == 0
22    // ---
23    //  0| 1 => v % 2 == 0
24    // 27|28 => v % 2 == 1
25    //  35.. => v % 2 == 1
26    // ---
27    //   ..2 => v % 2 == 0
28    //     _ => v % 2 == 1
29    let cmp = (v <= 1) as u64;
30    Some(v % 2 == cmp)
31}
32
33/// Returns `true` if the given `v` value is valid for any of the known Ethereum parity encodings.
34#[inline]
35const fn is_valid_v(v: u64) -> bool {
36    matches!(
37        v,
38        // Case 1: raw/bare
39        0 | 1
40        // Case 2: non-EIP-155 v value
41        | 27 | 28
42        // Case 3: EIP-155 V value
43        | 35..
44    )
45}
46
47/// Normalizes a `v` value, respecting raw, legacy, and EIP-155 values.
48///
49/// This function covers the entire u64 range, producing v-values as follows:
50/// - 0-26 - raw/bare. 0-3 are legal. In order to ensure that all values are covered, we also handle
51///   4-26 here by returning v % 4.
52/// - 27-34 - legacy. 27-30 are legal. By legacy bitcoin convention range 27-30 signals uncompressed
53///   pubkeys, while 31-34 signals compressed pubkeys. We do not respect the compression convention.
54///   All Ethereum keys are uncompressed.
55/// - 35+ - EIP-155. By EIP-155 convention, `v = 35 + CHAIN_ID * 2 + 0/1` We return (v-1 % 2) here.
56///
57/// NB: raw and legacy support values 2, and 3, while EIP-155 does not.
58/// Recovery values of 2 and 3 are unlikely to occur in practice. In the
59/// vanishingly unlikely event  that you encounter an EIP-155 signature with a
60/// recovery value of 2 or 3, you should normalize out of band.
61#[cfg(feature = "k256")]
62#[inline]
63pub(crate) const fn normalize_v_to_recid(v: u64) -> k256::ecdsa::RecoveryId {
64    let byte = normalize_v_to_byte(v);
65    debug_assert!(byte <= k256::ecdsa::RecoveryId::MAX);
66    match k256::ecdsa::RecoveryId::from_byte(byte) {
67        Some(recid) => recid,
68        None => unsafe { core::hint::unreachable_unchecked() },
69    }
70}
71
72/// Normalize the v value to a single byte.
73#[inline]
74pub(crate) const fn normalize_v_to_byte(v: u64) -> u8 {
75    match v {
76        // Case 1: raw/bare
77        0..=26 => (v % 4) as u8,
78        // Case 2: non-EIP-155 v value
79        27..=34 => ((v - 27) % 4) as u8,
80        // Case 3: EIP-155 V value
81        35.. => ((v - 1) % 2) as u8,
82    }
83}
84
85#[cfg(test)]
86mod test {
87    use super::*;
88
89    #[test]
90    fn normalizes_v() {
91        assert_eq!(normalize_v(0), Some(false));
92        assert_eq!(normalize_v(1), Some(true));
93
94        for invalid_v in 2..27 {
95            assert_eq!(normalize_v(invalid_v), None);
96        }
97
98        assert_eq!(normalize_v(27), Some(false));
99        assert_eq!(normalize_v(28), Some(true));
100
101        for invalid_v in 29..35 {
102            assert_eq!(normalize_v(invalid_v), None);
103        }
104
105        assert_eq!(normalize_v(35), Some(false));
106        assert_eq!(normalize_v(36), Some(true));
107        for v in 35..100 {
108            assert_eq!(normalize_v(v), Some((v - 35) % 2 != 0));
109        }
110    }
111
112    #[test]
113    #[cfg(feature = "k256")]
114    fn normalizes_v_to_recid() {
115        assert_eq!(normalize_v_to_recid(27), k256::ecdsa::RecoveryId::from_byte(0).unwrap());
116        assert_eq!(normalize_v_to_recid(28), k256::ecdsa::RecoveryId::from_byte(1).unwrap());
117    }
118}