aurora_engine_precompiles/
secp256r1.rs

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