// Copyright (C) 2015-2026 The Neo Project. // // NativeContract.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. using Neo.Cryptography.ECC; using Neo.SmartContract.Manifest; using Neo.VM; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; namespace Neo.SmartContract.Native { /// /// The base class of all native contracts. /// public abstract class NativeContract { private class NativeContractsCache { public record CacheEntry(Dictionary Methods, byte[] Script); internal Dictionary NativeContracts { get; set; } = new(); public CacheEntry GetAllowedMethods(NativeContract native, ApplicationEngine engine) { if (NativeContracts.TryGetValue(native.Id, out var value)) return value; uint index = engine.PersistingBlock is null ? Ledger.CurrentIndex(engine.SnapshotCache) : engine.PersistingBlock.Index; CacheEntry methods = native.GetAllowedMethods(engine.ProtocolSettings.IsHardforkEnabled, index); NativeContracts[native.Id] = methods; return methods; } } public delegate bool IsHardforkEnabledDelegate(Hardfork hf, uint blockHeight); private static readonly List s_contractsList = []; private static readonly Dictionary s_contractsDictionary = new(); private readonly ImmutableHashSet _usedHardforks; private readonly ReadOnlyCollection _methodDescriptors; private readonly ReadOnlyCollection _eventsDescriptors; private static int idCounter = 0; #region Named Native Contracts /// /// Gets the instance of the class. /// public static ContractManagement ContractManagement { get; } = new(); /// /// Gets the instance of the class. /// public static StdLib StdLib { get; } = new(); /// /// Gets the instance of the class. /// public static CryptoLib CryptoLib { get; } = new(); /// /// Gets the instance of the class. /// public static LedgerContract Ledger { get; } = new(); /// /// Gets the instance of the class. /// public static NeoToken NEO { get; } = new(); /// /// Gets the instance of the class. /// public static GasToken GAS { get; } = new(); /// /// Gets the instance of the class. /// public static PolicyContract Policy { get; } = new(); /// /// Gets the instance of the class. /// public static RoleManagement RoleManagement { get; } = new(); /// /// Gets the instance of the class. /// public static OracleContract Oracle { get; } = new(); /// /// Gets the instance of the class. /// public static Notary Notary { get; } = new(); /// /// Gets the instance of the class. /// public static Treasury Treasury { get; } = new(); /// /// Gets the instance of the class. /// public static TemporaryStorage TemporaryStorage { get; } = new(); #endregion /// /// Gets all native contracts. /// public static IReadOnlyCollection Contracts { get; } = s_contractsList; /// /// The name of the native contract. /// public string Name => GetType().Name; /// /// Since Hardfork has to start having access to the native contract. /// public Hardfork? ActiveIn => Activations.FirstOrDefault(); /// /// The set of hardforks that contract should be updated at, except the ActiveIn /// the first entry is the hardfork when the contract will be activated. /// public virtual ImmutableHashSet Activations { get; } = []; /// /// The hash of the native contract. /// public UInt160 Hash { get; } /// /// The id of the native contract. /// public int Id { get; } = --idCounter; /// /// Initializes a new instance of the class. /// protected NativeContract() { Hash = Helper.GetContractHash(UInt160.Zero, 0, Name); // Reflection to get the methods List listMethods = []; foreach (var member in GetType().GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) { 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(); // Reflection to get the events _eventsDescriptors = GetType().GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, Array.Empty(), null)!. GetCustomAttributes(). // Take into account not only the contract constructor, but also the base type constructor for proper FungibleToken events handling. Concat(GetType().BaseType?.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, Array.Empty(), null)!. GetCustomAttributes() ?? []). OrderBy(p => p.Order).ToList().AsReadOnly(); // Calculate the initializations forks _usedHardforks = _methodDescriptors.Select(u => u.ActiveIn) .Concat(_methodDescriptors.Select(u => u.DeprecatedIn)) .Concat(_eventsDescriptors.Select(u => u.DeprecatedIn)) .Concat(_eventsDescriptors.Select(u => u.ActiveIn)) .Concat(Activations) .Where(u => u.HasValue) .Select(u => u!.Value) .OrderBy(u => (byte)u) .Cast().ToImmutableHashSet(); s_contractsList.Add(this); s_contractsDictionary.Add(Hash, this); } /// /// The allowed methods and his offsets. /// /// Hardfork checker /// Block height. Used to check the hardforks and active methods. /// The . private NativeContractsCache.CacheEntry GetAllowedMethods(IsHardforkEnabledDelegate hfChecker, uint blockHeight) { Dictionary methods = new(); // Reflection to get the ContractMethods byte[] script; using (ScriptBuilder sb = new()) { foreach (ContractMethodMetadata method in _methodDescriptors.Where(u => IsActive(u, hfChecker, blockHeight))) { method.Descriptor.Offset = sb.Length; sb.EmitPush(0); //version methods.Add(sb.Length, method); sb.EmitSysCall(ApplicationEngine.System_Contract_CallNative); sb.Emit(OpCode.RET); } script = sb.ToArray(); } return new NativeContractsCache.CacheEntry(methods, script); } /// /// The of the native contract. /// /// The where the HardForks are configured. /// Block index /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public ContractState GetContractState(ProtocolSettings settings, uint blockHeight) => GetContractState(settings.IsHardforkEnabled, blockHeight); internal static bool IsActive(IHardforkActivable u, IsHardforkEnabledDelegate hfChecker, uint blockHeight) { // Method/event is active iff ActiveIn hardfork IS active AND DeprecatedIn hardfork IS NOT active. return (u.ActiveIn is null || hfChecker(u.ActiveIn.Value, blockHeight)) && (u.DeprecatedIn is null || !hfChecker(u.DeprecatedIn.Value, blockHeight)); } /// /// The of the native contract. /// /// Hardfork checker /// Block height. Used to check hardforks and active methods. /// The . public ContractState GetContractState(IsHardforkEnabledDelegate hfChecker, uint blockHeight) { // Get allowed methods and nef script var allowedMethods = GetAllowedMethods(hfChecker, blockHeight); // Compose nef file var nef = new NefFile() { Compiler = "neo-core-v3.0", Source = string.Empty, Tokens = [], Script = allowedMethods.Script }; nef.CheckSum = NefFile.ComputeChecksum(nef); // Compose manifest var manifest = new ContractManifest() { Name = Name, Groups = [], SupportedStandards = [], Abi = new ContractAbi { Events = _eventsDescriptors .Where(u => IsActive(u, hfChecker, blockHeight)) .Select(p => p.Descriptor).ToArray(), Methods = allowedMethods.Methods.Values .Select(p => p.Descriptor).ToArray() }, Permissions = [ContractPermission.DefaultPermission], Trusts = WildcardContainer.Create(), Extra = null }; OnManifestCompose(hfChecker, blockHeight, manifest); // Return ContractState return new ContractState { Id = Id, Nef = nef, Hash = Hash, Manifest = manifest }; } protected virtual void OnManifestCompose(IsHardforkEnabledDelegate hfChecker, uint blockHeight, ContractManifest manifest) { } /// /// It is the initialize block /// /// The where the HardForks are configured. /// Block index /// Active hardforks /// True if the native contract must be initialized internal bool IsInitializeBlock(ProtocolSettings settings, uint index, [NotNullWhen(true)] out Hardfork[]? hardforks) { var hfs = new List(); // If is in the hardfork height, add them to return array foreach (var hf in _usedHardforks) { if (!settings.Hardforks.TryGetValue(hf, out var activeIn)) { // If hf is not set in the configuration (with EnsureOmmitedHardforks applied over it), it is treated as disabled. continue; } if (activeIn == index) { hfs.Add(hf); } } // Return all initialize hardforks if (hfs.Count > 0) { hardforks = hfs.ToArray(); return true; } // If is not configured, the Genesis is an initialization block. if (index == 0 && ActiveIn is null) { hardforks = hfs.ToArray(); return true; } // Initialized not required hardforks = null; return false; } /// /// Is the native contract active /// /// The where the HardForks are configured. /// Block height /// True if the native contract is active public bool IsActive(ProtocolSettings settings, uint blockHeight) { if (ActiveIn is null) return true; if (!settings.Hardforks.TryGetValue(ActiveIn.Value, out var activeIn)) { // If is not set in the configuration is treated as enabled from the genesis activeIn = 0; } return activeIn <= blockHeight; } /// /// Checks whether the committee has witnessed the current transaction. /// /// The that is executing the contract. /// if the committee has witnessed the current transaction; otherwise, . [MethodImpl(MethodImplOptions.AggressiveInlining)] protected static bool CheckCommittee(ApplicationEngine engine) { var committeeMultiSigAddr = NEO.GetCommitteeAddress(engine.SnapshotCache); return engine.CheckWitnessInternal(committeeMultiSigAddr); } [MethodImpl(MethodImplOptions.AggressiveInlining)] protected static void AssertCommittee(ApplicationEngine engine) { if (!CheckCommittee(engine)) throw new InvalidOperationException("Invalid committee signature. It should be a multisig(len(committee) - (len(committee) - 1) / 2))."); } [MethodImpl(MethodImplOptions.AggressiveInlining)] protected static UInt160 AssertAlmostFullCommittee(ApplicationEngine engine) { // Signed by maximum of (half committee + 1) and (committee - 2) UInt160 committeeMultiSigAddr; var committees = NativeContract.NEO.GetCommittee(engine.SnapshotCache); // Min must be almost the committee address var min = Math.Max(1, committees.Length - (committees.Length - 1) / 2); committeeMultiSigAddr = Contract.CreateMultiSigRedeemScript(Math.Max(min, committees.Length - 2), committees).ToScriptHash(); if (!engine.CheckWitnessInternal(committeeMultiSigAddr)) throw new InvalidOperationException("Invalid committee signature. It should be a multisig(max(1,len(committee) - 2)))."); return committeeMultiSigAddr; } #region Storage keys [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected StorageKey CreateStorageKey(byte prefix) => StorageKey.Create(Id, prefix); [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected StorageKey CreateStorageKey(byte prefix, byte data) => StorageKey.Create(Id, prefix, data); [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected StorageKey CreateStorageKey(byte prefix, int bigEndianKey) => StorageKey.Create(Id, prefix, bigEndianKey); [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected StorageKey CreateStorageKey(byte prefix, uint bigEndianKey) => StorageKey.Create(Id, prefix, bigEndianKey); [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected StorageKey CreateStorageKey(byte prefix, long bigEndianKey) => StorageKey.Create(Id, prefix, bigEndianKey); [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected StorageKey CreateStorageKey(byte prefix, ulong bigEndianKey) => StorageKey.Create(Id, prefix, bigEndianKey); [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected StorageKey CreateStorageKey(byte prefix, ReadOnlySpan content) => StorageKey.Create(Id, prefix, content); [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected StorageKey CreateStorageKey(byte prefix, UInt160 hash) => StorageKey.Create(Id, prefix, hash); [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected StorageKey CreateStorageKey(byte prefix, UInt256 hash) => StorageKey.Create(Id, prefix, hash); [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected StorageKey CreateStorageKey(byte prefix, ECPoint pubKey) => StorageKey.Create(Id, prefix, pubKey); [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected StorageKey CreateStorageKey(byte prefix, UInt256 hash, UInt160 signer) => StorageKey.Create(Id, prefix, hash, signer); [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected StorageKey CreateStorageKey(byte prefix, UInt160 hash, int bigEndian) => StorageKey.Create(Id, prefix, hash, bigEndian); #endregion /// /// Gets the native contract with the specified hash. /// /// The hash of the native contract. /// The native contract with the specified hash. public static NativeContract? GetContract(UInt160 hash) { s_contractsDictionary.TryGetValue(hash, out var contract); 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 { if (version != 0) throw new InvalidOperationException($"The native contract of version {version} is not active."); // Get native contracts invocation cache var currentAllowedMethods = GetContractMethods(engine); // Check if the method is allowed 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}."); var state = context.GetState(); if (!state.CallFlags.HasFlag(method.RequiredCallFlags)) throw new InvalidOperationException($"Cannot call this method with the flag {state.CallFlags}."); // Check native-whitelist if (!engine.IsHardforkEnabled(Hardfork.HF_Faun) || !Policy.IsWhitelistFeeContract(engine.SnapshotCache, Hash, method.Descriptor, out var fixedFee)) { // In the unit of picoGAS, 1 picoGAS = 1e-12 GAS engine.AddFee( (method.CpuFee * engine.ExecFeePicoFactor) + (method.StorageFee * engine.StoragePrice * ApplicationEngine.FeeFactor), false); } List parameters = new(); if (method.NeedApplicationEngine) parameters.Add(engine); if (method.NeedSnapshot) parameters.Add(engine.SnapshotCache); for (int i = 0; i < method.Parameters.Length; i++) parameters.Add(engine.Convert(context.EvaluationStack.Peek(i), method.Parameters[i])); object? returnValue = method.Handler.Invoke(this, parameters.ToArray()); if (returnValue is ContractTask task) { await task; returnValue = task.GetResult(); } for (int i = 0; i < method.Parameters.Length; i++) { context.EvaluationStack.Pop(); } if (method.Handler.ReturnType != typeof(void) && method.Handler.ReturnType != typeof(ContractTask)) { context.EvaluationStack.Push(engine.Convert(returnValue)); } } catch (Exception ex) { engine.Throw(ex); } } /// /// Determine whether the specified contract is a native contract. /// /// The hash of the contract. /// if the contract is native; otherwise, . public static bool IsNative(UInt160 hash) { return s_contractsDictionary.ContainsKey(hash); } internal virtual ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardFork) { return ContractTask.CompletedTask; } internal virtual ContractTask OnPersistAsync(ApplicationEngine engine) { return ContractTask.CompletedTask; } internal virtual ContractTask PostPersistAsync(ApplicationEngine engine) { return ContractTask.CompletedTask; } } }