Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1a443fe
Add Ethereum-compatible aliases for BLS12-381
Jim8y Sep 22, 2025
67b0fbb
Format codebase
Jim8y Sep 22, 2025
7f3fe50
Merge branch 'dev' into feature/bls12-eth-aliases
ajara87 Sep 25, 2025
dc9b374
Merge branch 'dev' into feature/bls12-eth-aliases
Jim8y Oct 14, 2025
1f82a9b
Implement BLS12-381 multi exponentiation
Jim8y Oct 17, 2025
be21a84
Merge branch 'dev' into feature/bls12-eth-aliases
shargon Oct 18, 2025
3f69ba9
Merge branch 'dev' into feature/bls12-eth-aliases
shargon Oct 20, 2025
9607bd7
Update src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs
shargon Oct 20, 2025
b6fb04b
Merge branch 'dev' into feature/bls12-eth-aliases
Jim8y Oct 23, 2025
5f4dbe5
Harden BLS12-381 multi exponentiation
Jim8y Oct 23, 2025
2e8378c
Merge branch 'dev' into feature/bls12-eth-aliases
cschuchardt88 Oct 23, 2025
434c62e
Merge branch 'dev' into feature/bls12-eth-aliases
shargon Oct 23, 2025
dc23a56
Add subgroup validation tests for BLS multi exp
Jim8y Nov 4, 2025
bcabd4e
Merge origin/dev into feature/bls12-eth-aliases
Jim8y Nov 4, 2025
5b40385
Fix BLS12-381 multiexp endianness and add Ethereum tests
Jim8y Nov 6, 2025
15cb14b
Document bls12381MultiExp scalar encoding
Jim8y Nov 6, 2025
a39ed8e
Revert "Document bls12381MultiExp scalar encoding"
Jim8y Nov 6, 2025
7fa60b7
Validate pairing inputs for BLS12-381
Jim8y Nov 6, 2025
a49a6b7
Merge branch 'dev' into feature/bls12-eth-aliases
Jim8y Nov 8, 2025
7d468ce
Add EVM-compatible BLS12 alias entrypoints
Jim8y Nov 10, 2025
df66f45
Add EIP-2537 serialize helpers and boundary tests
Jim8y Nov 20, 2025
93713e8
Add edge-case coverage for eth BLS serialize/deserialize
Jim8y Nov 20, 2025
8dedeed
Tweak assertion message
Jim8y Nov 20, 2025
f2c75e6
Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs
shargon Nov 27, 2025
421bc93
Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs
shargon Nov 27, 2025
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_Faun, CpuFee = 1 << 23)]
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

Copy link
Member

Choose a reason for hiding this comment

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

Cpu should be paid in each iteration

{
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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link

@txhsl txhsl Nov 10, 2025

Choose a reason for hiding this comment

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

Similar to what I've mentioned in #4185 (comment), MultiExp() is also a choice here, ref https://github.com/ethereum/go-ethereum/blob/v1.16.7/core/vm/contracts.go#L1015. Only for performance.

}
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];
Copy link

Choose a reason for hiding this comment

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

Is this an incorrect naming or really using the little-endian encoding? Since you're trying to handle big-endian in BN254, and it is big-endian in Gnark and EVM, at least we need to keep the same encoding.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i'll check

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