Skip to content

Commit 69f61e9

Browse files
refactor(ec): store EcState in projective coordinates (#1654)
`EcState` was stored as an affine pair, so every `ec_state_add` ended in `to_affine()` — a modular inversion — only for the next add to undo it. Storing the accumulator projectively defers that to a single inversion in `ec_state_try_finalize_nz`, so the cost of a chain of adds no longer grows with its length.
1 parent 5e2c8b6 commit 69f61e9

19 files changed

Lines changed: 565 additions & 285 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ sierra-emu = { path = "debug_utils/sierra-emu", version = "0.9.0-rc.7" }
8686
smallvec = "1.13.2"
8787
starknet-crypto = "0.8.1"
8888
starknet-curve = "0.6.0"
89-
starknet-types-core = { version = "0.2.3", features = ["hash"]}
89+
starknet-types-core = { version = "0.2.4", features = ["hash"]}
9090
stats_alloc = "0.1.10"
9191
tempfile = "3.15.0"
9292
test-case = "3.3"

debug_utils/sierra-emu/src/value.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,17 @@ pub enum Value {
5454
x: Felt,
5555
y: Felt,
5656
},
57+
/// An elliptic curve accumulator in **projective** coordinates `[x : y : z]`,
58+
/// representing the affine point `(x/z, y/z)`. `z == 0` is the point at
59+
/// infinity.
60+
///
61+
/// Note that `PartialEq` here is bitwise on the representative, not
62+
/// projective equivalence, so the emulator must perform the same sequence of
63+
/// curve operations as the runtime.
5764
EcState {
5865
x: Felt,
5966
y: Felt,
67+
z: Felt,
6068
},
6169
I128(i128),
6270
I64(i64),

debug_utils/sierra-emu/src/vm.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ impl VirtualMachine {
262262
CoreTypeConcrete::RangeCheck(_)
263263
| CoreTypeConcrete::RangeCheck96(_)
264264
| CoreTypeConcrete::Bitwise(_)
265+
| CoreTypeConcrete::EcOp(_)
265266
| CoreTypeConcrete::Pedersen(_)
266267
| CoreTypeConcrete::Poseidon(_)
267268
| CoreTypeConcrete::SegmentArena(_)

debug_utils/sierra-emu/src/vm/ec.rs

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,13 @@ fn eval_state_init(
112112
_info: &SignatureOnlyConcreteLibfunc,
113113
_args: Vec<Value>,
114114
) -> EvalAction {
115+
// The canonical projective identity `[0 : 1 : 0]`.
115116
EvalAction::NormalBranch(
116117
0,
117118
smallvec![Value::EcState {
118119
x: 0.into(),
119-
y: 0.into()
120+
y: 1.into(),
121+
z: 0.into(),
120122
}],
121123
)
122124
}
@@ -126,65 +128,80 @@ fn eval_state_add(
126128
_info: &SignatureOnlyConcreteLibfunc,
127129
args: Vec<Value>,
128130
) -> EvalAction {
129-
let [Value::EcState { x: s_x, y: s_y }, Value::EcPoint { x, y }]: [Value; 2] =
130-
args.try_into().unwrap()
131+
let [Value::EcState {
132+
x: s_x,
133+
y: s_y,
134+
z: s_z,
135+
}, Value::EcPoint { x, y }]: [Value; 2] = args.try_into().unwrap()
131136
else {
132137
panic!()
133138
};
134139

135-
if s_x.is_zero() && s_y.is_zero() {
136-
return EvalAction::NormalBranch(0, smallvec![Value::EcState { x, y }]);
137-
}
138-
let mut state = ProjectivePoint::from_affine(s_x, s_y).unwrap();
139-
let point = AffinePoint::new(x, y).unwrap();
140+
let mut state = ProjectivePoint::new_unchecked(s_x, s_y, s_z);
141+
let point = ProjectivePoint::from_affine(x, y).unwrap();
140142

141143
state += &point;
142-
let (x, y) = match state.to_affine() {
143-
Ok(state) => (state.x(), state.y()),
144-
Err(_) => (Felt::ZERO, Felt::ZERO),
145-
};
146-
EvalAction::NormalBranch(0, smallvec![Value::EcState { x, y }])
144+
EvalAction::NormalBranch(
145+
0,
146+
smallvec![Value::EcState {
147+
x: state.x(),
148+
y: state.y(),
149+
z: state.z(),
150+
}],
151+
)
147152
}
148153

149154
fn eval_state_add_mul(
150155
_registry: &ProgramRegistry<CoreType, CoreLibfunc>,
151156
_info: &SignatureOnlyConcreteLibfunc,
152157
args: Vec<Value>,
153158
) -> EvalAction {
154-
let [ec @ Value::Unit, Value::EcState { x: s_x, y: s_y }, Value::Felt(scalar), Value::EcPoint { x, y }]: [Value; 4] =
155-
args.try_into().unwrap()
159+
let [ec @ Value::Unit, Value::EcState {
160+
x: s_x,
161+
y: s_y,
162+
z: s_z,
163+
}, Value::Felt(scalar), Value::EcPoint { x, y }]: [Value; 4] = args.try_into().unwrap()
156164
else {
157165
panic!()
158166
};
159167

160-
let mut state = if s_x.is_zero() && s_y.is_zero() {
161-
ProjectivePoint::identity()
162-
} else {
163-
ProjectivePoint::from_affine(s_x, s_y).unwrap()
164-
};
168+
let mut state = ProjectivePoint::new_unchecked(s_x, s_y, s_z);
165169
let point = ProjectivePoint::from_affine(x, y).unwrap();
166170

167171
state += &point.mul(scalar);
168-
let (x, y) = match state.to_affine() {
169-
Ok(state) => (state.x(), state.y()),
170-
Err(_) => (Felt::ZERO, Felt::ZERO),
171-
};
172-
EvalAction::NormalBranch(0, smallvec![ec, Value::EcState { x, y }])
172+
EvalAction::NormalBranch(
173+
0,
174+
smallvec![
175+
ec,
176+
Value::EcState {
177+
x: state.x(),
178+
y: state.y(),
179+
z: state.z(),
180+
}
181+
],
182+
)
173183
}
174184

175185
fn eval_state_finalize(
176186
_registry: &ProgramRegistry<CoreType, CoreLibfunc>,
177187
_info: &SignatureOnlyConcreteLibfunc,
178188
args: Vec<Value>,
179189
) -> EvalAction {
180-
let [Value::EcState { x, y }]: [Value; 1] = args.try_into().unwrap() else {
190+
let [Value::EcState { x, y, z }]: [Value; 1] = args.try_into().unwrap() else {
181191
panic!()
182192
};
183193

184-
if x.is_zero() && y.is_zero() {
185-
EvalAction::NormalBranch(1, smallvec![])
186-
} else {
187-
EvalAction::NormalBranch(0, smallvec![Value::EcPoint { x, y }])
194+
// The single normalisation of the pipeline. `to_affine` fails exactly when
195+
// `z == 0`, so it doubles as the point-at-infinity test.
196+
match ProjectivePoint::new_unchecked(x, y, z).to_affine() {
197+
Ok(point) => EvalAction::NormalBranch(
198+
0,
199+
smallvec![Value::EcPoint {
200+
x: point.x(),
201+
y: point.y(),
202+
}],
203+
),
204+
Err(_) => EvalAction::NormalBranch(1, smallvec![]),
188205
}
189206
}
190207

debug_utils/sierra-emu/tests/common/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@ pub fn value_to_felt(value: &Value) -> Vec<Felt> {
2929
felts.push(modulus.into());
3030
felts
3131
}
32-
Value::EcPoint { x, y } | Value::EcState { x, y } => {
32+
Value::EcPoint { x, y } => {
3333
vec![*x, *y]
3434
}
35+
// Projective: three felts, unlike `EcPoint`.
36+
Value::EcState { x, y, z } => {
37+
vec![*x, *y, *z]
38+
}
3539
Value::Enum {
3640
self_ty,
3741
index,

docs/debugging.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,8 @@ Once you have generated the traces for both the Sierra emulator and Cairo Native
408408
```
409409
2. Look for the first significant difference between the traces. Not all the differences are significant, for example:
410410
1. Sometimes the emulator and Cairo Native differ in the Gas builtin. It usually doesn’t affect the outcome of the contract.
411-
2. The ec_state_init libfunc randomizes an elliptic curve point, which is why they always differ.
411+
2. An `EcState` is projective; `[X : Y : Z]` and `[λX : λY : λZ]` are the same point — compare
412+
`X/Z`, `Y/Z` before concluding a real difference.
412413
3. Find the index of the statement executed immediately previous to the first difference.
413414
4. Open `traces/prog_0.sierra` and look for that statement.
414415
1. If it’s a return, then you are dealing with a control flow bug. These are difficult to debug.

src/arch.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,10 @@ impl AbiArgument for ValueWithInfoWrapper<'_> {
113113
y.to_bytes(buffer)?;
114114
}
115115
(Value::EcState(x, y), CoreTypeConcrete::EcState(_)) => {
116-
x.to_bytes(buffer)?;
117-
y.to_bytes(buffer)?;
116+
// Native representation is projective; see `crate::types::ec_state`.
117+
for felt in crate::types::ec_state::to_projective(*x, *y) {
118+
felt.to_bytes(buffer)?;
119+
}
118120
}
119121
(Value::QM31(a, b, c, d), CoreTypeConcrete::QM31(_)) => {
120122
a.to_bytes(buffer)?;

src/libfuncs/ec.rs

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::{
66
execution_result::EC_OP_BUILTIN_SIZE,
77
libfuncs::increment_builtin_counter_conditionally_by,
88
metadata::{runtime_bindings::RuntimeBindingsMeta, MetadataStorage},
9+
types::{ec_point::ec_point_ty, ec_state::ec_state_ty},
910
utils::{get_integer_layout, ProgramRegistryExt, PRIME},
1011
};
1112
use cairo_lang_sierra::{
@@ -178,14 +179,7 @@ pub fn build_point_from_x<'ctx, 'this>(
178179
metadata: &mut MetadataStorage,
179180
_info: &SignatureOnlyConcreteLibfunc,
180181
) -> Result<()> {
181-
let ec_point_ty = llvm::r#type::r#struct(
182-
context,
183-
&[
184-
IntegerType::new(context, 252).into(),
185-
IntegerType::new(context, 252).into(),
186-
],
187-
false,
188-
);
182+
let ec_point_ty = ec_point_ty(context);
189183

190184
let point_ptr = helper.init_block().alloca1(
191185
context,
@@ -240,14 +234,7 @@ pub fn build_state_add<'ctx, 'this>(
240234
metadata: &mut MetadataStorage,
241235
_info: &SignatureOnlyConcreteLibfunc,
242236
) -> Result<()> {
243-
let ec_state_ty = llvm::r#type::r#struct(
244-
context,
245-
&[
246-
IntegerType::new(context, 252).into(),
247-
IntegerType::new(context, 252).into(),
248-
],
249-
false,
250-
);
237+
let ec_state_ty = ec_state_ty(context);
251238

252239
let state_ptr = helper.init_block().alloca1(
253240
context,
@@ -258,7 +245,7 @@ pub fn build_state_add<'ctx, 'this>(
258245
let point_ptr = helper.init_block().alloca1(
259246
context,
260247
location,
261-
ec_state_ty,
248+
ec_point_ty(context),
262249
get_integer_layout(252).align(),
263250
)?;
264251

@@ -296,8 +283,8 @@ pub fn build_state_add_mul<'ctx, 'this>(
296283
)?;
297284

298285
let felt252_ty = IntegerType::new(context, 252).into();
299-
let ec_state_ty = llvm::r#type::r#struct(context, &[felt252_ty, felt252_ty], false);
300-
let ec_point_ty = llvm::r#type::r#struct(context, &[felt252_ty, felt252_ty], false);
286+
let ec_state_ty = ec_state_ty(context);
287+
let ec_point_ty = ec_point_ty(context);
301288

302289
let state_ptr = helper.init_block().alloca1(
303290
context,
@@ -344,9 +331,8 @@ pub fn build_state_finalize<'ctx, 'this>(
344331
metadata: &mut MetadataStorage,
345332
_info: &SignatureOnlyConcreteLibfunc,
346333
) -> Result<()> {
347-
let felt252_ty = IntegerType::new(context, 252).into();
348-
let ec_state_ty = llvm::r#type::r#struct(context, &[felt252_ty, felt252_ty], false);
349-
let ec_point_ty = llvm::r#type::r#struct(context, &[felt252_ty, felt252_ty], false);
334+
let ec_state_ty = ec_state_ty(context);
335+
let ec_point_ty = ec_point_ty(context);
350336

351337
let point_ptr = helper.init_block().alloca1(
352338
context,
@@ -385,13 +371,17 @@ pub fn build_state_init<'ctx, 'this>(
385371
_metadata: &mut MetadataStorage,
386372
_info: &SignatureOnlyConcreteLibfunc,
387373
) -> Result<()> {
388-
let felt252_ty = IntegerType::new(context, 252).into();
389-
let ec_state_ty = llvm::r#type::r#struct(context, &[felt252_ty, felt252_ty], false);
374+
let ec_state_ty = ec_state_ty(context);
390375

376+
// The canonical projective identity `[0 : 1 : 0]`. Any `Z == 0` is the point
377+
// at infinity, but emitting the canonical form keeps memory dumps readable
378+
// and matches `ProjectivePoint::identity`.
391379
let k0 = entry.const_int(context, location, 0, 252)?;
380+
let k1 = entry.const_int(context, location, 1, 252)?;
392381
let state = entry.append_op_result(llvm::undef(ec_state_ty, location))?;
393382
let state = entry.insert_value(context, location, state, k0, 0)?;
394-
let state = entry.insert_value(context, location, state, k0, 1)?;
383+
let state = entry.insert_value(context, location, state, k1, 1)?;
384+
let state = entry.insert_value(context, location, state, k0, 2)?;
395385

396386
helper.br(entry, 0, &[state], location)
397387
}
@@ -406,14 +396,7 @@ pub fn build_try_new<'ctx, 'this>(
406396
metadata: &mut MetadataStorage,
407397
_info: &SignatureOnlyConcreteLibfunc,
408398
) -> Result<()> {
409-
let ec_point_ty = llvm::r#type::r#struct(
410-
context,
411-
&[
412-
IntegerType::new(context, 252).into(),
413-
IntegerType::new(context, 252).into(),
414-
],
415-
false,
416-
);
399+
let ec_point_ty = ec_point_ty(context);
417400

418401
let point_ptr = helper.init_block().alloca1(
419402
context,

src/metadata/trace_dump.rs

Lines changed: 17 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ pub mod trace_dump_runtime {
229229
use crate::{
230230
starknet::ArrayAbi,
231231
types::TypeBuilder,
232-
utils::{get_integer_layout, layout_repeat},
232+
utils::{felt_from_slot, get_integer_layout, layout_repeat},
233233
};
234234

235235
use crate::runtime::FeltDict;
@@ -296,6 +296,14 @@ pub mod trace_dump_runtime {
296296
.push(StateDump::new(StatementIdx(statement_idx as usize), items));
297297
}
298298

299+
/// Read `N` consecutive `felt252` slots starting at `ptr`.
300+
///
301+
/// Each slot is 32 bytes: `get_integer_layout(252)` is size 32, align 16, so
302+
/// repeating it gives a stride of 32.
303+
unsafe fn read_felts<const N: usize>(ptr: NonNull<()>) -> [Felt; N] {
304+
std::array::from_fn(|i| felt_from_slot(ptr.byte_add(i * 32).cast().as_ref()))
305+
}
306+
299307
/// TODO: Can we reuse `cairo_native::Value::from_ptr`?
300308
unsafe fn value_from_ptr(
301309
registry: &ProgramRegistry<CoreType, CoreLibfunc>,
@@ -309,7 +317,8 @@ pub mod trace_dump_runtime {
309317
| CoreTypeConcrete::Starknet(StarknetTypeConcrete::ClassHash(_))
310318
| CoreTypeConcrete::Starknet(StarknetTypeConcrete::StorageAddress(_))
311319
| CoreTypeConcrete::Starknet(StarknetTypeConcrete::StorageBaseAddress(_)) => {
312-
Value::Felt(Felt::from_bytes_le(value_ptr.cast().as_ref()))
320+
let [value] = read_felts(value_ptr);
321+
Value::Felt(value)
313322
}
314323
CoreTypeConcrete::Uint8(_) => Value::U8(value_ptr.cast().read()),
315324
CoreTypeConcrete::Uint16(_) => Value::U16(value_ptr.cast().read()),
@@ -338,42 +347,15 @@ pub mod trace_dump_runtime {
338347
}
339348

340349
CoreTypeConcrete::EcPoint(_) => {
341-
let layout = Layout::new::<()>();
342-
let (x, layout) = {
343-
let (layout, offset) = layout.extend(Layout::new::<[u128; 2]>()).unwrap();
344-
(
345-
Felt::from_bytes_le(value_ptr.byte_add(offset).cast().as_ref()),
346-
layout,
347-
)
348-
};
349-
let (y, _) = {
350-
let (layout, offset) = layout.extend(Layout::new::<[u128; 2]>()).unwrap();
351-
(
352-
Felt::from_bytes_le(value_ptr.byte_add(offset).cast().as_ref()),
353-
layout,
354-
)
355-
};
356-
350+
let [x, y] = read_felts(value_ptr);
357351
Value::EcPoint { x, y }
358352
}
359353
CoreTypeConcrete::EcState(_) => {
360-
let layout = Layout::new::<()>();
361-
let (x, layout) = {
362-
let (layout, offset) = layout.extend(Layout::new::<[u128; 2]>()).unwrap();
363-
(
364-
Felt::from_bytes_le(value_ptr.byte_add(offset).cast().as_ref()),
365-
layout,
366-
)
367-
};
368-
let (y, _) = {
369-
let (layout, offset) = layout.extend(Layout::new::<[u128; 2]>()).unwrap();
370-
(
371-
Felt::from_bytes_le(value_ptr.byte_add(offset).cast().as_ref()),
372-
layout,
373-
)
374-
};
375-
376-
Value::EcState { x, y }
354+
// Projective `[x : y : z]`, matching the native representation;
355+
// `sierra_emu::Value::EcState` is projective too, so no
356+
// normalisation is needed for the traces to compare.
357+
let [x, y, z] = read_felts(value_ptr);
358+
Value::EcState { x, y, z }
377359
}
378360

379361
CoreTypeConcrete::Uninitialized(info) => Value::Uninitialized {

0 commit comments

Comments
 (0)