diff --git a/integration_tests/builtin_tests/ecdsa_test.cairo b/integration_tests/builtin_tests/ecdsa_test.cairo new file mode 100644 index 000000000..25f4ba39e --- /dev/null +++ b/integration_tests/builtin_tests/ecdsa_test.cairo @@ -0,0 +1,13 @@ +%builtins ecdsa +from starkware.cairo.common.cairo_builtins import SignatureBuiltin +from starkware.cairo.common.signature import verify_ecdsa_signature + +func main{ ecdsa_ptr: SignatureBuiltin*}() { + verify_ecdsa_signature( + 2718, + 1735102664668487605176656616876767369909409133946409161569774794110049207117, + 3086480810278599376317923499561306189851900463386393948998357832163236918254, + 598673427589502599949712887611119751108407514580626464031881322743364689811, + ); + return (); +} diff --git a/integration_tests/cairozero_test.go b/integration_tests/cairozero_test.go index 3c2e7905d..e7ff36f9a 100644 --- a/integration_tests/cairozero_test.go +++ b/integration_tests/cairozero_test.go @@ -269,6 +269,17 @@ func TestPedersen(t *testing.T) { clean("./builtin_tests/") } +func TestECDSA(t *testing.T) { + compiledOutput, err := compileZeroCode("./builtin_tests/ecdsa_test.cairo") + require.NoError(t, err) + + _, _, _, err = runVm(compiledOutput) + //Note: This fails because no addSiganture hint + require.Error(t, err) + + clean("./builtin_tests/") +} + func TestEcOp(t *testing.T) { compiledOutput, err := compileZeroCode("./builtin_tests/ecop.cairo") require.NoError(t, err) @@ -276,8 +287,8 @@ func TestEcOp(t *testing.T) { _, _, _, 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/") + + clean("./builtin_tests/") } func TestKeccak(t *testing.T) { diff --git a/pkg/vm/builtins/builtin_runner.go b/pkg/vm/builtins/builtin_runner.go index fbad1545f..efa3c911d 100644 --- a/pkg/vm/builtins/builtin_runner.go +++ b/pkg/vm/builtins/builtin_runner.go @@ -14,7 +14,7 @@ func Runner(name starknetParser.Builtin) memory.BuiltinRunner { case starknetParser.Pedersen: return &Pedersen{} case starknetParser.ECDSA: - panic("Not implemented") + return &ECDSA{} case starknetParser.Keccak: return &Keccak{} case starknetParser.Bitwise: diff --git a/pkg/vm/builtins/ecdsa.go b/pkg/vm/builtins/ecdsa.go new file mode 100644 index 000000000..c9dfd100e --- /dev/null +++ b/pkg/vm/builtins/ecdsa.go @@ -0,0 +1,156 @@ +package builtins + +import ( + "fmt" + + "github.com/NethermindEth/cairo-vm-go/pkg/utils" + "github.com/NethermindEth/cairo-vm-go/pkg/vm/memory" + starkcurve "github.com/consensys/gnark-crypto/ecc/stark-curve" + ecdsa "github.com/consensys/gnark-crypto/ecc/stark-curve/ecdsa" + "github.com/consensys/gnark-crypto/ecc/stark-curve/fp" +) + +const ECDSAName = "ecdsa" +const cellsPerECDSA = 2 + +type ECDSA struct { + signatures map[uint64]ecdsa.Signature +} + +// verify_ecdsa_signature(message_hash, public_key, sig_r, sig_s) +func (e *ECDSA) CheckWrite(segment *memory.Segment, offset uint64, value *memory.MemoryValue) error { + ecdsaIndex := offset % cellsPerECDSA + pubOffset := offset - ecdsaIndex + msgOffset := pubOffset + 1 + + pub := segment.Peek(pubOffset) + msg := segment.Peek(msgOffset) + + //Both must be known to check the signature + if !msg.Known() || !pub.Known() { + return nil + } + + pubX, err := pub.FieldElement() //X element of the sig + if err != nil { + return err + } + + msgField, err := msg.FieldElement() + if err != nil { + return err + } + + //Recover Y part of the public key + posY, negY, err := recoverY(pubX) + if err != nil { + return err + } + + //Try first with positive y + key := starkcurve.G1Affine{X: *pubX, Y: posY} + if !key.IsOnCurve() { + return fmt.Errorf("key is not on curve") + } + + pubKey := &ecdsa.PublicKey{A: key} + sig, ok := e.signatures[pubOffset] + if !ok { + return fmt.Errorf("signature is missing form ECDA builtin") + } + + msgBytes := msgField.Bytes() + valid, err := pubKey.Verify(sig.Bytes(), msgBytes[:], nil) + if err != nil { + return err + } + + if !valid { + // Now try with Neg Y. Already know the point is on the curve so no need to check again + key = starkcurve.G1Affine{X: *pubX, Y: negY} + pubKey = &ecdsa.PublicKey{A: key} + valid, err := pubKey.Verify(sig.Bytes(), msgBytes[:], nil) + if err != nil { + return err + } + if !valid { + return fmt.Errorf("signature is not valid") + } + } + return nil +} + +func (e *ECDSA) InferValue(segment *memory.Segment, offset uint64) error { + return fmt.Errorf("can't infer value") +} + +/* +Hint that will call this function looks like this: + + "hints": { + "6": [ + { + "accessible_scopes": [ + "starkware.cairo.common.signature", + "starkware.cairo.common.signature.verify_ecdsa_signature" + ], + "code": "ecdsa_builtin.add_signature(ids.ecdsa_ptr.address_, (ids.signature_r, ids.signature_s))", + "flow_tracking_data": { + "ap_tracking": { + "group": 2, + "offset": 0 + }, + "reference_ids": { + "starkware.cairo.common.signature.verify_ecdsa_signature.ecdsa_ptr": 4, + "starkware.cairo.common.signature.verify_ecdsa_signature.message": 0, + "starkware.cairo.common.signature.verify_ecdsa_signature.public_key": 1, + "starkware.cairo.common.signature.verify_ecdsa_signature.signature_r": 2, + "starkware.cairo.common.signature.verify_ecdsa_signature.signature_s": 3 + } + } + } + ] + }, +*/ +func (e *ECDSA) AddSignature(pubOffset uint64, r, s *fp.Element) error { + if e.signatures == nil { + e.signatures = make(map[uint64]ecdsa.Signature) + } + bytes := make([]byte, 0, 64) + rBytes := r.Bytes() + bytes = append(bytes, rBytes[:]...) + sBytes := s.Bytes() + bytes = append(bytes, sBytes[:]...) + + sig := ecdsa.Signature{} + _, err := sig.SetBytes(bytes) + if err != nil { + return err + } + + e.signatures[pubOffset] = sig + return nil +} + +func (e *ECDSA) String() string { + return ECDSAName +} + +// recoverY recovers the y and -y coordinate of x. True y can be either y or -y +func recoverY(x *fp.Element) (fp.Element, fp.Element, error) { + // y_squared = (x * x * x + ALPHA * x + BETA) % FIELD_PRIME + ax := &fp.Element{} + ax.Mul(&utils.Alpha, x) + x2 := &fp.Element{} + x2.Mul(x, x) + x2.Mul(x2, x) + x2.Add(x2, ax) + x2.Add(x2, &utils.Beta) + y := x2.Sqrt(x2) + if y == nil { + return fp.Element{}, fp.Element{}, fmt.Errorf("Invalid Public key") + } + negY := fp.Element{} + negY.Neg(y) + return *y, negY, nil +} diff --git a/pkg/vm/builtins/ecdsa_test.go b/pkg/vm/builtins/ecdsa_test.go new file mode 100644 index 000000000..21eff949f --- /dev/null +++ b/pkg/vm/builtins/ecdsa_test.go @@ -0,0 +1,47 @@ +package builtins + +import ( + "testing" + + "github.com/NethermindEth/cairo-vm-go/pkg/vm/memory" + "github.com/consensys/gnark-crypto/ecc/stark-curve/fp" + "github.com/stretchr/testify/require" +) + +func TestECDSA(t *testing.T) { + ecdsa := &ECDSA{} + segment := memory.EmptySegmentWithLength(5) + segment.WithBuiltinRunner(ecdsa) + + pubkey, _ := new(fp.Element).SetString("1735102664668487605176656616876767369909409133946409161569774794110049207117") + msg, _ := new(fp.Element).SetString("2718") + r, _ := new(fp.Element).SetString("3086480810278599376317923499561306189851900463386393948998357832163236918254") + s, _ := new(fp.Element).SetString("598673427589502599949712887611119751108407514580626464031881322743364689811") + + pubkeyValue := memory.MemoryValueFromFieldElement(pubkey) + msgValue := memory.MemoryValueFromFieldElement(msg) + + require.NoError(t, ecdsa.AddSignature(0, r, s)) + require.NoError(t, segment.Write(1, &msgValue)) + require.NoError(t, segment.Write(0, &pubkeyValue)) + +} +func TestECDSAInvalidSig(t *testing.T) { + ecdsa := &ECDSA{} + segment := memory.EmptySegmentWithLength(5) + segment.WithBuiltinRunner(ecdsa) + + pubkey, _ := new(fp.Element).SetString("1735102664668487605176656616876767369909409133946409161569774794110049207117") + msg, _ := new(fp.Element).SetString("999999999999999") + r, _ := new(fp.Element).SetString("4123123123213") + s, _ := new(fp.Element).SetString("31231231313") + + pubkeyValue := memory.MemoryValueFromFieldElement(pubkey) + msgValue := memory.MemoryValueFromFieldElement(msg) + + require.NoError(t, ecdsa.AddSignature(0, r, s)) + require.NoError(t, segment.Write(0, &pubkeyValue)) + err := segment.Write(1, &msgValue) + require.ErrorContains(t, err, "signature is not valid") + +}