Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/native-contracts-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ When calling a native contract method by transaction script, there are several t
| bls12381Equal | Determines whether the specified points are equal. | InteropInterface(*x*), InteropInterface(*y*) | Boolean | 1<<5 | 0 | -- | -- |
| bls12381Add | Add operation of two points. | InteropInterface(*x*), InteropInterface(*y*) | InteropInterface | 1<<19 | 0 | -- | -- |
| bls12381Mul | Mul operation of gt point and multiplier | InteropInterface(*x*), Byte[](*mul*), Boolean(*neg*) | InteropInterface | 1<<21 | 0 | -- | -- |
| bls12381MultiExp | Multi exponentiation operation for bls12381 points. | Array(*pairs*) | InteropInterface | 1<<23 | 0 | -- | HF_Gorgon |
| bls12381Pairing | Pairing operation of g1 and g2 | InteropInterface(*g1*), InteropInterface(*g2*) | InteropInterface | 1<<23 | 0 | -- | -- |
| recoverSecp256K1 | Recovers the public key from a secp256k1 signature in a single byte array format. | Byte[](*messageHash*), Byte[](*signature*) | Byte[] | 1<<15 | 0 | -- | HF_Echidna |
| ripemd160 | Computes the hash value for the specified byte array using the ripemd160 algorithm. | Byte[](*data*) | Byte[] | 1<<15 | 0 | -- | -- |
Expand Down
109 changes: 109 additions & 0 deletions src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using Neo.Cryptography.BLS12_381;
using Neo.VM.Types;
using System;
using Array = Neo.VM.Types.Array;
using VMBuffer = Neo.VM.Types.Buffer;

namespace Neo.SmartContract.Native
{
Expand Down Expand Up @@ -119,6 +121,77 @@ public static InteropInterface Bls12381Mul(InteropInterface x, byte[] mul, bool
};
}

/// <summary>
/// Multi exponentiation operation for bls12381 points.
/// </summary>
/// <param name="pairs">Array of [point, scalar] pairs.</param>
/// <returns>The accumulated point.</returns>
[ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 23)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Faun

public static InteropInterface Bls12381MultiExp(Array pairs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we need to check the length, a max is required or it could deny the service with 1024 pairs

{
if (pairs is null || pairs.Count == 0)
throw new ArgumentException("BLS12-381 multi exponent requires at least one pair");

bool? useG2 = null;
G1Projective g1Accumulator = G1Projective.Identity;
G2Projective g2Accumulator = G2Projective.Identity;

foreach (StackItem item in pairs)
{
if (item is not Array pair || pair.Count != 2)
throw new ArgumentException("BLS12-381 multi exponent pair must contain point and scalar");

if (pair[0] is not InteropInterface pointInterface)
throw new ArgumentException("BLS12-381 multi exponent requires interop points");

var point = pointInterface.GetInterface<object>();
switch (point)
{
case G1Affine g1Affine:
EnsureGroupType(ref useG2, false);
{
var scalar = ParseScalar(pair[1]);
if (!scalar.IsZero)
g1Accumulator += new G1Projective(g1Affine) * scalar;
Copy link

@txhsl txhsl Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to make sure that subgroup check is executed before any multiplication operation. This was fixed in Ethereum through ethereum/EIPs#8456.

Briefly speaking, we need:

  1. "IsOnCurve" check after G1 point decoding and G2 point decoding, e.g. https://github.com/ethereum/go-ethereum/blob/v1.16.5/core/vm/contracts.go#L1212;
  2. "IsInSubGroup" check before multiply and pairing computation, e.g. https://github.com/ethereum/go-ethereum/blob/v1.16.5/core/vm/contracts.go#L1005 and https://github.com/ethereum/go-ethereum/blob/v1.16.5/core/vm/contracts.go#L1173.

About the detailed implementation of these checks, please ref https://github.com/Consensys/gnark-crypto/blob/v0.19.0/ecc/bls12-381/g1.go#L193-L218 and https://github.com/Consensys/gnark-crypto/blob/v0.19.0/ecc/bls12-381/g2.go#L200-L223.

}
break;
case G1Projective g1Projective:
EnsureGroupType(ref useG2, false);
{
var scalar = ParseScalar(pair[1]);
if (!scalar.IsZero)
g1Accumulator += g1Projective * scalar;
}
break;
case G2Affine g2Affine:
EnsureGroupType(ref useG2, true);
{
var scalar = ParseScalar(pair[1]);
if (!scalar.IsZero)
g2Accumulator += new G2Projective(g2Affine) * scalar;
}
break;
case G2Projective g2Projective:
EnsureGroupType(ref useG2, true);
{
var scalar = ParseScalar(pair[1]);
if (!scalar.IsZero)
g2Accumulator += g2Projective * scalar;
}
break;
default:
throw new ArgumentException("BLS12-381 type mismatch");
}
}

if (useG2 is null)
throw new ArgumentException("BLS12-381 multi exponent requires at least one valid pair");

return useG2.Value
? new InteropInterface(g2Accumulator)
: new InteropInterface(g1Accumulator);
}

/// <summary>
/// Pairing operation of g1 and g2
/// </summary>
Expand All @@ -142,5 +215,41 @@ public static InteropInterface Bls12381Pairing(InteropInterface g1, InteropInter
};
return new(Bls12.Pairing(in g1a, in g2a));
}

private static void EnsureGroupType(ref bool? current, bool isG2)
{
if (current is null)
{
current = isG2;
}
else if (current.Value != isG2)
{
throw new ArgumentException("BLS12-381 multi exponent cannot mix groups");
}
}

private static Scalar ParseScalar(StackItem scalarItem)
{
ReadOnlySpan<byte> data = scalarItem switch
{
ByteString bs when bs.GetSpan().Length == Scalar.Size => bs.GetSpan(),
VMBuffer buffer when buffer.Size == Scalar.Size => buffer.InnerBuffer.Span,
_ => throw new ArgumentException("BLS12-381 scalar must be 32 bytes"),
};

Span<byte> littleEndian = stackalloc byte[Scalar.Size];
data.CopyTo(littleEndian);

try
{
return Scalar.FromBytes(littleEndian);
}
catch (FormatException)
{
var wide = new byte[Scalar.Size * 2];
littleEndian.CopyTo(wide);
return Scalar.FromBytesWide(wide);
}
}
}
}
123 changes: 120 additions & 3 deletions tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@
using Neo.Ledger;
using Neo.Network.P2P;
using Neo.Network.P2P.Payloads;
using Neo.Persistence;
using Neo.SmartContract;
using Neo.SmartContract.Native;
using Neo.VM;
using Neo.VM.Types;
using Org.BouncyCastle.Utilities.Encoders;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using VMArray = Neo.VM.Types.Array;

namespace Neo.UnitTests.SmartContract.Native
{
Expand Down Expand Up @@ -263,6 +267,101 @@ public void TestBls12381Pairing()
Assert.AreEqual(expected.ToLower(), result.GetInterface<Gt>().ToArray().ToHexString());
}

[TestMethod]
public void TestBls12381MultiExpG1()
{
var g1Point = G1Affine.FromCompressed(g1);
var pair1 = new VMArray(new StackItem[]
{
StackItem.FromInterface(g1Point),
new ByteString(CreateScalarBytes(1))
});
var pair2 = new VMArray(new StackItem[]
{
StackItem.FromInterface(g1Point),
new ByteString(CreateScalarBytes(2))
});
var pairs = new VMArray(new StackItem[] { pair1, pair2 });

var result = CryptoLib.Bls12381MultiExp(pairs);
var actual = result.GetInterface<G1Projective>();

var expected = new G1Projective(g1Point) * CreateScalar(3);
Assert.AreEqual(new G1Affine(expected).ToCompressed().ToHexString(),
new G1Affine(actual).ToCompressed().ToHexString());
}

[TestMethod]
public void TestBls12381MultiExpG2()
{
var g2Point = G2Affine.FromCompressed(g2);
var pair = new VMArray(new StackItem[]
{
StackItem.FromInterface(new G2Projective(g2Point)),
new ByteString(CreateScalarBytes(5))
});
var pairs = new VMArray(new StackItem[] { pair });

var result = CryptoLib.Bls12381MultiExp(pairs);
var actual = result.GetInterface<G2Projective>();

var expected = new G2Projective(g2Point) * CreateScalar(5);
Assert.AreEqual(new G2Affine(expected).ToCompressed().ToHexString(),
new G2Affine(actual).ToCompressed().ToHexString());
}

[TestMethod]
public void TestBls12381MultiExpReducesScalar()
{
var g1Point = G1Affine.FromCompressed(g1);
var oversized = (BigInteger.One << 260) + 5;
var scalarBytes = CreateScalarBytes(oversized);
var pair = new VMArray(new StackItem[]
{
StackItem.FromInterface(g1Point),
new ByteString(scalarBytes)
});
var pairs = new VMArray(new StackItem[] { pair });

var wide = new byte[Scalar.Size * 2];
System.Array.Copy(scalarBytes, wide, scalarBytes.Length);
var reducedScalar = Scalar.FromBytesWide(wide);

var result = CryptoLib.Bls12381MultiExp(pairs);
var actual = result.GetInterface<G1Projective>();

var expected = new G1Projective(g1Point) * reducedScalar;
Assert.AreEqual(new G1Affine(expected).ToCompressed().ToHexString(),
new G1Affine(actual).ToCompressed().ToHexString());
}

[TestMethod]
public void TestBls12381MultiExpMixedGroupFails()
{
var g1Point = G1Affine.FromCompressed(g1);
var g2Point = G2Affine.FromCompressed(g2);
var pair1 = new VMArray(new StackItem[]
{
StackItem.FromInterface(g1Point),
new ByteString(CreateScalarBytes(1))
});
var pair2 = new VMArray(new StackItem[]
{
StackItem.FromInterface(g2Point),
new ByteString(CreateScalarBytes(1))
});
var pairs = new VMArray(new StackItem[] { pair1, pair2 });

Assert.ThrowsExactly<ArgumentException>(() => CryptoLib.Bls12381MultiExp(pairs));
}

[TestMethod]
public void TestBls12381MultiExpEmptyFails()
{
var pairs = new VMArray();
Assert.ThrowsExactly<ArgumentException>(() => CryptoLib.Bls12381MultiExp(pairs));
}

[TestMethod]
public void Bls12381Equal()
{
Expand Down Expand Up @@ -1125,7 +1224,7 @@ public void TestVerifyWithEd25519()
{
// byte[] privateKey = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60".HexToBytes();
byte[] publicKey = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a".HexToBytes();
byte[] message = Array.Empty<byte>();
byte[] message = System.Array.Empty<byte>();
byte[] signature = ("e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155" +
"5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b").HexToBytes();

Expand All @@ -1141,13 +1240,13 @@ public void TestVerifyWithEd25519()

// Test with an invalid signature
byte[] invalidSignature = new byte[signature.Length];
Array.Copy(signature, invalidSignature, signature.Length);
System.Array.Copy(signature, invalidSignature, signature.Length);
invalidSignature[0] ^= 0x01; // Flip one bit
Assert.IsFalse(CallVerifyWithEd25519(message, publicKey, invalidSignature));

// Test with an invalid public key
byte[] invalidPublicKey = new byte[publicKey.Length];
Array.Copy(publicKey, invalidPublicKey, publicKey.Length);
System.Array.Copy(publicKey, invalidPublicKey, publicKey.Length);
invalidPublicKey[0] ^= 0x01; // Flip one bit
Assert.IsFalse(CallVerifyWithEd25519(message, invalidPublicKey, signature));
}
Expand All @@ -1174,5 +1273,23 @@ private bool CallVerifyWithEd25519(byte[] message, byte[] publicKey, byte[] sign
return engine.ResultStack.Pop().GetBoolean();
}
}

private static byte[] CreateScalarBytes(BigInteger value)
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value));

var bytes = new byte[Scalar.Size];
var mask = (BigInteger.One << (Scalar.Size * 8)) - BigInteger.One;
var truncated = value & mask;
if (!truncated.TryWriteBytes(bytes, out _, isBigEndian: false))
throw new InvalidOperationException("Unable to encode scalar value.");
return bytes;
}

private static byte[] CreateScalarBytes(uint value) => CreateScalarBytes(new BigInteger(value));

private static Scalar CreateScalar(uint value) => Scalar.FromBytes(CreateScalarBytes(value));

}
}
Loading