diff --git a/docs/native-contracts-api.md b/docs/native-contracts-api.md index d89d261bf3..38e7fc436e 100644 --- a/docs/native-contracts-api.md +++ b/docs/native-contracts-api.md @@ -78,6 +78,7 @@ When calling a native contract method by transaction script, there are several t | bls12381Equal | Determines whether the specified points are equal. | InteropInterface(*x*), InteropInterface(*y*) | Boolean | 1<<5 | 0 | -- | -- | | bls12381Add | Add operation of two points. | InteropInterface(*x*), InteropInterface(*y*) | InteropInterface | 1<<19 | 0 | -- | -- | | bls12381Mul | Mul operation of gt point and multiplier | InteropInterface(*x*), Byte[](*mul*), Boolean(*neg*) | InteropInterface | 1<<21 | 0 | -- | -- | +| bls12381MultiExp | Multi exponentiation operation for bls12381 points. | Array(*pairs*) | InteropInterface | 1<<23 | 0 | -- | HF_Faun | | bls12381Pairing | Pairing operation of g1 and g2 | InteropInterface(*g1*), InteropInterface(*g2*) | InteropInterface | 1<<23 | 0 | -- | -- | | recoverSecp256K1 | Recovers the public key from a secp256k1 signature in a single byte array format. | Byte[](*messageHash*), Byte[](*signature*) | Byte[] | 1<<15 | 0 | -- | HF_Echidna | | ripemd160 | Computes the hash value for the specified byte array using the ripemd160 algorithm. | Byte[](*data*) | Byte[] | 1<<15 | 0 | -- | -- | diff --git a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs index 51ef7f6b90..299a4dd3a6 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs @@ -12,11 +12,15 @@ 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; + /// /// Serialize a bls12381 point. /// @@ -119,6 +123,83 @@ public static InteropInterface Bls12381Mul(InteropInterface x, byte[] mul, bool }; } + /// + /// 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 /// @@ -142,5 +223,57 @@ public static InteropInterface Bls12381Pairing(InteropInterface g1, InteropInter }; return new(Bls12.Pairing(in g1a, in g2a)); } + + private static void EnsureGroupType(ref bool? current, bool isG2) + { + if (current is null) + { + current = isG2; + } + else if (current.Value != isG2) + { + throw new ArgumentException("BLS12-381 multi exponent cannot mix groups"); + } + } + + private static Scalar ParseScalar(StackItem scalarItem) + { + ReadOnlySpan data = scalarItem switch + { + ByteString bs when bs.GetSpan().Length == Scalar.Size => bs.GetSpan(), + VMBuffer buffer when buffer.Size == Scalar.Size => buffer.InnerBuffer.Span, + _ => throw new ArgumentException("BLS12-381 scalar must be 32 bytes"), + }; + + Span littleEndian = stackalloc byte[Scalar.Size]; + data.CopyTo(littleEndian); + + try + { + return Scalar.FromBytes(littleEndian); + } + catch (FormatException) + { + var wide = new byte[Scalar.Size * 2]; + littleEndian.CopyTo(wide); + return Scalar.FromBytesWide(wide); + } + } + + 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"); + } } } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs index e70b35083d..f143452ec8 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 { @@ -263,6 +267,118 @@ 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 }); + + var wide = new byte[Scalar.Size * 2]; + System.Array.Copy(scalarBytes, wide, scalarBytes.Length); + var reducedScalar = Scalar.FromBytesWide(wide); + + var result = CryptoLib.Bls12381MultiExp(pairs); + var actual = result.GetInterface(); + + 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 Bls12381Equal() { @@ -1125,7 +1241,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 +1257,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 +1290,23 @@ private bool CallVerifyWithEd25519(byte[] message, byte[] publicKey, byte[] sign return engine.ResultStack.Pop().GetBoolean(); } } + + private static byte[] CreateScalarBytes(BigInteger value) + { + if (value < 0) + throw new ArgumentOutOfRangeException(nameof(value)); + + var bytes = new byte[Scalar.Size]; + var mask = (BigInteger.One << (Scalar.Size * 8)) - BigInteger.One; + var truncated = value & mask; + if (!truncated.TryWriteBytes(bytes, out _, isBigEndian: false)) + throw new InvalidOperationException("Unable to encode scalar value."); + return bytes; + } + + private static byte[] CreateScalarBytes(uint value) => CreateScalarBytes(new BigInteger(value)); + + private static Scalar CreateScalar(uint value) => Scalar.FromBytes(CreateScalarBytes(value)); + } } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs index 38d1d715fc..3d56e505df 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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1782461736},"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":"keccak256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":49,"safe":true},{"name":"murmur32","parameters":[{"name":"data","type":"ByteArray"},{"name":"seed","type":"Integer"}],"returntype":"ByteArray","offset":56,"safe":true},{"name":"recoverSecp256K1","parameters":[{"name":"messageHash","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"ByteArray","offset":63,"safe":true},{"name":"ripemd160","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":70,"safe":true},{"name":"sha256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":77,"safe":true},{"name":"verifyWithECDsa","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"},{"name":"curveHash","type":"Integer"}],"returntype":"Boolean","offset":84,"safe":true},{"name":"verifyWithEd25519","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":91,"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}}"""}, @@ -298,8 +298,12 @@ public void TestGenerateNativeContractApi() markdownTables[(contract.Id, contract.Name)] = GenMarkdownTable(contractName, contractMethods); } - var currentDir = Directory.GetParent(Directory.GetCurrentDirectory()).Parent.Parent.Parent; - Assert.AreEqual(currentDir.Name, "neo"); // neo/bin/tests/Neo.UnitTests/net9.0 + var currentDir = new DirectoryInfo(Directory.GetCurrentDirectory()); + while (currentDir is not null && !Directory.Exists(Path.Combine(currentDir.FullName, "docs"))) + { + currentDir = currentDir.Parent; + } + Assert.IsNotNull(currentDir, "Unable to locate repository root containing a docs directory."); var outputPath = Path.Combine(currentDir.FullName, "docs", "native-contracts-api.md"); using (var writer = new StreamWriter(outputPath))