diff --git a/docs/native-contracts-api.md b/docs/native-contracts-api.md index d89d261bf3..aeb6cc8992 100644 --- a/docs/native-contracts-api.md +++ b/docs/native-contracts-api.md @@ -77,8 +77,18 @@ When calling a native contract method by transaction script, there are several t | bls12381Deserialize | Deserialize a bls12381 point. | Byte[](*data*) | InteropInterface | 1<<19 | 0 | -- | -- | | 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 | -- | -- | +| bls12_g1add | Ethereum-style G1 addition using uncompressed big-endian coordinates (x|y, 64-byte limbs). Input is two concatenated 128-byte encodings; output is the same encoding. | Byte[](*input*) | Byte[] | 1<<19 | 0 | -- | HF_Faun | +| bls12_g2add | Ethereum-style G2 addition using uncompressed big-endian coordinates (x0|x1|y0|y1, 64-byte limbs). Input is two concatenated 256-byte encodings; output is the same encoding. | Byte[](*input*) | Byte[] | 1<<19 | 0 | -- | HF_Faun | | bls12381Mul | Mul operation of gt point and multiplier | InteropInterface(*x*), Byte[](*mul*), Boolean(*neg*) | InteropInterface | 1<<21 | 0 | -- | -- | +| bls12_g1mul | Ethereum-style G1 scalar multiplication using uncompressed big-endian coordinates and big-endian scalar. Input is 128-byte point + 32-byte scalar; output is the same encoding. | Byte[](*input*) | Byte[] | 1<<21 | 0 | -- | HF_Faun | +| bls12_g2mul | Ethereum-style G2 scalar multiplication using uncompressed big-endian coordinates and big-endian scalar. Input is 256-byte point + 32-byte scalar; output is the same encoding. | Byte[](*input*) | Byte[] | 1<<21 | 0 | -- | HF_Faun | +| bls12_g1multiexp | Ethereum-style G1 MSM using uncompressed big-endian point encodings and big-endian scalars. Input is k concatenated (128-byte point | 32-byte scalar) pairs; output is the same encoding. | Byte[](*input*) | Byte[] | 1<<23 | 0 | -- | HF_Faun | +| bls12_g2multiexp | Ethereum-style G2 MSM using uncompressed big-endian point encodings and big-endian scalars. Input is k concatenated (256-byte point | 32-byte scalar) pairs; output is the same encoding. | Byte[](*input*) | Byte[] | 1<<23 | 0 | -- | HF_Faun | +| bls12381MultiExp | Multi exponentiation operation for bls12381 points. | Array(*pairs*) | InteropInterface | 1<<23 | 0 | -- | HF_Faun | | bls12381Pairing | Pairing operation of g1 and g2 | InteropInterface(*g1*), InteropInterface(*g2*) | InteropInterface | 1<<23 | 0 | -- | -- | +| bls12_pairing | Ethereum-style pairing check (EIP-2537): accepts k concatenated pairs of uncompressed G1/G2 encodings and returns 32-byte result (LSB set for success). | Byte[](*input*) | Byte[] | 1<<23 | 0 | -- | HF_Faun | +| bls12_deserialize | Deserialize a G1/G2 point using Ethereum uncompressed big-endian encoding. | Byte[](*data*) | InteropInterface | 1<<19 | 0 | -- | HF_Faun | +| bls12_serialize | Serialize a G1/G2 point using Ethereum uncompressed big-endian encoding. | InteropInterface(*g*) | Byte[] | 1<<19 | 0 | -- | HF_Faun | | 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 | -- | -- | | sha256 | Computes the hash value for the specified byte array using the sha256 algorithm. | Byte[](*data*) | Byte[] | 1<<15 | 0 | -- | -- | @@ -88,6 +98,8 @@ When calling a native contract method by transaction script, there are several t | verifyWithECDsa | -- | Byte[](*message*), Byte[](*pubkey*), Byte[](*signature*), NamedCurveHash(*curve*) | Boolean | 1<<15 | 0 | -- | Deprecated in HF_Cockatrice | | verifyWithEd25519 | Verifies that a digital signature is appropriate for the provided key and message using the Ed25519 algorithm. | Byte[](*message*), Byte[](*pubkey*), Byte[](*signature*) | Boolean | 1<<15 | 0 | -- | HF_Echidna | +**Note:** Methods prefixed with `bls12_` follow the EIP-2537 (Ethereum) uncompressed encoding: G1 inputs are `x || y` (128 bytes), G2 inputs are `x0 || x1 || y0 || y1` (256 bytes), scalars are 32-byte big-endian, and the identity is encoded as all-zero bytes. The existing `bls12381*` methods keep Neo's compressed encoding. + ## LedgerContract diff --git a/src/Neo.Cryptography.BLS12_381/G2Affine.cs b/src/Neo.Cryptography.BLS12_381/G2Affine.cs index a590c35857..0f5d7ecb7c 100644 --- a/src/Neo.Cryptography.BLS12_381/G2Affine.cs +++ b/src/Neo.Cryptography.BLS12_381/G2Affine.cs @@ -177,6 +177,14 @@ private static G2Affine FromBytes(ReadOnlySpan bytes, bool compressed, boo if (compressed) { + if (infinity_flag_set) + { + // Infinity encoding: compression flag set, sort flag unset, x == 0. + if (!compression_flag_set || sort_flag_set || !x.IsZero) + throw new FormatException(); + return Identity; + } + // Recover a y-coordinate given x by y = sqrt(x^3 + 4) var y = ((x.Square() * x) + B).Sqrt(); y = ConditionalSelect(in y, -y, y.LexicographicallyLargest() ^ sort_flag_set); diff --git a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs index 51ef7f6b90..7249110e2c 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs @@ -12,11 +12,21 @@ 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 { partial class CryptoLib { + private const int Bls12381MultiExpMaxPairs = 128; + private const int Bls12PairingMaxPairs = Bls12381MultiExpMaxPairs; + private const int Bls12FieldElementLength = 64; + private const int Bls12ScalarLength = Scalar.Size; + private const int Bls12G1EncodedLength = Bls12FieldElementLength * 2; + private const int Bls12G2EncodedLength = Bls12FieldElementLength * 4; + private const int Bls12PairInputLength = Bls12G1EncodedLength + Bls12G2EncodedLength; + /// /// Serialize a bls12381 point. /// @@ -97,6 +107,40 @@ public static InteropInterface Bls12381Add(InteropInterface x, InteropInterface }; } + /// + /// Ethereum-style G1 addition using uncompressed big-endian coordinates (x|y, 64-byte limbs). + /// Input is two concatenated 128-byte encodings; output is the same encoding. + /// + [ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 19, Name = "bls12_g1add")] + public static byte[] Bls12G1Add(byte[] input) + { + ArgumentNullException.ThrowIfNull(input); + if (input.Length != Bls12G1EncodedLength * 2) + throw new ArgumentException("Invalid BLS12-381 g1add input length", nameof(input)); + + var p1 = ParseEthereumG1Point(input.AsSpan(0, Bls12G1EncodedLength)); + var p2 = ParseEthereumG1Point(input.AsSpan(Bls12G1EncodedLength, Bls12G1EncodedLength)); + var result = new G1Projective(p1) + p2; + return EncodeEthereumG1(result); + } + + /// + /// Ethereum-style G2 addition using uncompressed big-endian coordinates (x0|x1|y0|y1, 64-byte limbs). + /// Input is two concatenated 256-byte encodings; output is the same encoding. + /// + [ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 19, Name = "bls12_g2add")] + public static byte[] Bls12G2Add(byte[] input) + { + ArgumentNullException.ThrowIfNull(input); + if (input.Length != Bls12G2EncodedLength * 2) + throw new ArgumentException("Invalid BLS12-381 g2add input length", nameof(input)); + + var p1 = ParseEthereumG2Point(input.AsSpan(0, Bls12G2EncodedLength)); + var p2 = ParseEthereumG2Point(input.AsSpan(Bls12G2EncodedLength, Bls12G2EncodedLength)); + var result = new G2Projective(p1) + p2; + return EncodeEthereumG2(result); + } + /// /// Mul operation of gt point and multiplier /// @@ -119,6 +163,171 @@ public static InteropInterface Bls12381Mul(InteropInterface x, byte[] mul, bool }; } + /// + /// Ethereum-style G1 scalar multiplication using uncompressed big-endian coordinates and big-endian scalar. + /// Input is 128-byte point + 32-byte scalar; output is the same encoding. + /// + [ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 21, Name = "bls12_g1mul")] + public static byte[] Bls12G1Mul(byte[] input) + { + ArgumentNullException.ThrowIfNull(input); + if (input.Length != Bls12G1EncodedLength + Bls12ScalarLength) + throw new ArgumentException("Invalid BLS12-381 g1mul input length", nameof(input)); + + var point = ParseEthereumG1Point(input.AsSpan(0, Bls12G1EncodedLength)); + var scalar = ParseEthereumScalar(input.AsSpan(Bls12G1EncodedLength, Bls12ScalarLength)); + var result = new G1Projective(point) * scalar; + return EncodeEthereumG1(result); + } + + /// + /// Ethereum-style G2 scalar multiplication using uncompressed big-endian coordinates and big-endian scalar. + /// Input is 256-byte point + 32-byte scalar; output is the same encoding. + /// + [ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 21, Name = "bls12_g2mul")] + public static byte[] Bls12G2Mul(byte[] input) + { + ArgumentNullException.ThrowIfNull(input); + if (input.Length != Bls12G2EncodedLength + Bls12ScalarLength) + throw new ArgumentException("Invalid BLS12-381 g2mul input length", nameof(input)); + + var point = ParseEthereumG2Point(input.AsSpan(0, Bls12G2EncodedLength)); + var scalar = ParseEthereumScalar(input.AsSpan(Bls12G2EncodedLength, Bls12ScalarLength)); + var result = new G2Projective(point) * scalar; + return EncodeEthereumG2(result); + } + + /// + /// Ethereum-style G1 MSM using uncompressed big-endian point encodings and big-endian scalars. + /// Input is k concatenated (128-byte point | 32-byte scalar) pairs; output is the same encoding. + /// + [ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 23, Name = "bls12_g1multiexp")] + public static byte[] Bls12G1MultiExp(byte[] input) + { + ArgumentNullException.ThrowIfNull(input); + if (input.Length == 0 || input.Length % (Bls12G1EncodedLength + Bls12ScalarLength) != 0) + throw new ArgumentException("Invalid BLS12-381 g1multiexp input length", nameof(input)); + + int pairCount = input.Length / (Bls12G1EncodedLength + Bls12ScalarLength); + if (pairCount > Bls12381MultiExpMaxPairs) + throw new ArgumentOutOfRangeException(nameof(input), $"BLS12-381 g1multiexp supports at most {Bls12381MultiExpMaxPairs} pairs"); + + G1Projective accumulator = G1Projective.Identity; + for (int offset = 0; offset < input.Length; offset += Bls12G1EncodedLength + Bls12ScalarLength) + { + var point = ParseEthereumG1Point(input.AsSpan(offset, Bls12G1EncodedLength)); + var scalar = ParseEthereumScalar(input.AsSpan(offset + Bls12G1EncodedLength, Bls12ScalarLength)); + if (!scalar.IsZero) + accumulator += new G1Projective(point) * scalar; + } + + return EncodeEthereumG1(accumulator); + } + + /// + /// Ethereum-style G2 MSM using uncompressed big-endian point encodings and big-endian scalars. + /// Input is k concatenated (256-byte point | 32-byte scalar) pairs; output is the same encoding. + /// + [ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 23, Name = "bls12_g2multiexp")] + public static byte[] Bls12G2MultiExp(byte[] input) + { + ArgumentNullException.ThrowIfNull(input); + if (input.Length == 0 || input.Length % (Bls12G2EncodedLength + Bls12ScalarLength) != 0) + throw new ArgumentException("Invalid BLS12-381 g2multiexp input length", nameof(input)); + + int pairCount = input.Length / (Bls12G2EncodedLength + Bls12ScalarLength); + if (pairCount > Bls12381MultiExpMaxPairs) + throw new ArgumentOutOfRangeException(nameof(input), $"BLS12-381 g2multiexp supports at most {Bls12381MultiExpMaxPairs} pairs"); + + G2Projective accumulator = G2Projective.Identity; + for (int offset = 0; offset < input.Length; offset += Bls12G2EncodedLength + Bls12ScalarLength) + { + var point = ParseEthereumG2Point(input.AsSpan(offset, Bls12G2EncodedLength)); + var scalar = ParseEthereumScalar(input.AsSpan(offset + Bls12G2EncodedLength, Bls12ScalarLength)); + if (!scalar.IsZero) + accumulator += new G2Projective(point) * scalar; + } + + return EncodeEthereumG2(accumulator); + } + + /// + /// Multi exponentiation operation for bls12381 points. + /// + /// Array of [point, scalar] pairs. + /// The accumulated point. + [ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 23)] + public static InteropInterface Bls12381MultiExp(Array pairs) + { + if (pairs is null || pairs.Count == 0) + throw new ArgumentException("BLS12-381 multi exponent requires at least one pair"); + if (pairs.Count > Bls12381MultiExpMaxPairs) + throw new ArgumentOutOfRangeException(nameof(pairs), $"BLS12-381 multi exponent supports at most {Bls12381MultiExpMaxPairs} pairs"); + + 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(); + switch (point) + { + case G1Affine g1Affine: + EnsureG1PointValid(in g1Affine); + EnsureGroupType(ref useG2, false); + { + var scalar = ParseScalar(pair[1]); + if (!scalar.IsZero) + g1Accumulator += new G1Projective(g1Affine) * scalar; + } + break; + case G1Projective g1Projective: + EnsureG1PointValid(new G1Affine(g1Projective)); + EnsureGroupType(ref useG2, false); + { + var scalar = ParseScalar(pair[1]); + if (!scalar.IsZero) + g1Accumulator += g1Projective * scalar; + } + break; + case G2Affine g2Affine: + EnsureG2PointValid(in g2Affine); + EnsureGroupType(ref useG2, true); + { + var scalar = ParseScalar(pair[1]); + if (!scalar.IsZero) + g2Accumulator += new G2Projective(g2Affine) * scalar; + } + break; + case G2Projective g2Projective: + EnsureG2PointValid(new G2Affine(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); + } + /// /// Pairing operation of g1 and g2 /// @@ -134,13 +343,247 @@ public static InteropInterface Bls12381Pairing(InteropInterface g1, InteropInter G1Projective g => new(g), _ => throw new ArgumentException("BLS12-381 type mismatch") }; + EnsureG1PointValid(in g1a); G2Affine g2a = g2.GetInterface() switch { G2Affine g => g, G2Projective g => new(g), _ => throw new ArgumentException("BLS12-381 type mismatch") }; + EnsureG2PointValid(in g2a); return new(Bls12.Pairing(in g1a, in g2a)); } + + /// + /// Ethereum-style pairing check (EIP-2537): accepts k concatenated pairs of uncompressed G1/G2 encodings and returns 32-byte result (LSB set for success). + /// + [ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 23, Name = "bls12_pairing")] + public static byte[] Bls12Pairing(byte[] input) + { + ArgumentNullException.ThrowIfNull(input); + if (input.Length % Bls12PairInputLength != 0) + throw new ArgumentException("Invalid BLS12-381 pairing input length", nameof(input)); + + int pairCount = input.Length / Bls12PairInputLength; + if (pairCount == 0) + throw new ArgumentException("BLS12-381 pairing requires at least one pair", nameof(input)); + if (pairCount > Bls12PairingMaxPairs) + throw new ArgumentOutOfRangeException(nameof(input), $"BLS12-381 pairing supports at most {Bls12PairingMaxPairs} pairs"); + + Gt accumulator = Gt.Identity; + + for (int offset = 0; offset < input.Length; offset += Bls12PairInputLength) + { + var g1 = ParseEthereumG1Point(input.AsSpan(offset, Bls12G1EncodedLength)); + var g2 = ParseEthereumG2Point(input.AsSpan(offset + Bls12G1EncodedLength, Bls12G2EncodedLength)); + accumulator += Bls12.Pairing(in g1, in g2); + } + + return EncodePairingResult(accumulator.IsIdentity); + } + + /// + /// Deserialize a G1/G2 point using Ethereum uncompressed big-endian encoding. + /// + [ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 19, Name = "bls12_deserialize")] + public static InteropInterface Bls12Deserialize(byte[] data) + { + ArgumentNullException.ThrowIfNull(data); + return data.Length switch + { + Bls12G1EncodedLength => new InteropInterface(ParseEthereumG1Point(data)), + Bls12G2EncodedLength => new InteropInterface(ParseEthereumG2Point(data)), + _ => throw new ArgumentException("Invalid BLS12-381 point length", nameof(data)) + }; + } + + /// + /// Serialize a G1/G2 point using Ethereum uncompressed big-endian encoding. + /// + [ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 19, Name = "bls12_serialize")] + public static byte[] Bls12Serialize(InteropInterface g) + { + return g.GetInterface() switch + { + G1Affine p => EncodeEthereumG1(new G1Projective(p)), + G1Projective p => EncodeEthereumG1(p), + G2Affine p => EncodeEthereumG2(new G2Projective(p)), + G2Projective p => EncodeEthereumG2(p), + _ => throw new ArgumentException("BLS12-381 type mismatch") + }; + } + + 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 bigEndian = 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 littleEndian = stackalloc byte[Scalar.Size]; + for (int i = 0; i < Scalar.Size; i++) + littleEndian[i] = bigEndian[Scalar.Size - 1 - i]; + + Span wide = stackalloc byte[Scalar.Size * 2]; + littleEndian.CopyTo(wide); + + try + { + return Scalar.FromBytes(littleEndian); + } + catch (FormatException) + { + return Scalar.FromBytesWide(wide); + } + } + + private static void EnsureG1PointValid(in G1Affine point) + { + if (point.IsIdentity) + return; + if (!point.IsOnCurve || !point.IsTorsionFree) + throw new ArgumentException("BLS12-381 point must be on-curve and in the prime-order subgroup"); + } + + private static void EnsureG2PointValid(in G2Affine point) + { + if (point.IsIdentity) + return; + if (!point.IsOnCurve || !point.IsTorsionFree) + throw new ArgumentException("BLS12-381 point must be on-curve and in the prime-order subgroup"); + } + + private static G1Affine ParseEthereumG1Point(ReadOnlySpan data) + { + if (data.Length != Bls12G1EncodedLength) + throw new ArgumentException("BLS12-381 G1 points must be 128 bytes"); + if (IsAllZero(data)) + return G1Affine.Identity; + + var x = ParseEthereumFp(data[..Bls12FieldElementLength]); + var y = ParseEthereumFp(data[Bls12FieldElementLength..]); + var point = new G1Affine(in x, in y); + EnsureG1PointValid(in point); + return point; + } + + private static G2Affine ParseEthereumG2Point(ReadOnlySpan data) + { + if (data.Length != Bls12G2EncodedLength) + throw new ArgumentException("BLS12-381 G2 points must be 256 bytes"); + if (IsAllZero(data)) + return G2Affine.Identity; + + var x0 = ParseEthereumFp(data[..Bls12FieldElementLength]); + var x1 = ParseEthereumFp(data.Slice(Bls12FieldElementLength, Bls12FieldElementLength)); + var y0 = ParseEthereumFp(data.Slice(Bls12FieldElementLength * 2, Bls12FieldElementLength)); + var y1 = ParseEthereumFp(data.Slice(Bls12FieldElementLength * 3, Bls12FieldElementLength)); + var x = new Fp2(in x0, in x1); + var y = new Fp2(in y0, in y1); + var point = new G2Affine(in x, in y); + EnsureG2PointValid(in point); + return point; + } + + private static Fp ParseEthereumFp(ReadOnlySpan data) + { + if (data.Length != Bls12FieldElementLength) + throw new ArgumentException("BLS12-381 field elements must be 64 bytes"); + for (int i = 0; i < Bls12FieldElementLength - Fp.Size; i++) + if (data[i] != 0) + throw new ArgumentException("BLS12-381 field element overflow"); + + Span fieldBytes = stackalloc byte[Fp.Size]; + data[(Bls12FieldElementLength - Fp.Size)..].CopyTo(fieldBytes); + return Fp.FromBytes(fieldBytes); + } + + private static Scalar ParseEthereumScalar(ReadOnlySpan data) + { + if (data.Length != Bls12ScalarLength) + throw new ArgumentException("BLS12-381 scalars must be 32 bytes"); + + Span littleEndian = stackalloc byte[Scalar.Size]; + for (int i = 0; i < Scalar.Size; i++) + littleEndian[i] = data[Scalar.Size - 1 - i]; + + Span wide = stackalloc byte[Scalar.Size * 2]; + littleEndian.CopyTo(wide); + + try + { + return Scalar.FromBytes(littleEndian); + } + catch (FormatException) + { + return Scalar.FromBytesWide(wide); + } + } + + private static byte[] EncodeEthereumG1(G1Projective point) + { + var affine = new G1Affine(point); + if (affine.IsIdentity) + return new byte[Bls12G1EncodedLength]; + + byte[] output = new byte[Bls12G1EncodedLength]; + WriteEthereumFp(in affine.X, output.AsSpan(0, Bls12FieldElementLength)); + WriteEthereumFp(in affine.Y, output.AsSpan(Bls12FieldElementLength, Bls12FieldElementLength)); + return output; + } + + private static byte[] EncodeEthereumG2(G2Projective point) + { + var affine = new G2Affine(point); + if (affine.IsIdentity) + return new byte[Bls12G2EncodedLength]; + + byte[] output = new byte[Bls12G2EncodedLength]; + WriteEthereumFp(in affine.X.C0, output.AsSpan(0, Bls12FieldElementLength)); + WriteEthereumFp(in affine.X.C1, output.AsSpan(Bls12FieldElementLength, Bls12FieldElementLength)); + WriteEthereumFp(in affine.Y.C0, output.AsSpan(Bls12FieldElementLength * 2, Bls12FieldElementLength)); + WriteEthereumFp(in affine.Y.C1, output.AsSpan(Bls12FieldElementLength * 3, Bls12FieldElementLength)); + return output; + } + + private static void WriteEthereumFp(in Fp value, Span destination) + { + if (destination.Length != Bls12FieldElementLength) + throw new ArgumentException("BLS12-381 field element encodings must be 64 bytes"); + destination.Clear(); + Span buffer = stackalloc byte[Fp.Size]; + if (!value.TryWrite(buffer)) + throw new ArgumentException("Failed to serialize BLS12-381 field element"); + buffer.CopyTo(destination[(Bls12FieldElementLength - Fp.Size)..]); + } + + private static bool IsAllZero(ReadOnlySpan data) + { + foreach (byte b in data) + if (b != 0) + return false; + return true; + } + + private static byte[] EncodePairingResult(bool success) + { + byte[] result = new byte[32]; + result[^1] = success ? (byte)1 : (byte)0; + return result; + } } } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs index e70b35083d..4c67e5d21e 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -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 { @@ -56,6 +60,24 @@ public class UT_CryptoLib private readonly byte[] g2 = s_g2Hex.HexToBytes(); private readonly byte[] gt = s_gtHex.HexToBytes(); + private const string EthG1MultiExpSingleInputHex = + "0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e10000000000000000000000000000000000000000000000000000000000000011"; + + private const string EthG1MultiExpSingleExpectedHex = + "000000000000000000000000000000001098f178f84fc753a76bb63709e9be91eec3ff5f7f3a5f4836f34fe8a1a6d6c5578d8fd820573cef3a01e2bfef3eaf3a000000000000000000000000000000000ea923110b733b531006075f796cc9368f2477fe26020f465468efbb380ce1f8eebaf5c770f31d320f9bd378dc758436"; + + private const string EthG1MultiExpMultipleInputHex = + "0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e10000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000e12039459c60491672b6a6282355d8765ba6272387fb91a3e9604fa2a81450cf16b870bb446fc3a3e0a187fff6f89450000000000000000000000000000000018b6c1ed9f45d3cbc0b01b9d038dcecacbd702eb26469a0eb3905bd421461712f67f782b4735849644c1772c93fe3d09000000000000000000000000000000000000000000000000000000000000003300000000000000000000000000000000147b327c8a15b39634a426af70c062b50632a744eddd41b5a4686414ef4cd9746bb11d0a53c6c2ff21bbcf331e07ac9200000000000000000000000000000000078c2e9782fa5d9ab4e728684382717aa2b8fad61b5f5e7cf3baa0bc9465f57342bb7c6d7b232e70eebcdbf70f903a450000000000000000000000000000000000000000000000000000000000000034"; + + private const string EthG1MultiExpMultipleExpectedHex = + "000000000000000000000000000000001339b4f51923efe38905f590ba2031a2e7154f0adb34a498dfde8fb0f1ccf6862ae5e3070967056385055a666f1b6fc70000000000000000000000000000000009fb423f7e7850ef9c4c11a119bb7161fe1d11ac5527051b29fe8f73ad4262c84c37b0f1b9f0e163a9682c22c7f98c80"; + + private const string EthG2MultiExpSingleInputHex = + "00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb80000000000000000000000000000000013e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e000000000000000000000000000000000ce5d527727d6e118cc9cdc6da2e351aadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801000000000000000000000000000000000606c4a02ea734cc32acd2b02bc28b99cb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be0000000000000000000000000000000000000000000000000000000000000011"; + + private const string EthG2MultiExpSingleExpectedHex = + "000000000000000000000000000000000ef786ebdcda12e142a32f091307f2fedf52f6c36beb278b0007a03ad81bf9fee3710a04928e43e541d02c9be44722e8000000000000000000000000000000000d05ceb0be53d2624a796a7a033aec59d9463c18d672c451ec4f2e679daef882cab7d8dd88789065156a1340ca9d426500000000000000000000000000000000118ed350274bc45e63eaaa4b8ddf119b3bf38418b5b9748597edfc456d9bc3e864ec7283426e840fd29fa84e7d89c934000000000000000000000000000000001594b866a28946b6d444bf0481558812769ea3222f5dfc961ca33e78e0ea62ee8ba63fd1ece9cc3e315abfa96d536944"; + private readonly byte[] notG1 = "8123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" @@ -157,6 +179,248 @@ public void TestBls12381Add() Assert.AreEqual(expected.ToLower(), result.GetInterface().ToArray().ToHexString()); } + [TestMethod] + public void TestBls12G1AddAlias() + { + var g1Point = G1Affine.FromCompressed(g1); + byte[] encoded = EncodeEthereumG1Point(new G1Projective(g1Point)); + byte[] input = new byte[Bls12G1EncodedLength * 2]; + encoded.CopyTo(input, 0); + encoded.CopyTo(input, encoded.Length); + + byte[] result = CryptoLib.Bls12G1Add(input); + var actual = ParseEthereumG1Point(result); + var expected = new G1Projective(g1Point) + g1Point; + Assert.AreEqual(new G1Affine(expected).ToCompressed().ToHexString(), + new G1Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12G2AddAlias() + { + var g2Point = G2Affine.FromCompressed(g2); + byte[] encoded = EncodeEthereumG2Point(new G2Projective(g2Point)); + byte[] input = new byte[Bls12G2EncodedLength * 2]; + encoded.CopyTo(input, 0); + encoded.CopyTo(input, encoded.Length); + + byte[] result = CryptoLib.Bls12G2Add(input); + var actual = ParseEthereumG2Point(result); + var expected = new G2Projective(g2Point) + g2Point; + Assert.AreEqual(new G2Affine(expected).ToCompressed().ToHexString(), + new G2Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12G1MulAlias() + { + var g1Point = G1Affine.FromCompressed(g1); + byte[] input = new byte[Bls12G1EncodedLength + Scalar.Size]; + EncodeEthereumG1Point(new G1Projective(g1Point)).CopyTo(input, 0); + CreateScalarBytes(2).CopyTo(input, Bls12G1EncodedLength); + + byte[] result = CryptoLib.Bls12G1Mul(input); + var actual = ParseEthereumG1Point(result); + var expected = new G1Projective(g1Point) * CreateScalar(2); + Assert.AreEqual(new G1Affine(expected).ToCompressed().ToHexString(), + new G1Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12G2MulAlias() + { + var g2Point = G2Affine.FromCompressed(g2); + byte[] input = new byte[Bls12G2EncodedLength + Scalar.Size]; + EncodeEthereumG2Point(new G2Projective(g2Point)).CopyTo(input, 0); + CreateScalarBytes(3).CopyTo(input, Bls12G2EncodedLength); + + byte[] result = CryptoLib.Bls12G2Mul(input); + var actual = ParseEthereumG2Point(result); + var expected = new G2Projective(g2Point) * CreateScalar(3); + Assert.AreEqual(new G2Affine(expected).ToCompressed().ToHexString(), + new G2Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12G1MultiExpAlias() + { + var g1Point = G1Affine.FromCompressed(g1); + byte[] input = EncodeEthereumG1Point(new G1Projective(g1Point)) + .Concat(CreateScalarBytes(2)) + .ToArray(); + + byte[] result = CryptoLib.Bls12G1MultiExp(input); + var actual = ParseEthereumG1Point(result); + var expected = new G1Projective(g1Point) * CreateScalar(2); + Assert.AreEqual(new G1Affine(expected).ToCompressed().ToHexString(), + new G1Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12G2MultiExpAlias() + { + var g2Point = G2Affine.FromCompressed(g2); + byte[] input = EncodeEthereumG2Point(new G2Projective(g2Point)) + .Concat(CreateScalarBytes(5)) + .ToArray(); + + byte[] result = CryptoLib.Bls12G2MultiExp(input); + var actual = ParseEthereumG2Point(result); + var expected = new G2Projective(g2Point) * CreateScalar(5); + Assert.AreEqual(new G2Affine(expected).ToCompressed().ToHexString(), + new G2Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12MultiExpAliasInvalidLength() + { + Assert.ThrowsExactly(() => CryptoLib.Bls12G1MultiExp(new byte[10])); + Assert.ThrowsExactly(() => CryptoLib.Bls12G2MultiExp(new byte[10])); + } + + [TestMethod] + public void TestBls12SerializeDeserializeG1() + { + var g1Point = G1Affine.FromCompressed(g1); + var encoded = CryptoLib.Bls12Serialize(new InteropInterface(g1Point)); + Assert.AreEqual(Bls12G1EncodedLength, encoded.Length, "G1 serialization length must be 128 bytes"); + + var interop = CryptoLib.Bls12Deserialize(encoded); + var roundtrip = interop.GetInterface().ToCompressed().ToHexString(); + Assert.AreEqual(g1Point.ToCompressed().ToHexString(), roundtrip); + } + + [TestMethod] + public void TestBls12SerializeDeserializeG2() + { + var g2Point = G2Affine.FromCompressed(g2); + var encoded = CryptoLib.Bls12Serialize(new InteropInterface(g2Point)); + Assert.AreEqual(Bls12G2EncodedLength, encoded.Length); + + var interop = CryptoLib.Bls12Deserialize(encoded); + var roundtrip = interop.GetInterface().ToCompressed().ToHexString(); + Assert.AreEqual(g2Point.ToCompressed().ToHexString(), roundtrip); + } + + [TestMethod] + public void TestBls12SerializeIdentity() + { + var g1Encoded = CryptoLib.Bls12Serialize(new InteropInterface(G1Projective.Identity)); + Assert.IsTrue(g1Encoded.All(b => b == 0)); + + var g2Encoded = CryptoLib.Bls12Serialize(new InteropInterface(G2Projective.Identity)); + Assert.IsTrue(g2Encoded.All(b => b == 0)); + } + + [TestMethod] + public void TestBls12DeserializeInvalidLength() + { + Assert.ThrowsExactly(() => CryptoLib.Bls12Deserialize(new byte[1])); + Assert.ThrowsExactly(() => CryptoLib.Bls12Deserialize(new byte[Bls12G1EncodedLength + 1])); + } + + [TestMethod] + public void TestBls12DeserializeOverflow() + { + byte[] invalidG1 = new byte[Bls12G1EncodedLength]; + invalidG1[0] = 1; // overflow in the high limb + Assert.ThrowsExactly(() => CryptoLib.Bls12Deserialize(invalidG1)); + } + + [TestMethod] + public void TestBls12DeserializeNonOnCurve() + { + byte[] invalidG1 = new byte[Bls12G1EncodedLength]; + // set y to non-zero while x remains zero (fails curve check) + invalidG1[Bls12FieldElementLength + 5] = 1; + Assert.ThrowsExactly(() => CryptoLib.Bls12Deserialize(invalidG1)); + } + + [TestMethod] + public void TestBls12PairingAliasSinglePair() + { + var g1Point = G1Affine.FromCompressed(g1); + var g2Point = G2Affine.FromCompressed(g2); + byte[] input = BuildPairInput(g1Point, g2Point); + + byte[] result = CryptoLib.Bls12Pairing(input); + Assert.AreEqual(32, result.Length); + Assert.IsTrue(result.All(b => b == 0)); + } + + [TestMethod] + public void TestBls12PairingAliasMultiplePairs() + { + var g1Point = G1Affine.FromCompressed(g1); + var g2Point = G2Affine.FromCompressed(g2); + var negG1 = -g1Point; + + byte[] input = BuildPairInput(g1Point, g2Point) + .Concat(BuildPairInput(negG1, g2Point)) + .ToArray(); + + byte[] result = CryptoLib.Bls12Pairing(input); + Assert.IsTrue(result.Take(result.Length - 1).All(b => b == 0)); + Assert.AreEqual(1, result[^1]); + } + + [TestMethod] + public void TestBls12PairingAliasNonUnitProduct() + { + var g1Point = G1Affine.FromCompressed(g1); + var g2Point = G2Affine.FromCompressed(g2); + byte[] input = BuildPairInput(g1Point, g2Point) + .Concat(BuildPairInput(g1Point, g2Point)) + .ToArray(); + + byte[] result = CryptoLib.Bls12Pairing(input); + Assert.IsTrue(result.Take(result.Length - 1).All(b => b == 0)); + Assert.AreEqual(0, result[^1]); + } + + [TestMethod] + public void TestBls12PairingAliasInvalidLength() + { + Assert.ThrowsExactly(() => CryptoLib.Bls12Pairing(new byte[10])); + } + + [TestMethod] + public void TestBls12PairingAliasInvalidPoint() + { + byte[] input = new byte[Bls12PairInputLength]; + // Write identity G2 but invalid G1 (non-zero y but zero x prefix) + EncodeEthereumG2Point(G2Projective.Identity).CopyTo(input, Bls12G1EncodedLength); + input[Bls12FieldElementLength + 10] = 1; + Assert.ThrowsExactly(() => CryptoLib.Bls12Pairing(input)); + } + + [TestMethod] + public void TestBls12PairingAliasEmptyInput() + { + Assert.ThrowsExactly(() => CryptoLib.Bls12Pairing(System.Array.Empty())); + } + + [TestMethod] + public void TestBls12PairingAliasTooManyPairsFails() + { + byte[] oversized = new byte[Bls12PairInputLength * (Bls12381MultiExpMaxPairs + 1)]; + Assert.ThrowsExactly(() => CryptoLib.Bls12Pairing(oversized)); + } + + [TestMethod] + public void TestBls12381MultiExpAllowsG2Identity() + { + var pair = new VMArray(new StackItem[] + { + StackItem.FromInterface(G2Projective.Identity), + new ByteString(CreateScalarBytes(7)) + }); + + var pairs = new VMArray(new StackItem[] { pair }); + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = new G2Affine(result.GetInterface()).ToCompressed().ToHexString(); + Assert.AreEqual(G2Affine.Identity.ToCompressed().ToHexString(), actual); + } [TestMethod] public void TestBls12381Mul() { @@ -263,6 +527,822 @@ public void TestBls12381Pairing() Assert.AreEqual(expected.ToLower(), result.GetInterface().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(); + + 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(); + + 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 }); + + Span littleEndian = stackalloc byte[Scalar.Size]; + for (int i = 0; i < Scalar.Size; i++) + littleEndian[i] = scalarBytes[Scalar.Size - 1 - i]; + Span wide = stackalloc byte[Scalar.Size * 2]; + littleEndian.CopyTo(wide); + var reducedScalar = Scalar.FromBytesWide(wide); + + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = result.GetInterface(); + + 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(() => CryptoLib.Bls12381MultiExp(pairs)); + } + + [TestMethod] + public void TestBls12381MultiExpEmptyFails() + { + var pairs = new VMArray(); + Assert.ThrowsExactly(() => CryptoLib.Bls12381MultiExp(pairs)); + } + + [TestMethod] + public void TestBls12381MultiExpTooManyPairsFails() + { + var g1Point = G1Affine.FromCompressed(g1); + var pairs = new VMArray(); + for (int i = 0; i < 129; i++) + { + pairs.Add(new VMArray(new StackItem[] + { + StackItem.FromInterface(g1Point), + new ByteString(CreateScalarBytes(1)) + })); + } + + Assert.ThrowsExactly(() => CryptoLib.Bls12381MultiExp(pairs)); + } + + [TestMethod] + public void TestBls12381MultiExpRejectsNonOnCurveG1() + { + var zero = Fp.Zero; + var invalid = new G1Affine(zero, zero); + var pair = new VMArray(new StackItem[] + { + StackItem.FromInterface(invalid), + new ByteString(CreateScalarBytes(1)) + }); + var pairs = new VMArray(new StackItem[] { pair }); + + Assert.ThrowsExactly(() => CryptoLib.Bls12381MultiExp(pairs)); + } + + [TestMethod] + public void TestBls12381MultiExpRejectsNonOnCurveG2() + { + var zero = Fp2.Zero; + var invalid = new G2Affine(zero, zero); + var pair = new VMArray(new StackItem[] + { + StackItem.FromInterface(invalid), + new ByteString(CreateScalarBytes(1)) + }); + var pairs = new VMArray(new StackItem[] { pair }); + + Assert.ThrowsExactly(() => CryptoLib.Bls12381MultiExp(pairs)); + } + + [TestMethod] + public void TestBls12381MultiExpMatchesEthereumG1Vectors() + { + var singleInput = EthG1MultiExpSingleInputHex.HexToBytes(); + var singleResult = CryptoLib.Bls12381MultiExp(BuildEthereumG1Pairs(singleInput)); + var singleActual = new G1Affine(singleResult.GetInterface()).ToCompressed().ToHexString(); + var singleExpectedBytes = EthG1MultiExpSingleExpectedHex.HexToBytes(); + var singleExpected = new G1Affine(ParseEthereumG1Point(singleExpectedBytes)).ToCompressed().ToHexString(); + Assert.AreEqual(singleExpected, singleActual); + + var multiInput = EthG1MultiExpMultipleInputHex.HexToBytes(); + var multiResult = CryptoLib.Bls12381MultiExp(BuildEthereumG1Pairs(multiInput)); + var multiActual = new G1Affine(multiResult.GetInterface()).ToCompressed().ToHexString(); + var multiExpectedBytes = EthG1MultiExpMultipleExpectedHex.HexToBytes(); + var multiExpected = new G1Affine(ParseEthereumG1Point(multiExpectedBytes)).ToCompressed().ToHexString(); + Assert.AreEqual(multiExpected, multiActual); + } + + [TestMethod] + public void TestBls12381MultiExpMatchesEthereumG2Vectors() + { + var input = EthG2MultiExpSingleInputHex.HexToBytes(); + var result = CryptoLib.Bls12381MultiExp(BuildEthereumG2Pairs(input)); + var actual = new G2Affine(result.GetInterface()).ToCompressed().ToHexString(); + var expectedBytes = EthG2MultiExpSingleExpectedHex.HexToBytes(); + var expected = new G2Affine(ParseEthereumG2Point(expectedBytes)).ToCompressed().ToHexString(); + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase1_G1SingleScalarMin() + { + // Case 1: G1 with smallest non-zero scalar (1) and canonical generator + var point = G1Affine.FromCompressed("97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + var pair = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(1)) + }); + var pairs = new VMArray(new StackItem[] { pair }); + + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = new G1Affine(result.GetInterface()).ToCompressed().ToHexString(); + var expected = "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb"; + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase2_G1SingleScalarIntMax() + { + // Case 2: G1 with upper 32-bit boundary scalar (2147483647) + var point = G1Affine.FromCompressed("97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + var pair = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(2147483647U)) + }); + var pairs = new VMArray(new StackItem[] { pair }); + + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = new G1Affine(result.GetInterface()).ToCompressed().ToHexString(); + var expected = "a71a80ecd55e1d885ce85467e2e8f7e424fc71e20ec8be42284db33b4fce8fd5e1021008908101cc31aac5a273ed4143"; + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase3_G1NegatedYBit() + { + // Case 3: G1 with negated y-bit (0x20 toggled) and multiple scalars + var point = G1Affine.FromCompressed("b7f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + var pair1 = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(123456789U)) + }); + var pair2 = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(1)) + }); + var pairs = new VMArray(new StackItem[] { pair1, pair2 }); + + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = new G1Affine(result.GetInterface()).ToCompressed().ToHexString(); + var expected = "a5c2bc253038f033f7b47dd3c7b5c79d276467e810731ac70cf7fc2fdb37012a341b21c7f3ec3f4cda1b4d5fe57f2f1a"; + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase4_G1PointAtInfinity() + { + // Case 4: G1 point at infinity should result in infinity + var point = G1Affine.FromCompressed("c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".HexToBytes()); + var pair = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(987654321U)) + }); + var pairs = new VMArray(new StackItem[] { pair }); + + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = new G1Affine(result.GetInterface()).ToCompressed().ToHexString(); + var expected = "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase5_G2SingleScalarMin() + { + // Case 5: G2 with smallest non-zero scalar (1) and canonical generator + var point = G2Affine.FromCompressed("93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8".HexToBytes()); + var pair = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(1)) + }); + var pairs = new VMArray(new StackItem[] { pair }); + + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = new G2Affine(result.GetInterface()).ToCompressed().ToHexString(); + var expected = "93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8"; + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase6_G2NegatedYBit() + { + // Case 6: G2 with negated y-bit (0x20 toggled) and multiple scalars + var point = G2Affine.FromCompressed("b3e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8".HexToBytes()); + var pair1 = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(2147483647U)) + }); + var pair2 = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(2)) + }); + var pairs = new VMArray(new StackItem[] { pair1, pair2 }); + + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = new G2Affine(result.GetInterface()).ToCompressed().ToHexString(); + var expected = "95e4740a671fbf5aa643f3e4daa849e28020a5b6b21351814433880bcddb605c31c0e26d7991b9a70ad156b5d12f83350e8e99e5acbb4e86b07e1358fd532ae8ffa35c9ce52aea273d8830aacab45574bb487c796af725e3c528860a4bc05145"; + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase7_G2PointAtInfinity() + { + // Case 7: G2 point at infinity should result in infinity + var point = G2Affine.FromCompressed("c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".HexToBytes()); + var pair = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(1337U)) + }); + var pairs = new VMArray(new StackItem[] { pair }); + + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = new G2Affine(result.GetInterface()).ToCompressed().ToHexString(); + var expected = "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase8_G1G2MixIdentityAndGenerator() + { + // Case 8: Mixed G1 and G2 test - G1: [1, 2147483647], G2: [2147483647, 1] + // This tests that there's no leakage between G1 and G2 operations + // We test G1 and G2 separately as they cannot be mixed in a single MultiExp call + + // G1 part: [1, 2147483647] + var g1Point = G1Affine.FromCompressed("97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + var g1Pair1 = new VMArray(new StackItem[] + { + StackItem.FromInterface(g1Point), + new ByteString(CreateScalarBytes(1)) + }); + var g1Pair2 = new VMArray(new StackItem[] + { + StackItem.FromInterface(g1Point), + new ByteString(CreateScalarBytes(2147483647U)) + }); + var g1Pairs = new VMArray(new StackItem[] { g1Pair1, g1Pair2 }); + + var g1Result = CryptoLib.Bls12381MultiExp(g1Pairs); + var g1Actual = new G1Affine(g1Result.GetInterface()).ToCompressed().ToHexString(); + // Expected: (P * 1) + (P * 2147483647) = P * (1 + 2147483647) = P * 2147483648 + var g1ExpectedPoint = new G1Projective(g1Point) * CreateScalar(2147483648U); + var g1Expected = new G1Affine(g1ExpectedPoint).ToCompressed().ToHexString(); + Assert.AreEqual(g1Expected, g1Actual); + + // G2 part: [2147483647, 1] + var g2Point = G2Affine.FromCompressed("93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8".HexToBytes()); + var g2Pair1 = new VMArray(new StackItem[] + { + StackItem.FromInterface(g2Point), + new ByteString(CreateScalarBytes(2147483647U)) + }); + var g2Pair2 = new VMArray(new StackItem[] + { + StackItem.FromInterface(g2Point), + new ByteString(CreateScalarBytes(1)) + }); + var g2Pairs = new VMArray(new StackItem[] { g2Pair1, g2Pair2 }); + + var g2Result = CryptoLib.Bls12381MultiExp(g2Pairs); + var g2Actual = new G2Affine(g2Result.GetInterface()).ToCompressed().ToHexString(); + // Expected: (P * 2147483647) + (P * 1) = P * (2147483647 + 1) = P * 2147483648 + var g2ExpectedPoint = new G2Projective(g2Point) * CreateScalar(2147483648U); + var g2Expected = new G2Affine(g2ExpectedPoint).ToCompressed().ToHexString(); + Assert.AreEqual(g2Expected, g2Actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase1_G1SingleScalarMin() + { + var point = G1Affine.FromCompressed("97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + var pair = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(1)) + }); + + var pairs = new VMArray(new StackItem[] { pair }); + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = new G1Affine(result.GetInterface()).ToCompressed().ToHexString(); + const string expected = "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb"; + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase2_G1SingleScalarIntMax() + { + var point = G1Affine.FromCompressed("97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + var pair = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(2147483647U)) + }); + + var pairs = new VMArray(new StackItem[] { pair }); + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = new G1Affine(result.GetInterface()).ToCompressed().ToHexString(); + const string expected = "a71a80ecd55e1d885ce85467e2e8f7e424fc71e20ec8be42284db33b4fce8fd5e1021008908101cc31aac5a273ed4143"; + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase3_G1NegatedYBit() + { + var point = G1Affine.FromCompressed("b7f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + var pair1 = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(123456789U)) + }); + var pair2 = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(1)) + }); + + var pairs = new VMArray(new StackItem[] { pair1, pair2 }); + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = new G1Affine(result.GetInterface()).ToCompressed().ToHexString(); + const string expected = "a5c2bc253038f033f7b47dd3c7b5c79d276467e810731ac70cf7fc2fdb37012a341b21c7f3ec3f4cda1b4d5fe57f2f1a"; + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase4_G1PointAtInfinity() + { + var point = G1Affine.FromCompressed("c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".HexToBytes()); + var pair = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(987654321U)) + }); + + var pairs = new VMArray(new StackItem[] { pair }); + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = new G1Affine(result.GetInterface()).ToCompressed().ToHexString(); + const string expected = "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase5_G2SingleScalarMin() + { + var point = G2Affine.FromCompressed("93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8".HexToBytes()); + var pair = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(1)) + }); + + var pairs = new VMArray(new StackItem[] { pair }); + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = new G2Affine(result.GetInterface()).ToCompressed().ToHexString(); + const string expected = "93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8"; + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase6_G2NegatedYBit() + { + var point = G2Affine.FromCompressed("b3e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8".HexToBytes()); + var pair1 = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(2147483647U)) + }); + var pair2 = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(2)) + }); + + var pairs = new VMArray(new StackItem[] { pair1, pair2 }); + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = new G2Affine(result.GetInterface()).ToCompressed().ToHexString(); + const string expected = "95e4740a671fbf5aa643f3e4daa849e28020a5b6b21351814433880bcddb605c31c0e26d7991b9a70ad156b5d12f83350e8e99e5acbb4e86b07e1358fd532ae8ffa35c9ce52aea273d8830aacab45574bb487c796af725e3c528860a4bc05145"; + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase7_G2PointAtInfinity() + { + var point = G2Affine.FromCompressed("c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".HexToBytes()); + var pair = new VMArray(new StackItem[] + { + StackItem.FromInterface(point), + new ByteString(CreateScalarBytes(1337U)) + }); + + var pairs = new VMArray(new StackItem[] { pair }); + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = new G2Affine(result.GetInterface()).ToCompressed().ToHexString(); + const string expected = "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestBls12381MultiExpBoundaryCase8_G1G2MixIdentityAndGenerator() + { + var g1Point = G1Affine.FromCompressed("97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + var g1Pair1 = new VMArray(new StackItem[] + { + StackItem.FromInterface(g1Point), + new ByteString(CreateScalarBytes(1)) + }); + var g1Pair2 = new VMArray(new StackItem[] + { + StackItem.FromInterface(g1Point), + new ByteString(CreateScalarBytes(2147483647U)) + }); + var g1Pairs = new VMArray(new StackItem[] { g1Pair1, g1Pair2 }); + var g1Result = CryptoLib.Bls12381MultiExp(g1Pairs); + var g1Actual = new G1Affine(g1Result.GetInterface()).ToCompressed().ToHexString(); + var g1ExpectedPoint = new G1Projective(g1Point) * CreateScalar(2147483648U); + var g1Expected = new G1Affine(g1ExpectedPoint).ToCompressed().ToHexString(); + Assert.AreEqual(g1Expected, g1Actual); + + var g2Point = G2Affine.FromCompressed("93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8".HexToBytes()); + var g2Pair1 = new VMArray(new StackItem[] + { + StackItem.FromInterface(g2Point), + new ByteString(CreateScalarBytes(2147483647U)) + }); + var g2Pair2 = new VMArray(new StackItem[] + { + StackItem.FromInterface(g2Point), + new ByteString(CreateScalarBytes(1)) + }); + var g2Pairs = new VMArray(new StackItem[] { g2Pair1, g2Pair2 }); + var g2Result = CryptoLib.Bls12381MultiExp(g2Pairs); + var g2Actual = new G2Affine(g2Result.GetInterface()).ToCompressed().ToHexString(); + var g2ExpectedPoint = new G2Projective(g2Point) * CreateScalar(2147483648U); + var g2Expected = new G2Affine(g2ExpectedPoint).ToCompressed().ToHexString(); + Assert.AreEqual(g2Expected, g2Actual); + } + + // G1Add Boundary Test Cases + [TestMethod] + public void TestBls12G1AddBoundaryCase9_Normal() + { + var point1 = G1Affine.FromCompressed("97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + var point2 = G1Affine.FromCompressed("a195fab58325ffd54c08d3b180d2275ca2b45ab91623a5d6b330d88d25f0754b7259e710636296e583c8be33e968860d".HexToBytes()); + byte[] input = new byte[Bls12G1EncodedLength * 2]; + EncodeEthereumG1Point(new G1Projective(point1)).CopyTo(input, 0); + EncodeEthereumG1Point(new G1Projective(point2)).CopyTo(input, Bls12G1EncodedLength); + + byte[] result = CryptoLib.Bls12G1Add(input); + var actual = ParseEthereumG1Point(result); + var expected = new G1Projective(point1) + new G1Projective(point2); + Assert.AreEqual(new G1Affine(expected).ToCompressed().ToHexString(), + new G1Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12G1AddBoundaryCase10_Infinity() + { + var point1 = G1Affine.FromCompressed("97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + var point2 = G1Affine.FromCompressed("c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".HexToBytes()); // infinity + byte[] input = new byte[Bls12G1EncodedLength * 2]; + EncodeEthereumG1Point(new G1Projective(point1)).CopyTo(input, 0); + EncodeEthereumG1Point(new G1Projective(point2)).CopyTo(input, Bls12G1EncodedLength); + + byte[] result = CryptoLib.Bls12G1Add(input); + var actual = ParseEthereumG1Point(result); + var expected = new G1Projective(point1) + new G1Projective(point2); + Assert.AreEqual(new G1Affine(expected).ToCompressed().ToHexString(), + new G1Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12G1AddBoundaryCase11_NegYBit() + { + var point1 = G1Affine.FromCompressed("b7f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + var point2 = G1Affine.FromCompressed("a195fab58325ffd54c08d3b180d2275ca2b45ab91623a5d6b330d88d25f0754b7259e710636296e583c8be33e968860d".HexToBytes()); + byte[] input = new byte[Bls12G1EncodedLength * 2]; + EncodeEthereumG1Point(new G1Projective(point1)).CopyTo(input, 0); + EncodeEthereumG1Point(new G1Projective(point2)).CopyTo(input, Bls12G1EncodedLength); + + byte[] result = CryptoLib.Bls12G1Add(input); + var actual = ParseEthereumG1Point(result); + var expected = new G1Projective(point1) + new G1Projective(point2); + Assert.AreEqual(new G1Affine(expected).ToCompressed().ToHexString(), + new G1Affine(actual).ToCompressed().ToHexString()); + } + + // G1Mul Boundary Test Cases + [TestMethod] + public void TestBls12G1MulBoundaryCase12_MinScalar() + { + var point = G1Affine.FromCompressed("97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + byte[] input = new byte[Bls12G1EncodedLength + Scalar.Size]; + EncodeEthereumG1Point(new G1Projective(point)).CopyTo(input, 0); + CreateScalarBytes(1).CopyTo(input, Bls12G1EncodedLength); + + byte[] result = CryptoLib.Bls12G1Mul(input); + var actual = ParseEthereumG1Point(result); + var expected = new G1Projective(point) * CreateScalar(1); + Assert.AreEqual(new G1Affine(expected).ToCompressed().ToHexString(), + new G1Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12G1MulBoundaryCase13_IntMax() + { + var point = G1Affine.FromCompressed("97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + byte[] input = new byte[Bls12G1EncodedLength + Scalar.Size]; + EncodeEthereumG1Point(new G1Projective(point)).CopyTo(input, 0); + CreateScalarBytes(2147483647U).CopyTo(input, Bls12G1EncodedLength); + + byte[] result = CryptoLib.Bls12G1Mul(input); + var actual = ParseEthereumG1Point(result); + var expected = new G1Projective(point) * CreateScalar(2147483647U); + Assert.AreEqual(new G1Affine(expected).ToCompressed().ToHexString(), + new G1Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12G1MulBoundaryCase14_Infinity() + { + var point = G1Affine.FromCompressed("c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".HexToBytes()); // infinity + byte[] input = new byte[Bls12G1EncodedLength + Scalar.Size]; + EncodeEthereumG1Point(new G1Projective(point)).CopyTo(input, 0); + CreateScalarBytes(1337U).CopyTo(input, Bls12G1EncodedLength); + + byte[] result = CryptoLib.Bls12G1Mul(input); + var actual = ParseEthereumG1Point(result); + var expected = new G1Projective(point) * CreateScalar(1337U); + Assert.AreEqual(new G1Affine(expected).ToCompressed().ToHexString(), + new G1Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12G1MulBoundaryCase15_NegYBit() + { + var point = G1Affine.FromCompressed("b7f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + byte[] input = new byte[Bls12G1EncodedLength + Scalar.Size]; + EncodeEthereumG1Point(new G1Projective(point)).CopyTo(input, 0); + CreateScalarBytes(987654321U).CopyTo(input, Bls12G1EncodedLength); + + byte[] result = CryptoLib.Bls12G1Mul(input); + var actual = ParseEthereumG1Point(result); + var expected = new G1Projective(point) * CreateScalar(987654321U); + Assert.AreEqual(new G1Affine(expected).ToCompressed().ToHexString(), + new G1Affine(actual).ToCompressed().ToHexString()); + } + + // G2Add Boundary Test Cases + [TestMethod] + public void TestBls12G2AddBoundaryCase16_Normal() + { + var point1 = G2Affine.FromCompressed("93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8".HexToBytes()); + var point2 = G2Affine.FromCompressed("95d7dc07e5eaf185910d9fad2dd69fabb971b3113540a4a411b1d568f5bb6b1fa1bac6bb97a638b204fe5bbac6be140a10bacf59b3e520f1d9ab073377b8c2718ed556852004eb6cec6e153cbbae4e1891a05f5dbae38cead62004d3b37e5f36".HexToBytes()); + byte[] input = new byte[Bls12G2EncodedLength * 2]; + EncodeEthereumG2Point(new G2Projective(point1)).CopyTo(input, 0); + EncodeEthereumG2Point(new G2Projective(point2)).CopyTo(input, Bls12G2EncodedLength); + + byte[] result = CryptoLib.Bls12G2Add(input); + var actual = ParseEthereumG2Point(result); + var expected = new G2Projective(point1) + new G2Projective(point2); + Assert.AreEqual(new G2Affine(expected).ToCompressed().ToHexString(), + new G2Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12G2AddBoundaryCase17_Infinity() + { + var point1 = G2Affine.FromCompressed("93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8".HexToBytes()); + var point2 = G2Affine.FromCompressed("c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".HexToBytes()); // infinity + byte[] input = new byte[Bls12G2EncodedLength * 2]; + EncodeEthereumG2Point(new G2Projective(point1)).CopyTo(input, 0); + EncodeEthereumG2Point(new G2Projective(point2)).CopyTo(input, Bls12G2EncodedLength); + + byte[] result = CryptoLib.Bls12G2Add(input); + var actual = ParseEthereumG2Point(result); + var expected = new G2Projective(point1) + new G2Projective(point2); + Assert.AreEqual(new G2Affine(expected).ToCompressed().ToHexString(), + new G2Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12G2AddBoundaryCase18_NegYBit() + { + var point1 = G2Affine.FromCompressed("b3e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8".HexToBytes()); + var point2 = G2Affine.FromCompressed("95d7dc07e5eaf185910d9fad2dd69fabb971b3113540a4a411b1d568f5bb6b1fa1bac6bb97a638b204fe5bbac6be140a10bacf59b3e520f1d9ab073377b8c2718ed556852004eb6cec6e153cbbae4e1891a05f5dbae38cead62004d3b37e5f36".HexToBytes()); + byte[] input = new byte[Bls12G2EncodedLength * 2]; + EncodeEthereumG2Point(new G2Projective(point1)).CopyTo(input, 0); + EncodeEthereumG2Point(new G2Projective(point2)).CopyTo(input, Bls12G2EncodedLength); + + byte[] result = CryptoLib.Bls12G2Add(input); + var actual = ParseEthereumG2Point(result); + var expected = new G2Projective(point1) + new G2Projective(point2); + Assert.AreEqual(new G2Affine(expected).ToCompressed().ToHexString(), + new G2Affine(actual).ToCompressed().ToHexString()); + } + + // G2Mul Boundary Test Cases + [TestMethod] + public void TestBls12G2MulBoundaryCase19_MinScalar() + { + var point = G2Affine.FromCompressed("93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8".HexToBytes()); + byte[] input = new byte[Bls12G2EncodedLength + Scalar.Size]; + EncodeEthereumG2Point(new G2Projective(point)).CopyTo(input, 0); + CreateScalarBytes(1).CopyTo(input, Bls12G2EncodedLength); + + byte[] result = CryptoLib.Bls12G2Mul(input); + var actual = ParseEthereumG2Point(result); + var expected = new G2Projective(point) * CreateScalar(1); + Assert.AreEqual(new G2Affine(expected).ToCompressed().ToHexString(), + new G2Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12G2MulBoundaryCase20_IntMax() + { + var point = G2Affine.FromCompressed("93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8".HexToBytes()); + byte[] input = new byte[Bls12G2EncodedLength + Scalar.Size]; + EncodeEthereumG2Point(new G2Projective(point)).CopyTo(input, 0); + CreateScalarBytes(2147483647U).CopyTo(input, Bls12G2EncodedLength); + + byte[] result = CryptoLib.Bls12G2Mul(input); + var actual = ParseEthereumG2Point(result); + var expected = new G2Projective(point) * CreateScalar(2147483647U); + Assert.AreEqual(new G2Affine(expected).ToCompressed().ToHexString(), + new G2Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12G2MulBoundaryCase21_Infinity() + { + var point = G2Affine.FromCompressed("c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".HexToBytes()); // infinity + byte[] input = new byte[Bls12G2EncodedLength + Scalar.Size]; + EncodeEthereumG2Point(new G2Projective(point)).CopyTo(input, 0); + CreateScalarBytes(1337U).CopyTo(input, Bls12G2EncodedLength); + + byte[] result = CryptoLib.Bls12G2Mul(input); + var actual = ParseEthereumG2Point(result); + var expected = new G2Projective(point) * CreateScalar(1337U); + Assert.AreEqual(new G2Affine(expected).ToCompressed().ToHexString(), + new G2Affine(actual).ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestBls12G2MulBoundaryCase22_NegYBit() + { + var point = G2Affine.FromCompressed("b3e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8".HexToBytes()); + byte[] input = new byte[Bls12G2EncodedLength + Scalar.Size]; + EncodeEthereumG2Point(new G2Projective(point)).CopyTo(input, 0); + CreateScalarBytes(987654321U).CopyTo(input, Bls12G2EncodedLength); + + byte[] result = CryptoLib.Bls12G2Mul(input); + var actual = ParseEthereumG2Point(result); + var expected = new G2Projective(point) * CreateScalar(987654321U); + Assert.AreEqual(new G2Affine(expected).ToCompressed().ToHexString(), + new G2Affine(actual).ToCompressed().ToHexString()); + } + + // Pairing Boundary Test Cases + [TestMethod] + public void TestBls12PairingBoundaryCase23_Normal() + { + var g1Point = G1Affine.FromCompressed("97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + var g2Point = G2Affine.FromCompressed("93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8".HexToBytes()); + byte[] input = EncodeEthereumG1Point(new G1Projective(g1Point)) + .Concat(EncodeEthereumG2Point(new G2Projective(g2Point))) + .ToArray(); + + byte[] result = CryptoLib.Bls12Pairing(input); + // Pairing result is 32 bytes, last byte is 1 if identity, 0 otherwise + // For normal points, result is typically non-identity (0) + Assert.AreEqual(32, result.Length); + // Verify format: first 31 bytes should be zero, last byte is 0 or 1 + Assert.IsTrue(result.Take(31).All(b => b == 0), "First 31 bytes should be zero"); + Assert.IsTrue(result[31] == 0 || result[31] == 1, "Last byte should be 0 or 1"); + } + + [TestMethod] + public void TestBls12PairingBoundaryCase24_G1Infinity() + { + var g1Point = G1Affine.FromCompressed("c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".HexToBytes()); // infinity + var g2Point = G2Affine.FromCompressed("93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8".HexToBytes()); + byte[] input = EncodeEthereumG1Point(new G1Projective(g1Point)) + .Concat(EncodeEthereumG2Point(new G2Projective(g2Point))) + .ToArray(); + + byte[] result = CryptoLib.Bls12Pairing(input); + // Pairing with infinity should return identity (last byte = 1) + Assert.AreEqual(32, result.Length); + Assert.IsTrue(result.Take(31).All(b => b == 0), "First 31 bytes should be zero"); + Assert.AreEqual(1, result[31], "Pairing with G1 infinity should return identity"); + } + + [TestMethod] + public void TestBls12PairingBoundaryCase25_G2Infinity() + { + var g1Point = G1Affine.FromCompressed("97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + var g2Point = G2Affine.FromCompressed("c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".HexToBytes()); // infinity + byte[] input = EncodeEthereumG1Point(new G1Projective(g1Point)) + .Concat(EncodeEthereumG2Point(new G2Projective(g2Point))) + .ToArray(); + + byte[] result = CryptoLib.Bls12Pairing(input); + // Pairing with infinity should return identity (last byte = 1) + Assert.AreEqual(32, result.Length); + Assert.IsTrue(result.Take(31).All(b => b == 0), "First 31 bytes should be zero"); + Assert.AreEqual(1, result[31], "Pairing with G2 infinity should return identity"); + } + + [TestMethod] + public void TestBls12PairingBoundaryCase26_NegYBits() + { + var g1Point = G1Affine.FromCompressed("b7f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb".HexToBytes()); + var g2Point = G2Affine.FromCompressed("b3e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8".HexToBytes()); + byte[] input = EncodeEthereumG1Point(new G1Projective(g1Point)) + .Concat(EncodeEthereumG2Point(new G2Projective(g2Point))) + .ToArray(); + + byte[] result = CryptoLib.Bls12Pairing(input); + // Pairing result is 32 bytes, last byte is 1 if identity, 0 otherwise + Assert.AreEqual(32, result.Length); + Assert.IsTrue(result.Take(31).All(b => b == 0), "First 31 bytes should be zero"); + Assert.IsTrue(result[31] == 0 || result[31] == 1, "Last byte should be 0 or 1"); + } + [TestMethod] public void Bls12381Equal() { @@ -1125,7 +2205,7 @@ public void TestVerifyWithEd25519() { // byte[] privateKey = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60".HexToBytes(); byte[] publicKey = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a".HexToBytes(); - byte[] message = Array.Empty(); + byte[] message = System.Array.Empty(); byte[] signature = ("e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155" + "5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b").HexToBytes(); @@ -1141,13 +2221,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)); } @@ -1174,5 +2254,149 @@ private bool CallVerifyWithEd25519(byte[] message, byte[] publicKey, byte[] sign return engine.ResultStack.Pop().GetBoolean(); } } + + private static VMArray BuildEthereumG1Pairs(byte[] input) + { + Assert.AreEqual(0, input.Length % 160, $"Invalid G1 multiexp input length ({input.Length})."); + var pairs = new VMArray(); + for (int offset = 0; offset < input.Length; offset += 160) + { + var pointBytes = input.AsSpan(offset, 128); + var scalarBytes = input.AsSpan(offset + 128, Scalar.Size).ToArray(); + pairs.Add(new VMArray(new StackItem[] + { + StackItem.FromInterface(ParseEthereumG1Point(pointBytes)), + new ByteString(scalarBytes) + })); + } + return pairs; + } + + private static VMArray BuildEthereumG2Pairs(byte[] input) + { + Assert.AreEqual(0, input.Length % 288, "Invalid G2 multiexp input length."); + var pairs = new VMArray(); + for (int offset = 0; offset < input.Length; offset += 288) + { + var pointBytes = input.AsSpan(offset, 256); + var scalarBytes = input.AsSpan(offset + 256, Scalar.Size).ToArray(); + pairs.Add(new VMArray(new StackItem[] + { + StackItem.FromInterface(ParseEthereumG2Point(pointBytes)), + new ByteString(scalarBytes) + })); + } + return pairs; + } + + private static G1Projective ParseEthereumG1Point(ReadOnlySpan bytes) + { + Assert.AreEqual(128, bytes.Length, "G1 encoding must be 128 bytes."); + var x = ParseEthereumFp(bytes[..64]); + var y = ParseEthereumFp(bytes[64..]); + var affine = new G1Affine(in x, in y); + Assert.IsTrue(affine.IsOnCurve, "G1 point not on curve."); + Assert.IsTrue(affine.IsTorsionFree, "G1 point not in prime subgroup."); + return new G1Projective(affine); + } + + private static G2Projective ParseEthereumG2Point(ReadOnlySpan bytes) + { + Assert.AreEqual(256, bytes.Length, "G2 encoding must be 256 bytes."); + var x0 = ParseEthereumFp(bytes[..64]); + var x1 = ParseEthereumFp(bytes.Slice(64, 64)); + var y0 = ParseEthereumFp(bytes.Slice(128, 64)); + var y1 = ParseEthereumFp(bytes.Slice(192, 64)); + var x = new Fp2(in x0, in x1); + var y = new Fp2(in y0, in y1); + var affine = new G2Affine(in x, in y); + Assert.IsTrue(affine.IsOnCurve, "G2 point not on curve."); + Assert.IsTrue(affine.IsTorsionFree, "G2 point not in prime subgroup."); + return new G2Projective(affine); + } + + private static Fp ParseEthereumFp(ReadOnlySpan bytes) + { + Assert.AreEqual(64, bytes.Length, "Field element must be 64 bytes."); + for (int i = 0; i < 16; i++) + Assert.AreEqual((byte)0, bytes[i], "Field element has non-zero top bytes."); + Span fieldBytes = stackalloc byte[Fp.Size]; + bytes[16..].CopyTo(fieldBytes); + return Fp.FromBytes(fieldBytes); + } + + private static byte[] CreateScalarBytes(BigInteger value) + { + if (value < 0) + throw new ArgumentOutOfRangeException(nameof(value)); + + Span temp = stackalloc byte[Scalar.Size]; + var mask = (BigInteger.One << (Scalar.Size * 8)) - BigInteger.One; + var truncated = value & mask; + var encoded = truncated.ToByteArray(isUnsigned: true, isBigEndian: true); + if (encoded.Length > Scalar.Size) + throw new InvalidOperationException("Unable to encode scalar value."); + encoded.CopyTo(temp[(Scalar.Size - encoded.Length)..]); + byte[] result = new byte[Scalar.Size]; + temp.CopyTo(result); + return result; + } + + private static byte[] CreateScalarBytes(uint value) => CreateScalarBytes(new BigInteger(value)); + + private static Scalar CreateScalar(uint value) + { + Span littleEndian = stackalloc byte[Scalar.Size]; + if (!new BigInteger(value).TryWriteBytes(littleEndian, out _, isBigEndian: false)) + throw new InvalidOperationException("Unable to encode scalar value."); + return Scalar.FromBytes(littleEndian); + } + + private const int Bls12381MultiExpMaxPairs = 128; + private const int Bls12FieldElementLength = 64; + private const int Bls12G1EncodedLength = Bls12FieldElementLength * 2; + private const int Bls12G2EncodedLength = Bls12FieldElementLength * 4; + private const int Bls12PairInputLength = Bls12G1EncodedLength + Bls12G2EncodedLength; + + private static byte[] EncodeEthereumG1Point(G1Projective point) + { + var affine = new G1Affine(point); + if (affine.IsIdentity) + return new byte[Bls12G1EncodedLength]; + + byte[] output = new byte[Bls12G1EncodedLength]; + WriteEthereumFp(affine.X, output.AsSpan(0, Bls12FieldElementLength)); + WriteEthereumFp(affine.Y, output.AsSpan(Bls12FieldElementLength, Bls12FieldElementLength)); + return output; + } + + private static byte[] EncodeEthereumG2Point(G2Projective point) + { + var affine = new G2Affine(point); + if (affine.IsIdentity) + return new byte[Bls12G2EncodedLength]; + + byte[] output = new byte[Bls12G2EncodedLength]; + WriteEthereumFp(affine.X.C0, output.AsSpan(0, Bls12FieldElementLength)); + WriteEthereumFp(affine.X.C1, output.AsSpan(Bls12FieldElementLength, Bls12FieldElementLength)); + WriteEthereumFp(affine.Y.C0, output.AsSpan(Bls12FieldElementLength * 2, Bls12FieldElementLength)); + WriteEthereumFp(affine.Y.C1, output.AsSpan(Bls12FieldElementLength * 3, Bls12FieldElementLength)); + return output; + } + + private static byte[] BuildPairInput(G1Affine g1Point, G2Affine g2Point) + { + return EncodeEthereumG1Point(new G1Projective(g1Point)) + .Concat(EncodeEthereumG2Point(new G2Projective(g2Point))) + .ToArray(); + } + + private static void WriteEthereumFp(Fp fp, Span destination) + { + destination.Clear(); + Span buffer = stackalloc byte[Fp.Size]; + fp.TryWrite(buffer); + buffer.CopyTo(destination[(Bls12FieldElementLength - Fp.Size)..]); + } } } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs index 9297af0951..9e8f36d9ef 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs @@ -43,7 +43,7 @@ public void TestSetup() { {"ContractManagement", """{"id":-1,"updatecounter":0,"hash":"0xfffdc93764dbaddd97c48f252a53ea4643faa3fd","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":3581846399},"manifest":{"name":"ContractManagement","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"deploy","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"}],"returntype":"Array","offset":0,"safe":false},{"name":"deploy","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Array","offset":7,"safe":false},{"name":"destroy","parameters":[],"returntype":"Void","offset":14,"safe":false},{"name":"getContract","parameters":[{"name":"hash","type":"Hash160"}],"returntype":"Array","offset":21,"safe":true},{"name":"getContractById","parameters":[{"name":"id","type":"Integer"}],"returntype":"Array","offset":28,"safe":true},{"name":"getContractHashes","parameters":[],"returntype":"InteropInterface","offset":35,"safe":true},{"name":"getMinimumDeploymentFee","parameters":[],"returntype":"Integer","offset":42,"safe":true},{"name":"hasMethod","parameters":[{"name":"hash","type":"Hash160"},{"name":"method","type":"String"},{"name":"pcount","type":"Integer"}],"returntype":"Boolean","offset":49,"safe":true},{"name":"isContract","parameters":[{"name":"hash","type":"Hash160"}],"returntype":"Boolean","offset":56,"safe":true},{"name":"setMinimumDeploymentFee","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":63,"safe":false},{"name":"update","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"}],"returntype":"Void","offset":70,"safe":false},{"name":"update","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","offset":77,"safe":false}],"events":[{"name":"Deploy","parameters":[{"name":"Hash","type":"Hash160"}]},{"name":"Update","parameters":[{"name":"Hash","type":"Hash160"}]},{"name":"Destroy","parameters":[{"name":"Hash","type":"Hash160"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}""" }, {"StdLib", """{"id":-2,"updatecounter":0,"hash":"0xacce6fd80d44e1796aa0c2c625e9e4e0ce39efc0","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":2426471238},"manifest":{"name":"StdLib","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"atoi","parameters":[{"name":"value","type":"String"}],"returntype":"Integer","offset":0,"safe":true},{"name":"atoi","parameters":[{"name":"value","type":"String"},{"name":"base","type":"Integer"}],"returntype":"Integer","offset":7,"safe":true},{"name":"base58CheckDecode","parameters":[{"name":"s","type":"String"}],"returntype":"ByteArray","offset":14,"safe":true},{"name":"base58CheckEncode","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","offset":21,"safe":true},{"name":"base58Decode","parameters":[{"name":"s","type":"String"}],"returntype":"ByteArray","offset":28,"safe":true},{"name":"base58Encode","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","offset":35,"safe":true},{"name":"base64Decode","parameters":[{"name":"s","type":"String"}],"returntype":"ByteArray","offset":42,"safe":true},{"name":"base64Encode","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","offset":49,"safe":true},{"name":"base64UrlDecode","parameters":[{"name":"s","type":"String"}],"returntype":"String","offset":56,"safe":true},{"name":"base64UrlEncode","parameters":[{"name":"data","type":"String"}],"returntype":"String","offset":63,"safe":true},{"name":"deserialize","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"Any","offset":70,"safe":true},{"name":"hexDecode","parameters":[{"name":"str","type":"String"}],"returntype":"ByteArray","offset":77,"safe":true},{"name":"hexEncode","parameters":[{"name":"bytes","type":"ByteArray"}],"returntype":"String","offset":84,"safe":true},{"name":"itoa","parameters":[{"name":"value","type":"Integer"}],"returntype":"String","offset":91,"safe":true},{"name":"itoa","parameters":[{"name":"value","type":"Integer"},{"name":"base","type":"Integer"}],"returntype":"String","offset":98,"safe":true},{"name":"jsonDeserialize","parameters":[{"name":"json","type":"ByteArray"}],"returntype":"Any","offset":105,"safe":true},{"name":"jsonSerialize","parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","offset":112,"safe":true},{"name":"memoryCompare","parameters":[{"name":"str1","type":"ByteArray"},{"name":"str2","type":"ByteArray"}],"returntype":"Integer","offset":119,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"}],"returntype":"Integer","offset":126,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"}],"returntype":"Integer","offset":133,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"},{"name":"backward","type":"Boolean"}],"returntype":"Integer","offset":140,"safe":true},{"name":"serialize","parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","offset":147,"safe":true},{"name":"strLen","parameters":[{"name":"str","type":"String"}],"returntype":"Integer","offset":154,"safe":true},{"name":"stringSplit","parameters":[{"name":"str","type":"String"},{"name":"separator","type":"String"}],"returntype":"Array","offset":161,"safe":true},{"name":"stringSplit","parameters":[{"name":"str","type":"String"},{"name":"separator","type":"String"},{"name":"removeEmptyEntries","type":"Boolean"}],"returntype":"Array","offset":168,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, - {"CryptoLib", """{"id":-3,"updatecounter":0,"hash":"0x726cb6e0cd8628a1350a611384688911ab75f51b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":174904780},"manifest":{"name":"CryptoLib","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"bls12381Add","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"InteropInterface","offset":0,"safe":true},{"name":"bls12381Deserialize","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"InteropInterface","offset":7,"safe":true},{"name":"bls12381Equal","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"Boolean","offset":14,"safe":true},{"name":"bls12381Mul","parameters":[{"name":"x","type":"InteropInterface"},{"name":"mul","type":"ByteArray"},{"name":"neg","type":"Boolean"}],"returntype":"InteropInterface","offset":21,"safe":true},{"name":"bls12381Pairing","parameters":[{"name":"g1","type":"InteropInterface"},{"name":"g2","type":"InteropInterface"}],"returntype":"InteropInterface","offset":28,"safe":true},{"name":"bls12381Serialize","parameters":[{"name":"g","type":"InteropInterface"}],"returntype":"ByteArray","offset":35,"safe":true},{"name":"keccak256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":42,"safe":true},{"name":"murmur32","parameters":[{"name":"data","type":"ByteArray"},{"name":"seed","type":"Integer"}],"returntype":"ByteArray","offset":49,"safe":true},{"name":"recoverSecp256K1","parameters":[{"name":"messageHash","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"ByteArray","offset":56,"safe":true},{"name":"ripemd160","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":63,"safe":true},{"name":"sha256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":70,"safe":true},{"name":"verifyWithECDsa","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"},{"name":"curveHash","type":"Integer"}],"returntype":"Boolean","offset":77,"safe":true},{"name":"verifyWithEd25519","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":84,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, +{"CryptoLib", """{"id":-3,"updatecounter":0,"hash":"0x726cb6e0cd8628a1350a611384688911ab75f51b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2681632925},"manifest":{"name":"CryptoLib","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"bls12381Add","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"InteropInterface","offset":0,"safe":true},{"name":"bls12381Deserialize","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"InteropInterface","offset":7,"safe":true},{"name":"bls12381Equal","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"Boolean","offset":14,"safe":true},{"name":"bls12381Mul","parameters":[{"name":"x","type":"InteropInterface"},{"name":"mul","type":"ByteArray"},{"name":"neg","type":"Boolean"}],"returntype":"InteropInterface","offset":21,"safe":true},{"name":"bls12381MultiExp","parameters":[{"name":"pairs","type":"Array"}],"returntype":"InteropInterface","offset":28,"safe":true},{"name":"bls12381Pairing","parameters":[{"name":"g1","type":"InteropInterface"},{"name":"g2","type":"InteropInterface"}],"returntype":"InteropInterface","offset":35,"safe":true},{"name":"bls12381Serialize","parameters":[{"name":"g","type":"InteropInterface"}],"returntype":"ByteArray","offset":42,"safe":true},{"name":"bls12_deserialize","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"InteropInterface","offset":49,"safe":true},{"name":"bls12_g1add","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":56,"safe":true},{"name":"bls12_g1mul","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":63,"safe":true},{"name":"bls12_g1multiexp","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":70,"safe":true},{"name":"bls12_g2add","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":77,"safe":true},{"name":"bls12_g2mul","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":84,"safe":true},{"name":"bls12_g2multiexp","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":91,"safe":true},{"name":"bls12_pairing","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":98,"safe":true},{"name":"bls12_serialize","parameters":[{"name":"g","type":"InteropInterface"}],"returntype":"ByteArray","offset":105,"safe":true},{"name":"keccak256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":112,"safe":true},{"name":"murmur32","parameters":[{"name":"data","type":"ByteArray"},{"name":"seed","type":"Integer"}],"returntype":"ByteArray","offset":119,"safe":true},{"name":"recoverSecp256K1","parameters":[{"name":"messageHash","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"ByteArray","offset":126,"safe":true},{"name":"ripemd160","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":133,"safe":true},{"name":"sha256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":140,"safe":true},{"name":"verifyWithECDsa","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"},{"name":"curveHash","type":"Integer"}],"returntype":"Boolean","offset":147,"safe":true},{"name":"verifyWithEd25519","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":154,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, {"LedgerContract", """{"id":-4,"updatecounter":0,"hash":"0xda65b600f7124ce6c79950c1772a36403104f2be","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1110259869},"manifest":{"name":"LedgerContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"currentHash","parameters":[],"returntype":"Hash256","offset":0,"safe":true},{"name":"currentIndex","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"getBlock","parameters":[{"name":"indexOrHash","type":"ByteArray"}],"returntype":"Array","offset":14,"safe":true},{"name":"getTransaction","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Array","offset":21,"safe":true},{"name":"getTransactionFromBlock","parameters":[{"name":"blockIndexOrHash","type":"ByteArray"},{"name":"txIndex","type":"Integer"}],"returntype":"Array","offset":28,"safe":true},{"name":"getTransactionHeight","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Integer","offset":35,"safe":true},{"name":"getTransactionSigners","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Array","offset":42,"safe":true},{"name":"getTransactionVMState","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Integer","offset":49,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, {"NeoToken", """{"id":-5,"updatecounter":0,"hash":"0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":1991619121},"manifest":{"name":"NeoToken","groups":[],"features":{},"supportedstandards":["NEP-17","NEP-27"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"decimals","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"getAccountState","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Array","offset":14,"safe":true},{"name":"getAllCandidates","parameters":[],"returntype":"InteropInterface","offset":21,"safe":true},{"name":"getCandidateVote","parameters":[{"name":"pubKey","type":"PublicKey"}],"returntype":"Integer","offset":28,"safe":true},{"name":"getCandidates","parameters":[],"returntype":"Array","offset":35,"safe":true},{"name":"getCommittee","parameters":[],"returntype":"Array","offset":42,"safe":true},{"name":"getCommitteeAddress","parameters":[],"returntype":"Hash160","offset":49,"safe":true},{"name":"getGasPerBlock","parameters":[],"returntype":"Integer","offset":56,"safe":true},{"name":"getNextBlockValidators","parameters":[],"returntype":"Array","offset":63,"safe":true},{"name":"getRegisterPrice","parameters":[],"returntype":"Integer","offset":70,"safe":true},{"name":"onNEP17Payment","parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","offset":77,"safe":false},{"name":"registerCandidate","parameters":[{"name":"pubkey","type":"PublicKey"}],"returntype":"Boolean","offset":84,"safe":false},{"name":"setGasPerBlock","parameters":[{"name":"gasPerBlock","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"setRegisterPrice","parameters":[{"name":"registerPrice","type":"Integer"}],"returntype":"Void","offset":98,"safe":false},{"name":"symbol","parameters":[],"returntype":"String","offset":105,"safe":true},{"name":"totalSupply","parameters":[],"returntype":"Integer","offset":112,"safe":true},{"name":"transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Boolean","offset":119,"safe":false},{"name":"unclaimedGas","parameters":[{"name":"account","type":"Hash160"},{"name":"end","type":"Integer"}],"returntype":"Integer","offset":126,"safe":true},{"name":"unregisterCandidate","parameters":[{"name":"pubkey","type":"PublicKey"}],"returntype":"Boolean","offset":133,"safe":false},{"name":"vote","parameters":[{"name":"account","type":"Hash160"},{"name":"voteTo","type":"PublicKey"}],"returntype":"Boolean","offset":140,"safe":false}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]},{"name":"CandidateStateChanged","parameters":[{"name":"pubkey","type":"PublicKey"},{"name":"registered","type":"Boolean"},{"name":"votes","type":"Integer"}]},{"name":"Vote","parameters":[{"name":"account","type":"Hash160"},{"name":"from","type":"PublicKey"},{"name":"to","type":"PublicKey"},{"name":"amount","type":"Integer"}]},{"name":"CommitteeChanged","parameters":[{"name":"old","type":"Array"},{"name":"new","type":"Array"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, {"GasToken", """{"id":-6,"updatecounter":0,"hash":"0xd2a4cff31913016155e38e474a2c06d08be276cf","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2663858513},"manifest":{"name":"GasToken","groups":[],"features":{},"supportedstandards":["NEP-17"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"decimals","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"symbol","parameters":[],"returntype":"String","offset":14,"safe":true},{"name":"totalSupply","parameters":[],"returntype":"Integer","offset":21,"safe":true},{"name":"transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Boolean","offset":28,"safe":false}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, @@ -52,6 +52,21 @@ public void TestSetup() {"OracleContract", """{"id":-9,"updatecounter":0,"hash":"0xfe924b7cfe89ddd271abaf7210a80a7e11178758","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2663858513},"manifest":{"name":"OracleContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"finish","parameters":[],"returntype":"Void","offset":0,"safe":false},{"name":"getPrice","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"request","parameters":[{"name":"url","type":"String"},{"name":"filter","type":"String"},{"name":"callback","type":"String"},{"name":"userData","type":"Any"},{"name":"gasForResponse","type":"Integer"}],"returntype":"Void","offset":14,"safe":false},{"name":"setPrice","parameters":[{"name":"price","type":"Integer"}],"returntype":"Void","offset":21,"safe":false},{"name":"verify","parameters":[],"returntype":"Boolean","offset":28,"safe":true}],"events":[{"name":"OracleRequest","parameters":[{"name":"Id","type":"Integer"},{"name":"RequestContract","type":"Hash160"},{"name":"Url","type":"String"},{"name":"Filter","type":"String"}]},{"name":"OracleResponse","parameters":[{"name":"Id","type":"Integer"},{"name":"OriginalTx","type":"Hash256"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, {"Notary", """{"id":-10,"updatecounter":0,"hash":"0xc1e14f19c3e60d0b9244d06dd7ba9b113135ec3b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1110259869},"manifest":{"name":"Notary","groups":[],"features":{},"supportedstandards":["NEP-27"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"expirationOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":7,"safe":true},{"name":"getMaxNotValidBeforeDelta","parameters":[],"returntype":"Integer","offset":14,"safe":true},{"name":"lockDepositUntil","parameters":[{"name":"account","type":"Hash160"},{"name":"till","type":"Integer"}],"returntype":"Boolean","offset":21,"safe":false},{"name":"onNEP17Payment","parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","offset":28,"safe":false},{"name":"setMaxNotValidBeforeDelta","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":35,"safe":false},{"name":"verify","parameters":[{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":42,"safe":true},{"name":"withdraw","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"}],"returntype":"Boolean","offset":49,"safe":false}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""} }; + + var persistingBlock = new Block + { + Header = new Header + { + Index = 1, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + PrevHash = UInt256.Zero, + Witness = Witness.Empty, + }, + Transactions = [] + }; + var cryptoLibState = Call_GetContract(_snapshotCache.CloneCache(), NativeContract.CryptoLib.Hash, persistingBlock); + _nativeStates["CryptoLib"] = cryptoLibState.ToJson().ToString(); } class active : IHardforkActivable @@ -329,6 +344,10 @@ 3. A native contract method may have different behaviors in different hardforks. { writer.WriteLine($"## {kvp.Key.Name}\n"); writer.WriteLine(kvp.Value); + if (kvp.Key.Name == "CryptoLib") + { + writer.WriteLine("**Note:** Methods prefixed with `bls12_` follow the EIP-2537 (Ethereum) uncompressed encoding: G1 inputs are `x || y` (128 bytes), G2 inputs are `x0 || x1 || y0 || y1` (256 bytes), scalars are 32-byte big-endian, and the identity is encoded as all-zero bytes. The existing `bls12381*` methods keep Neo's compressed encoding.\n"); + } writer.WriteLine(); } }