Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EIP/RIP-7212 #7135

Merged
merged 10 commits into from
Jun 13, 2024
Merged

EIP/RIP-7212 #7135

merged 10 commits into from
Jun 13, 2024

Conversation

alexb5dh
Copy link
Contributor

@alexb5dh alexb5dh commented Jun 4, 2024

Changes

Adds secp256r1 precompile contract.
For now intended to be used in Optimism, but may be enabled in Ethereum as agreed.

EIP | RIP | Discussion | Geth PR

Types of changes

What types of changes does your code introduce?

  • New feature (a non-breaking change that adds functionality)

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

Requires explanation in Release Notes

  • Yes
  • No

@alexb5dh alexb5dh changed the title EIP-7212 EIP/RIP-7212 Jun 11, 2024
@alexb5dh alexb5dh self-assigned this Jun 11, 2024
@alexb5dh
Copy link
Contributor Author

alexb5dh commented Jun 12, 2024

Current implementation uses built-in .NET ECDsa, which is not the fastest option. Benchmarks comparing different versions:

.NET

Windows 10

Method Mean Error StdDev
DotNet_Ecdsa_WithCreate 436.08 us 4.865 us 6.819 us
DotNet_Ecdsa_WithImportParameters 448.28 us 4.319 us 6.330 us
BouncyCastle 227.36 us 3.874 us 5.798 us
Go_Ecdsa_MarshallingString 68.06 us 0.399 us 0.572 us
Go_Ecdsa_MarshallingSlice 66.98 us 0.346 us 0.507 us

Ubuntu 22

Method Mean Error StdDev
DotNet_Ecdsa_WithCreate 144.05 us 1.038 us 1.488 us
DotNet_Ecdsa_WithImportParameters 142.64 us 0.755 us 1.107 us
BouncyCastle 208.14 us 1.339 us 1.962 us
Go_Ecdsa_MarshallingString 68.89 us 0.356 us 0.533 us
Go_Ecdsa_MarshallingSlice 69.93 us 1.772 us 2.365 us

Go

Windows 10

Method Mean
Go_Ecdsa 66.05 us

Ubuntu 22

Method Mean
Go_Ecdsa 64.92 us

@alexb5dh alexb5dh marked this pull request as ready for review June 12, 2024 01:11
@benaadams
Copy link
Member

Which go lib specifically is that?

@alexb5dh
Copy link
Contributor Author

alexb5dh commented Jun 12, 2024

Which go lib specifically is that?

@benaadams, built-in ecdsa package.

Go code used:

import (
    "encoding/hex"
    "crypto/ecdsa"
    "crypto/elliptic"
    "math/big"
    "fmt"
    "C"
)

//export VerifyHex
func VerifyHex(input *C.char) bool {
    var h = C.GoString(input)

    bytes, err := hex.DecodeString(h)
    if err != nil {
        return false
    }

    return VerifyBytes(bytes)
}

//export VerifyBytes
func VerifyBytes(bytes []byte) bool {
    if len(bytes) != 160 {
        return false
    }

    var hash = bytes[0:32]
    var r, s = new(big.Int).SetBytes(bytes[32:64]), new(big.Int).SetBytes(bytes[64:96])
    var x, y = new(big.Int).SetBytes(bytes[96:128]), new(big.Int).SetBytes(bytes[128:160])
    var publicKey = &ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}

    return ecdsa.Verify(publicKey, hash, r, s)
}

Can be built into a library via

go build -ldflags="-s -w" -buildmode=c-shared -o secp256r1.dll main.go

@alexb5dh
Copy link
Contributor Author

C# benchmarking code:

public class Secp256r1VerifyBenchmarks
{
    private const string InputHex = "4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e";
    private static readonly byte[] Input = Convert.FromHexString(InputHex);

    private ECDsa _ecdsaShared;
    private X9ECParameters _bouncyCurve;
    private ISigner _bouncySigner;
    private ECDomainParameters _bouncyDomainParameters;

    [GlobalSetup]
    public void Setup()
    {
        _ecdsaShared = ECDsa.Create();

        _bouncyCurve = CustomNamedCurves.GetByName("secp256r1");
        _bouncySigner = SignerUtilities.GetSigner("NONEWITHPLAIN-ECDSA");
        _bouncyDomainParameters = new(_bouncyCurve);
    }

    [Benchmark]
    public bool DotNet_Ecdsa_WithCreate()
    {
        ReadOnlySpan<byte> input = Input;
        ReadOnlySpan<byte> hash = input[..32];
        ReadOnlySpan<byte> sig = input[32..96];
        ReadOnlySpan<byte> x = input[96..128];
        ReadOnlySpan<byte> y = input[128..160];

        var ecdsa = ECDsa.Create(new ECParameters
        {
            Curve = ECCurve.NamedCurves.nistP256,
            Q = new() { X = x.ToArray(), Y = y.ToArray() }
        });

        return ecdsa.VerifyHash(hash, sig);
    }

    [Benchmark]
    public bool DotNet_Ecdsa_WithImportParameters()
    {
        ReadOnlySpan<byte> input = Input;
        ReadOnlySpan<byte> hash = input[..32];
        ReadOnlySpan<byte> sig = input[32..96];
        ReadOnlySpan<byte> x = input[96..128];
        ReadOnlySpan<byte> y = input[128..160];

        ECDsa ecdsa = _ecdsaShared;
        ecdsa.ImportParameters(new ECParameters
        {
            Curve = ECCurve.NamedCurves.nistP256,
            Q = new() { X = x.ToArray(), Y = y.ToArray() }
        });

        return ecdsa.VerifyHash(hash, sig);
    }

    [Benchmark] // 211.2 us
    public bool BouncyCastle()
    {
        ReadOnlySpan<byte> input = Input;
        ReadOnlySpan<byte> hash = input[..32];
        ReadOnlySpan<byte> sig = input[32..96];
        ReadOnlySpan<byte> x = input[96..128];
        ReadOnlySpan<byte> y = input[128..160];

        ECPublicKeyParameters parameters = new(
            "ECDSA",
            _bouncyCurve.Curve.CreatePoint(new Org.BouncyCastle.Math.BigInteger(1, x), new Org.BouncyCastle.Math.BigInteger(1, y)),
            _bouncyDomainParameters
        );

        _bouncySigner.Init(false, parameters);
        _bouncySigner.BlockUpdate(hash);
        return _bouncySigner.VerifySignature(sig.ToArray());
    }

#if Windows
    [DllImport("secp256r1.dll", CallingConvention = CallingConvention.Cdecl)]
#elif Linux
    [DllImport("secp256r1.so", CallingConvention = CallingConvention.Cdecl)]
#endif
    private static extern byte VerifyHex(string hash);

    [Benchmark]
    public bool Go_Ecdsa_MarshallingString() => VerifyHex(InputHex) != 0;

#if Windows
    [DllImport("secp256r1.dll", CallingConvention = CallingConvention.Cdecl)]
#elif Linux
    [DllImport("secp256r1.so", CallingConvention = CallingConvention.Cdecl)]
#endif
    private static extern byte VerifyBytes(GoSlice hash);

    private struct GoSlice(IntPtr data, long len, long cap)
    {
        public IntPtr Data = data;
        public long Len = len, Cap = cap;
    }

    [Benchmark]
    public bool Go_Ecdsa_MarshallingSlice()
    {
        unsafe
        {
            fixed (byte* p = Input)
            {
                var ptr = (IntPtr) p;
                var slice = new GoSlice(ptr, Input.Length, Input.Length);
                return VerifyBytes(slice) != 0;
            }
        }
    }

    // Other options:
    // https://github.com/starkbank/ecdsa-dotnet - needs bridge/pinvoke from Rust
    // https://github.com/MystenLabs/fastcrypto - very slow actually
}

@alexb5dh alexb5dh merged commit 57a24e9 into master Jun 13, 2024
68 checks passed
@alexb5dh alexb5dh deleted the feature/eip-7212 branch June 13, 2024 12:14
@benaadams
Copy link
Member

Aside; this is different to the SecP256k1 lib we use for Ecdsa sign and recover?

@alexb5dh
Copy link
Contributor Author

It's for secP256_r_1

@alexb5dh
Copy link
Contributor Author

Noticed that Go implementation does some caching, at least when converting public key to C version, so updated benchmarks to try to eliminate/reduce any caching benefits. Didn't find any noticeable difference so:

public class Secp256r1VerifyBenchmarks
{
    public byte[][] Inputs;
    public string[] InputsHexes;

    private ECDsa _ecdsaShared;
    private X9ECParameters _bouncyCurve;
    private ISigner _bouncySigner;
    private ECDomainParameters _bouncyDomainParameters;

    [Params(1, 100, 10_000)]
    public int SamplesCount;

    [GlobalSetup]
    public void Setup()
    {
        _ecdsaShared = ECDsa.Create();

        _bouncyCurve = CustomNamedCurves.GetByName("secp256r1");
        _bouncySigner = SignerUtilities.GetSigner("NONEWITHPLAIN-ECDSA");
        _bouncyDomainParameters = new(_bouncyCurve);

        var rng = RandomNumberGenerator.Create();
        Inputs = new byte[SamplesCount][];
        InputsHexes = new string[SamplesCount];
        for (var i = 0; i < Inputs.Length; i++)
        {
            var hash = new byte[32];
            rng.GetBytes(hash);

            var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
            ECParameters pub = ecdsa.ExportParameters(false);
            byte[] sig = ecdsa.SignHash(hash, DSASignatureFormat.IeeeP1363FixedFieldConcatenation);
            (byte[] x, byte[] y) = (pub.Q.X, pub.Q.Y);
            Inputs[i] = [..hash, ..sig, ..x, ..y];
            InputsHexes[i] = Convert.ToHexString(Inputs[i]);
        }
    }

    private int Index => Random.Shared.Next(Inputs.Length);
    private byte[] Input => Inputs[Index];
    private string InputHex => InputsHexes[Index];

    [Benchmark]
    public bool DotNet_Ecdsa_WithCreate()
    {
        ReadOnlySpan<byte> input = Input;
        ReadOnlySpan<byte> hash = input[..32];
        ReadOnlySpan<byte> sig = input[32..96];
        ReadOnlySpan<byte> x = input[96..128];
        ReadOnlySpan<byte> y = input[128..160];

        var ecdsa = ECDsa.Create(new ECParameters
        {
            Curve = ECCurve.NamedCurves.nistP256,
            Q = new() { X = x.ToArray(), Y = y.ToArray() }
        });

        return ecdsa.VerifyHash(hash, sig);
    }

    [Benchmark]
    public bool DotNet_Ecdsa_WithImportParameters()
    {
        ReadOnlySpan<byte> input = Input;
        ReadOnlySpan<byte> hash = input[..32];
        ReadOnlySpan<byte> sig = input[32..96];
        ReadOnlySpan<byte> x = input[96..128];
        ReadOnlySpan<byte> y = input[128..160];

        ECDsa ecdsa = _ecdsaShared;
        ecdsa.ImportParameters(new ECParameters
        {
            Curve = ECCurve.NamedCurves.nistP256,
            Q = new() { X = x.ToArray(), Y = y.ToArray() }
        });

        return ecdsa.VerifyHash(hash, sig);
    }

    [Benchmark]
    public bool BouncyCastle()
    {
        ReadOnlySpan<byte> input = Input;
        ReadOnlySpan<byte> hash = input[..32];
        ReadOnlySpan<byte> sig = input[32..96];
        ReadOnlySpan<byte> x = input[96..128];
        ReadOnlySpan<byte> y = input[128..160];

        ECPublicKeyParameters parameters = new(
            "ECDSA",
            _bouncyCurve.Curve.CreatePoint(new Org.BouncyCastle.Math.BigInteger(1, x), new Org.BouncyCastle.Math.BigInteger(1, y)),
            _bouncyDomainParameters
        );

        _bouncySigner.Init(false, parameters);
        _bouncySigner.BlockUpdate(hash);
        return _bouncySigner.VerifySignature(sig.ToArray());
    }

#if Windows
    [DllImport("secp256r1.dll", CallingConvention = CallingConvention.Cdecl)]
#elif Linux
    [DllImport("secp256r1.so", CallingConvention = CallingConvention.Cdecl)]
#endif
    private static extern byte VerifyHex(string hash);

    [Benchmark]
    public bool Go_Ecdsa_MarshallingString() => VerifyHex(InputHex) != 0;

#if Windows
    [DllImport("secp256r1.dll", CallingConvention = CallingConvention.Cdecl)]
#elif Linux
    [DllImport("secp256r1.so", CallingConvention = CallingConvention.Cdecl)]
#endif
    private static extern byte VerifyBytes(GoSlice hash);

    private struct GoSlice(IntPtr data, long len, long cap)
    {
        public IntPtr Data = data;
        public long Len = len, Cap = cap;
    }

    [Benchmark]
    public bool Go_Ecdsa_MarshallingSlice()
    {
        ReadOnlySpan<byte> input = Input;

        unsafe
        {
            fixed (byte* p = input)
            {
                var ptr = (IntPtr) p;
                var slice = new GoSlice(ptr, input.Length, input.Length);
                return VerifyBytes(slice) != 0;
            }
        }
    }

    // Other options:
    // https://github.com/starkbank/ecdsa-dotnet - needs bridge/pinvoke from Rust
    // https://github.com/MystenLabs/fastcrypto - very slow actually
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants