diff --git a/src/Neo/ProtocolSettings.cs b/src/Neo/ProtocolSettings.cs index 19011dc24c..cef7ef6de1 100644 --- a/src/Neo/ProtocolSettings.cs +++ b/src/Neo/ProtocolSettings.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Linq; namespace Neo @@ -123,19 +124,32 @@ public record ProtocolSettings public static ProtocolSettings Custom { get; set; } + /// + /// Loads the from the specified stream. + /// + /// The stream of the settings. + /// The loaded . + public static ProtocolSettings Load(Stream stream) + { + var config = new ConfigurationBuilder().AddJsonStream(stream).Build(); + var section = config.GetSection("ProtocolConfiguration"); + return Load(section); + } + /// /// Loads the at the specified path. /// /// The path of the settings file. - /// Indicates whether the file is optional. /// The loaded . - public static ProtocolSettings Load(string path, bool optional = true) + public static ProtocolSettings Load(string path) { - IConfigurationRoot config = new ConfigurationBuilder().AddJsonFile(path, optional).Build(); - IConfigurationSection section = config.GetSection("ProtocolConfiguration"); - var settings = Load(section); - CheckingHardfork(settings); - return settings; + if (!File.Exists(path)) + { + return Default; + } + + using var stream = File.OpenRead(path); + return Load(stream); } /// @@ -165,6 +179,7 @@ public static ProtocolSettings Load(IConfigurationSection section) ? EnsureOmmitedHardforks(section.GetSection("Hardforks").GetChildren().ToDictionary(p => Enum.Parse(p.Key, true), p => uint.Parse(p.Value))).ToImmutableDictionary() : Default.Hardforks }; + CheckingHardfork(Custom); return Custom; } diff --git a/src/Neo/SmartContract/Native/ContractMethodAttribute.cs b/src/Neo/SmartContract/Native/ContractMethodAttribute.cs index 38bc065533..c13329a206 100644 --- a/src/Neo/SmartContract/Native/ContractMethodAttribute.cs +++ b/src/Neo/SmartContract/Native/ContractMethodAttribute.cs @@ -15,7 +15,8 @@ namespace Neo.SmartContract.Native { [DebuggerDisplay("{Name}")] - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false)] + // We allow multiple attributes because the fees or requiredCallFlags may change between hard forks. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true)] internal class ContractMethodAttribute : Attribute, IHardforkActivable { public string Name { get; init; } diff --git a/src/Neo/SmartContract/Native/ContractMethodMetadata.cs b/src/Neo/SmartContract/Native/ContractMethodMetadata.cs index fd5a02be6a..58b67fbf01 100644 --- a/src/Neo/SmartContract/Native/ContractMethodMetadata.cs +++ b/src/Neo/SmartContract/Native/ContractMethodMetadata.cs @@ -40,7 +40,8 @@ internal class ContractMethodMetadata : IHardforkActivable public ContractMethodMetadata(MemberInfo member, ContractMethodAttribute attribute) { - Name = attribute.Name ?? member.Name.ToLower()[0] + member.Name[1..]; + Name = attribute.Name ?? member.Name; + Name = Name.ToLowerInvariant()[0] + Name[1..]; Handler = member switch { MethodInfo m => m, diff --git a/src/Neo/SmartContract/Native/CryptoLib.cs b/src/Neo/SmartContract/Native/CryptoLib.cs index 9027298752..9ee46d9490 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.cs @@ -100,7 +100,7 @@ public static bool VerifyWithECDsa(byte[] message, byte[] pubkey, byte[] signatu } // This is for solving the hardfork issue in https://github.com/neo-project/neo/pull/3209 - [ContractMethod(true, Hardfork.HF_Cockatrice, CpuFee = 1 << 15, Name = "verifyWithECDsa")] + [ContractMethod(true, Hardfork.HF_Cockatrice, CpuFee = 1 << 15, Name = nameof(VerifyWithECDsa))] public static bool VerifyWithECDsaV0(byte[] message, byte[] pubkey, byte[] signature, NamedCurveHash curve) { if (curve != NamedCurveHash.secp256k1SHA256 && curve != NamedCurveHash.secp256r1SHA256) diff --git a/src/Neo/SmartContract/Native/LedgerContract.cs b/src/Neo/SmartContract/Native/LedgerContract.cs index 3d2d16e023..feb2801280 100644 --- a/src/Neo/SmartContract/Native/LedgerContract.cs +++ b/src/Neo/SmartContract/Native/LedgerContract.cs @@ -278,7 +278,7 @@ public Transaction GetTransaction(DataCache snapshot, UInt256 hash) return GetTransactionState(snapshot, hash)?.Transaction; } - [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates, Name = "getTransaction")] + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates, Name = nameof(GetTransaction))] private Transaction GetTransactionForContract(ApplicationEngine engine, UInt256 hash) { TransactionState state = GetTransactionState(engine.SnapshotCache, hash); diff --git a/src/Neo/SmartContract/Native/NativeContract.cs b/src/Neo/SmartContract/Native/NativeContract.cs index 97bb0ab69a..8b355464c9 100644 --- a/src/Neo/SmartContract/Native/NativeContract.cs +++ b/src/Neo/SmartContract/Native/NativeContract.cs @@ -140,11 +140,12 @@ protected NativeContract() // Reflection to get the methods List listMethods = []; - foreach (MemberInfo member in GetType().GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) + foreach (var member in GetType().GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) { - ContractMethodAttribute attribute = member.GetCustomAttribute(); - if (attribute is null) continue; - listMethods.Add(new ContractMethodMetadata(member, attribute)); + foreach (var attribute in member.GetCustomAttributes()) + { + listMethods.Add(new ContractMethodMetadata(member, attribute)); + } } _methodDescriptors = listMethods.OrderBy(p => p.Name, StringComparer.Ordinal).ThenBy(p => p.Parameters.Length).ToList().AsReadOnly(); @@ -363,6 +364,13 @@ public static NativeContract GetContract(UInt160 hash) return contract; } + internal Dictionary GetContractMethods(ApplicationEngine engine) + { + var nativeContracts = engine.GetState(() => new NativeContractsCache()); + var currentAllowedMethods = nativeContracts.GetAllowedMethods(this, engine); + return currentAllowedMethods.Methods; + } + internal async void Invoke(ApplicationEngine engine, byte version) { try @@ -370,16 +378,15 @@ internal async void Invoke(ApplicationEngine engine, byte version) if (version != 0) throw new InvalidOperationException($"The native contract of version {version} is not active."); // Get native contracts invocation cache - NativeContractsCache nativeContracts = engine.GetState(() => new NativeContractsCache()); - NativeContractsCache.CacheEntry currentAllowedMethods = nativeContracts.GetAllowedMethods(this, engine); + var currentAllowedMethods = GetContractMethods(engine); // Check if the method is allowed - ExecutionContext context = engine.CurrentContext; - ContractMethodMetadata method = currentAllowedMethods.Methods[context.InstructionPointer]; + var context = engine.CurrentContext; + var method = currentAllowedMethods[context.InstructionPointer]; if (method.ActiveIn is not null && !engine.IsHardforkEnabled(method.ActiveIn.Value)) throw new InvalidOperationException($"Cannot call this method before hardfork {method.ActiveIn}."); if (method.DeprecatedIn is not null && engine.IsHardforkEnabled(method.DeprecatedIn.Value)) throw new InvalidOperationException($"Cannot call this method after hardfork {method.DeprecatedIn}."); - ExecutionContextState state = context.GetState(); + var state = context.GetState(); if (!state.CallFlags.HasFlag(method.RequiredCallFlags)) throw new InvalidOperationException($"Cannot call this method with the flag {state.CallFlags}."); // In the unit of datoshi, 1 datoshi = 1e-8 GAS diff --git a/src/Neo/SmartContract/Native/NeoToken.cs b/src/Neo/SmartContract/Native/NeoToken.cs index 85309b4246..37f1ee78a0 100644 --- a/src/Neo/SmartContract/Native/NeoToken.cs +++ b/src/Neo/SmartContract/Native/NeoToken.cs @@ -327,7 +327,8 @@ public BigInteger UnclaimedGas(DataCache snapshot, UInt160 account, uint end) return CalculateBonus(snapshot, state, end); } - [ContractMethod(RequiredCallFlags = CallFlags.States)] + [ContractMethod(true, Hardfork.HF_Echidna, RequiredCallFlags = CallFlags.States)] + [ContractMethod(Hardfork.HF_Echidna, /* */ RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] private bool RegisterCandidate(ApplicationEngine engine, ECPoint pubkey) { if (!engine.CheckWitnessInternal(Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash())) @@ -344,7 +345,8 @@ private bool RegisterCandidate(ApplicationEngine engine, ECPoint pubkey) return true; } - [ContractMethod(CpuFee = 1 << 16, RequiredCallFlags = CallFlags.States)] + [ContractMethod(true, Hardfork.HF_Echidna, CpuFee = 1 << 16, RequiredCallFlags = CallFlags.States)] + [ContractMethod(Hardfork.HF_Echidna, /* */ CpuFee = 1 << 16, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] private bool UnregisterCandidate(ApplicationEngine engine, ECPoint pubkey) { if (!engine.CheckWitnessInternal(Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash())) @@ -361,7 +363,8 @@ private bool UnregisterCandidate(ApplicationEngine engine, ECPoint pubkey) return true; } - [ContractMethod(CpuFee = 1 << 16, RequiredCallFlags = CallFlags.States)] + [ContractMethod(true, Hardfork.HF_Echidna, CpuFee = 1 << 16, RequiredCallFlags = CallFlags.States)] + [ContractMethod(Hardfork.HF_Echidna, /* */ CpuFee = 1 << 16, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] private async ContractTask Vote(ApplicationEngine engine, UInt160 account, ECPoint voteTo) { if (!engine.CheckWitnessInternal(account)) return false; diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs index 5de9448013..291d6d2529 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs @@ -70,7 +70,7 @@ public void TestActiveDeprecatedIn() string json = UT_ProtocolSettings.CreateHFSettings("\"HF_Cockatrice\": 20"); var file = Path.GetTempFileName(); File.WriteAllText(file, json); - ProtocolSettings settings = ProtocolSettings.Load(file, false); + ProtocolSettings settings = ProtocolSettings.Load(file); File.Delete(file); Assert.IsFalse(NativeContract.IsActive(new active() { ActiveIn = Hardfork.HF_Cockatrice, DeprecatedIn = null }, settings.IsHardforkEnabled, 1)); @@ -86,7 +86,7 @@ public void TestActiveDeprecatedInRoleManagement() string json = UT_ProtocolSettings.CreateHFSettings("\"HF_Echidna\": 20"); var file = Path.GetTempFileName(); File.WriteAllText(file, json); - ProtocolSettings settings = ProtocolSettings.Load(file, false); + ProtocolSettings settings = ProtocolSettings.Load(file); File.Delete(file); var before = NativeContract.RoleManagement.GetContractState(settings.IsHardforkEnabled, 19); @@ -111,7 +111,7 @@ public void TestIsInitializeBlock() var file = Path.GetTempFileName(); File.WriteAllText(file, json); - ProtocolSettings settings = ProtocolSettings.Load(file, false); + ProtocolSettings settings = ProtocolSettings.Load(file); File.Delete(file); Assert.IsTrue(NativeContract.CryptoLib.IsInitializeBlock(settings, 0, out var hf)); diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NeoToken.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NeoToken.cs index 2ff07d5a69..c5f889a252 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_NeoToken.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_NeoToken.cs @@ -22,8 +22,11 @@ using Neo.VM; using Neo.Wallets; using System; +using System.IO; using System.Linq; using System.Numerics; +using System.Security.Principal; +using System.Text; using static Neo.SmartContract.Native.NeoToken; namespace Neo.UnitTests.SmartContract.Native @@ -54,6 +57,48 @@ public void TestSetup() [TestMethod] public void Check_Decimals() => NativeContract.NEO.Decimals(_snapshotCache).Should().Be(0); + [TestMethod] + public void Test_HF_EchidnaStates() + { + string json = UT_ProtocolSettings.CreateHFSettings("\"HF_Echidna\": 10"); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + var settings = ProtocolSettings.Load(stream); + + var clonedCache = _snapshotCache.CloneCache(); + var persistingBlock = new Block { Header = new Header() }; + + foreach (var method in new string[] { "vote", "registerCandidate", "unregisterCandidate" }) + { + // Test WITHOUT HF_Echidna + + persistingBlock.Header.Index = 9; + + using (var engine = ApplicationEngine.Create(TriggerType.Application, + new Nep17NativeContractExtensions.ManualWitness(UInt160.Zero), clonedCache, persistingBlock, settings: settings)) + { + var methods = NativeContract.NEO.GetContractMethods(engine); + var entries = methods.Values.Where(u => u.Name == method).ToArray(); + + Assert.AreEqual(entries.Length, 1); + Assert.AreEqual(entries[0].RequiredCallFlags, CallFlags.States); + } + + // Test WITH HF_Echidna + + persistingBlock.Header.Index = 10; + + using (var engine = ApplicationEngine.Create(TriggerType.Application, + new Nep17NativeContractExtensions.ManualWitness(UInt160.Zero), clonedCache, persistingBlock, settings: settings)) + { + var methods = NativeContract.NEO.GetContractMethods(engine); + var entries = methods.Values.Where(u => u.Name == method).ToArray(); + + Assert.AreEqual(entries.Length, 1); + Assert.AreEqual(entries[0].RequiredCallFlags, CallFlags.States | CallFlags.AllowNotify); + } + } + } + [TestMethod] public void Check_Vote() { diff --git a/tests/Neo.UnitTests/UT_ProtocolSettings.cs b/tests/Neo.UnitTests/UT_ProtocolSettings.cs index 97a36e5907..757ca5f174 100644 --- a/tests/Neo.UnitTests/UT_ProtocolSettings.cs +++ b/tests/Neo.UnitTests/UT_ProtocolSettings.cs @@ -56,7 +56,7 @@ public void HardForkTestBAndNotA() var file = Path.GetTempFileName(); File.WriteAllText(file, json); - ProtocolSettings settings = ProtocolSettings.Load(file, false); + ProtocolSettings settings = ProtocolSettings.Load(file); File.Delete(file); settings.Hardforks[Hardfork.HF_Aspidochelone].Should().Be(0); @@ -78,7 +78,7 @@ public void HardForkTestAAndNotB() var file = Path.GetTempFileName(); File.WriteAllText(file, json); - ProtocolSettings settings = ProtocolSettings.Load(file, false); + ProtocolSettings settings = ProtocolSettings.Load(file); File.Delete(file); settings.Hardforks[Hardfork.HF_Aspidochelone].Should().Be(0); @@ -100,7 +100,7 @@ public void HardForkTestNone() var file = Path.GetTempFileName(); File.WriteAllText(file, json); - ProtocolSettings settings = ProtocolSettings.Load(file, false); + ProtocolSettings settings = ProtocolSettings.Load(file); File.Delete(file); settings.Hardforks[Hardfork.HF_Aspidochelone].Should().Be(0); @@ -120,7 +120,7 @@ public void HardForkTestAMoreThanB() string json = CreateHFSettings("\"HF_Aspidochelone\": 4120001, \"HF_Basilisk\": 4120000"); var file = Path.GetTempFileName(); File.WriteAllText(file, json); - Assert.ThrowsException(() => ProtocolSettings.Load(file, false)); + Assert.ThrowsException(() => ProtocolSettings.Load(file)); File.Delete(file); } @@ -316,7 +316,7 @@ public void TestTimePerBlockCalculation() [TestMethod] public void TestLoad() { - var loadedSetting = ProtocolSettings.Load("test.config.json", false); + var loadedSetting = ProtocolSettings.Load("test.config.json"); // Comparing all properties TestProtocolSettings.Default.Network.Should().Be(loadedSetting.Network);