From ef117692231f8c6ef06b76784b3aea6947e9d6e9 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 10 May 2024 05:36:06 +0300 Subject: [PATCH] Support Koblitz-based and Keccak256-based custom witness verification (#3209) * Native: extend CryptoLib's verifyWithECDsa with hasher parameter A port of https://github.com/nspcc-dev/neo-go/pull/3425/commits/1e2b438b5580ca9ada7b452130b95a249733d31a. This commit contains minor protocol extension needed for custom Koblitz-based verification scripts (an alternative to https://github.com/neo-project/neo/pull/3205). Replace native CryptoLib's verifyWithECDsa `curve` parameter by `curveHash` parameter which is a enum over supported pairs of named curves and hash functions. NamedCurve enum mark as deprecated and replaced by NamedCurveHash with compatible behaviour. Even though this change is a compatible extension of the protocol, it changes the genesis state due to parameter renaming (CryptoLib's manifest is changed). But we're going to resync chain in 3.7 release anyway, so it's not a big deal. Also, we need to check mainnet and testnet compatibility in case if anyone has ever called verifyWithECDsa with 24 or 25 `curve` value. Signed-off-by: Anna Shaleva * SmartContract: add extension to ScriptBuilder for System.Contract.Call Group the set of common operations required to emit System.Contract.Call appcall. Signed-off-by: Anna Shaleva * Native: add an example of custom Koblitz signature verification Koblitz-based and Keccak-based transaction witness verification for single signature and multisignature ported from https://github.com/nspcc-dev/neo-go/pull/3425. An alternative to https://github.com/neo-project/neo/pull/3205. Signed-off-by: Anna Shaleva * SmartContract: make multisig koblitz easier to parse 1. Make prologue be exactly the same as regular CheckMultisig. 2. But instead of "SYSCALL System.Crypto.CheckMultisig" do INITSLOT and K check. 3. This makes all of the code from INITSLOT below be independent of N/M, so one can parse the script beginning in the same way CheckMultisig is parsed and then just compare the rest of it with some known-good blob. 4. The script becomes a tiny bit larger now, but properties above are too good. Ported from https://github.com/nspcc-dev/neo-go/pull/3425/commits/34ee29408663c51aefefd258b8ab44207c915868. Signed-off-by: Anna Shaleva * SmartContract: use ABORT in Koblitz multisig Make the script a bit shorter. ABORTMSG would cost a bit more. Ported from https://github.com/nspcc-dev/neo-go/pull/3425/commits/fb16891e2ae84f601624ec1797a5f85f8d05cb47. Ref. https://github.com/nspcc-dev/neo-go/pull/3425#discussion_r1588972899. Signed-off-by: Anna Shaleva * SmartContract: reduce callflag scope for Koblitz verification scripts All flag is too wide. A port of https://github.com/nspcc-dev/neo-go/pull/3425/commits/fe292f3f39d2e388be0c408fbc22107e9ce09515. Ref. https://github.com/nspcc-dev/neo-go/pull/3425#discussion_r1589240074. Signed-off-by: Anna Shaleva * Native: add tests for CryptoLib's verifyWithECDsa No functional changes, just add more unit-tests. Signed-off-by: Anna Shaleva * Native: update NamedCurveHash values for Keccak256 hasher Use 122 and 123 respectively for secp256k1Keccak256 and secp256r1Keccak256. Signed-off-by: Anna Shaleva * SmartContract: move EmitAppCallNoArgs to the testing code We're not going to implement custom Koblitz witness generation at the core, and thus, the only user of this API is testing code. Signed-off-by: Anna Shaleva * Apply suggestions from code review clean ut lines * fix names * Cryptography: cache ECDomainParameters for Secp256r1 and Secp256k1 Signed-off-by: Anna Shaleva * Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs * Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs * Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs * Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs * Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs * Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs * Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs * Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs * Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs * Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs * Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs * Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs * Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs * Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs * Update tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs --------- Signed-off-by: Anna Shaleva Co-authored-by: Shargon Co-authored-by: Jimmy --- src/Neo/Cryptography/Crypto.cs | 71 ++- src/Neo/Cryptography/Hasher.cs | 29 ++ src/Neo/Cryptography/Helper.cs | 35 ++ src/Neo/SmartContract/Native/CryptoLib.cs | 22 +- src/Neo/SmartContract/Native/NamedCurve.cs | 8 +- .../SmartContract/Native/NamedCurveHash.cs | 40 ++ .../SmartContract/Native/UT_CryptoLib.cs | 478 ++++++++++++++++++ .../SmartContract/Native/UT_NativeContract.cs | 2 +- 8 files changed, 655 insertions(+), 30 deletions(-) create mode 100644 src/Neo/Cryptography/Hasher.cs create mode 100644 src/Neo/SmartContract/Native/NamedCurveHash.cs diff --git a/src/Neo/Cryptography/Crypto.cs b/src/Neo/Cryptography/Crypto.cs index e419d268a2..85ed0411a8 100644 --- a/src/Neo/Cryptography/Crypto.cs +++ b/src/Neo/Cryptography/Crypto.cs @@ -28,6 +28,17 @@ public static class Crypto private static readonly bool IsOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); private static readonly ECCurve secP256k1 = ECCurve.CreateFromFriendlyName("secP256k1"); private static readonly X9ECParameters bouncySecp256k1 = Org.BouncyCastle.Asn1.Sec.SecNamedCurves.GetByName("secp256k1"); + private static readonly X9ECParameters bouncySecp256r1 = Org.BouncyCastle.Asn1.Sec.SecNamedCurves.GetByName("secp256r1"); + + /// + /// Holds domain parameters for Secp256r1 elliptic curve. + /// + private static readonly ECDomainParameters secp256r1DomainParams = new ECDomainParameters(bouncySecp256r1.Curve, bouncySecp256r1.G, bouncySecp256r1.N, bouncySecp256r1.H); + + /// + /// Holds domain parameters for Secp256k1 elliptic curve. + /// + private static readonly ECDomainParameters secp256k1DomainParams = new ECDomainParameters(bouncySecp256k1.Curve, bouncySecp256k1.G, bouncySecp256k1.N, bouncySecp256k1.H); /// /// Calculates the 160-bit hash value of the specified message. @@ -50,22 +61,30 @@ public static byte[] Hash256(ReadOnlySpan message) } /// - /// Signs the specified message using the ECDSA algorithm. + /// Signs the specified message using the ECDSA algorithm and specified hash algorithm. /// /// The message to be signed. /// The private key to be used. /// The curve of the signature, default is . + /// The hash algorithm to hash the message, default is SHA256. /// The ECDSA signature for the specified message. - public static byte[] Sign(byte[] message, byte[] priKey, ECC.ECCurve ecCurve = null) + public static byte[] Sign(byte[] message, byte[] priKey, ECC.ECCurve ecCurve = null, Hasher hasher = Hasher.SHA256) { - if (IsOSX && ecCurve == ECC.ECCurve.Secp256k1) + if (hasher == Hasher.Keccak256 || (IsOSX && ecCurve == ECC.ECCurve.Secp256k1)) { - var domain = new ECDomainParameters(bouncySecp256k1.Curve, bouncySecp256k1.G, bouncySecp256k1.N, bouncySecp256k1.H); + var domain = + ecCurve == null || ecCurve == ECC.ECCurve.Secp256r1 ? secp256r1DomainParams : + ecCurve == ECC.ECCurve.Secp256k1 ? secp256k1DomainParams : + throw new NotSupportedException(nameof(ecCurve)); var signer = new Org.BouncyCastle.Crypto.Signers.ECDsaSigner(); var privateKey = new BigInteger(1, priKey); var priKeyParameters = new ECPrivateKeyParameters(privateKey, domain); signer.Init(true, priKeyParameters); - var signature = signer.GenerateSignature(message.Sha256()); + var messageHash = + hasher == Hasher.SHA256 ? message.Sha256() : + hasher == Hasher.Keccak256 ? message.Keccak256() : + throw new NotSupportedException(nameof(hasher)); + var signature = signer.GenerateSignature(messageHash); var signatureBytes = new byte[64]; var rBytes = signature[0].ToByteArrayUnsigned(); @@ -87,24 +106,35 @@ public static byte[] Sign(byte[] message, byte[] priKey, ECC.ECCurve ecCurve = n Curve = curve, D = priKey, }); - return ecdsa.SignData(message, HashAlgorithmName.SHA256); + var hashAlg = + hasher == Hasher.SHA256 ? HashAlgorithmName.SHA256 : + throw new NotSupportedException(nameof(hasher)); + return ecdsa.SignData(message, hashAlg); } /// - /// Verifies that a digital signature is appropriate for the provided key and message. + /// Verifies that a digital signature is appropriate for the provided key, message and hash algorithm. /// /// The signed message. /// The signature to be verified. /// The public key to be used. + /// The hash algorithm to be used to hash the message, the default is SHA256. /// if the signature is valid; otherwise, . - public static bool VerifySignature(ReadOnlySpan message, ReadOnlySpan signature, ECC.ECPoint pubkey) + public static bool VerifySignature(ReadOnlySpan message, ReadOnlySpan signature, ECC.ECPoint pubkey, Hasher hasher = Hasher.SHA256) { if (signature.Length != 64) return false; - if (IsOSX && pubkey.Curve == ECC.ECCurve.Secp256k1) + if (hasher == Hasher.Keccak256 || (IsOSX && pubkey.Curve == ECC.ECCurve.Secp256k1)) { - var domain = new ECDomainParameters(bouncySecp256k1.Curve, bouncySecp256k1.G, bouncySecp256k1.N, bouncySecp256k1.H); - var point = bouncySecp256k1.Curve.CreatePoint( + var domain = + pubkey.Curve == ECC.ECCurve.Secp256r1 ? secp256r1DomainParams : + pubkey.Curve == ECC.ECCurve.Secp256k1 ? secp256k1DomainParams : + throw new NotSupportedException(nameof(pubkey.Curve)); + var curve = + pubkey.Curve == ECC.ECCurve.Secp256r1 ? bouncySecp256r1.Curve : + bouncySecp256k1.Curve; + + var point = curve.CreatePoint( new BigInteger(pubkey.X.Value.ToString()), new BigInteger(pubkey.Y.Value.ToString())); var pubKey = new ECPublicKeyParameters("ECDSA", point, domain); @@ -115,11 +145,19 @@ public static bool VerifySignature(ReadOnlySpan message, ReadOnlySpan @@ -153,16 +191,17 @@ public static ECDsa CreateECDsa(ECC.ECPoint pubkey) } /// - /// Verifies that a digital signature is appropriate for the provided key and message. + /// Verifies that a digital signature is appropriate for the provided key, curve, message and hasher. /// /// The signed message. /// The signature to be verified. /// The public key to be used. /// The curve to be used by the ECDSA algorithm. + /// The hash algorithm to be used hash the message, the default is SHA256. /// if the signature is valid; otherwise, . - public static bool VerifySignature(ReadOnlySpan message, ReadOnlySpan signature, ReadOnlySpan pubkey, ECC.ECCurve curve) + public static bool VerifySignature(ReadOnlySpan message, ReadOnlySpan signature, ReadOnlySpan pubkey, ECC.ECCurve curve, Hasher hasher = Hasher.SHA256) { - return VerifySignature(message, signature, ECC.ECPoint.DecodePoint(pubkey, curve)); + return VerifySignature(message, signature, ECC.ECPoint.DecodePoint(pubkey, curve), hasher); } } } diff --git a/src/Neo/Cryptography/Hasher.cs b/src/Neo/Cryptography/Hasher.cs new file mode 100644 index 0000000000..21c5fef6a8 --- /dev/null +++ b/src/Neo/Cryptography/Hasher.cs @@ -0,0 +1,29 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Hasher.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.Cryptography +{ + /// + /// Represents hash function identifiers supported by ECDSA message signature and verification. + /// + public enum Hasher : byte + { + /// + /// The SHA256 hash algorithm. + /// + SHA256 = 0x00, + + /// + /// The Keccak256 hash algorithm. + /// + Keccak256 = 0x01, + } +} diff --git a/src/Neo/Cryptography/Helper.cs b/src/Neo/Cryptography/Helper.cs index a087620034..f13fa08118 100644 --- a/src/Neo/Cryptography/Helper.cs +++ b/src/Neo/Cryptography/Helper.cs @@ -12,6 +12,7 @@ using Neo.IO; using Neo.Network.P2P.Payloads; using Neo.Wallets; +using Org.BouncyCastle.Crypto.Digests; using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Modes; using Org.BouncyCastle.Crypto.Parameters; @@ -153,6 +154,40 @@ public static byte[] Sha256(this Span value) return Sha256((ReadOnlySpan)value); } + /// + /// Computes the hash value for the specified byte array using the keccak256 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + public static byte[] Keccak256(this byte[] value) + { + KeccakDigest keccak = new(256); + keccak.BlockUpdate(value, 0, value.Length); + byte[] result = new byte[keccak.GetDigestSize()]; + keccak.DoFinal(result, 0); + return result; + } + + /// + /// Computes the hash value for the specified byte array using the keccak256 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + public static byte[] Keccak256(this ReadOnlySpan value) + { + return Keccak256(value.ToArray()); + } + + /// + /// Computes the hash value for the specified byte array using the keccak256 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + public static byte[] Keccak256(this Span value) + { + return Keccak256(value.ToArray()); + } + public static byte[] AES256Encrypt(this byte[] plainData, byte[] key, byte[] nonce, byte[] associatedData = null) { if (nonce.Length != 12) throw new ArgumentOutOfRangeException(nameof(nonce)); diff --git a/src/Neo/SmartContract/Native/CryptoLib.cs b/src/Neo/SmartContract/Native/CryptoLib.cs index a7b822e39c..6e2876a860 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.cs @@ -11,7 +11,6 @@ using Neo.Cryptography; using Neo.Cryptography.ECC; -using Org.BouncyCastle.Crypto.Digests; using System; using System.Collections.Generic; @@ -22,10 +21,12 @@ namespace Neo.SmartContract.Native /// public sealed partial class CryptoLib : NativeContract { - private static readonly Dictionary curves = new() + private static readonly Dictionary s_curves = new() { - [NamedCurve.secp256k1] = ECCurve.Secp256k1, - [NamedCurve.secp256r1] = ECCurve.Secp256r1 + [NamedCurveHash.secp256k1SHA256] = (ECCurve.Secp256k1, Hasher.SHA256), + [NamedCurveHash.secp256r1SHA256] = (ECCurve.Secp256r1, Hasher.SHA256), + [NamedCurveHash.secp256k1Keccak256] = (ECCurve.Secp256k1, Hasher.Keccak256), + [NamedCurveHash.secp256r1Keccak256] = (ECCurve.Secp256r1, Hasher.Keccak256), }; internal CryptoLib() : base() { } @@ -73,11 +74,7 @@ public static byte[] Murmur32(byte[] data, uint seed) [ContractMethod(Hardfork.HF_Cockatrice, CpuFee = 1 << 15)] public static byte[] Keccak256(byte[] data) { - KeccakDigest keccak = new(256); - keccak.BlockUpdate(data, 0, data.Length); - byte[] result = new byte[keccak.GetDigestSize()]; - keccak.DoFinal(result, 0); - return result; + return data.Keccak256(); } /// @@ -86,14 +83,15 @@ public static byte[] Keccak256(byte[] data) /// The signed message. /// The public key to be used. /// The signature to be verified. - /// The curve to be used by the ECDSA algorithm. + /// A pair of the curve to be used by the ECDSA algorithm and the hasher function to be used to hash message. /// if the signature is valid; otherwise, . [ContractMethod(CpuFee = 1 << 15)] - public static bool VerifyWithECDsa(byte[] message, byte[] pubkey, byte[] signature, NamedCurve curve) + public static bool VerifyWithECDsa(byte[] message, byte[] pubkey, byte[] signature, NamedCurveHash curveHash) { try { - return Crypto.VerifySignature(message, signature, pubkey, curves[curve]); + var ch = s_curves[curveHash]; + return Crypto.VerifySignature(message, signature, pubkey, ch.Curve, ch.Hasher); } catch (ArgumentException) { diff --git a/src/Neo/SmartContract/Native/NamedCurve.cs b/src/Neo/SmartContract/Native/NamedCurve.cs index 9e97472cfc..8c7a9107e9 100644 --- a/src/Neo/SmartContract/Native/NamedCurve.cs +++ b/src/Neo/SmartContract/Native/NamedCurve.cs @@ -9,24 +9,30 @@ // Redistribution and use in source and binary forms with or without // modifications are permitted. +using System; + namespace Neo.SmartContract.Native { /// - /// Represents the named curve used in ECDSA. + /// Represents the named curve used in ECDSA. This enum is obsolete + /// and will be removed in future versions. Please, use an extended instead. /// /// /// https://tools.ietf.org/html/rfc4492#section-5.1.1 /// + [Obsolete("NamedCurve enum is obsolete and will be removed in future versions. Please, use an extended NamedCurveHash instead.")] public enum NamedCurve : byte { /// /// The secp256k1 curve. /// + [Obsolete("secp256k1 value is obsolete and will be removed in future versions. Please, use NamedCurveHash.secp256k1SHA256 for compatible behaviour.")] secp256k1 = 22, /// /// The secp256r1 curve, which known as prime256v1 or nistP-256. /// + [Obsolete("secp256r1 value is obsolete and will be removed in future versions. Please, use NamedCurveHash.secp256r1SHA256 for compatible behaviour.")] secp256r1 = 23 } } diff --git a/src/Neo/SmartContract/Native/NamedCurveHash.cs b/src/Neo/SmartContract/Native/NamedCurveHash.cs new file mode 100644 index 0000000000..8bd63bf874 --- /dev/null +++ b/src/Neo/SmartContract/Native/NamedCurveHash.cs @@ -0,0 +1,40 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// NamedCurveHash.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.SmartContract.Native +{ + /// + /// Represents a pair of the named curve used in ECDSA and a hash algorithm used to hash message. + /// This is a compatible extension of an obsolete enum. + /// + public enum NamedCurveHash : byte + { + /// + /// The secp256k1 curve and SHA256 hash algorithm. + /// + secp256k1SHA256 = 22, + + /// + /// The secp256r1 curve, which known as prime256v1 or nistP-256, and SHA256 hash algorithm. + /// + secp256r1SHA256 = 23, + + /// + /// The secp256k1 curve and Keccak256 hash algorithm. + /// + secp256k1Keccak256 = 122, + + /// + /// The secp256r1 curve, which known as prime256v1 or nistP-256, and Keccak256 hash algorithm. + /// + secp256r1Keccak256 = 123 + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs index 6f150cca12..f65e061093 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -11,11 +11,20 @@ using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Cryptography; using Neo.Cryptography.BLS12_381; +using Neo.Cryptography.ECC; +using Neo.IO; +using Neo.Ledger; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; using Neo.SmartContract; using Neo.SmartContract.Native; using Neo.VM; using Org.BouncyCastle.Utilities.Encoders; +using System; +using System.Collections.Generic; +using System.Linq; namespace Neo.UnitTests.SmartContract.Native { @@ -427,5 +436,474 @@ public void TestKeccak256_BlankString() // Assert Assert.AreEqual(expectedHashHex, outputHashHex, "Keccak256 hash did not match expected value for blank string."); } + + // TestVerifyWithECDsa_CustomTxWitness_SingleSig builds custom witness verification script for single Koblitz public key + // and ensures witness check is passed for the following message: + // + // keccak256([4-bytes-network-magic-LE, txHash-bytes-BE]) + // + // The proposed witness verification script has 110 bytes length, verification costs 2154270 * 10e-8GAS including Invocation script execution. + // The user has to sign the keccak256([4-bytes-network-magic-LE, txHash-bytes-BE]). + [TestMethod] + public void TestVerifyWithECDsa_CustomTxWitness_SingleSig() + { + byte[] privkey = "7177f0d04c79fa0b8c91fe90c1cf1d44772d1fba6e5eb9b281a22cd3aafb51fe".HexToBytes(); + ECPoint pubKey = ECPoint.Parse("04fd0a8c1ce5ae5570fdd46e7599c16b175bf0ebdfe9c178f1ab848fb16dac74a5d301b0534c7bcf1b3760881f0c420d17084907edd771e1c9c8e941bbf6ff9108", ECCurve.Secp256k1); + + // vrf is a builder of witness verification script corresponding to the public key. + using ScriptBuilder vrf = new(); + vrf.EmitPush((byte)NamedCurveHash.secp256k1Keccak256); // push Koblitz curve identifier and Keccak256 hasher. + vrf.Emit(OpCode.SWAP); // swap curve identifier with the signature. + vrf.EmitPush(pubKey.EncodePoint(true)); // emit the caller's public key. + + // Construct and push the signed message. The signed message is effectively the network-dependent transaction hash, + // i.e. msg = [4-network-magic-bytes-LE, tx-hash-BE] + // Firstly, retrieve network magic (it's uint32 wrapped into BigInteger and represented as Integer stackitem on stack). + vrf.EmitSysCall(ApplicationEngine.System_Runtime_GetNetwork); // push network magic (Integer stackitem), can have 0-5 bytes length serialized. + + // Convert network magic to 4-bytes-length LE byte array representation. + vrf.EmitPush(0x100000000); // push 0x100000000. + vrf.Emit(OpCode.ADD, // the result is some new number that is 5 bytes at least when serialized, but first 4 bytes are intact network value (LE). + OpCode.PUSH4, OpCode.LEFT); // cut the first 4 bytes out of a number that is at least 5 bytes long, the result is 4-bytes-length LE network representation. + + // Retrieve executing transaction hash. + vrf.EmitSysCall(ApplicationEngine.System_Runtime_GetScriptContainer); // push the script container (executing transaction, actually). + vrf.Emit(OpCode.PUSH0, OpCode.PICKITEM); // pick 0-th transaction item (the transaction hash). + + // Concatenate network magic and transaction hash. + vrf.Emit(OpCode.CAT); // this instruction will convert network magic to bytes using BigInteger rules of conversion. + + // Continue construction of 'verifyWithECDsa' call. + vrf.Emit(OpCode.PUSH4, OpCode.PACK); // pack arguments for 'verifyWithECDsa' call. + EmitAppCallNoArgs(vrf, CryptoLib.CryptoLib.Hash, "verifyWithECDsa", CallFlags.None); // emit the call to 'verifyWithECDsa' itself. + + // Account is a hash of verification script. + var vrfScript = vrf.ToArray(); + var acc = vrfScript.ToScriptHash(); + + var tx = new Transaction + { + Attributes = [], + NetworkFee = 1_0000_0000, + Nonce = (uint)Environment.TickCount, + Script = new byte[Transaction.MaxTransactionSize / 100], + Signers = [new Signer { Account = acc }], + SystemFee = 0, + ValidUntilBlock = 10, + Version = 0, + Witnesses = [] + }; + var tx_signature = Crypto.Sign(tx.GetSignData(TestBlockchain.TheNeoSystem.Settings.Network), privkey, ECCurve.Secp256k1, Hasher.Keccak256); + + // inv is a builder of witness invocation script corresponding to the public key. + using ScriptBuilder inv = new(); + inv.EmitPush(tx_signature); // push signature. + + tx.Witnesses = + [ + new Witness { InvocationScript = inv.ToArray(), VerificationScript = vrfScript } + ]; + + tx.VerifyStateIndependent(TestProtocolSettings.Default).Should().Be(VerifyResult.Succeed); + + var snapshot = TestBlockchain.GetTestSnapshot(); + + // Create fake balance to pay the fees. + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestBlockchain.TheNeoSystem.Settings, gas: long.MaxValue); + _ = NativeContract.GAS.Mint(engine, acc, 5_0000_0000, false); + snapshot.Commit(); + + var txVrfContext = new TransactionVerificationContext(); + var conflicts = new List(); + tx.VerifyStateDependent(TestProtocolSettings.Default, snapshot, txVrfContext, conflicts).Should().Be(VerifyResult.Succeed); + + // The resulting witness verification cost is 2154270 * 10e-8GAS. + // The resulting witness Invocation script (66 bytes length): + // NEO-VM > loadbase64 DEARoaaEjM/3VulrBDUod7eiZgWQS2iXIM0+I24iyJYmffhosZoQjfnnRymF/7+FaBPb9qvQwxLLSVo9ROlrdFdC + // READY: loaded 66 instructions + // NEO-VM 0 > ops + // INDEX OPCODE PARAMETER + // 0 PUSHDATA1 11a1a6848ccff756e96b04352877b7a26605904b689720cd3e236e22c896267df868b19a108df9e7472985ffbf856813dbf6abd0c312cb495a3d44e96b745742 << + // + // + // The resulting witness verificaiton script (110 bytes): + // NEO-VM 0 > loadbase64 ABhQDCEC/QqMHOWuVXD91G51mcFrF1vw69/pwXjxq4SPsW2sdKVBxfug4AMAAAAAAQAAAJ4UjUEtUQgwEM6LFMAfDA92ZXJpZnlXaXRoRUNEc2EMFBv1dasRiWiEE2EKNaEohs3gtmxyQWJ9W1I= + // READY: loaded 110 instructions + // NEO-VM 0 > pos + // Error: No help topic for 'pos' + // NEO-VM 0 > ops + // INDEX OPCODE PARAMETER + // 0 PUSHINT8 122 (7a) << + // 2 SWAP + // 3 PUSHDATA1 02fd0a8c1ce5ae5570fdd46e7599c16b175bf0ebdfe9c178f1ab848fb16dac74a5 + // 38 SYSCALL System.Runtime.GetNetwork (c5fba0e0) + // 43 PUSHINT64 4294967296 (0000000001000000) + // 52 ADD + // 53 PUSH4 + // 54 LEFT + // 55 SYSCALL System.Runtime.GetScriptContainer (2d510830) + // 60 PUSH0 + // 61 PICKITEM + // 62 CAT + // 63 PUSH4 + // 64 PACK + // 65 PUSH0 + // 66 PUSHDATA1 766572696679576974684543447361 ("verifyWithECDsa") + // 83 PUSHDATA1 1bf575ab1189688413610a35a12886cde0b66c72 ("NNToUmdQBe5n8o53BTzjTFAnSEcpouyy3B", "0x726cb6e0cd8628a1350a611384688911ab75f51b") + // 105 SYSCALL System.Contract.Call (627d5b52) + } + + // TestVerifyWithECDsa_CustomTxWitness_MultiSig builds custom multisignature witness verification script for Koblitz public keys + // and ensures witness check is passed for the M out of N multisignature of message: + // + // keccak256([4-bytes-network-magic-LE, txHash-bytes-BE]) + // + // The proposed witness verification script has 264 bytes length, verification costs 8390070 * 10e-8GAS including Invocation script execution. + // The users have to sign the keccak256([4-bytes-network-magic-LE, txHash-bytes-BE]). + [TestMethod] + public void TestVerifyWithECDsa_CustomTxWitness_MultiSig() + { + byte[] privkey1 = "b2dde592bfce654ef03f1ceea452d2b0112e90f9f52099bcd86697a2bd0a2b60".HexToBytes(); + ECPoint pubKey1 = ECPoint.Parse("040486468683c112125978ffe876245b2006bfe739aca8539b67335079262cb27ad0dedc9e5583f99b61c6f46bf80b97eaec3654b87add0e5bd7106c69922a229d", ECCurve.Secp256k1); + byte[] privkey2 = "b9879e26941872ee6c9e6f01045681496d8170ed2cc4a54ce617b39ae1891b3a".HexToBytes(); + ECPoint pubKey2 = ECPoint.Parse("040d26fc2ad3b1aae20f040b5f83380670f8ef5c2b2ac921ba3bdd79fd0af0525177715fd4370b1012ddd10579698d186ab342c223da3e884ece9cab9b6638c7bb", ECCurve.Secp256k1); + byte[] privkey3 = "4e1fe2561a6da01ee030589d504d62b23c26bfd56c5e07dfc9b8b74e4602832a".HexToBytes(); + ECPoint pubKey3 = ECPoint.Parse("047b4e72ae854b6a0955b3e02d92651ab7fa641a936066776ad438f95bb674a269a63ff98544691663d91a6cfcd215831f01bfb7a226363a6c5c67ef14541dba07", ECCurve.Secp256k1); + byte[] privkey4 = "6dfd066bb989d3786043aa5c1f0476215d6f5c44f5fc3392dd15e2599b67a728".HexToBytes(); + ECPoint pubKey4 = ECPoint.Parse("04b62ac4c8a352a892feceb18d7e2e3a62c8c1ecbaae5523d89d747b0219276e225be2556a137e0e806e4915762d816cdb43f572730d23bb1b1cba750011c4edc6", ECCurve.Secp256k1); + + // Public keys must be sorted, exactly like for standard CreateMultiSigRedeemScript. + var keys = new List<(byte[], ECPoint)>() + { + (privkey1, pubKey1), + (privkey2, pubKey2), + (privkey3, pubKey3), + (privkey4, pubKey4), + }.OrderBy(k => k.Item2).ToList(); + + // Consider 4 users willing to sign 3/4 multisignature transaction with their Secp256k1 private keys. + var m = 3; + var n = keys.Count(); + + // Must ensure the following conditions are met before verification script construction: + n.Should().BeGreaterThan(0); + m.Should().BeLessThanOrEqualTo(n); + keys.Select(k => k.Item2).Distinct().Count().Should().Be(n); + + // In fact, the following algorithm is implemented via NeoVM instructions: + // + // func Check(sigs []interop.Signature) bool { + // if m != len(sigs) { + // return false + // } + // var pubs []interop.PublicKey = []interop.PublicKey{...} + // msg := append(convert.ToBytes(runtime.GetNetwork()), runtime.GetScriptContainer().Hash...) + // var sigCnt = 0 + // var pubCnt = 0 + // for ; sigCnt < m && pubCnt < n; { // sigs must be sorted by pub + // sigCnt += crypto.VerifyWithECDsa(msg, pubs[pubCnt], sigs[sigCnt], crypto.Secp256k1Keccak256) + // pubCnt++ + // } + // return sigCnt == m + // } + + // vrf is a builder of M out of N multisig witness verification script corresponding to the public keys. + using ScriptBuilder vrf = new(); + + // Start the same way as regular multisig script. + vrf.EmitPush(m); // push m. + foreach (var tuple in keys) + { + vrf.EmitPush(tuple.Item2.EncodePoint(true)); // push public keys in compressed form. + } + vrf.EmitPush(n); // push n. + + // Initialize slots for local variables. Locals slot scheme: + // LOC0 -> sigs + // LOC1 -> pubs + // LOC2 -> msg (ByteString) + // LOC3 -> sigCnt (Integer) + // LOC4 -> pubCnt (Integer) + // LOC5 -> n + // LOC6 -> m + vrf.Emit(OpCode.INITSLOT, new ReadOnlySpan([7, 0])); // 7 locals, no args. + + // Store n. + vrf.Emit(OpCode.STLOC5); + + // Pack public keys and store at LOC1. + vrf.Emit(OpCode.LDLOC5, // load n. + OpCode.PACK, OpCode.STLOC1); // pack pubs and store. + + // Store m. + vrf.Emit(OpCode.STLOC6); + + // Check the number of signatures is m. Abort the execution if not. + vrf.Emit(OpCode.DEPTH); // push the number of signatures onto stack. + vrf.Emit(OpCode.LDLOC6); // load m. + vrf.Emit(OpCode.JMPEQ, new ReadOnlySpan([0])); // here and below short jumps are sufficient. Offset will be filled later. + var sigsLenCheckEndOffset = vrf.Length; + vrf.Emit(OpCode.ABORT); // abort the execution if length of the signatures not equal to m. + + // Start the verification itself. + var checkStartOffset = vrf.Length; + + // Pack signatures and store at LOC0. + vrf.Emit(OpCode.LDLOC6); // load m. + vrf.Emit(OpCode.PACK, OpCode.STLOC0); + + // Get message and store it at LOC2. + // msg = [4-network-magic-bytes-LE, tx-hash-BE] + vrf.EmitSysCall(ApplicationEngine.System_Runtime_GetNetwork); // push network magic (Integer stackitem), can have 0-5 bytes length serialized. + // Convert network magic to 4-bytes-length LE byte array representation. + vrf.EmitPush(0x100000000); // push 0x100000000. + vrf.Emit(OpCode.ADD, // the result is some new number that is 5 bytes at least when serialized, but first 4 bytes are intact network value (LE). + OpCode.PUSH4, OpCode.LEFT); // cut the first 4 bytes out of a number that is at least 5 bytes long, the result is 4-bytes-length LE network representation. + // Retrieve executing transaction hash. + vrf.EmitSysCall(ApplicationEngine.System_Runtime_GetScriptContainer); // push the script container (executing transaction, actually). + vrf.Emit(OpCode.PUSH0, OpCode.PICKITEM); // pick 0-th transaction item (the transaction hash). + // Concatenate network magic and transaction hash. + vrf.Emit(OpCode.CAT); // this instruction will convert network magic to bytes using BigInteger rules of conversion. + vrf.Emit(OpCode.STLOC2); // store msg as a local variable #2. + + // Initialize local variables: sigCnt, pubCnt. + vrf.Emit(OpCode.PUSH0, OpCode.STLOC3, // initialize sigCnt. + OpCode.PUSH0, OpCode.STLOC4); // initialize pubCnt. + + // Loop condition check. + var loopStartOffset = vrf.Length; + vrf.Emit(OpCode.LDLOC3); // load sigCnt. + vrf.Emit(OpCode.LDLOC6); // load m. + vrf.Emit(OpCode.GE, // sigCnt >= m + OpCode.LDLOC4); // load pubCnt + vrf.Emit(OpCode.LDLOC5); // load n. + vrf.Emit(OpCode.GE, // pubCnt >= n + OpCode.OR); // sigCnt >= m || pubCnt >= n + vrf.Emit(OpCode.JMPIF, new ReadOnlySpan([0])); // jump to the end of the script if (sigCnt >= m || pubCnt >= n). + var loopConditionOffset = vrf.Length; + + // Loop start. Prepare arguments and call CryptoLib's verifyWithECDsa. + vrf.EmitPush((byte)NamedCurveHash.secp256k1Keccak256); // push Koblitz curve identifier and Keccak256 hasher. + vrf.Emit(OpCode.LDLOC0, // load signatures. + OpCode.LDLOC3, // load sigCnt. + OpCode.PICKITEM, // pick signature at index sigCnt. + OpCode.LDLOC1, // load pubs. + OpCode.LDLOC4, // load pubCnt. + OpCode.PICKITEM, // pick pub at index pubCnt. + OpCode.LDLOC2, // load msg. + OpCode.PUSH4, OpCode.PACK); // pack 4 arguments for 'verifyWithECDsa' call. + EmitAppCallNoArgs(vrf, CryptoLib.CryptoLib.Hash, "verifyWithECDsa", CallFlags.None); // emit the call to 'verifyWithECDsa' itself. + + // Update loop variables. + vrf.Emit(OpCode.LDLOC3, OpCode.ADD, OpCode.STLOC3, // increment sigCnt if signature is valid. + OpCode.LDLOC4, OpCode.INC, OpCode.STLOC4); // increment pubCnt. + + // End of the loop. + vrf.Emit(OpCode.JMP, new ReadOnlySpan([0])); // jump to the start of cycle. + var loopEndOffset = vrf.Length; + // Return condition: the number of valid signatures should be equal to m. + var progRetOffset = vrf.Length; + vrf.Emit(OpCode.LDLOC3); // load sigCnt. + vrf.Emit(OpCode.LDLOC6); // load m. + vrf.Emit(OpCode.NUMEQUAL); // push m == sigCnt. + + var vrfScript = vrf.ToArray(); + + // Set JMP* instructions offsets. "-1" is for short JMP parameter offset. JMP parameters + // are relative offsets. + vrfScript[sigsLenCheckEndOffset - 1] = (byte)(checkStartOffset - sigsLenCheckEndOffset + 2); + vrfScript[loopEndOffset - 1] = (byte)(loopStartOffset - loopEndOffset + 2); + vrfScript[loopConditionOffset - 1] = (byte)(progRetOffset - loopConditionOffset + 2); + + // Account is a hash of verification script. + var acc = vrfScript.ToScriptHash(); + + var tx = new Transaction + { + Attributes = [], + NetworkFee = 1_0000_0000, + Nonce = (uint)Environment.TickCount, + Script = new byte[Transaction.MaxTransactionSize / 100], + Signers = [new Signer { Account = acc }], + SystemFee = 0, + ValidUntilBlock = 10, + Version = 0, + Witnesses = [] + }; + // inv is a builder of witness invocation script corresponding to the public key. + using ScriptBuilder inv = new(); + for (var i = 0; i < n; i++) + { + if (i == 1) // Skip one key since we need only 3 signatures. + continue; + var sig = Crypto.Sign(tx.GetSignData(TestBlockchain.TheNeoSystem.Settings.Network), keys[i].Item1, ECCurve.Secp256k1, Hasher.Keccak256); + inv.EmitPush(sig); + } + + tx.Witnesses = + [ + new Witness { InvocationScript = inv.ToArray(), VerificationScript = vrfScript } + ]; + + tx.VerifyStateIndependent(TestProtocolSettings.Default).Should().Be(VerifyResult.Succeed); + + var snapshot = TestBlockchain.GetTestSnapshot(); + + // Create fake balance to pay the fees. + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestBlockchain.TheNeoSystem.Settings, gas: long.MaxValue); + _ = NativeContract.GAS.Mint(engine, acc, 5_0000_0000, false); + snapshot.Commit(); + + // Check that witness verification passes. + var txVrfContext = new TransactionVerificationContext(); + var conflicts = new List(); + tx.VerifyStateDependent(TestProtocolSettings.Default, snapshot, txVrfContext, conflicts).Should().Be(VerifyResult.Succeed); + + // The resulting witness verification cost for 3/4 multisig is 8389470 * 10e-8GAS. Cost depends on M/N. + // The resulting witness Invocation script (198 bytes for 3 signatures): + // NEO-VM 0 > loadbase64 DEDM23XByPvDK9XRAHRhfGH7/Mp5jdaci3/GpTZ3D9SZx2Zw89tAaOtmQSIutXbCxRQA1kSeUD4AteJGoNXFhFzIDECgeHoey0rYdlFyTVfDJSsuS+VwzC5OtYGCVR2V/MttmLXWA/FWZH/MjmU0obgQXa9zoBxqYQUUJKefivZFxVcTDEAZT6L6ZFybeXbm8+RlVNS7KshusT54d2ImQ6vFvxETphhJOwcQ0yNL6qJKsrLAKAnzicY4az3ct0G35mI17/gQ + // READY: loaded 198 instructions + // NEO-VM 0 > ops + // INDEX OPCODE PARAMETER + // 0 PUSHDATA1 ccdb75c1c8fbc32bd5d10074617c61fbfcca798dd69c8b7fc6a536770fd499c76670f3db4068eb6641222eb576c2c51400d6449e503e00b5e246a0d5c5845cc8 << + // 66 PUSHDATA1 a0787a1ecb4ad87651724d57c3252b2e4be570cc2e4eb58182551d95fccb6d98b5d603f156647fcc8e6534a1b8105daf73a01c6a61051424a79f8af645c55713 + // 132 PUSHDATA1 194fa2fa645c9b7976e6f3e46554d4bb2ac86eb13e7877622643abc5bf1113a618493b0710d3234beaa24ab2b2c02809f389c6386b3ddcb741b7e66235eff810 + // + // + // Resulting witness verification script (266 bytes for 3/4 multisig): + // NEO-VM 0 > loadbase64 EwwhAwSGRoaDwRISWXj/6HYkWyAGv+c5rKhTm2czUHkmLLJ6DCEDDSb8KtOxquIPBAtfgzgGcPjvXCsqySG6O915/QrwUlEMIQN7TnKuhUtqCVWz4C2SZRq3+mQak2Bmd2rUOPlbtnSiaQwhArYqxMijUqiS/s6xjX4uOmLIwey6rlUj2J10ewIZJ24iFFcHAHVtwHF2Q24oAzhuwHBBxfug4AMAAAAAAQAAAJ4UjUEtUQgwEM6LchBzEHRrbrhsbbiSJEIAGGhrzmlszmoUwB8MD3ZlcmlmeVdpdGhFQ0RzYQwUG/V1qxGJaIQTYQo1oSiGzeC2bHJBYn1bUmuec2ycdCK5a26z + // READY: loaded 264 instructions + // NEO-VM 0 > ops + // INDEX OPCODE PARAMETER + // 0 PUSH3 << + // 1 PUSHDATA1 030486468683c112125978ffe876245b2006bfe739aca8539b67335079262cb27a + // 36 PUSHDATA1 030d26fc2ad3b1aae20f040b5f83380670f8ef5c2b2ac921ba3bdd79fd0af05251 + // 71 PUSHDATA1 037b4e72ae854b6a0955b3e02d92651ab7fa641a936066776ad438f95bb674a269 + // 106 PUSHDATA1 02b62ac4c8a352a892feceb18d7e2e3a62c8c1ecbaae5523d89d747b0219276e22 + // 141 PUSH4 + // 142 INITSLOT 7 local, 0 arg + // 145 STLOC5 + // 146 LDLOC5 + // 147 PACK + // 148 STLOC1 + // 149 STLOC6 + // 150 DEPTH + // 151 LDLOC6 + // 152 JMPEQ 155 (3/03) + // 154 ABORT + // 155 LDLOC6 + // 156 PACK + // 157 STLOC0 + // 158 SYSCALL System.Runtime.GetNetwork (c5fba0e0) + // 163 PUSHINT64 4294967296 (0000000001000000) + // 172 ADD + // 173 PUSH4 + // 174 LEFT + // 175 SYSCALL System.Runtime.GetScriptContainer (2d510830) + // 180 PUSH0 + // 181 PICKITEM + // 182 CAT + // 183 STLOC2 + // 184 PUSH0 + // 185 STLOC3 + // 186 PUSH0 + // 187 STLOC4 + // 188 LDLOC3 + // 189 LDLOC6 + // 190 GE + // 191 LDLOC4 + // 192 LDLOC5 + // 193 GE + // 194 OR + // 195 JMPIF 261 (66/42) + // 197 PUSHINT8 122 (7a) + // 199 LDLOC0 + // 200 LDLOC3 + // 201 PICKITEM + // 202 LDLOC1 + // 203 LDLOC4 + // 204 PICKITEM + // 205 LDLOC2 + // 206 PUSH4 + // 207 PACK + // 208 PUSH0 + // 209 PUSHDATA1 766572696679576974684543447361 ("verifyWithECDsa") + // 226 PUSHDATA1 1bf575ab1189688413610a35a12886cde0b66c72 ("NNToUmdQBe5n8o53BTzjTFAnSEcpouyy3B", "0x726cb6e0cd8628a1350a611384688911ab75f51b") + // 248 SYSCALL System.Contract.Call (627d5b52) + // 253 LDLOC3 + // 254 ADD + // 255 STLOC3 + // 256 LDLOC4 + // 257 INC + // 258 STLOC4 + // 259 JMP 188 (-71/b9) + // 261 LDLOC3 + // 262 LDLOC6 + // 263 NUMEQUAL + } + + // EmitAppCallNoArgs is a helper method that emits all parameters of System.Contract.Call interop + // except the method arguments. + private static ScriptBuilder EmitAppCallNoArgs(ScriptBuilder builder, UInt160 contractHash, string method, CallFlags f) + { + builder.EmitPush((byte)f); + builder.EmitPush(method); + builder.EmitPush(contractHash); + builder.EmitSysCall(ApplicationEngine.System_Contract_Call); + return builder; + } + + [TestMethod] + public void TestVerifyWithECDsa() + { + byte[] privR1 = "6e63fda41e9e3aba9bb5696d58a75731f044a9bdc48fe546da571543b2fa460e".HexToBytes(); + ECPoint pubR1 = ECPoint.Parse("04cae768e1cf58d50260cab808da8d6d83d5d3ab91eac41cdce577ce5862d736413643bdecd6d21c3b66f122ab080f9219204b10aa8bbceb86c1896974768648f3", ECCurve.Secp256r1); + byte[] privK1 = "0b5fb3a050385196b327be7d86cbce6e40a04c8832445af83ad19c82103b3ed9".HexToBytes(); + ECPoint pubK1 = ECPoint.Parse("04b6363b353c3ee1620c5af58594458aa00abf43a6d134d7c4cb2d901dc0f474fd74c94740bd7169aa0b1ef7bc657e824b1d7f4283c547e7ec18c8576acf84418a", ECCurve.Secp256k1); + byte[] message = System.Text.Encoding.Default.GetBytes("HelloWorld"); + + // secp256r1 + SHA256 + byte[] signature = Crypto.Sign(message, privR1, ECCurve.Secp256r1, Hasher.SHA256); + Crypto.VerifySignature(message, signature, pubR1).Should().BeTrue(); // SHA256 hash is used by default. + CallVerifyWithECDsa(message, pubR1, signature, NamedCurveHash.secp256r1SHA256).Should().Be(true); + + // secp256r1 + Keccak256 + signature = Crypto.Sign(message, privR1, ECCurve.Secp256r1, Hasher.Keccak256); + Crypto.VerifySignature(message, signature, pubR1, Hasher.Keccak256).Should().BeTrue(); + CallVerifyWithECDsa(message, pubR1, signature, NamedCurveHash.secp256r1Keccak256).Should().Be(true); + + // secp256k1 + SHA256 + signature = Crypto.Sign(message, privK1, ECCurve.Secp256k1, Hasher.SHA256); + Crypto.VerifySignature(message, signature, pubK1).Should().BeTrue(); // SHA256 hash is used by default. + CallVerifyWithECDsa(message, pubK1, signature, NamedCurveHash.secp256k1SHA256).Should().Be(true); + + // secp256k1 + Keccak256 + signature = Crypto.Sign(message, privK1, ECCurve.Secp256k1, Hasher.Keccak256); + Crypto.VerifySignature(message, signature, pubK1, Hasher.Keccak256).Should().BeTrue(); + CallVerifyWithECDsa(message, pubK1, signature, NamedCurveHash.secp256k1Keccak256).Should().Be(true); + } + + private bool CallVerifyWithECDsa(byte[] message, ECPoint pub, byte[] signature, NamedCurveHash curveHash) + { + var snapshot = TestBlockchain.GetTestSnapshot(); + using (ScriptBuilder script = new()) + { + script.EmitPush((int)curveHash); + script.EmitPush(signature); + script.EmitPush(pub.EncodePoint(true)); + script.EmitPush(message); + script.EmitPush(4); + script.Emit(OpCode.PACK); + script.EmitPush(CallFlags.All); + script.EmitPush("verifyWithECDsa"); + script.EmitPush(NativeContract.CryptoLib.Hash); + script.EmitSysCall(ApplicationEngine.System_Contract_Call); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestBlockchain.TheNeoSystem.Settings); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + return engine.ResultStack.Pop().GetBoolean(); + } + } } } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs index 181ff139ec..d7be3b4dcd 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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1094259016},"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":"setMinimumDeploymentFee","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":56,"safe":false},{"name":"update","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"}],"returntype":"Void","offset":63,"safe":false},{"name":"update","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","offset":70,"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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":1991619121},"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":"deserialize","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"Any","offset":56,"safe":true},{"name":"itoa","parameters":[{"name":"value","type":"Integer"}],"returntype":"String","offset":63,"safe":true},{"name":"itoa","parameters":[{"name":"value","type":"Integer"},{"name":"base","type":"Integer"}],"returntype":"String","offset":70,"safe":true},{"name":"jsonDeserialize","parameters":[{"name":"json","type":"ByteArray"}],"returntype":"Any","offset":77,"safe":true},{"name":"jsonSerialize","parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","offset":84,"safe":true},{"name":"memoryCompare","parameters":[{"name":"str1","type":"ByteArray"},{"name":"str2","type":"ByteArray"}],"returntype":"Integer","offset":91,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"}],"returntype":"Integer","offset":98,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"}],"returntype":"Integer","offset":105,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"},{"name":"backward","type":"Boolean"}],"returntype":"Integer","offset":112,"safe":true},{"name":"serialize","parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","offset":119,"safe":true},{"name":"strLen","parameters":[{"name":"str","type":"String"}],"returntype":"Integer","offset":126,"safe":true},{"name":"stringSplit","parameters":[{"name":"str","type":"String"},{"name":"separator","type":"String"}],"returntype":"Array","offset":133,"safe":true},{"name":"stringSplit","parameters":[{"name":"str","type":"String"},{"name":"separator","type":"String"},{"name":"removeEmptyEntries","type":"Boolean"}],"returntype":"Array","offset":140,"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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1094259016},"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":"ripemd160","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":56,"safe":true},{"name":"sha256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":63,"safe":true},{"name":"verifyWithECDsa","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"},{"name":"curve","type":"Integer"}],"returntype":"Boolean","offset":70,"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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1094259016},"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":"ripemd160","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":56,"safe":true},{"name":"sha256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":63,"safe":true},{"name":"verifyWithECDsa","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"},{"name":"curveHash","type":"Integer"}],"returntype":"Boolean","offset":70,"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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1325686241},"manifest":{"name":"NeoToken","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":"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":"registerCandidate","parameters":[{"name":"pubkey","type":"PublicKey"}],"returntype":"Boolean","offset":77,"safe":false},{"name":"setGasPerBlock","parameters":[{"name":"gasPerBlock","type":"Integer"}],"returntype":"Void","offset":84,"safe":false},{"name":"setRegisterPrice","parameters":[{"name":"registerPrice","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"symbol","parameters":[],"returntype":"String","offset":98,"safe":true},{"name":"totalSupply","parameters":[],"returntype":"Integer","offset":105,"safe":true},{"name":"transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Boolean","offset":112,"safe":false},{"name":"unclaimedGas","parameters":[{"name":"account","type":"Hash160"},{"name":"end","type":"Integer"}],"returntype":"Integer","offset":119,"safe":true},{"name":"unregisterCandidate","parameters":[{"name":"pubkey","type":"PublicKey"}],"returntype":"Boolean","offset":126,"safe":false},{"name":"vote","parameters":[{"name":"account","type":"Hash160"},{"name":"voteTo","type":"PublicKey"}],"returntype":"Boolean","offset":133,"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}}"""},