Skip to content

Commit bd6251f

Browse files
blockifier: add secp256r1 syscall point validation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3fdf8f7 commit bd6251f

6 files changed

Lines changed: 396 additions & 12 deletions

File tree

crates/blockifier/src/execution.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub mod errors;
1111
pub mod execution_utils;
1212
pub mod hint_code;
1313
pub mod secp;
14+
pub mod secp_shadow;
1415

1516
#[cfg(feature = "cairo_native")]
1617
pub mod native;

crates/blockifier/src/execution/native/syscall_handler.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ use crate::execution::native::utils::{
4848
default_tx_v2_info,
4949
tx_v2_info_to_tx_v3_info,
5050
};
51-
use crate::execution::secp;
5251
use crate::execution::syscalls::common_syscall_logic::base_keccak;
5352
use crate::execution::syscalls::hint_processor::{SyscallExecutionError, OUT_OF_GAS_ERROR_FELT};
5453
use crate::execution::syscalls::syscall_base::SyscallHandlerBase;
@@ -58,6 +57,7 @@ use crate::execution::syscalls::vm_syscall_utils::{
5857
SyscallSelector,
5958
TryExtractRevert,
6059
};
60+
use crate::execution::{secp, secp_shadow};
6161
use crate::state::state_api::State;
6262
use crate::transaction::objects::TransactionInfo;
6363
use crate::utils::u64_from_usize;
@@ -762,7 +762,14 @@ impl StarknetSyscallHandler for &mut NativeSyscallHandler<'_> {
762762
SyscallSelector::Secp256r1Add,
763763
)?;
764764

765-
Ok(Secp256Point::add(p0.into(), p1.into()).into())
765+
let p0_point: Secp256Point<ark_secp256r1::Config> = p0.into();
766+
let p1_point: Secp256Point<ark_secp256r1::Config> = p1.into();
767+
secp_shadow::reject_zero_x_in_add(&p0_point.0, &p1_point.0)
768+
.map_err(|err| self.handle_error(remaining_gas, err.into()))?;
769+
Secp256Point::add(p0_point, p1_point)
770+
.reject_zero_x()
771+
.map(Into::into)
772+
.map_err(|err| self.handle_error(remaining_gas, err))
766773
}
767774

768775
fn secp256r1_mul(
@@ -777,7 +784,13 @@ impl StarknetSyscallHandler for &mut NativeSyscallHandler<'_> {
777784
SyscallSelector::Secp256r1Mul,
778785
)?;
779786

780-
Ok(Secp256Point::mul(p.into(), m).into())
787+
let p_point: Secp256Point<ark_secp256r1::Config> = p.into();
788+
secp_shadow::reject_zero_x_in_mul(&p_point.0, &u256_to_biguint(m))
789+
.map_err(|err| self.handle_error(remaining_gas, err.into()))?;
790+
Secp256Point::mul(p_point, m)
791+
.reject_zero_x()
792+
.map(Into::into)
793+
.map_err(|err| self.handle_error(remaining_gas, err))
781794
}
782795

783796
fn secp256r1_get_point_from_x(
@@ -883,7 +896,7 @@ impl From<Secp256r1Point> for Secp256Point<ark_secp256r1::Config> {
883896
}
884897
}
885898

886-
impl<Curve: SWCurveConfig> Secp256Point<Curve>
899+
impl<Curve: SWCurveConfig + secp::SecpZeroXPolicy> Secp256Point<Curve>
887900
where
888901
Curve::BaseField: PrimeField, // constraint for get_point_by_id
889902
{
@@ -895,11 +908,17 @@ where
895908
{
896909
match result {
897910
Ok(None) => Ok(None),
898-
Ok(Some(point)) => Ok(Some(Secp256Point(point.into()))),
911+
Ok(Some(point)) => Ok(Some(Secp256Point(point.into()).reject_zero_x()?)),
899912
Err(error) => Err(error),
900913
}
901914
}
902915

916+
/// Rejects secp256r1 points with x-coordinate 0.
917+
fn reject_zero_x(self) -> Result<Self, SyscallExecutionError> {
918+
secp::reject_zero_x_point(&self.0)?;
919+
Ok(self)
920+
}
921+
903922
/// Given an (x, y) pair, this function:
904923
/// - Returns the point at infinity for (0, 0).
905924
/// - Returns `Err` if either `x` or `y` is outside the modulus.

crates/blockifier/src/execution/secp.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,41 @@
11
use ark_ec::short_weierstrass::{Affine, SWCurveConfig};
22
use ark_ff::{BigInteger, PrimeField, Zero};
3+
use starknet_types_core::felt::Felt;
34

45
use crate::execution::syscalls::hint_processor::INVALID_ARGUMENT_FELT;
56
use crate::execution::syscalls::vm_syscall_utils::SyscallExecutorBaseError;
67

8+
/// Whether points with x-coordinate 0 are rejected. Enabled for secp256r1 only.
9+
pub trait SecpZeroXPolicy {
10+
const REJECT_ZERO_X_POINT: bool;
11+
}
12+
13+
impl SecpZeroXPolicy for ark_secp256r1::Config {
14+
const REJECT_ZERO_X_POINT: bool = true;
15+
}
16+
17+
impl SecpZeroXPolicy for ark_secp256k1::Config {
18+
const REJECT_ZERO_X_POINT: bool = false;
19+
}
20+
21+
/// The error returned when a secp256r1 point with x-coordinate 0 is rejected.
22+
pub(crate) fn zero_x_point_error() -> SyscallExecutorBaseError {
23+
SyscallExecutorBaseError::InvalidSyscallInput {
24+
input: Felt::ZERO,
25+
info: "secp256r1 points with x-coordinate 0 are not allowed".to_string(),
26+
}
27+
}
28+
29+
/// Rejects secp256r1 points with x-coordinate 0. Shared by the VM and Cairo Native syscall paths.
30+
pub fn reject_zero_x_point<Curve: SWCurveConfig + SecpZeroXPolicy>(
31+
point: &Affine<Curve>,
32+
) -> Result<(), SyscallExecutorBaseError> {
33+
if Curve::REJECT_ZERO_X_POINT && !point.infinity && point.x.is_zero() {
34+
return Err(zero_x_point_error());
35+
}
36+
Ok(())
37+
}
38+
739
pub fn get_point_from_x<Curve: SWCurveConfig>(
840
x: num_bigint::BigUint,
941
y_parity: bool,
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
//! Extra input validation for the secp256r1 `add` / `mul` syscalls: rejects an operation that
2+
//! involves a point with x-coordinate 0.
3+
//!
4+
//! The functions below mirror the secp256r1 EC routines in
5+
//! `starkware/cairo/common/secp256r1/ec.cairo` so they visit the same sequence of points; they only
6+
//! inspect those points and never affect the syscall's computed result. Each `fn` here mirrors the
7+
//! cairo `func` of the same name — keep them in sync.
8+
9+
use ark_ec::short_weierstrass::{Affine, Projective, SWCurveConfig};
10+
use ark_ff::{PrimeField, Zero};
11+
use num_bigint::BigUint;
12+
13+
use crate::execution::secp::{zero_x_point_error, SecpZeroXPolicy};
14+
use crate::execution::syscalls::vm_syscall_utils::SyscallExecutorBaseError;
15+
16+
type ShadowResult<T> = Result<T, SyscallExecutorBaseError>;
17+
18+
/// Whether `point` has affine x-coordinate 0 and is not the point at infinity. Tested on the
19+
/// projective `X` coordinate to avoid an inversion: the affine `x` is `X` times a nonzero factor,
20+
/// so it is zero iff `X == 0` and the point is not at infinity.
21+
fn is_zero_x_point<Curve: SWCurveConfig>(point: &Projective<Curve>) -> bool {
22+
!point.is_zero() && point.x.is_zero()
23+
}
24+
25+
/// Rejects a non-infinity point with x-coordinate 0.
26+
fn reject_zero_x_operand<Curve: SWCurveConfig + SecpZeroXPolicy>(
27+
point: &Projective<Curve>,
28+
) -> ShadowResult<()>
29+
where
30+
Curve::BaseField: PrimeField,
31+
{
32+
if is_zero_x_point(point) {
33+
return Err(zero_x_point_error());
34+
}
35+
Ok(())
36+
}
37+
38+
/// Mirror of `ec_double`.
39+
fn ec_double<Curve: SWCurveConfig + SecpZeroXPolicy>(
40+
point: Projective<Curve>,
41+
) -> ShadowResult<Projective<Curve>>
42+
where
43+
Curve::BaseField: PrimeField,
44+
{
45+
reject_zero_x_operand(&point)?;
46+
Ok(point + point)
47+
}
48+
49+
/// Mirror of both `ec_add` and `fast_ec_add`, which are identical here: only the two operands are
50+
/// inspected. Every `ec_add` branch (`fast_ec_add`, `ec_double`, or the point at infinity) operates
51+
/// on `point0` / `point1`, and the point at infinity is naturally distinguished in projective form
52+
/// (`Z == 0` vs `X == 0`).
53+
fn ec_add<Curve: SWCurveConfig + SecpZeroXPolicy>(
54+
point0: Projective<Curve>,
55+
point1: Projective<Curve>,
56+
) -> ShadowResult<Projective<Curve>>
57+
where
58+
Curve::BaseField: PrimeField,
59+
{
60+
reject_zero_x_operand(&point0)?;
61+
reject_zero_x_operand(&point1)?;
62+
Ok(point0 + point1)
63+
}
64+
65+
/// Mirror of `build_ec_mul_table`: returns `table[i] = i * point` for `i` in `0..=15`.
66+
fn build_ec_mul_table<Curve: SWCurveConfig + SecpZeroXPolicy>(
67+
point: Projective<Curve>,
68+
) -> ShadowResult<[Projective<Curve>; 16]>
69+
where
70+
Curve::BaseField: PrimeField,
71+
{
72+
// table[0] is the point at infinity.
73+
let mut table = [Projective::<Curve>::zero(); 16];
74+
table[1] = point;
75+
table[2] = ec_double(table[1])?;
76+
// The cairo unrolls table[3..=15]; table[i] = fast_ec_add(table[i - 1], point).
77+
for i in 3..16 {
78+
table[i] = ec_add(table[i - 1], point)?;
79+
}
80+
Ok(table)
81+
}
82+
83+
/// Mirror of `fast_ec_mul_inner`: for each nibble (consumed most significant first), multiplies the
84+
/// accumulator by 16 (four doublings) and adds the nibble's table entry.
85+
fn fast_ec_mul_inner<Curve: SWCurveConfig + SecpZeroXPolicy>(
86+
table: &[Projective<Curve>; 16],
87+
mut point: Projective<Curve>,
88+
nibbles: impl Iterator<Item = usize>,
89+
) -> ShadowResult<Projective<Curve>>
90+
where
91+
Curve::BaseField: PrimeField,
92+
{
93+
for nibble in nibbles {
94+
for _ in 0..4 {
95+
point = ec_double(point)?;
96+
}
97+
point = ec_add(point, table[nibble])?;
98+
}
99+
Ok(point)
100+
}
101+
102+
/// The 64 nibbles (4 bits each) of the 256-bit scalar, `nibbles[0]` least significant.
103+
fn scalar_nibbles(scalar: &BigUint) -> [usize; 64] {
104+
let bytes = scalar.to_bytes_le();
105+
core::array::from_fn(|k| {
106+
let byte = bytes.get(k / 2).copied().unwrap_or(0);
107+
usize::from(if k.is_multiple_of(2) { byte & 0x0f } else { byte >> 4 })
108+
})
109+
}
110+
111+
/// Mirror of `ec_mul_by_uint256`: a 16-entry precompute table plus a windowed double-and-add over
112+
/// the scalar's 64 nibbles, most significant first.
113+
fn ec_mul_by_uint256<Curve: SWCurveConfig + SecpZeroXPolicy>(
114+
point: &Affine<Curve>,
115+
scalar: &BigUint,
116+
) -> ShadowResult<Projective<Curve>>
117+
where
118+
Curve::BaseField: PrimeField,
119+
{
120+
let table = build_ec_mul_table(Projective::from(*point))?;
121+
let nibbles = scalar_nibbles(scalar);
122+
123+
// first_nibble = nibbles[63], last_nibble = nibbles[0].
124+
let mut res = table[nibbles[63]];
125+
// First inner call (cairo m = 124): nibbles 62..=32, most significant first.
126+
res = fast_ec_mul_inner(&table, res, (32..=62).rev().map(|k| nibbles[k]))?;
127+
// Second inner call (cairo m = 124): nibbles 31..=1.
128+
res = fast_ec_mul_inner(&table, res, (1..=31).rev().map(|k| nibbles[k]))?;
129+
// Final window for the least-significant nibble: 16 * res, then ec_add with its table entry.
130+
for _ in 0..4 {
131+
res = ec_double(res)?;
132+
}
133+
ec_add(res, table[nibbles[0]])
134+
}
135+
136+
/// Rejects a `secp256r1_add(point0, point1)` syscall that involves a point with x-coordinate 0.
137+
/// No-op for curves without [`SecpZeroXPolicy::REJECT_ZERO_X_POINT`].
138+
pub fn reject_zero_x_in_add<Curve: SWCurveConfig + SecpZeroXPolicy>(
139+
point0: &Affine<Curve>,
140+
point1: &Affine<Curve>,
141+
) -> ShadowResult<()>
142+
where
143+
Curve::BaseField: PrimeField,
144+
{
145+
if !Curve::REJECT_ZERO_X_POINT {
146+
return Ok(());
147+
}
148+
// Only the rejection matters; the computed point is discarded.
149+
ec_add(Projective::from(*point0), Projective::from(*point1)).map(|_| ())
150+
}
151+
152+
/// Rejects a `secp256r1_mul(point, scalar)` syscall that involves a point with x-coordinate 0 at
153+
/// any point in the computation. No-op for curves without [`SecpZeroXPolicy::REJECT_ZERO_X_POINT`].
154+
pub fn reject_zero_x_in_mul<Curve: SWCurveConfig + SecpZeroXPolicy>(
155+
point: &Affine<Curve>,
156+
scalar: &BigUint,
157+
) -> ShadowResult<()>
158+
where
159+
Curve::BaseField: PrimeField,
160+
{
161+
if !Curve::REJECT_ZERO_X_POINT {
162+
return Ok(());
163+
}
164+
// Only the rejection matters; the computed point is discarded.
165+
ec_mul_by_uint256(point, scalar).map(|_| ())
166+
}
167+
168+
#[cfg(test)]
169+
mod tests {
170+
use ark_ec::short_weierstrass::Affine;
171+
use num_bigint::BigUint;
172+
173+
use super::{reject_zero_x_in_add, reject_zero_x_in_mul};
174+
175+
fn secp256r1_point(x_hex: &str, y_hex: &str) -> Affine<ark_secp256r1::Config> {
176+
let parse = |hex: &str| BigUint::parse_bytes(hex.as_bytes(), 16).unwrap();
177+
Affine::new_unchecked(parse(x_hex).into(), parse(y_hex).into())
178+
}
179+
180+
fn zero_x_point() -> Affine<ark_secp256r1::Config> {
181+
secp256r1_point("0", "66485c780e2f83d72433bd5d84a06bb6541c2af31dae871728bf856a174f93f4")
182+
}
183+
184+
fn generator() -> Affine<ark_secp256r1::Config> {
185+
secp256r1_point(
186+
"6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296",
187+
"4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5",
188+
)
189+
}
190+
191+
/// mul rejects a point with x-coordinate 0 that is reached early in the computation.
192+
#[test]
193+
fn mul_rejects_zero_x_point() {
194+
let half = secp256r1_point(
195+
"81bfb55b010b1bdf08b8d9d8590087aa278e28febff3b05632eeff09011c5579",
196+
"8cd2f199d9815d7585073034eb76c93d50799b354b0fb1e77eb75eba8bff3d58",
197+
);
198+
for scalar in [3_u32, 5, 7] {
199+
assert!(reject_zero_x_in_mul(&half, &BigUint::from(scalar)).is_err());
200+
}
201+
let quarter = secp256r1_point(
202+
"02495ac9f43e45aae30d3366e351cc08828cf3e11cc3b7209fbd1730c4a14f4e",
203+
"b55567231f26b356bbac703086b614bb21448433dd75ab263264c3d12206b9ee",
204+
);
205+
assert!(reject_zero_x_in_mul(&quarter, &BigUint::from(7_u32)).is_err());
206+
}
207+
208+
/// mul rejects a point with x-coordinate 0 reached only deep in the computation.
209+
#[test]
210+
fn mul_rejects_zero_x_point_large_scalar() {
211+
let sixteenth = secp256r1_point(
212+
"776aef1acb82b628e132cc29440988f0a15d4cc2b4f328aecb063c9b86e5018e",
213+
"6e44dfc60444faa9c4e36bc217451f7ac2956cb3b2e9bbd655eba297163d1f34",
214+
);
215+
let scalar = BigUint::from(1_u8) << 252_u32;
216+
assert!(reject_zero_x_in_mul(&sixteenth, &scalar).is_err());
217+
}
218+
219+
/// mul accepts a multiplication that never involves a point with x-coordinate 0.
220+
#[test]
221+
fn mul_accepts_regular_point() {
222+
assert!(reject_zero_x_in_mul(&generator(), &BigUint::from(5_u32)).is_ok());
223+
}
224+
225+
/// add rejects an operand with x-coordinate 0 but not regular points.
226+
#[test]
227+
fn add_rejects_zero_x_operand() {
228+
assert!(reject_zero_x_in_add(&generator(), &generator()).is_ok());
229+
assert!(reject_zero_x_in_add(&zero_x_point(), &generator()).is_err());
230+
assert!(reject_zero_x_in_add(&generator(), &zero_x_point()).is_err());
231+
}
232+
}

0 commit comments

Comments
 (0)