From 1a443fe02410cb46b2c2626c2d0b60fcb7f8bf13 Mon Sep 17 00:00:00 2001 From: jimmy Date: Mon, 22 Sep 2025 12:25:51 +0800 Subject: [PATCH 01/16] Add Ethereum-compatible aliases for BLS12-381 --- .../Native/CryptoLib.BLS12_381.cs | 20 +++++ .../SmartContract/Native/UT_CryptoLib.cs | 85 +++++++++++++++++++ .../SmartContract/Native/UT_NativeContract.cs | 2 +- 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs index 51ef7f6b90..d5588ddef8 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs @@ -97,6 +97,14 @@ public static InteropInterface Bls12381Add(InteropInterface x, InteropInterface }; } + [ContractMethod(CpuFee = 1 << 19, Name = "bls12_g1add")] + public static InteropInterface Bls12G1Add(InteropInterface x, InteropInterface y) + => Bls12381Add(x, y); + + [ContractMethod(CpuFee = 1 << 19, Name = "bls12_g2add")] + public static InteropInterface Bls12G2Add(InteropInterface x, InteropInterface y) + => Bls12381Add(x, y); + /// /// Mul operation of gt point and multiplier /// @@ -119,6 +127,14 @@ public static InteropInterface Bls12381Mul(InteropInterface x, byte[] mul, bool }; } + [ContractMethod(CpuFee = 1 << 21, Name = "bls12_g1mul")] + public static InteropInterface Bls12G1Mul(InteropInterface x, byte[] mul, bool neg) + => Bls12381Mul(x, mul, neg); + + [ContractMethod(CpuFee = 1 << 21, Name = "bls12_g2mul")] + public static InteropInterface Bls12G2Mul(InteropInterface x, byte[] mul, bool neg) + => Bls12381Mul(x, mul, neg); + /// /// Pairing operation of g1 and g2 /// @@ -142,5 +158,9 @@ public static InteropInterface Bls12381Pairing(InteropInterface g1, InteropInter }; return new(Bls12.Pairing(in g1a, in g2a)); } + + [ContractMethod(CpuFee = 1 << 23, Name = "bls12_pairing")] + public static InteropInterface Bls12Pairing(InteropInterface g1, InteropInterface g2) + => Bls12381Pairing(g1, g2); } } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs index e70b35083d..bd32935916 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -18,6 +18,7 @@ using Neo.Network.P2P; using Neo.Network.P2P.Payloads; using Neo.SmartContract; +using Neo.Persistence; using Neo.SmartContract.Native; using Neo.VM; using Org.BouncyCastle.Utilities.Encoders; @@ -263,6 +264,33 @@ public void TestBls12381Pairing() Assert.AreEqual(expected.ToLower(), result.GetInterface().ToArray().ToHexString()); } + [TestMethod] + public void TestBls12AddAliases() + { + var expected = InvokeBlsAddMethod("bls12381Add"); + foreach (var alias in new[] { "bls12_g1add", "bls12_g2add" }) + { + CollectionAssert.AreEqual(expected, InvokeBlsAddMethod(alias)); + } + } + + [TestMethod] + public void TestBls12MulAliases() + { + var expected = InvokeBlsMulMethod("bls12381Mul", false); + foreach (var alias in new[] { "bls12_g1mul", "bls12_g2mul" }) + { + CollectionAssert.AreEqual(expected, InvokeBlsMulMethod(alias, false)); + } + } + + [TestMethod] + public void TestBls12PairingAlias() + { + var expected = InvokeBlsPairingMethod("bls12381Pairing"); + CollectionAssert.AreEqual(expected, InvokeBlsPairingMethod("bls12_pairing")); + } + [TestMethod] public void Bls12381Equal() { @@ -1174,5 +1202,62 @@ private bool CallVerifyWithEd25519(byte[] message, byte[] publicKey, byte[] sign return engine.ResultStack.Pop().GetBoolean(); } } + + private byte[] InvokeBlsAddMethod(string methodName) + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + using ScriptBuilder script = new(); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", gt); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", gt); + script.EmitPush(2); + script.Emit(OpCode.PACK); + script.EmitPush(CallFlags.All); + script.EmitPush(methodName); + script.EmitPush(NativeContract.CryptoLib.Hash); + script.EmitSysCall(ApplicationEngine.System_Contract_Call); + return ExecuteBlsScript(script, snapshotCache); + } + + private byte[] InvokeBlsMulMethod(string methodName, bool neg) + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + using ScriptBuilder script = new(); + byte[] data = new byte[32]; + data[0] = 0x03; + script.EmitPush(neg); + script.EmitPush(data); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", gt); + script.EmitPush(3); + script.Emit(OpCode.PACK); + script.EmitPush(CallFlags.All); + script.EmitPush(methodName); + script.EmitPush(NativeContract.CryptoLib.Hash); + script.EmitSysCall(ApplicationEngine.System_Contract_Call); + return ExecuteBlsScript(script, snapshotCache); + } + + private byte[] InvokeBlsPairingMethod(string methodName) + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + using ScriptBuilder script = new(); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", g2); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", g1); + script.EmitPush(2); + script.Emit(OpCode.PACK); + script.EmitPush(CallFlags.All); + script.EmitPush(methodName); + script.EmitPush(NativeContract.CryptoLib.Hash); + script.EmitSysCall(ApplicationEngine.System_Contract_Call); + return ExecuteBlsScript(script, snapshotCache); + } + + private byte[] ExecuteBlsScript(ScriptBuilder script, StoreCache snapshotCache) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, + settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + return engine.ResultStack.Pop().GetInterface().ToArray(); + } } } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs index 40026d9783..10d800b53b 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs @@ -42,7 +42,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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":2208257578},"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":"bls12_g1add","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"InteropInterface","offset":42,"safe":true},{"name":"bls12_g1mul","parameters":[{"name":"x","type":"InteropInterface"},{"name":"mul","type":"ByteArray"},{"name":"neg","type":"Boolean"}],"returntype":"InteropInterface","offset":49,"safe":true},{"name":"bls12_g2add","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"InteropInterface","offset":56,"safe":true},{"name":"bls12_g2mul","parameters":[{"name":"x","type":"InteropInterface"},{"name":"mul","type":"ByteArray"},{"name":"neg","type":"Boolean"}],"returntype":"InteropInterface","offset":63,"safe":true},{"name":"bls12_pairing","parameters":[{"name":"g1","type":"InteropInterface"},{"name":"g2","type":"InteropInterface"}],"returntype":"InteropInterface","offset":70,"safe":true},{"name":"keccak256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":77,"safe":true},{"name":"murmur32","parameters":[{"name":"data","type":"ByteArray"},{"name":"seed","type":"Integer"}],"returntype":"ByteArray","offset":84,"safe":true},{"name":"recoverSecp256K1","parameters":[{"name":"messageHash","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"ByteArray","offset":91,"safe":true},{"name":"ripemd160","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":98,"safe":true},{"name":"sha256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":105,"safe":true},{"name":"verifyWithECDsa","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"},{"name":"curveHash","type":"Integer"}],"returntype":"Boolean","offset":112,"safe":true},{"name":"verifyWithEd25519","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":119,"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}}"""}, From 67b0fbbc4d5dc5bd7a73d1b68c06b759eb1b5869 Mon Sep 17 00:00:00 2001 From: jimmy Date: Mon, 22 Sep 2025 12:33:45 +0800 Subject: [PATCH 02/16] Format codebase --- src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs | 10 +++++----- .../Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs index d5588ddef8..263443498b 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs @@ -97,11 +97,11 @@ public static InteropInterface Bls12381Add(InteropInterface x, InteropInterface }; } - [ContractMethod(CpuFee = 1 << 19, Name = "bls12_g1add")] + [ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 19, Name = "bls12_g1add")] public static InteropInterface Bls12G1Add(InteropInterface x, InteropInterface y) => Bls12381Add(x, y); - [ContractMethod(CpuFee = 1 << 19, Name = "bls12_g2add")] + [ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 19, Name = "bls12_g2add")] public static InteropInterface Bls12G2Add(InteropInterface x, InteropInterface y) => Bls12381Add(x, y); @@ -127,11 +127,11 @@ public static InteropInterface Bls12381Mul(InteropInterface x, byte[] mul, bool }; } - [ContractMethod(CpuFee = 1 << 21, Name = "bls12_g1mul")] + [ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 21, Name = "bls12_g1mul")] public static InteropInterface Bls12G1Mul(InteropInterface x, byte[] mul, bool neg) => Bls12381Mul(x, mul, neg); - [ContractMethod(CpuFee = 1 << 21, Name = "bls12_g2mul")] + [ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 21, Name = "bls12_g2mul")] public static InteropInterface Bls12G2Mul(InteropInterface x, byte[] mul, bool neg) => Bls12381Mul(x, mul, neg); @@ -159,7 +159,7 @@ public static InteropInterface Bls12381Pairing(InteropInterface g1, InteropInter return new(Bls12.Pairing(in g1a, in g2a)); } - [ContractMethod(CpuFee = 1 << 23, Name = "bls12_pairing")] + [ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 23, Name = "bls12_pairing")] public static InteropInterface Bls12Pairing(InteropInterface g1, InteropInterface g2) => Bls12381Pairing(g1, g2); } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs index bd32935916..ac4aabd9bb 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -17,8 +17,8 @@ using Neo.Ledger; using Neo.Network.P2P; using Neo.Network.P2P.Payloads; -using Neo.SmartContract; using Neo.Persistence; +using Neo.SmartContract; using Neo.SmartContract.Native; using Neo.VM; using Org.BouncyCastle.Utilities.Encoders; From 1f82a9b8482b6ec85b974e0b1e64fe23d86bcf5f Mon Sep 17 00:00:00 2001 From: jimmy Date: Fri, 17 Oct 2025 10:26:43 +0800 Subject: [PATCH 03/16] Implement BLS12-381 multi exponentiation --- docs/native-contracts-api.md | 1 + .../Native/CryptoLib.BLS12_381.cs | 123 +++++++++++-- .../SmartContract/Native/UT_CryptoLib.cs | 166 +++++++++++------- .../SmartContract/Native/UT_NativeContract.cs | 2 +- 4 files changed, 207 insertions(+), 85 deletions(-) diff --git a/docs/native-contracts-api.md b/docs/native-contracts-api.md index d89d261bf3..ed4d683e6c 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_Gorgon | | bls12381Pairing | Pairing operation of g1 and g2 | InteropInterface(*g1*), InteropInterface(*g2*) | InteropInterface | 1<<23 | 0 | -- | -- | | recoverSecp256K1 | Recovers the public key from a secp256k1 signature in a single byte array format. | Byte[](*messageHash*), Byte[](*signature*) | Byte[] | 1<<15 | 0 | -- | HF_Echidna | | ripemd160 | Computes the hash value for the specified byte array using the ripemd160 algorithm. | Byte[](*data*) | Byte[] | 1<<15 | 0 | -- | -- | diff --git a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs index 263443498b..20640e56f0 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs @@ -12,6 +12,8 @@ using Neo.Cryptography.BLS12_381; using Neo.VM.Types; using System; +using Array = Neo.VM.Types.Array; +using VMBuffer = Neo.VM.Types.Buffer; namespace Neo.SmartContract.Native { @@ -97,14 +99,6 @@ public static InteropInterface Bls12381Add(InteropInterface x, InteropInterface }; } - [ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 19, Name = "bls12_g1add")] - public static InteropInterface Bls12G1Add(InteropInterface x, InteropInterface y) - => Bls12381Add(x, y); - - [ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 19, Name = "bls12_g2add")] - public static InteropInterface Bls12G2Add(InteropInterface x, InteropInterface y) - => Bls12381Add(x, y); - /// /// Mul operation of gt point and multiplier /// @@ -127,13 +121,76 @@ public static InteropInterface Bls12381Mul(InteropInterface x, byte[] mul, bool }; } - [ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 21, Name = "bls12_g1mul")] - public static InteropInterface Bls12G1Mul(InteropInterface x, byte[] mul, bool neg) - => Bls12381Mul(x, mul, neg); + /// + /// Multi exponentiation operation for bls12381 points. + /// + /// Array of [point, scalar] pairs. + /// The accumulated point. + [ContractMethod(Hardfork.HF_Gorgon, 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"); + + bool? useG2 = null; + G1Projective g1Accumulator = G1Projective.Identity; + G2Projective g2Accumulator = G2Projective.Identity; - [ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 21, Name = "bls12_g2mul")] - public static InteropInterface Bls12G2Mul(InteropInterface x, byte[] mul, bool neg) - => Bls12381Mul(x, mul, neg); + 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: + EnsureGroupType(ref useG2, false); + { + var scalar = ParseScalar(pair[1]); + if (!scalar.IsZero) + g1Accumulator += new G1Projective(g1Affine) * scalar; + } + break; + case G1Projective g1Projective: + EnsureGroupType(ref useG2, false); + { + var scalar = ParseScalar(pair[1]); + if (!scalar.IsZero) + g1Accumulator += g1Projective * scalar; + } + break; + case G2Affine g2Affine: + EnsureGroupType(ref useG2, true); + { + var scalar = ParseScalar(pair[1]); + if (!scalar.IsZero) + g2Accumulator += new G2Projective(g2Affine) * scalar; + } + break; + case G2Projective g2Projective: + EnsureGroupType(ref useG2, true); + { + var scalar = ParseScalar(pair[1]); + if (!scalar.IsZero) + g2Accumulator += g2Projective * scalar; + } + break; + default: + throw new ArgumentException("BLS12-381 type mismatch"); + } + } + + if (useG2 is null) + throw new ArgumentException("BLS12-381 multi exponent requires at least one valid pair"); + + return useG2.Value + ? new InteropInterface(g2Accumulator) + : new InteropInterface(g1Accumulator); + } /// /// Pairing operation of g1 and g2 @@ -159,8 +216,40 @@ public static InteropInterface Bls12381Pairing(InteropInterface g1, InteropInter return new(Bls12.Pairing(in g1a, in g2a)); } - [ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 23, Name = "bls12_pairing")] - public static InteropInterface Bls12Pairing(InteropInterface g1, InteropInterface g2) - => Bls12381Pairing(g1, g2); + 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); + } + } } } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs index ac4aabd9bb..d883b170a8 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -21,11 +21,14 @@ 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 { @@ -265,30 +268,98 @@ public void TestBls12381Pairing() } [TestMethod] - public void TestBls12AddAliases() + public void TestBls12381MultiExpG1() { - var expected = InvokeBlsAddMethod("bls12381Add"); - foreach (var alias in new[] { "bls12_g1add", "bls12_g2add" }) + var g1Point = G1Affine.FromCompressed(g1); + var pair1 = new VMArray(new StackItem[] { - CollectionAssert.AreEqual(expected, InvokeBlsAddMethod(alias)); - } + 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 TestBls12MulAliases() + public void TestBls12381MultiExpG2() { - var expected = InvokeBlsMulMethod("bls12381Mul", false); - foreach (var alias in new[] { "bls12_g1mul", "bls12_g2mul" }) + var g2Point = G2Affine.FromCompressed(g2); + var pair = new VMArray(new StackItem[] { - CollectionAssert.AreEqual(expected, InvokeBlsMulMethod(alias, false)); - } + 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 TestBls12PairingAlias() + public void TestBls12381MultiExpReducesScalar() { - var expected = InvokeBlsPairingMethod("bls12381Pairing"); - CollectionAssert.AreEqual(expected, InvokeBlsPairingMethod("bls12_pairing")); + 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] @@ -1153,7 +1224,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(); @@ -1169,13 +1240,13 @@ public void TestVerifyWithEd25519() // Test with an invalid signature byte[] invalidSignature = new byte[signature.Length]; - Array.Copy(signature, invalidSignature, signature.Length); + System.Array.Copy(signature, invalidSignature, signature.Length); invalidSignature[0] ^= 0x01; // Flip one bit Assert.IsFalse(CallVerifyWithEd25519(message, publicKey, invalidSignature)); // Test with an invalid public key byte[] invalidPublicKey = new byte[publicKey.Length]; - Array.Copy(publicKey, invalidPublicKey, publicKey.Length); + System.Array.Copy(publicKey, invalidPublicKey, publicKey.Length); invalidPublicKey[0] ^= 0x01; // Flip one bit Assert.IsFalse(CallVerifyWithEd25519(message, invalidPublicKey, signature)); } @@ -1203,61 +1274,22 @@ private bool CallVerifyWithEd25519(byte[] message, byte[] publicKey, byte[] sign } } - private byte[] InvokeBlsAddMethod(string methodName) + private static byte[] CreateScalarBytes(BigInteger value) { - var snapshotCache = TestBlockchain.GetTestSnapshotCache(); - using ScriptBuilder script = new(); - script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", gt); - script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", gt); - script.EmitPush(2); - script.Emit(OpCode.PACK); - script.EmitPush(CallFlags.All); - script.EmitPush(methodName); - script.EmitPush(NativeContract.CryptoLib.Hash); - script.EmitSysCall(ApplicationEngine.System_Contract_Call); - return ExecuteBlsScript(script, snapshotCache); + 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 byte[] InvokeBlsMulMethod(string methodName, bool neg) - { - var snapshotCache = TestBlockchain.GetTestSnapshotCache(); - using ScriptBuilder script = new(); - byte[] data = new byte[32]; - data[0] = 0x03; - script.EmitPush(neg); - script.EmitPush(data); - script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", gt); - script.EmitPush(3); - script.Emit(OpCode.PACK); - script.EmitPush(CallFlags.All); - script.EmitPush(methodName); - script.EmitPush(NativeContract.CryptoLib.Hash); - script.EmitSysCall(ApplicationEngine.System_Contract_Call); - return ExecuteBlsScript(script, snapshotCache); - } + private static byte[] CreateScalarBytes(uint value) => CreateScalarBytes(new BigInteger(value)); - private byte[] InvokeBlsPairingMethod(string methodName) - { - var snapshotCache = TestBlockchain.GetTestSnapshotCache(); - using ScriptBuilder script = new(); - script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", g2); - script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", g1); - script.EmitPush(2); - script.Emit(OpCode.PACK); - script.EmitPush(CallFlags.All); - script.EmitPush(methodName); - script.EmitPush(NativeContract.CryptoLib.Hash); - script.EmitSysCall(ApplicationEngine.System_Contract_Call); - return ExecuteBlsScript(script, snapshotCache); - } + private static Scalar CreateScalar(uint value) => Scalar.FromBytes(CreateScalarBytes(value)); - private byte[] ExecuteBlsScript(ScriptBuilder script, StoreCache snapshotCache) - { - using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, - settings: TestProtocolSettings.Default); - engine.LoadScript(script.ToArray()); - Assert.AreEqual(VMState.HALT, engine.Execute()); - return engine.ResultStack.Pop().GetInterface().ToArray(); - } } } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs index 70aa32775d..3d4313b442 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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":2208257578},"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":"bls12_g1add","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"InteropInterface","offset":42,"safe":true},{"name":"bls12_g1mul","parameters":[{"name":"x","type":"InteropInterface"},{"name":"mul","type":"ByteArray"},{"name":"neg","type":"Boolean"}],"returntype":"InteropInterface","offset":49,"safe":true},{"name":"bls12_g2add","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"InteropInterface","offset":56,"safe":true},{"name":"bls12_g2mul","parameters":[{"name":"x","type":"InteropInterface"},{"name":"mul","type":"ByteArray"},{"name":"neg","type":"Boolean"}],"returntype":"InteropInterface","offset":63,"safe":true},{"name":"bls12_pairing","parameters":[{"name":"g1","type":"InteropInterface"},{"name":"g2","type":"InteropInterface"}],"returntype":"InteropInterface","offset":70,"safe":true},{"name":"keccak256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":77,"safe":true},{"name":"murmur32","parameters":[{"name":"data","type":"ByteArray"},{"name":"seed","type":"Integer"}],"returntype":"ByteArray","offset":84,"safe":true},{"name":"recoverSecp256K1","parameters":[{"name":"messageHash","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"ByteArray","offset":91,"safe":true},{"name":"ripemd160","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":98,"safe":true},{"name":"sha256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":105,"safe":true},{"name":"verifyWithECDsa","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"},{"name":"curveHash","type":"Integer"}],"returntype":"Boolean","offset":112,"safe":true},{"name":"verifyWithEd25519","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":119,"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}}"""}, From 9607bd7a07e2739819a93bc0aff8acc0da89e3ab Mon Sep 17 00:00:00 2001 From: Shargon Date: Mon, 20 Oct 2025 02:34:33 -0700 Subject: [PATCH 04/16] Update src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs --- src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs index 20640e56f0..2954426723 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs @@ -126,7 +126,7 @@ public static InteropInterface Bls12381Mul(InteropInterface x, byte[] mul, bool /// /// Array of [point, scalar] pairs. /// The accumulated point. - [ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 23)] + [ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 23)] public static InteropInterface Bls12381MultiExp(Array pairs) { if (pairs is null || pairs.Count == 0) From 5f4dbe54fee78ced091da0b445c6c09ec9c08bd2 Mon Sep 17 00:00:00 2001 From: jimmy Date: Thu, 23 Oct 2025 13:47:55 +0800 Subject: [PATCH 05/16] Harden BLS12-381 multi exponentiation --- docs/native-contracts-api.md | 2 +- .../Native/CryptoLib.BLS12_381.cs | 24 +++++++++++++++++++ .../SmartContract/Native/UT_CryptoLib.cs | 17 +++++++++++++ .../SmartContract/Native/UT_NativeContract.cs | 8 +++++-- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/docs/native-contracts-api.md b/docs/native-contracts-api.md index ed4d683e6c..38e7fc436e 100644 --- a/docs/native-contracts-api.md +++ b/docs/native-contracts-api.md @@ -78,7 +78,7 @@ When calling a native contract method by transaction script, there are several t | bls12381Equal | Determines whether the specified points are equal. | InteropInterface(*x*), InteropInterface(*y*) | Boolean | 1<<5 | 0 | -- | -- | | bls12381Add | Add operation of two points. | InteropInterface(*x*), InteropInterface(*y*) | InteropInterface | 1<<19 | 0 | -- | -- | | bls12381Mul | Mul operation of gt point and multiplier | InteropInterface(*x*), Byte[](*mul*), Boolean(*neg*) | InteropInterface | 1<<21 | 0 | -- | -- | -| bls12381MultiExp | Multi exponentiation operation for bls12381 points. | Array(*pairs*) | InteropInterface | 1<<23 | 0 | -- | HF_Gorgon | +| 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 2954426723..299a4dd3a6 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs @@ -19,6 +19,8 @@ namespace Neo.SmartContract.Native { partial class CryptoLib { + private const int Bls12381MultiExpMaxPairs = 128; + /// /// Serialize a bls12381 point. /// @@ -131,6 +133,8 @@ 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; @@ -148,6 +152,7 @@ public static InteropInterface Bls12381MultiExp(Array pairs) switch (point) { case G1Affine g1Affine: + EnsureG1PointValid(in g1Affine); EnsureGroupType(ref useG2, false); { var scalar = ParseScalar(pair[1]); @@ -156,6 +161,7 @@ public static InteropInterface Bls12381MultiExp(Array pairs) } break; case G1Projective g1Projective: + EnsureG1PointValid(new G1Affine(g1Projective)); EnsureGroupType(ref useG2, false); { var scalar = ParseScalar(pair[1]); @@ -164,6 +170,7 @@ public static InteropInterface Bls12381MultiExp(Array pairs) } break; case G2Affine g2Affine: + EnsureG2PointValid(in g2Affine); EnsureGroupType(ref useG2, true); { var scalar = ParseScalar(pair[1]); @@ -172,6 +179,7 @@ public static InteropInterface Bls12381MultiExp(Array pairs) } break; case G2Projective g2Projective: + EnsureG2PointValid(new G2Affine(g2Projective)); EnsureGroupType(ref useG2, true); { var scalar = ParseScalar(pair[1]); @@ -251,5 +259,21 @@ ByteString bs when bs.GetSpan().Length == Scalar.Size => bs.GetSpan(), 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 d883b170a8..f143452ec8 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -362,6 +362,23 @@ public void TestBls12381MultiExpEmptyFails() 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() { diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs index 3d4313b442..3d56e505df 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs @@ -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)) From dc23a56b6f8f84d79b10101dec7aa0c4da4d8a73 Mon Sep 17 00:00:00 2001 From: jimmy Date: Tue, 4 Nov 2025 22:06:06 +0800 Subject: [PATCH 06/16] Add subgroup validation tests for BLS multi exp --- .../SmartContract/Native/UT_CryptoLib.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs index f143452ec8..0d334c333c 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -379,6 +379,36 @@ public void TestBls12381MultiExpTooManyPairsFails() 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 Bls12381Equal() { From 5b4038564c4a1d0215ebacc7616c84f242165087 Mon Sep 17 00:00:00 2001 From: jimmy Date: Thu, 6 Nov 2025 21:37:41 +0800 Subject: [PATCH 07/16] Fix BLS12-381 multiexp endianness and add Ethereum tests --- .../Native/CryptoLib.BLS12_381.cs | 10 +- .../SmartContract/Native/UT_CryptoLib.cs | 142 +++++++++++++++++- 2 files changed, 142 insertions(+), 10 deletions(-) diff --git a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs index 299a4dd3a6..da805a3900 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs @@ -238,7 +238,7 @@ private static void EnsureGroupType(ref bool? current, bool isG2) private static Scalar ParseScalar(StackItem scalarItem) { - ReadOnlySpan data = scalarItem switch + ReadOnlySpan bigEndian = scalarItem switch { ByteString bs when bs.GetSpan().Length == Scalar.Size => bs.GetSpan(), VMBuffer buffer when buffer.Size == Scalar.Size => buffer.InnerBuffer.Span, @@ -246,7 +246,11 @@ ByteString bs when bs.GetSpan().Length == Scalar.Size => bs.GetSpan(), }; Span littleEndian = stackalloc byte[Scalar.Size]; - data.CopyTo(littleEndian); + 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 { @@ -254,8 +258,6 @@ ByteString bs when bs.GetSpan().Length == Scalar.Size => bs.GetSpan(), } catch (FormatException) { - var wide = new byte[Scalar.Size * 2]; - littleEndian.CopyTo(wide); return Scalar.FromBytesWide(wide); } } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs index 0d334c333c..5d9d467883 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -60,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" @@ -323,8 +341,11 @@ public void TestBls12381MultiExpReducesScalar() }); var pairs = new VMArray(new StackItem[] { pair }); - var wide = new byte[Scalar.Size * 2]; - System.Array.Copy(scalarBytes, wide, scalarBytes.Length); + 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); @@ -409,6 +430,35 @@ public void TestBls12381MultiExpRejectsNonOnCurveG2() 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 Bls12381Equal() { @@ -1321,22 +1371,102 @@ private bool CallVerifyWithEd25519(byte[] message, byte[] publicKey, byte[] sign } } + 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)); - var bytes = new byte[Scalar.Size]; + Span temp = stackalloc byte[Scalar.Size]; var mask = (BigInteger.One << (Scalar.Size * 8)) - BigInteger.One; var truncated = value & mask; - if (!truncated.TryWriteBytes(bytes, out _, isBigEndian: false)) + var encoded = truncated.ToByteArray(isUnsigned: true, isBigEndian: true); + if (encoded.Length > Scalar.Size) throw new InvalidOperationException("Unable to encode scalar value."); - return bytes; + 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) => Scalar.FromBytes(CreateScalarBytes(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); + } } } From 15cb14b0988c74773d8f77941a8d49b015ea79d3 Mon Sep 17 00:00:00 2001 From: jimmy Date: Thu, 6 Nov 2025 21:41:46 +0800 Subject: [PATCH 08/16] Document bls12381MultiExp scalar encoding --- docs/native-contracts-api.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/native-contracts-api.md b/docs/native-contracts-api.md index 38e7fc436e..ee3a55df48 100644 --- a/docs/native-contracts-api.md +++ b/docs/native-contracts-api.md @@ -78,7 +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 | +| bls12381MultiExp | Multi exponentiation operation for bls12381 points. | Array(*pairs*) (each pair is [InteropInterface(*point*), Byte[](*scalar*, 32-byte big-endian)]) | 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 | -- | -- | @@ -203,4 +203,3 @@ When calling a native contract method by transaction script, there are several t | getMaxNotValidBeforeDelta | GetMaxNotValidBeforeDelta is Notary contract method and returns the maximum NotValidBefore delta. | -- | UInt32 | 1<<15 | 0 | ReadStates | -- | | setMaxNotValidBeforeDelta | SetMaxNotValidBeforeDelta is Notary contract method and sets the maximum NotValidBefore delta. | UInt32(*value*) | Void | 1<<15 | 0 | States | -- | - From a39ed8eb9a3eb56dc8299ff2b171d656aeede06a Mon Sep 17 00:00:00 2001 From: jimmy Date: Thu, 6 Nov 2025 21:44:46 +0800 Subject: [PATCH 09/16] Revert "Document bls12381MultiExp scalar encoding" This reverts commit 15cb14b0988c74773d8f77941a8d49b015ea79d3. --- docs/native-contracts-api.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/native-contracts-api.md b/docs/native-contracts-api.md index ee3a55df48..38e7fc436e 100644 --- a/docs/native-contracts-api.md +++ b/docs/native-contracts-api.md @@ -78,7 +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*) (each pair is [InteropInterface(*point*), Byte[](*scalar*, 32-byte big-endian)]) | InteropInterface | 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 | -- | -- | | 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 | -- | -- | @@ -203,3 +203,4 @@ When calling a native contract method by transaction script, there are several t | getMaxNotValidBeforeDelta | GetMaxNotValidBeforeDelta is Notary contract method and returns the maximum NotValidBefore delta. | -- | UInt32 | 1<<15 | 0 | ReadStates | -- | | setMaxNotValidBeforeDelta | SetMaxNotValidBeforeDelta is Notary contract method and sets the maximum NotValidBefore delta. | UInt32(*value*) | Void | 1<<15 | 0 | States | -- | + From 7fa60b79e9513cce0be740248fcd279443437529 Mon Sep 17 00:00:00 2001 From: jimmy Date: Thu, 6 Nov 2025 21:46:23 +0800 Subject: [PATCH 10/16] Validate pairing inputs for BLS12-381 --- src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs index da805a3900..96bc5c33ea 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs @@ -215,12 +215,14 @@ 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)); } From 7d468ce812e1a15a33c18917d251bf5c74eedfe8 Mon Sep 17 00:00:00 2001 From: jimmy Date: Mon, 10 Nov 2025 20:03:43 +0800 Subject: [PATCH 11/16] Add EVM-compatible BLS12 alias entrypoints --- docs/native-contracts-api.md | 5 + .../Native/CryptoLib.BLS12_381.cs | 198 ++++++++++++++++++ .../SmartContract/Native/UT_CryptoLib.cs | 172 +++++++++++++++ .../SmartContract/Native/UT_NativeContract.cs | 2 +- 4 files changed, 376 insertions(+), 1 deletion(-) diff --git a/docs/native-contracts-api.md b/docs/native-contracts-api.md index 38e7fc436e..9eb8eb92d0 100644 --- a/docs/native-contracts-api.md +++ b/docs/native-contracts-api.md @@ -77,9 +77,14 @@ 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 | -- | Byte[](*input*) | Byte[] | 1<<19 | 0 | -- | HF_Faun | +| bls12_g2add | -- | 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 | -- | Byte[](*input*) | Byte[] | 1<<21 | 0 | -- | HF_Faun | +| bls12_g2mul | -- | Byte[](*input*) | Byte[] | 1<<21 | 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 | -- | Byte[](*input*) | Byte[] | 1<<23 | 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 | -- | -- | diff --git a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs index 96bc5c33ea..a2969c3eed 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs @@ -20,6 +20,11 @@ namespace Neo.SmartContract.Native partial class CryptoLib { private const int Bls12381MultiExpMaxPairs = 128; + 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. @@ -101,6 +106,32 @@ public static InteropInterface Bls12381Add(InteropInterface x, InteropInterface }; } + [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); + } + + [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 /// @@ -123,6 +154,32 @@ public static InteropInterface Bls12381Mul(InteropInterface x, byte[] mul, bool }; } + [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); + } + + [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); + } + /// /// Multi exponentiation operation for bls12381 points. /// @@ -226,6 +283,28 @@ public static InteropInterface Bls12381Pairing(InteropInterface g1, InteropInter return new(Bls12.Pairing(in g1a, in g2a)); } + [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)); + + if (input.Length == 0) + return EncodePairingResult(true); + + 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); + } + private static void EnsureGroupType(ref bool? current, bool isG2) { if (current is null) @@ -279,5 +358,124 @@ private static void EnsureG2PointValid(in G2Affine point) 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 5d9d467883..7ca22a81d2 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -179,6 +179,133 @@ 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 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() + { + byte[] result = CryptoLib.Bls12Pairing(System.Array.Empty()); + Assert.AreEqual(1, result[^1]); + Assert.IsTrue(result.Take(result.Length - 1).All(b => b == 0)); + } [TestMethod] public void TestBls12381Mul() { @@ -1468,5 +1595,50 @@ private static Scalar CreateScalar(uint value) return Scalar.FromBytes(littleEndian); } + 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 9e8cf5da71..75e3b687ee 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":"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}}"""}, +{"CryptoLib", """{"id":-3,"updatecounter":0,"hash":"0x726cb6e0cd8628a1350a611384688911ab75f51b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":65467259},"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_g1add","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":49,"safe":true},{"name":"bls12_g1mul","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":56,"safe":true},{"name":"bls12_g2add","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":63,"safe":true},{"name":"bls12_g2mul","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":70,"safe":true},{"name":"bls12_pairing","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":77,"safe":true},{"name":"keccak256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":84,"safe":true},{"name":"murmur32","parameters":[{"name":"data","type":"ByteArray"},{"name":"seed","type":"Integer"}],"returntype":"ByteArray","offset":91,"safe":true},{"name":"recoverSecp256K1","parameters":[{"name":"messageHash","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"ByteArray","offset":98,"safe":true},{"name":"ripemd160","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":105,"safe":true},{"name":"sha256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":112,"safe":true},{"name":"verifyWithECDsa","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"},{"name":"curveHash","type":"Integer"}],"returntype":"Boolean","offset":119,"safe":true},{"name":"verifyWithEd25519","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":126,"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}}"""}, From df66f458bd97b58f89bf5e1574283c02e0af3522 Mon Sep 17 00:00:00 2001 From: jimmy Date: Thu, 20 Nov 2025 18:28:05 +0800 Subject: [PATCH 12/16] Add EIP-2537 serialize helpers and boundary tests --- docs/native-contracts-api.md | 16 +- src/Neo.Cryptography.BLS12_381/G2Affine.cs | 8 + .../Native/CryptoLib.BLS12_381.cs | 112 +++++++- .../SmartContract/Native/UT_CryptoLib.cs | 257 +++++++++++++++++- .../SmartContract/Native/UT_NativeContract.cs | 21 +- 5 files changed, 403 insertions(+), 11 deletions(-) diff --git a/docs/native-contracts-api.md b/docs/native-contracts-api.md index 9eb8eb92d0..aeb6cc8992 100644 --- a/docs/native-contracts-api.md +++ b/docs/native-contracts-api.md @@ -77,14 +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 | -- | Byte[](*input*) | Byte[] | 1<<19 | 0 | -- | HF_Faun | -| bls12_g2add | -- | Byte[](*input*) | Byte[] | 1<<19 | 0 | -- | HF_Faun | +| 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 | -- | Byte[](*input*) | Byte[] | 1<<21 | 0 | -- | HF_Faun | -| bls12_g2mul | -- | Byte[](*input*) | Byte[] | 1<<21 | 0 | -- | HF_Faun | +| 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 | -- | Byte[](*input*) | Byte[] | 1<<23 | 0 | -- | HF_Faun | +| 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 | -- | -- | @@ -94,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 a2969c3eed..7249110e2c 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs @@ -20,6 +20,7 @@ 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; @@ -106,6 +107,10 @@ 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) { @@ -119,6 +124,10 @@ public static byte[] Bls12G1Add(byte[] input) 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) { @@ -154,6 +163,10 @@ 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) { @@ -167,6 +180,10 @@ public static byte[] Bls12G1Mul(byte[] input) 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) { @@ -180,6 +197,60 @@ public static byte[] Bls12G2Mul(byte[] input) 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. /// @@ -283,6 +354,9 @@ public static InteropInterface Bls12381Pairing(InteropInterface g1, InteropInter 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) { @@ -290,8 +364,11 @@ public static byte[] Bls12Pairing(byte[] input) if (input.Length % Bls12PairInputLength != 0) throw new ArgumentException("Invalid BLS12-381 pairing input length", nameof(input)); - if (input.Length == 0) - return EncodePairingResult(true); + 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; @@ -305,6 +382,37 @@ public static byte[] Bls12Pairing(byte[] input) 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) diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs index 7ca22a81d2..55f614f761 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -241,6 +241,67 @@ public void TestBls12G2MulAlias() 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); + + 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 TestBls12PairingAliasSinglePair() { @@ -302,9 +363,29 @@ public void TestBls12PairingAliasInvalidPoint() [TestMethod] public void TestBls12PairingAliasEmptyInput() { - byte[] result = CryptoLib.Bls12Pairing(System.Array.Empty()); - Assert.AreEqual(1, result[^1]); - Assert.IsTrue(result.Take(result.Length - 1).All(b => b == 0)); + 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() @@ -586,6 +667,175 @@ public void TestBls12381MultiExpMatchesEthereumG2Vectors() Assert.AreEqual(expected, actual); } + [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); + } + [TestMethod] public void Bls12381Equal() { @@ -1595,6 +1845,7 @@ private static Scalar CreateScalar(uint 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; diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs index 75e3b687ee..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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":65467259},"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_g1add","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":49,"safe":true},{"name":"bls12_g1mul","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":56,"safe":true},{"name":"bls12_g2add","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":63,"safe":true},{"name":"bls12_g2mul","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":70,"safe":true},{"name":"bls12_pairing","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":77,"safe":true},{"name":"keccak256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":84,"safe":true},{"name":"murmur32","parameters":[{"name":"data","type":"ByteArray"},{"name":"seed","type":"Integer"}],"returntype":"ByteArray","offset":91,"safe":true},{"name":"recoverSecp256K1","parameters":[{"name":"messageHash","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"ByteArray","offset":98,"safe":true},{"name":"ripemd160","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":105,"safe":true},{"name":"sha256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":112,"safe":true},{"name":"verifyWithECDsa","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"},{"name":"curveHash","type":"Integer"}],"returntype":"Boolean","offset":119,"safe":true},{"name":"verifyWithEd25519","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":126,"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(); } } From 93713e82c8a50f6ac15755453e8cfd023e72d11c Mon Sep 17 00:00:00 2001 From: jimmy Date: Thu, 20 Nov 2025 18:35:41 +0800 Subject: [PATCH 13/16] Add edge-case coverage for eth BLS serialize/deserialize --- .../SmartContract/Native/UT_CryptoLib.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs index 55f614f761..13c85f6178 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -302,6 +302,40 @@ public void TestBls12SerializeDeserializeG2() 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() { From 8dedeedf8a6e2002657d6544cedd1132573e0677 Mon Sep 17 00:00:00 2001 From: jimmy Date: Thu, 20 Nov 2025 18:38:03 +0800 Subject: [PATCH 14/16] Tweak assertion message --- tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs index 13c85f6178..dc88a588d1 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -283,7 +283,7 @@ public void TestBls12SerializeDeserializeG1() { var g1Point = G1Affine.FromCompressed(g1); var encoded = CryptoLib.Bls12Serialize(new InteropInterface(g1Point)); - Assert.AreEqual(Bls12G1EncodedLength, encoded.Length); + Assert.AreEqual(Bls12G1EncodedLength, encoded.Length, "G1 serialization length must be 128 bytes"); var interop = CryptoLib.Bls12Deserialize(encoded); var roundtrip = interop.GetInterface().ToCompressed().ToHexString(); From f2c75e608284ebfd0fcb57ec168a5a6aaee97bca Mon Sep 17 00:00:00 2001 From: Shargon Date: Thu, 27 Nov 2025 05:52:53 -0800 Subject: [PATCH 15/16] Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs Co-authored-by: Owen <38493437+superboyiii@users.noreply.github.com> --- .../SmartContract/Native/UT_CryptoLib.cs | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs index dc88a588d1..37a760c319 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -701,6 +701,192 @@ public void TestBls12381MultiExpMatchesEthereumG2Vectors() 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() { From 421bc93c3415c0b05f0af05924577d75976d2192 Mon Sep 17 00:00:00 2001 From: Shargon Date: Thu, 27 Nov 2025 05:53:54 -0800 Subject: [PATCH 16/16] Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs Co-authored-by: Owen <38493437+superboyiii@users.noreply.github.com> --- .../SmartContract/Native/UT_CryptoLib.cs | 287 ++++++++++++++++++ 1 file changed, 287 insertions(+) diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs index 37a760c319..4c67e5d21e 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -1056,6 +1056,293 @@ public void TestBls12381MultiExpBoundaryCase8_G1G2MixIdentityAndGenerator() 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() {