diff --git a/src/Neo/Cryptography/Crypto.cs b/src/Neo/Cryptography/Crypto.cs index 1b8d46c48e..1ee1cdb5f6 100644 --- a/src/Neo/Cryptography/Crypto.cs +++ b/src/Neo/Cryptography/Crypto.cs @@ -13,7 +13,9 @@ using Org.BouncyCastle.Asn1.X9; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Math; +using Org.BouncyCastle.Utilities.Encoders; using System; +using System.Linq; using System.Runtime.InteropServices; using System.Security.Cryptography; @@ -24,21 +26,12 @@ namespace Neo.Cryptography /// public static class Crypto { + private static readonly BigInteger s_prime = new(1, + Hex.Decode("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F")); + private static readonly ECDsaCache CacheECDsa = new(); 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. @@ -86,18 +79,11 @@ public static byte[] Sign(byte[] message, byte[] priKey, ECC.ECCurve ecCurve = n { if (hashAlgorithm == HashAlgorithm.Keccak256 || (IsOSX && ecCurve == ECC.ECCurve.Secp256k1)) { - 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); + var priKeyParameters = new ECPrivateKeyParameters(privateKey, ecCurve.BouncyCastleDomainParams); signer.Init(true, priKeyParameters); - var messageHash = - hashAlgorithm == HashAlgorithm.SHA256 ? message.Sha256() : - hashAlgorithm == HashAlgorithm.Keccak256 ? message.Keccak256() : - throw new NotSupportedException(nameof(hashAlgorithm)); + var messageHash = GetMessageHash(message, hashAlgorithm); var signature = signer.GenerateSignature(messageHash); var signatureBytes = new byte[64]; @@ -154,18 +140,10 @@ public static bool VerifySignature(ReadOnlySpan message, ReadOnlySpan message, ReadOnlySpan message, ReadOnlySpan + /// Get hash from message. + /// + /// Original message + /// The hash algorithm to be used hash the message, the default is SHA256. + /// Hashed message + public static byte[] GetMessageHash(byte[] message, HashAlgorithm hashAlgorithm = HashAlgorithm.SHA256) + { + return hashAlgorithm switch + { + HashAlgorithm.SHA256 => message.Sha256(), + HashAlgorithm.Keccak256 => message.Keccak256(), + HashAlgorithm.None => message, + _ => throw new NotSupportedException(nameof(hashAlgorithm)) + }; + } + + /// + /// Get hash from message. + /// + /// Original message + /// The hash algorithm to be used hash the message, the default is SHA256. + /// Hashed message + public static byte[] GetMessageHash(ReadOnlySpan message, HashAlgorithm hashAlgorithm = HashAlgorithm.SHA256) + { + return hashAlgorithm switch + { + HashAlgorithm.SHA256 => message.Sha256(), + HashAlgorithm.Keccak256 => message.Keccak256(), + HashAlgorithm.None => message.ToArray(), + _ => throw new NotSupportedException(nameof(hashAlgorithm)) + }; + } + + /// + /// Recovers the public key from a signature and message hash. + /// + /// Signature, either 65 bytes (r[32] || s[32] || v[1]) or + /// 64 bytes in “compact” form (r[32] || yParityAndS[32]). + /// 32-byte message hash + /// The recovered public key + /// Thrown if signature or hash is invalid + public static ECC.ECPoint ECRecover(byte[] signature, byte[] hash) + { + if (signature.Length != 65 && signature.Length != 64) + throw new ArgumentException("Signature must be 65 or 64 bytes", nameof(signature)); + if (hash.Length != 32) + throw new ArgumentException("Message hash must be 32 bytes", nameof(hash)); + + try + { + // Extract (r, s) and compute integer recId + BigInteger r, s; + int recId; + + if (signature.Length == 65) + { + // Format: r[32] || s[32] || v[1] + r = new BigInteger(1, [.. signature.Take(32)]); + s = new BigInteger(1, [.. signature.Skip(32).Take(32)]); + + // v could be 0..3 or 27..30 (Ethereum style). + var v = signature[64]; + recId = v >= 27 ? v - 27 : v; // normalize + if (recId < 0 || recId > 3) + throw new ArgumentException("Recovery value must be in [0..3] after normalization.", nameof(signature)); + } + else + { + // 64 bytes “compact” format: r[32] || yParityAndS[32] + // yParity is fused into the top bit of s. + + r = new BigInteger(1, [.. signature.Take(32)]); + var yParityAndS = new BigInteger(1, signature.Skip(32).ToArray()); + + // Mask out top bit to get s + var mask = BigInteger.One.ShiftLeft(255).Subtract(BigInteger.One); + s = yParityAndS.And(mask); + + // Extract yParity (0 or 1) + var yParity = yParityAndS.TestBit(255); + + // For “compact,” map parity to recId in [0..1]. + // For typical usage, recId in {0,1} is enough: + recId = yParity ? 1 : 0; + } + + // Decompose recId into i = recId >> 1 and yBit = recId & 1 + var iPart = recId >> 1; // usually 0..1 + var yBit = (recId & 1) == 1; + + // BouncyCastle curve constants + var n = ECC.ECCurve.Secp256k1.BouncyCastleCurve.N; + var e = new BigInteger(1, hash); + + // eInv = -e mod n + var eInv = BigInteger.Zero.Subtract(e).Mod(n); + // rInv = (r^-1) mod n + var rInv = r.ModInverse(n); + // srInv = (s * r^-1) mod n + var srInv = rInv.Multiply(s).Mod(n); + // eInvrInv = (eInv * r^-1) mod n + var eInvrInv = rInv.Multiply(eInv).Mod(n); + + // x = r + iPart * n + var x = r.Add(BigInteger.ValueOf(iPart).Multiply(n)); + // Verify x is within the curve prime + if (x.CompareTo(s_prime) >= 0) + throw new ArgumentException("x is out of range of the secp256k1 prime.", nameof(signature)); + + // Decompress to get R + var decompressedRKey = DecompressKey(ECC.ECCurve.Secp256k1.BouncyCastleCurve.Curve, x, yBit); + // Check that R is on curve + if (!decompressedRKey.Multiply(n).IsInfinity) + throw new ArgumentException("R point is not valid on this curve.", nameof(signature)); + + // Q = (eInv * G) + (srInv * R) + var q = Org.BouncyCastle.Math.EC.ECAlgorithms.SumOfTwoMultiplies( + ECC.ECCurve.Secp256k1.BouncyCastleCurve.G, eInvrInv, + decompressedRKey, srInv); + + return ECC.ECPoint.FromBytes(q.Normalize().GetEncoded(false), ECC.ECCurve.Secp256k1); + } + catch (ArgumentException) + { + throw; + } + catch (Exception ex) + { + throw new ArgumentException("Invalid signature parameters", nameof(signature), ex); + } + } + + private static Org.BouncyCastle.Math.EC.ECPoint DecompressKey( + Org.BouncyCastle.Math.EC.ECCurve curve, BigInteger xBN, bool yBit) + { + var compEnc = X9IntegerConverter.IntegerToBytes(xBN, 1 + X9IntegerConverter.GetByteLength(curve)); + compEnc[0] = (byte)(yBit ? 0x03 : 0x02); + return curve.DecodePoint(compEnc); + } } } diff --git a/src/Neo/Cryptography/ECC/ECCurve.cs b/src/Neo/Cryptography/ECC/ECCurve.cs index c7b9dbe9bc..df7f8aa8e1 100644 --- a/src/Neo/Cryptography/ECC/ECCurve.cs +++ b/src/Neo/Cryptography/ECC/ECCurve.cs @@ -10,6 +10,7 @@ // modifications are permitted. using Neo.Extensions; +using Org.BouncyCastle.Crypto.Parameters; using System.Globalization; using System.Numerics; @@ -33,9 +34,14 @@ public class ECCurve /// public readonly ECPoint G; + public readonly Org.BouncyCastle.Asn1.X9.X9ECParameters BouncyCastleCurve; + /// + /// Holds domain parameters for Secp256r1 elliptic curve. + /// + public readonly ECDomainParameters BouncyCastleDomainParams; internal readonly int ExpectedECPointLength; - private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G) + private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G, string curveName) { this.Q = Q; ExpectedECPointLength = ((int)VM.Utility.GetBitLength(Q) + 7) / 8; @@ -44,6 +50,8 @@ private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G this.N = N; Infinity = new ECPoint(null, null, this); this.G = ECPoint.DecodePoint(G, this); + BouncyCastleCurve = Org.BouncyCastle.Asn1.Sec.SecNamedCurves.GetByName(curveName); + BouncyCastleDomainParams = new ECDomainParameters(BouncyCastleCurve.Curve, BouncyCastleCurve.G, BouncyCastleCurve.N, BouncyCastleCurve.H); } /// @@ -55,7 +63,8 @@ private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G BigInteger.Zero, 7, BigInteger.Parse("00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", NumberStyles.AllowHexSpecifier), - ("04" + "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798" + "483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8").HexToBytes() + ("04" + "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798" + "483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8").HexToBytes(), + "secp256k1" ); /// @@ -67,7 +76,8 @@ private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", NumberStyles.AllowHexSpecifier), BigInteger.Parse("005AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", NumberStyles.AllowHexSpecifier), BigInteger.Parse("00FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", NumberStyles.AllowHexSpecifier), - ("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes() + ("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes(), + "secp256r1" ); } } diff --git a/src/Neo/Cryptography/HashAlgorithm.cs b/src/Neo/Cryptography/HashAlgorithm.cs index 361dbd3b64..247ac007a6 100644 --- a/src/Neo/Cryptography/HashAlgorithm.cs +++ b/src/Neo/Cryptography/HashAlgorithm.cs @@ -22,5 +22,10 @@ public enum HashAlgorithm : byte /// The Keccak256 hash algorithm. /// Keccak256 = 0x01, + + /// + /// None + /// + None = 0xFF } } diff --git a/src/Neo/SmartContract/Native/CryptoLib.cs b/src/Neo/SmartContract/Native/CryptoLib.cs index 845eabb3c6..9e22c2457d 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.cs @@ -33,6 +33,34 @@ public sealed partial class CryptoLib : NativeContract internal CryptoLib() : base() { } + /// + /// Recovers the public key from a secp256k1 signature in a single byte array format. + /// + /// The original message that was signed. + /// The 65-byte signature in format: r[32] + s[32] + v[1]. 64-bytes for eip-2098, where v must be 27 or 28. + /// The hash algorithm to be used hash the message. + /// The recovered public key in compressed format, or null if recovery fails. + [ContractMethod(Hardfork.HF_Echidna, CpuFee = 1 << 10, Name = "recoverSecp256K1")] + public static byte[] RecoverSecp256K1(byte[] message, byte[] signature, HashAlgorithm hashAlgorithm) + { + // It will be checked in Crypto.ECRecover + // if (signature.Length != 65 && signature.Length != 64) + // throw new ArgumentException("Signature must be 65 or 64 bytes", nameof(signature)); + + try + { + var messageHash = Crypto.GetMessageHash(message, hashAlgorithm); + if (messageHash == null) return null; + + var point = Crypto.ECRecover(signature, messageHash); + return point?.EncodePoint(true); + } + catch + { + return null; + } + } + /// /// Computes the hash value for the specified byte array using the ripemd160 algorithm. /// diff --git a/tests/Neo.UnitTests/Cryptography/ECC/UT_ECFieldElement.cs b/tests/Neo.UnitTests/Cryptography/ECC/UT_ECFieldElement.cs index 80a33aad2d..b43b32558a 100644 --- a/tests/Neo.UnitTests/Cryptography/ECC/UT_ECFieldElement.cs +++ b/tests/Neo.UnitTests/Cryptography/ECC/UT_ECFieldElement.cs @@ -87,13 +87,15 @@ public void TestSqrt() ECFieldElement element = new(new BigInteger(100), ECCurve.Secp256k1); Assert.AreEqual(new ECFieldElement(BigInteger.Parse("115792089237316195423570985008687907853269984665640564039457584007908834671653"), ECCurve.Secp256k1), element.Sqrt()); - ConstructorInfo constructor = typeof(ECCurve).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(byte[]) }, null); - ECCurve testCruve = constructor.Invoke(new object[] { + var constructor = typeof(ECCurve).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, + [typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(byte[]), typeof(string)], null); + var testCruve = constructor.Invoke([ BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFF0", NumberStyles.AllowHexSpecifier), BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFF00", NumberStyles.AllowHexSpecifier), BigInteger.Parse("005AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", NumberStyles.AllowHexSpecifier), BigInteger.Parse("00FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", NumberStyles.AllowHexSpecifier), - ("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes() }) as ECCurve; + ("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes(), + "secp256k1"]) as ECCurve; element = new ECFieldElement(new BigInteger(200), testCruve); Assert.IsNull(element.Sqrt()); } diff --git a/tests/Neo.UnitTests/Cryptography/UT_Crypto.cs b/tests/Neo.UnitTests/Cryptography/UT_Crypto.cs index fd14e9f39a..0e286bd330 100644 --- a/tests/Neo.UnitTests/Cryptography/UT_Crypto.cs +++ b/tests/Neo.UnitTests/Cryptography/UT_Crypto.cs @@ -11,27 +11,25 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Neo.Cryptography; -using Neo.Cryptography.ECC; using Neo.Extensions; -using Neo.IO; using Neo.Wallets; using System; +using System.Linq; using System.Security.Cryptography; +using System.Text; namespace Neo.UnitTests.Cryptography { [TestClass] public class UT_Crypto { - private KeyPair key = null; - private readonly byte[] message = System.Text.Encoding.Default.GetBytes("HelloWorld"); - private readonly byte[] wrongKey = new byte[33]; - private readonly byte[] wrongKey2 = new byte[36]; + private KeyPair _key = null; + private readonly byte[] _message = Encoding.Default.GetBytes("HelloWorld"); public static KeyPair GenerateKey(int privateKeyLength) { - byte[] privateKey = new byte[privateKeyLength]; - using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + var privateKey = new byte[privateKeyLength]; + using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(privateKey); } @@ -40,8 +38,8 @@ public static KeyPair GenerateKey(int privateKeyLength) public static KeyPair GenerateCertainKey(int privateKeyLength) { - byte[] privateKey = new byte[privateKeyLength]; - for (int i = 0; i < privateKeyLength; i++) + var privateKey = new byte[privateKeyLength]; + for (var i = 0; i < privateKeyLength; i++) { privateKey[i] = (byte)((byte)i % byte.MaxValue); } @@ -51,20 +49,23 @@ public static KeyPair GenerateCertainKey(int privateKeyLength) [TestInitialize] public void TestSetup() { - key = GenerateKey(32); + _key = GenerateKey(32); } [TestMethod] public void TestVerifySignature() { - byte[] signature = Crypto.Sign(message, key.PrivateKey); - Assert.IsTrue(Crypto.VerifySignature(message, signature, key.PublicKey)); + var message = Encoding.Default.GetBytes("HelloWorld"); + var signature = Crypto.Sign(message, _key.PrivateKey); + Assert.IsTrue(Crypto.VerifySignature(message, signature, _key.PublicKey)); + var wrongKey2 = new byte[36]; + var wrongKey = new byte[33]; wrongKey[0] = 0x02; Assert.IsFalse(Crypto.VerifySignature(message, signature, wrongKey, Neo.Cryptography.ECC.ECCurve.Secp256r1)); wrongKey[0] = 0x03; - for (int i = 1; i < 33; i++) wrongKey[i] = byte.MaxValue; + for (var i = 1; i < 33; i++) wrongKey[i] = byte.MaxValue; Assert.ThrowsException(() => Crypto.VerifySignature(message, signature, wrongKey, Neo.Cryptography.ECC.ECCurve.Secp256r1)); Assert.ThrowsException(() => Crypto.VerifySignature(message, signature, wrongKey2, Neo.Cryptography.ECC.ECCurve.Secp256r1)); @@ -73,23 +74,147 @@ public void TestVerifySignature() [TestMethod] public void TestSecp256k1() { - byte[] privkey = "7177f0d04c79fa0b8c91fe90c1cf1d44772d1fba6e5eb9b281a22cd3aafb51fe".HexToBytes(); - byte[] message = "2d46a712699bae19a634563d74d04cc2da497b841456da270dccb75ac2f7c4e7".HexToBytes(); + var privkey = "7177f0d04c79fa0b8c91fe90c1cf1d44772d1fba6e5eb9b281a22cd3aafb51fe".HexToBytes(); + var message = "2d46a712699bae19a634563d74d04cc2da497b841456da270dccb75ac2f7c4e7".HexToBytes(); var signature = Crypto.Sign(message, privkey, Neo.Cryptography.ECC.ECCurve.Secp256k1); - byte[] pubKey = "04fd0a8c1ce5ae5570fdd46e7599c16b175bf0ebdfe9c178f1ab848fb16dac74a5d301b0534c7bcf1b3760881f0c420d17084907edd771e1c9c8e941bbf6ff9108".HexToBytes(); + var pubKey = "04fd0a8c1ce5ae5570fdd46e7599c16b175bf0ebdfe9c178f1ab848fb16dac74a5d301b0534c7bcf1b3760881f0c420d17084907edd771e1c9c8e941bbf6ff9108".HexToBytes(); Assert.IsTrue(Crypto.VerifySignature(message, signature, pubKey, Neo.Cryptography.ECC.ECCurve.Secp256k1)); - message = System.Text.Encoding.Default.GetBytes("world"); + message = Encoding.Default.GetBytes("world"); signature = Crypto.Sign(message, privkey, Neo.Cryptography.ECC.ECCurve.Secp256k1); Assert.IsTrue(Crypto.VerifySignature(message, signature, pubKey, Neo.Cryptography.ECC.ECCurve.Secp256k1)); - message = System.Text.Encoding.Default.GetBytes("中文"); + message = Encoding.Default.GetBytes("中文"); signature = "b8cba1ff42304d74d083e87706058f59cdd4f755b995926d2cd80a734c5a3c37e4583bfd4339ac762c1c91eee3782660a6baf62cd29e407eccd3da3e9de55a02".HexToBytes(); pubKey = "03661b86d54eb3a8e7ea2399e0db36ab65753f95fff661da53ae0121278b881ad0".HexToBytes(); Assert.IsTrue(Crypto.VerifySignature(message, signature, pubKey, Neo.Cryptography.ECC.ECCurve.Secp256k1)); + + var messageHash = message.Sha256(); + // append v to signature + signature = [.. signature, .. new byte[] { 27 }]; + var recover = Crypto.ECRecover(signature, messageHash); + + CollectionAssert.AreEqual(pubKey, recover.ToArray()); + } + + [TestMethod] + public void TestECRecover() + { + // Test case 1 + var message1 = "5c868fedb8026979ebd26f1ba07c27eedf4ff6d10443505a96ecaf21ba8c4f0937b3cd23ffdc3dd429d4cd1905fb8dbcceeff1350020e18b58d2ba70887baa3a9b783ad30d3fbf210331cdd7df8d77defa398cdacdfc2e359c7ba4cae46bb74401deb417f8b912a1aa966aeeba9c39c7dd22479ae2b30719dca2f2206c5eb4b7".HexToBytes(); + var messageHash1 = "5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0".HexToBytes(); + var signature1 = "45c0b7f8c09a9e1f1cea0c25785594427b6bf8f9f878a8af0b1abbb48e16d0920d8becd0c220f67c51217eecfd7184ef0732481c843857e6bc7fc095c4f6b78801".HexToBytes(); + var expectedPubKey1 = "034a071e8a6e10aada2b8cf39fa3b5fb3400b04e99ea8ae64ceea1a977dbeaf5d5".HexToBytes(); + + var recoveredKey1 = Crypto.ECRecover(signature1, messageHash1); + CollectionAssert.AreEqual(expectedPubKey1, recoveredKey1.EncodePoint(true)); + + // Test case 2 + var message2 = "17cd4a74d724d55355b6fb2b0759ca095298e3fd1856b87ca1cb2df5409058022736d21be071d820b16dfc441be97fbcea5df787edc886e759475469e2128b22f26b82ca993be6695ab190e673285d561d3b6d42fcc1edd6d12db12dcda0823e9d6079e7bc5ff54cd452dad308d52a15ce9c7edd6ef3dad6a27becd8e001e80f".HexToBytes(); + var messageHash2 = "586052916fb6f746e1d417766cceffbe1baf95579bab67ad49addaaa6e798862".HexToBytes(); + var signature2 = "4e0ea79d4a476276e4b067facdec7460d2c98c8a65326a6e5c998fd7c65061140e45aea5034af973410e65cf97651b3f2b976e3fc79c6a93065ed7cb69a2ab5a01".HexToBytes(); + var expectedPubKey2 = "02dbf1f4092deb3cfd4246b2011f7b24840bc5dbedae02f28471ce5b3bfbf06e71".HexToBytes(); + + var recoveredKey2 = Crypto.ECRecover(signature2, messageHash2); + CollectionAssert.AreEqual(expectedPubKey2, recoveredKey2.EncodePoint(true)); + + // Test case 3 - recovery param 0 + var message3 = "db0d31717b04802adbbae1997487da8773440923c09b869e12a57c36dda34af11b8897f266cd81c02a762c6b74ea6aaf45aaa3c52867eb8f270f5092a36b498f88b65b2ebda24afe675da6f25379d1e194d093e7a2f66e450568dbdffebff97c4597a00c96a5be9ba26deefcca8761c1354429622c8db269d6a0ec0cc7a8585c".HexToBytes(); + var messageHash3 = "c36d0ecf4bfd178835c97aae7585f6a87de7dfa23cc927944f99a8d60feff68b".HexToBytes(); + var signature3 = "f25b86e1d8a11d72475b3ed273b0781c7d7f6f9e1dae0dd5d3ee9b84f3fab89163d9c4e1391de077244583e9a6e3d8e8e1f236a3bf5963735353b93b1a3ba93500".HexToBytes(); + var expectedPubKey3 = "03414549fd05bfb7803ae507ff86b99becd36f8d66037a7f5ba612792841d42eb9".HexToBytes(); + + var recoveredKey3 = Crypto.ECRecover(signature3, messageHash3); + CollectionAssert.AreEqual(expectedPubKey3, recoveredKey3.EncodePoint(true)); + + // Test invalid cases + var invalidSignature = new byte[65]; + Assert.ThrowsException(() => Crypto.ECRecover(invalidSignature, messageHash1)); + + // Test with invalid recovery value + var invalidRecoverySignature = signature1.ToArray(); + invalidRecoverySignature[64] = 29; // Invalid recovery value + Assert.ThrowsException(() => Crypto.ECRecover(invalidRecoverySignature, messageHash1)); + + // Test with wrong message hash + var recoveredWrongHash = Crypto.ECRecover(signature1, messageHash2); + CollectionAssert.AreNotEquivalent(expectedPubKey1, recoveredWrongHash.EncodePoint(true)); + } + + [TestMethod] + public void TestERC2098() + { + // Test from https://eips.ethereum.org/EIPS/eip-2098 + + // Private Key: 0x1234567890123456789012345678901234567890123456789012345678901234 + // Message: "Hello World" + // Signature: + // r: 0x68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b90 + // s: 0x7e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064 + // v: 27 + + var privateKey = "1234567890123456789012345678901234567890123456789012345678901234".HexToBytes(); + var expectedPubKey1 = (Neo.Cryptography.ECC.ECCurve.Secp256k1.G * privateKey).ToArray(); + + Console.WriteLine($"Expected PubKey: {expectedPubKey1.ToHexString()}"); + var message1 = Encoding.UTF8.GetBytes("Hello World"); + var messageHash1 = new byte[] { 0x19 }.Concat(Encoding.UTF8.GetBytes($"Ethereum Signed Message:\n{message1.Count()}")).Concat(message1).ToArray().Keccak256(); + Console.WriteLine($"Message Hash: {Convert.ToHexString(messageHash1)}"); + + // Signature values from EIP-2098 test case + var r = "68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b90".HexToBytes(); + var s = "7e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064".HexToBytes(); + var signature1 = new byte[65]; + Array.Copy(r, 0, signature1, 0, 32); + Array.Copy(s, 0, signature1, 32, 32); + signature1[64] = 27; + + Console.WriteLine($"r: {Convert.ToHexString(signature1.Take(32).ToArray())}"); + Console.WriteLine($"s: {Convert.ToHexString(signature1.Skip(32).Take(32).ToArray())}"); + Console.WriteLine($"yParity: {(signature1[32] & 0x80) != 0}"); + + var recoveredKey1 = Crypto.ECRecover(signature1, messageHash1); + + // Private Key: 0x1234567890123456789012345678901234567890123456789012345678901234 + // Message: "It's a small(er) world" + // Signature: + // r: 0x9328da16089fcba9bececa81663203989f2df5fe1faa6291a45381c81bd17f76 + // s: 0x139c6d6b623b42da56557e5e734a43dc83345ddfadec52cbe24d0cc64f550793 + // v: 28 + + var sig = "68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b90".HexToBytes() + .Concat("7e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064".HexToBytes()) + .Concat(new byte[] { 0x1B }) + .ToArray(); + + var pubKey = Crypto.ECRecover(sig, messageHash1); + + Console.WriteLine($"Recovered PubKey: {pubKey.EncodePoint(true).ToHexString()}"); + Console.WriteLine($"Recovered PubKey: {recoveredKey1.EncodePoint(true).ToHexString()}"); + Assert.AreEqual(recoveredKey1, recoveredKey1); + + var message2Body = Encoding.UTF8.GetBytes("It's a small(er) world"); + var message2 = new byte[] { 0x19 }.Concat(Encoding.UTF8.GetBytes($"Ethereum Signed Message:\n{message2Body.Count()}")).Concat(message2Body).ToArray(); + var messageHash2 = message2.Keccak256(); + Console.WriteLine($"\nMessage Hash 2: {Convert.ToHexString(messageHash2)}"); + + // Second test case from EIP-2098 + var r2 = "9328da16089fcba9bececa81663203989f2df5fe1faa6291a45381c81bd17f76".HexToBytes(); + var s2 = "939c6d6b623b42da56557e5e734a43dc83345ddfadec52cbe24d0cc64f550793".HexToBytes(); + var signature2 = new byte[64]; + Array.Copy(r2, 0, signature2, 0, 32); + Array.Copy(s2, 0, signature2, 32, 32); + + Console.WriteLine($"r: {Convert.ToHexString(signature2.Take(32).ToArray())}"); + Console.WriteLine($"s: {Convert.ToHexString(signature2.Skip(32).Take(32).ToArray())}"); + Console.WriteLine($"yParity: {(signature2[32] & 0x80) != 0}"); + + var recoveredKey2 = Crypto.ECRecover(signature2, messageHash2); + CollectionAssert.AreEqual(expectedPubKey1, recoveredKey2.EncodePoint(true)); + Assert.IsTrue(Crypto.VerifySignature(message2, signature2, recoveredKey2, Neo.Cryptography.HashAlgorithm.Keccak256)); } } } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs index 3096c5a1b1..7efd3b333c 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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2681632925},"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":"itoa","parameters":[{"name":"value","type":"Integer"}],"returntype":"String","offset":77,"safe":true},{"name":"itoa","parameters":[{"name":"value","type":"Integer"},{"name":"base","type":"Integer"}],"returntype":"String","offset":84,"safe":true},{"name":"jsonDeserialize","parameters":[{"name":"json","type":"ByteArray"}],"returntype":"Any","offset":91,"safe":true},{"name":"jsonSerialize","parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","offset":98,"safe":true},{"name":"memoryCompare","parameters":[{"name":"str1","type":"ByteArray"},{"name":"str2","type":"ByteArray"}],"returntype":"Integer","offset":105,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"}],"returntype":"Integer","offset":112,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"}],"returntype":"Integer","offset":119,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"},{"name":"backward","type":"Boolean"}],"returntype":"Integer","offset":126,"safe":true},{"name":"serialize","parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","offset":133,"safe":true},{"name":"strLen","parameters":[{"name":"str","type":"String"}],"returntype":"Integer","offset":140,"safe":true},{"name":"stringSplit","parameters":[{"name":"str","type":"String"},{"name":"separator","type":"String"}],"returntype":"Array","offset":147,"safe":true},{"name":"stringSplit","parameters":[{"name":"str","type":"String"},{"name":"separator","type":"String"},{"name":"removeEmptyEntries","type":"Boolean"}],"returntype":"Array","offset":154,"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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":3581846399},"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},{"name":"verifyWithEd25519","parameters":[{"name":"message","type":"ByteArray"},{"name":"publicKey","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":77,"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":"message","type":"ByteArray"},{"name":"signature","type":"ByteArray"},{"name":"hashAlgorithm","type":"Integer"}],"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":"publicKey","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":84,"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}}"""},