Skip to content

Commit

Permalink
Feat: EcOp Builtin (#159)
Browse files Browse the repository at this point in the history
* Add ecop builtin

* Add remaining felt values

* Add test

* overall improvements

* Improvements and bug fix

* Add integration test

* Add unit test for runner

* Move alpha and beta params to utils

* Fix comment and error checking
  • Loading branch information
rodrigo-pino authored Nov 10, 2023
1 parent 1915b0f commit 6089e75
Show file tree
Hide file tree
Showing 10 changed files with 404 additions and 7 deletions.
21 changes: 21 additions & 0 deletions integration_tests/builtin_tests/ecop.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
%builtins ec_op

from starkware.cairo.common.cairo_builtins import EcOpBuiltin
from starkware.cairo.common.ec_point import EcPoint
from starkware.cairo.common.ec import ec_op

func main{ec_op_ptr: EcOpBuiltin*}() {
let p = EcPoint(
0x6a4beaef5a93425b973179cdba0c9d42f30e01a5f1e2db73da0884b8d6756fc,
0x72565ec81bc09ff53fbfad99324a92aa5b39fb58267e395e8abe36290ebf24f,
);
let m = 34;
let q = EcPoint(
0x654fd7e67a123dd13868093b3b7777f1ffef596c2e324f25ceaf9146698482c,
0x4fad269cbf860980e38768fe9cb6b0b9ab03ee3fe84cfde2eccce597c874fd8,
);
let (r) = ec_op(p, m, q);
assert r.x = 108925483682366235368969256555281508851459278989259552980345066351008608800;
assert r.y = 1592365885972480102953613056006596671718206128324372995731808913669237079419;
return ();
}
11 changes: 11 additions & 0 deletions integration_tests/cairozero_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,17 @@ func TestPedersen(t *testing.T) {
clean("./builtin_tests/")
}

func TestEcOp(t *testing.T) {
compiledOutput, err := compileZeroCode("./builtin_tests/ecop.cairo")
require.NoError(t, err)

_, _, _, err = runVm(compiledOutput)
// todo(rodro): This test is failing due to the lack of hint processing. It should be address soon
require.Error(t, err)

clean("./builtin_tests/")
}

func TestKeccak(t *testing.T) {
compiledOutput, err := compileZeroCode("./builtin_tests/keccak_test.cairo")
require.NoError(t, err)
Expand Down
29 changes: 29 additions & 0 deletions pkg/runners/zero/zero_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,36 @@ func TestRangeCheckBuiltinError(t *testing.T) {

err = runner.Run()
require.ErrorContains(t, err, "cannot infer value")
}

func TestEcOpBuiltin(t *testing.T) {
// first, store P.x, P.y, Q.x, Q.y and m in the data segment
// then store them the EcOp builtin segment
// infer the values, effectively calculating EcOp
// assert the values are correct
runner := createRunner(`
[ap] = 0x6a4beaef5a93425b973179cdba0c9d42f30e01a5f1e2db73da0884b8d6756fc;
[ap + 1] = 0x72565ec81bc09ff53fbfad99324a92aa5b39fb58267e395e8abe36290ebf24f;
[ap + 2] = 0x654fd7e67a123dd13868093b3b7777f1ffef596c2e324f25ceaf9146698482c;
[ap + 3] = 0x4fad269cbf860980e38768fe9cb6b0b9ab03ee3fe84cfde2eccce597c874fd8;
[ap + 4] = 34;
[ap] = [[fp - 3]];
[ap + 1] = [[fp - 3] + 1];
[ap + 2] = [[fp - 3] + 2];
[ap + 3] = [[fp - 3] + 3];
[ap + 4] = [[fp - 3] + 4];
[ap + 5] = [[fp - 3] + 5];
[ap + 6] = [[fp - 3] + 6];
[ap + 5] = 108925483682366235368969256555281508851459278989259552980345066351008608800;
[ap + 6] = 1592365885972480102953613056006596671718206128324372995731808913669237079419;
ret;
`, sn.ECOP)

err := runner.Run()
require.NoError(t, err)
}

func createRunner(code string, builtins ...sn.Builtin) ZeroRunner {
Expand Down
23 changes: 23 additions & 0 deletions pkg/utils/constant.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import (
"github.com/holiman/uint256"
)

//
// Felt Constants
//

var FeltZero = fp.Element{}

var FeltOne = fp.Element{
Expand All @@ -14,4 +18,23 @@ var FeltOne = fp.Element{
// 1 << 128
var FeltMax128 = fp.Element{18446744073700081665, 17407, 18446744073709551584, 576460752142434320}

//
// Uint256 Constants
//

var Uint256Zero = uint256.Int{}

var Uint256One = uint256.Int{1, 0, 0, 0}

var Uint256Max128 = uint256.Int{18446744073709551615, 18446744073709551615, 0, 0}

// Alpha and Beta are paremeters required by the elliptic curve used by Cairo
// extracted from pedersen_params.json in https://github.com/starkware-libs/cairo-lang
var Alpha = fp.One()

var Beta = fp.Element([]uint64{
3863487492851900874,
7432612994240712710,
12360725113329547591,
88155977965380735,
})
2 changes: 1 addition & 1 deletion pkg/vm/builtins/bitwise.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func (b *Bitwise) InferValue(segment *memory.Segment, offset uint64) error {
bitwiseIndex := offset % cellsPerBitwise
// input cell
if bitwiseIndex < inputCellsPerBitwise {
return errors.New("cannot infer value")
return errors.New("cannot infer value from input cell")
}

xOffset := offset - bitwiseIndex
Expand Down
2 changes: 1 addition & 1 deletion pkg/vm/builtins/builtin_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func Runner(name starknetParser.Builtin) memory.BuiltinRunner {
case starknetParser.Bitwise:
return &Bitwise{}
case starknetParser.ECOP:
panic("Not implemented")
return &EcOp{}
case starknetParser.Poseidon:
panic("Not implemented")
case starknetParser.SegmentArena:
Expand Down
224 changes: 224 additions & 0 deletions pkg/vm/builtins/ecop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package builtins

import (
"errors"
"fmt"

"github.com/NethermindEth/cairo-vm-go/pkg/utils"
mem "github.com/NethermindEth/cairo-vm-go/pkg/vm/memory"
f "github.com/consensys/gnark-crypto/ecc/stark-curve/fp"
"github.com/holiman/uint256"
)

const EcOpName = "ec_op"
const cellsPerEcOp = 7
const inputCellsPerEcOp = 5

var feltThree f.Element = f.Element(
[]uint64{
18446744073709551521,
18446744073709551615,
18446744073709551615,
576460752303421872,
})

type EcOp struct{}

func (e *EcOp) String() string {
return EcOpName
}

func (e *EcOp) CheckWrite(segment *mem.Segment, offset uint64, value *mem.MemoryValue) error {
return nil
}

func (e *EcOp) InferValue(segment *mem.Segment, offset uint64) error {
// get the current slot index and verify it is an output cell
ecopIndex := offset % cellsPerEcOp
if ecopIndex < inputCellsPerEcOp {
return errors.New("cannot infer value from input cell")
}

// gather the input cells
inputOff := offset - ecopIndex
inputs := [5]mem.MemoryValue{
segment.Peek(inputOff),
segment.Peek(inputOff + 1),
segment.Peek(inputOff + 2),
segment.Peek(inputOff + 3),
segment.Peek(inputOff + 4),
}

// assert all values are known
for i := range inputs {
if !inputs[i].Known() {
return fmt.Errorf(
"cannot infer value: input value at offset %d is unknown", inputOff+uint64(i),
)
}
}

// unwrap the values as felts
inputsFelt := [5]*f.Element{}
for i := range inputs {
felt, err := inputs[i].FieldElement()
if err != nil {
return err
}
inputsFelt[i] = felt
}

// Note: the python vm has an upper limit on the size of `m`(the fifth input) but
// since it is always maxout at 2**252, I see no point on adding a check
// for it for now

// verify p and q are in the curve
p := point{*inputsFelt[0], *inputsFelt[1]}
q := point{*inputsFelt[2], *inputsFelt[3]}
if !p.onCurve(&utils.Alpha, &utils.Beta) {
return fmt.Errorf("point P(%s, %s) is not on the curve", &p.X, &p.Y)
}
if !q.onCurve(&utils.Alpha, &utils.Beta) {
return fmt.Errorf("point Q(%s, %s) is not on the curve", &q.X, &q.Y)
}

// calculate the elliptic curve operation
r, err := ecop(&p, &q, inputsFelt[4], &utils.Alpha)
if err != nil {
return err
}

// store the resulting point `r`
outputOff := inputOff + inputCellsPerEcOp

rxMV := mem.MemoryValueFromFieldElement(&r.X)
err = segment.Write(outputOff, &rxMV)
if err != nil {
return err
}

ryMV := mem.MemoryValueFromFieldElement(&r.Y)
err = segment.Write(outputOff+1, &ryMV)
return err
}

// structure to represent a point in the elliptic curve
type point struct {
X, Y f.Element
}

// returns true if a point `p` belongs to the `ec` curve ruled by the params `alpha` and
// `beta`. In other words, true if y^2 = x^3 + alpha * x + beta
func (p *point) onCurve(alpha, beta *f.Element) bool {
// calculate lhs
y2 := f.Element{}
y2.Square(&p.Y)

// calculate rhs
x3 := f.Element{}
x3.Square(&p.X)
x3.Mul(&x3, &p.X)

ax := f.Element{}
ax.Mul(alpha, &p.X)

x3.Add(&x3, &ax)
x3.Add(&x3, beta)

// return lhs == rhs
return y2.Equal(&x3)
}

// returns the result of the ecop operation on points `P` and `Q` with scalar
// `m` and param `alpha`. The resulting point `R` is equal to P + m * Q
func ecop(p *point, q *point, m, alpha *f.Element) (point, error) {
partialSum := *p
doublePoint := *q

mBytes := m.Bytes()
scalar := uint256.Int{}
scalar.SetBytes32(mBytes[:])

// Note: In the python VM the height is a parameter but it is always set at 256
// therefore we treat it as a constant
const height = 256
// todo(rodro): iteration could be cut short on the biggest bit with a one of the `scalar`
for i := 0; i < height && !scalar.IsZero(); i++ {
// we check that both points are always different between each others
// `ecadd` assume `x` ordinates are always different
// `ecdouble` assumes `y` coordinates are always different
if doublePoint.X.Equal(&partialSum.X) || doublePoint.Y.Equal(&utils.FeltZero) {
return point{}, fmt.Errorf(
"EcOp requires from P(%s, %s) and Q(%s, %s) that P.X != Q.X and Q.Y != 0 ",
&p.X, &p.Y, &q.X, &q.Y,
)
}
and := uint256.Int{}
and.And(&scalar, &utils.Uint256One)
if !and.IsZero() {
partialSum = ecadd(&partialSum, &doublePoint)
}

// todo(rodro): This loop can be optimized, potentially innecesary shift operations
doublePoint = ecdouble(&doublePoint, alpha)
scalar.Rsh(&scalar, 1)
}

return partialSum, nil
}

// performs elliptic curve addition over two points. Assumes `x` ordinates are
// always different
func ecadd(p *point, q *point) point {
// get the slope between the two points
slope := f.Element{}
slope.Sub(&p.Y, &q.Y)
denom := f.Element{}
denom.Sub(&p.X, &q.X)
slope.Div(&slope, &denom)

// get the x coordinate: x = slope^2 - p.X - q.X
x := f.Element{}
x.Square(&slope)
x.Sub(&x, &p.X)
x.Sub(&x, &q.X)

// get the y coordinate: y = slope * (p.X - x) - p.Y
y := f.Element{}
y.Sub(&p.X, &x)
y.Mul(&y, &slope)
y.Sub(&y, &p.Y)

return point{x, y}
}

// performs elliptic curve doubling over a point. Assumes `y` coordinate
// is different than 0
func ecdouble(p *point, alpha *f.Element) point {
// get the double slope
doubleSlope := f.Element{}
doubleSlope.Square(&p.X)
doubleSlope.Mul(
&doubleSlope,
&feltThree,
)
doubleSlope.Add(&doubleSlope, alpha)
denom := f.Element{}
denom.Double(&p.Y)
doubleSlope.Div(&doubleSlope, &denom)

// get the x coordinate: x = slope^2 - 2 * p.X
x := f.Element{}
x.Square(&doubleSlope)
doublePx := f.Element{}
doublePx.Double(&p.X)
x.Sub(&x, &doublePx)

// get the y coordinates: y = slope * (p.X - x) - p.Y
y := f.Element{}
y.Sub(&p.X, &x)
y.Mul(&y, &doubleSlope)
y.Sub(&y, &p.Y)

return point{x, y}
}
Loading

0 comments on commit 6089e75

Please sign in to comment.