aurora_engine_precompiles/
secp256r1.rs

1//! # Precompile for secp256r1 operations.
2//!
3//! <https://eips.ethereum.org/EIPS/eip-7951>
4
5use crate::prelude::types::{make_address, Address, EthGas};
6use crate::{EvmPrecompileResult, Precompile, PrecompileOutput, Vec};
7use aurora_evm::{Context, ExitError};
8use p256::{
9    ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey},
10    EncodedPoint,
11};
12
13/// Base gas fee for secp256r1
14pub const P256VERIFY_BASE_GAS_FEE: u64 = 6900;
15
16/// Input length for secp256r1: 32 x 5 = 160 bytes
17const INPUT_LENGTH: usize = 160;
18
19/// Success result: 32 bytes with last byte set to 1
20const SUCCESS_RESULT: [u8; 32] = [
21    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
22];
23
24pub struct Secp256r1;
25
26impl Secp256r1 {
27    pub const ADDRESS: Address = make_address(0, 0x100);
28
29    /// Executes the P256VERIFY operation (ECDSA signature verification over secp256r1).
30    ///
31    /// This function implements the specification defined in
32    /// [EIP-7951](https://eips.ethereum.org/EIPS/eip-7951)
33    ///
34    /// # Specification Compliance
35    ///
36    /// 1. **Input Validation**: Checks strict 160-byte input length.
37    /// 2. **Signature Validation**: Ensures `r` and `s` are in the range `(0, n)`.
38    /// 3. **Public Key Validation**: Ensures coordinates are in `[0, p)`, satisfy the curve equation,
39    ///    and the point is not at infinity.
40    /// 4. **Verification**: Performs ECDSA verification including the critical modular reduction
41    ///    fix ($r' \equiv r \pmod n$) required to prevent RIP-7212 vulnerabilities.
42    ///
43    /// # Returns
44    ///
45    /// * `Vec<u8>` - 32-byte success result (`0x00...01`) if verification passes.
46    /// * `None` - If any validation fails or signature is invalid. This results in empty output
47    ///   but consumes the full gas cost, as per "Gas Burning on Error" section.
48    fn execute(input: &[u8]) -> Option<Vec<u8>> {
49        // 1. Input length check
50        if input.len() != INPUT_LENGTH {
51            return None;
52        }
53
54        // 2. Parse Inputs
55        // Message hash (h)
56        let h_bytes = &input[0..32];
57        // Signature component (r)
58        let r_bytes = &input[32..64];
59        // Signature component (s)
60        let s_bytes = &input[64..96];
61        // Public key x-coordinate (qx)
62        let qx_bytes = &input[96..128];
63        // Public key y-coordinate (qy)
64        let qy_bytes = &input[128..160];
65
66        // 3. Signature Component Validation
67        // Spec: "Both r and s MUST satisfy 0 < r < n and 0 < s < n"
68        // `Signature::from_scalars` returns an Error if scalars are zero or >= group order (n).
69        let signature = Signature::from_scalars(
70            *p256::FieldBytes::from_slice(r_bytes),
71            *p256::FieldBytes::from_slice(s_bytes),
72        )
73        .ok()?;
74
75        // 4. Public Key Validation
76        // Spec: "Both qx and qy MUST satisfy 0 <= qx < p and 0 <= qy < p"
77        // Spec: "The point (qx, qy) MUST satisfy the curve equation"
78        // Spec: "The point (qx, qy) MUST NOT be the point at infinity"
79        //
80        // We reconstruct the point from raw coordinates (0x04 || x || y implicit format).
81        // `VerifyingKey::from_encoded_point` validates that the point satisfies y^2 = x^3 + ax + b.
82        // Since secp256r1 coefficient b != 0, the point at infinity (0, 0) does not satisfy
83        // the curve equation and will be rejected here.
84        let encoded_point =
85            EncodedPoint::from_affine_coordinates(qx_bytes.into(), qy_bytes.into(), false);
86        let public_key = VerifyingKey::from_encoded_point(&encoded_point).ok()?;
87
88        // 5. Signature Verification
89        // Spec: "s1 = s^(-1) (mod n)"
90        // Spec: "R' = (h * s1) * G + (r * s1) * (qx, qy)"
91        // Spec: "If R' is the point at infinity: return"
92        // Spec: "if r' == r (mod n): return success"
93        //
94        // The `verify_prehash` method implements FIPS 186-5 ECDSA verification.
95        // Crucially, it handles the modular reduction of the computed x-coordinate ($r' \pmod n$)
96        // before comparing it with the signature component $r$. This addresses the
97        // consensus vulnerability found in original RIP-7212.
98        if public_key.verify_prehash(h_bytes, &signature).is_ok() {
99            // Spec: "Output is 32 bytes... 0x00...01 for valid signatures"
100            Some(Vec::from(&SUCCESS_RESULT[..]))
101        } else {
102            // Spec: "return `` (failure)"
103            None
104        }
105    }
106}
107
108impl Precompile for Secp256r1 {
109    fn required_gas(_input: &[u8]) -> Result<EthGas, ExitError>
110    where
111        Self: Sized,
112    {
113        Ok(EthGas::new(P256VERIFY_BASE_GAS_FEE))
114    }
115
116    fn run(
117        &self,
118        input: &[u8],
119        target_gas: Option<EthGas>,
120        _context: &Context,
121        _is_static: bool,
122    ) -> EvmPrecompileResult {
123        let cost = Self::required_gas(input)?;
124        if let Some(target_gas) = target_gas {
125            if cost > target_gas {
126                return Err(ExitError::OutOfGas);
127            }
128        }
129
130        // Return empty output on failure according to EIP-7951
131        let output = Self::execute(input).unwrap_or_default();
132        Ok(PrecompileOutput::without_logs(cost, output))
133    }
134}
135
136#[cfg(test)]
137mod test {
138    use super::*;
139
140    fn context() -> Context {
141        Context {
142            address: Secp256r1::ADDRESS.raw(),
143            caller: Secp256r1::ADDRESS.raw(),
144            apparent_value: 0u128.into(),
145        }
146    }
147
148    /// Test vectors from <https://github.com/daimo-eth/p256-verifier/tree/master/test-vectors>
149    #[test]
150    fn test_sig_verify() {
151        let inputs = vec![
152            ("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e", true),
153            ("3fec5769b5cf4e310a7d150508e82fb8e3eda1c2c94c61492d3bd8aea99e06c9e22466e928fdccef0de49e3503d2657d00494a00e764fd437bdafa05f5922b1fbbb77c6817ccf50748419477e843d5bac67e6a70e97dde5a57e0c983b777e1ad31a80482dadf89de6302b1988c82c29544c9c07bb910596158f6062517eb089a2f54c9a0f348752950094d3228d3b940258c75fe2a413cb70baa21dc2e352fc5", true),
154            ("e775723953ead4a90411a02908fd1a629db584bc600664c609061f221ef6bf7c440066c8626b49daaa7bf2bcc0b74be4f7a1e3dcf0e869f1542fe821498cbf2de73ad398194129f635de4424a07ca715838aefe8fe69d1a391cfa70470795a80dd056866e6e1125aff94413921880c437c9e2570a28ced7267c8beef7e9b2d8d1547d76dfcf4bee592f5fefe10ddfb6aeb0991c5b9dbbee6ec80d11b17c0eb1a", true),
155            ("b5a77e7a90aa14e0bf5f337f06f597148676424fae26e175c6e5621c34351955289f319789da424845c9eac935245fcddd805950e2f02506d09be7e411199556d262144475b1fa46ad85250728c600c53dfd10f8b3f4adf140e27241aec3c2da3a81046703fccf468b48b145f939efdbb96c3786db712b3113bb2488ef286cdcef8afe82d200a5bb36b5462166e8ce77f2d831a52ef2135b2af188110beaefb1", true),
156            ("858b991cfd78f16537fe6d1f4afd10273384db08bdfc843562a22b0626766686f6aec8247599f40bfe01bec0e0ecf17b4319559022d4d9bf007fe929943004eb4866760dedf31b7c691f5ce665f8aae0bda895c23595c834fecc2390a5bcc203b04afcacbb4280713287a2d0c37e23f7513fab898f2c1fefa00ec09a924c335d9b629f1d4fb71901c3e59611afbfea354d101324e894c788d1c01f00b3c251b2", true),
157            ("3cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e", false),
158            ("afec5769b5cf4e310a7d150508e82fb8e3eda1c2c94c61492d3bd8aea99e06c9e22466e928fdccef0de49e3503d2657d00494a00e764fd437bdafa05f5922b1fbbb77c6817ccf50748419477e843d5bac67e6a70e97dde5a57e0c983b777e1ad31a80482dadf89de6302b1988c82c29544c9c07bb910596158f6062517eb089a2f54c9a0f348752950094d3228d3b940258c75fe2a413cb70baa21dc2e352fc5", false),
159            ("f775723953ead4a90411a02908fd1a629db584bc600664c609061f221ef6bf7c440066c8626b49daaa7bf2bcc0b74be4f7a1e3dcf0e869f1542fe821498cbf2de73ad398194129f635de4424a07ca715838aefe8fe69d1a391cfa70470795a80dd056866e6e1125aff94413921880c437c9e2570a28ced7267c8beef7e9b2d8d1547d76dfcf4bee592f5fefe10ddfb6aeb0991c5b9dbbee6ec80d11b17c0eb1a", false),
160            ("c5a77e7a90aa14e0bf5f337f06f597148676424fae26e175c6e5621c34351955289f319789da424845c9eac935245fcddd805950e2f02506d09be7e411199556d262144475b1fa46ad85250728c600c53dfd10f8b3f4adf140e27241aec3c2da3a81046703fccf468b48b145f939efdbb96c3786db712b3113bb2488ef286cdcef8afe82d200a5bb36b5462166e8ce77f2d831a52ef2135b2af188110beaefb1", false),
161            ("958b991cfd78f16537fe6d1f4afd10273384db08bdfc843562a22b0626766686f6aec8247599f40bfe01bec0e0ecf17b4319559022d4d9bf007fe929943004eb4866760dedf31b7c691f5ce665f8aae0bda895c23595c834fecc2390a5bcc203b04afcacbb4280713287a2d0c37e23f7513fab898f2c1fefa00ec09a924c335d9b629f1d4fb71901c3e59611afbfea354d101324e894c788d1c01f00b3c251b2", false),
162            ("4cee90eb86eaa050036147a12d49004b6a", false),
163            ("4cee90eb86eaa050036147a12d49004b6a958b991cfd78f16537fe6d1f4afd10273384db08bdfc843562a22b0626766686f6aec8247599f40bfe01bec0e0ecf17b4319559022d4d9bf007fe929943004eb4866760dedf319", false),
164            ("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e00", false),
165            ("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff4aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e", false),
166            ("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", false),
167            ("b5a77e7a90aa14e0bf5f337f06f597148676424fae26e175c6e5621c34351955289f319789da424845c9eac935245fcddd805950e2f02506d09be7e411199556d262144475b1fa46ad85250728c600c53dfd10f8b3f4adf140e27241aec3c2daaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaef8afe82d200a5bb36b5462166e8ce77f2d831a52ef2135b2af188110beaefb1", false),
168        ];
169        let p = Secp256r1;
170        for (input_hex, expect_success) in inputs {
171            let input = hex::decode(input_hex).unwrap();
172            let res = p.run(&input, None, &context(), false).unwrap();
173            if expect_success {
174                assert_eq!(
175                    res.output,
176                    SUCCESS_RESULT.to_vec(),
177                    "Input hex: {input_hex}",
178                );
179            } else {
180                assert_eq!(res.output.len(), 0, "Input hex: {input_hex}");
181            }
182        }
183    }
184
185    #[test]
186    fn test_not_enough_gas_errors() {
187        let input_hex = "4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e";
188        let p = Secp256r1;
189
190        let input = hex::decode(input_hex).unwrap();
191        let err = p
192            .run(&input, Some(EthGas::new(2_500)), &context(), false)
193            .unwrap_err();
194        assert_eq!(err, ExitError::OutOfGas);
195    }
196
197    #[test]
198    fn test_eip7951_spec_compliance_edge_cases() {
199        let p = Secp256r1;
200
201        let valid_full_hex = "4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e";
202
203        // Slicing string indices (each byte is 2 hex chars)
204        let h = &valid_full_hex[0..64];
205        let r = &valid_full_hex[64..128];
206        let s = &valid_full_hex[128..192];
207        let qx = &valid_full_hex[192..256];
208        let qy = &valid_full_hex[256..320];
209
210        // Constants for boundaries (hex strings must be exactly 64 chars)
211        let val_p = "ffffffff00000001000000000000000000000000ffffffffffffffffffffffff";
212        let val_n = "ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551";
213        let val_zero = "0000000000000000000000000000000000000000000000000000000000000000";
214
215        let failures = [
216            // Case 1: Public Key is Point at Infinity (0, 0)
217            // Spec: "The point (qx, qy) MUST NOT be the point at infinity (represented as (0, 0))"
218            format!("{h}{r}{s}{val_zero}{val_zero}"),
219            // Case 2: Public Key X coordinate >= P
220            // Spec: "Both qx and qy MUST satisfy 0 <= qx < p"
221            format!("{h}{r}{s}{val_p}{qy}"),
222            // Case 3: Public Key Y coordinate >= P
223            // Spec: "Both qx and qy MUST satisfy ... 0 <= qy < p"
224            format!("{h}{r}{s}{qx}{val_p}"),
225            // Case 4: Scalar r is Zero
226            // Spec: "Both r and s MUST satisfy 0 < r < n"
227            format!("{h}{val_zero}{s}{qx}{qy}"),
228            // Case 5: Scalar s is Zero
229            // Spec: "Both r and s MUST satisfy ... 0 < s < n"
230            format!("{h}{r}{val_zero}{qx}{qy}"),
231            // Case 6: Scalar r >= n
232            // Spec: "Both r and s MUST satisfy 0 < r < n"
233            format!("{h}{val_n}{s}{qx}{qy}"),
234            // Case 7: Scalar s >= n
235            // Spec: "Both r and s MUST satisfy ... 0 < s < n"
236            format!("{h}{r}{val_n}{qx}{qy}"),
237        ];
238
239        for (i, input_hex) in failures.iter().enumerate() {
240            let input = hex::decode(input_hex).unwrap_or_else(|e| {
241                panic!("Failed to decode hex for Case {i}: {e}");
242            });
243
244            // Ensure we built the test vector correctly
245            assert_eq!(input.len(), 160, "Case {i} input length is not 160 bytes");
246
247            let res = p.run(&input, None, &context(), false).unwrap();
248
249            // Should return empty bytes (failure), NOT revert
250            assert_eq!(
251                res.output.len(),
252                0,
253                "Case {i} expected failure (empty output), got success",
254            );
255        }
256    }
257
258    #[test]
259    fn test_eip7951_input_length_validation() {
260        let p = Secp256r1;
261
262        // A valid 160-byte input (derived from valid vector)
263        let valid_hex = "4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e";
264        let valid_bytes = hex::decode(valid_hex).unwrap();
265        assert_eq!(valid_bytes.len(), 160);
266
267        // Test Case 1: Empty input
268        // Spec: "if input_length != 160: return"
269        let res_empty = p.run(&[], None, &context(), false).unwrap();
270        assert_eq!(
271            res_empty.output.len(),
272            0,
273            "Empty input should return empty bytes"
274        );
275
276        // Test Case 2: Too short (159 bytes)
277        let input_short = &valid_bytes[0..159];
278        let res_short = p.run(input_short, None, &context(), false).unwrap();
279        assert_eq!(
280            res_short.output.len(),
281            0,
282            "159 bytes input should return empty bytes"
283        );
284
285        // Test Case 3: Too long (161 bytes)
286        let mut input_long = valid_bytes.clone();
287        input_long.push(0x00); // Add 1 byte
288        let res_long = p.run(&input_long, None, &context(), false).unwrap();
289        assert_eq!(
290            res_long.output.len(),
291            0,
292            "161 bytes input should return empty bytes"
293        );
294
295        // Test Case 4: Random length (e.g. 32 bytes)
296        let input_32 = &valid_bytes[0..32];
297        let res_32 = p.run(input_32, None, &context(), false).unwrap();
298        assert_eq!(
299            res_32.output.len(),
300            0,
301            "32 bytes input should return empty bytes"
302        );
303    }
304}