using Neo.IO;
using Neo.SmartContract.Manifest;
using Neo.VM;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Neo.SmartContract.Native;
public abstract class NativeContract
{
private class NativeContractsCache
{
public record CacheEntry(Dictionary<int, ContractMethodMetadata> Methods, byte[] Script);
internal Dictionary<int, CacheEntry> 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<NativeContract> s_contractsList = [];
private static readonly Dictionary<UInt160, NativeContract> s_contractsDictionary = new();
private readonly ImmutableHashSet<Hardfork> _usedHardforks;
private readonly ReadOnlyCollection<ContractMethodMetadata> _methodDescriptors;
private readonly ReadOnlyCollection<ContractEventAttribute> _eventsDescriptors;
#region Named Native Contracts
public static ContractManagement ContractManagement { get; } = new();
public static StdLib StdLib { get; } = new();
public static CryptoLib CryptoLib { get; } = new();
public static LedgerContract Ledger { get; } = new();
public static PolicyContract Policy { get; } = new();
public static RoleManagement RoleManagement { get; } = new();
public static OracleContract Oracle { get; } = new();
public static Notary Notary { get; } = new();
public static Treasury Treasury { get; } = new();
public static TokenManagement TokenManagement { get; } = new();
public static Governance Governance { get; } = new();
#endregion
public static IReadOnlyCollection<NativeContract> Contracts { get; } = s_contractsList;
public string Name => GetType().Name;
public virtual Hardfork? ActiveIn { get; } = null;
public UInt160 Hash { get; }
public int Id { get; }
protected NativeContract(int id)
{
Id = id;
Hash = Helper.GetContractHash(UInt160.Zero, 0, Name);
List<ContractMethodMetadata> listMethods = [];
foreach (var member in GetType().GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public))
{
foreach (var attribute in member.GetCustomAttributes<ContractMethodAttribute>())
{
listMethods.Add(new ContractMethodMetadata(member, attribute));
}
}
_methodDescriptors = listMethods.OrderBy(p => p.Name, StringComparer.Ordinal).ThenBy(p => p.Parameters.Length).ToList().AsReadOnly();
_eventsDescriptors = GetType()
.GetCustomAttributes<ContractEventAttribute>(true)
.OrderBy(p => p.Order)
.ToList()
.AsReadOnly();
_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([ActiveIn])
.Where(u => u.HasValue)
.Select(u => u!.Value)
.OrderBy(u => (byte)u)
.Cast<Hardfork>().ToImmutableHashSet();
s_contractsList.Add(this);
s_contractsDictionary.Add(Hash, this);
}
private NativeContractsCache.CacheEntry GetAllowedMethods(IsHardforkEnabledDelegate hfChecker, uint blockHeight)
{
Dictionary<int, ContractMethodMetadata> methods = new();
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); methods.Add(sb.Length, method);
sb.EmitSysCall(ApplicationEngine.System_Contract_CallNative);
sb.Emit(OpCode.RET);
}
script = sb.ToArray();
}
return new NativeContractsCache.CacheEntry(methods, script);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ContractState GetContractState(ProtocolSettings settings, uint blockHeight) => GetContractState(settings.IsHardforkEnabled, blockHeight);
internal static bool IsActive(IHardforkActivable u, IsHardforkEnabledDelegate hfChecker, uint blockHeight)
{
return u.ActiveIn is null && u.DeprecatedIn is null ||
u.DeprecatedIn is not null && hfChecker(u.DeprecatedIn.Value, blockHeight) == false ||
u.ActiveIn is not null && hfChecker(u.ActiveIn.Value, blockHeight);
}
public ContractState GetContractState(IsHardforkEnabledDelegate hfChecker, uint blockHeight)
{
var allowedMethods = GetAllowedMethods(hfChecker, blockHeight);
var nef = new NefFile()
{
Compiler = "neo-core-v3.0",
Source = string.Empty,
Tokens = [],
Script = allowedMethods.Script
};
nef.CheckSum = NefFile.ComputeChecksum(nef);
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<ContractPermissionDescriptor>.Create(),
Extra = null
};
OnManifestCompose(hfChecker, blockHeight, manifest);
return new ContractState
{
Id = Id,
Nef = nef,
Hash = Hash,
Manifest = manifest
};
}
protected virtual void OnManifestCompose(IsHardforkEnabledDelegate hfChecker, uint blockHeight, ContractManifest manifest) { }
internal bool IsInitializeBlock(ProtocolSettings settings, uint index, [NotNullWhen(true)] out Hardfork[]? hardforks)
{
var hfs = new List<Hardfork>();
foreach (var hf in _usedHardforks)
{
if (!settings.Hardforks.TryGetValue(hf, out var activeIn))
{
continue;
}
if (activeIn == index)
{
hfs.Add(hf);
}
}
if (hfs.Count > 0)
{
hardforks = hfs.ToArray();
return true;
}
if (index == 0 && ActiveIn is null)
{
hardforks = hfs.ToArray();
return true;
}
hardforks = null;
return false;
}
public bool IsActive(ProtocolSettings settings, uint blockHeight)
{
if (ActiveIn is null) return true;
if (!settings.Hardforks.TryGetValue(ActiveIn.Value, out var activeIn))
{
activeIn = 0;
}
return activeIn <= blockHeight;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static bool CheckCommittee(ApplicationEngine engine)
{
var committeeMultiSigAddr = Governance.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)).");
}
protected void Notify(ApplicationEngine engine, string eventName, params object?[] args)
{
engine.SendNotification(Hash, eventName, new(engine.ReferenceCounter, args.Select(engine.Convert)));
}
#region Storage keys
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix) => new KeyBuilder(Id, prefix);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, byte data) => new KeyBuilder(Id, prefix) { data };
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey<T>(byte prefix, T bigEndianKey) where T : unmanaged => new KeyBuilder(Id, prefix) { bigEndianKey };
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, ReadOnlySpan<byte> content) => new KeyBuilder(Id, prefix) { content };
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, params IEnumerable<ISerializableSpan> serializables)
{
var builder = new KeyBuilder(Id, prefix);
foreach (var serializable in serializables)
builder.Add(serializable);
return builder;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, UInt160 hash, int bigEndianKey)
=> new KeyBuilder(Id, prefix) { hash, bigEndianKey };
#endregion
public static NativeContract? GetContract(UInt160 hash)
{
s_contractsDictionary.TryGetValue(hash, out var contract);
return contract;
}
internal Dictionary<int, ContractMethodMetadata> 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.");
var currentAllowedMethods = GetContractMethods(engine);
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<ExecutionContextState>();
if (!state.CallFlags.HasFlag(method.RequiredCallFlags))
throw new InvalidOperationException($"Cannot call this method with the flag {state.CallFlags}.");
if (!Policy.IsWhitelistFeeContract(engine.SnapshotCache, Hash, method.Descriptor, out var fixedFee))
{
engine.AddFee(method.CpuFee * engine.ExecFeeFactor + method.StorageFee * engine.StoragePrice);
}
List<object?> 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);
}
}
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;
}
}