Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow contracts to load script at runtime dynamically #2756

Merged
merged 12 commits into from
Sep 16, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ protected override void DeserializeWithoutType(ref MemoryReader reader, int maxN

public override bool Match(ApplicationEngine engine)
{
return engine.CallingScriptHash is null || engine.CallingScriptHash == engine.EntryScriptHash;
var state = engine.CurrentContext.GetState<ExecutionContextState>();
return state.CallingContext is null || state.CallingContext == engine.EntryContext;
}

protected override void SerializeWithoutType(BinaryWriter writer)
Expand Down
2 changes: 1 addition & 1 deletion src/Neo/SmartContract/ApplicationEngine.Contract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ protected internal void CallContract(UInt160 contractHash, string method, CallFl
bool hasReturnValue = md.ReturnType != ContractParameterType.Void;

ExecutionContext context = CallContractInternal(contract, md, callFlags, hasReturnValue, args);
if (!hasReturnValue) context.GetState<ExecutionContextState>().PushNullWhenReturn = true;
context.GetState<ExecutionContextState>().IsDynamicCall = true;
}

/// <summary>
Expand Down
30 changes: 30 additions & 0 deletions src/Neo/SmartContract/ApplicationEngine.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Neo.IO;
using Neo.Network.P2P.Payloads;
using Neo.SmartContract.Native;
using Neo.VM;
using Neo.VM.Types;
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -90,6 +91,12 @@ partial class ApplicationEngine
/// </summary>
public static readonly InteropDescriptor System_Runtime_GetEntryScriptHash = Register("System.Runtime.GetEntryScriptHash", nameof(EntryScriptHash), 1 << 4, CallFlags.None);

/// <summary>
/// The <see cref="InteropDescriptor"/> of System.Runtime.LoadScript.
/// Loads a script at rumtime.
/// </summary>
public static readonly InteropDescriptor System_Runtime_LoadScript = Register("System.Runtime.LoadScript", nameof(RuntimeLoadScript), 1 << 15, CallFlags.AllowCall);

/// <summary>
/// The <see cref="InteropDescriptor"/> of System.Runtime.CheckWitness.
/// Determines whether the specified account has witnessed the current transaction.
Expand Down Expand Up @@ -189,6 +196,27 @@ protected internal StackItem GetScriptContainer()
return interop.ToStackItem(ReferenceCounter);
}

/// <summary>
/// The implementation of System.Runtime.LoadScript.
/// Loads a script at rumtime.
/// </summary>
protected internal void RuntimeLoadScript(byte[] script, CallFlags callFlags, Array args)
shargon marked this conversation as resolved.
Show resolved Hide resolved
{
if ((callFlags & ~CallFlags.All) != 0)
throw new ArgumentOutOfRangeException(nameof(callFlags));

ExecutionContextState state = CurrentContext.GetState<ExecutionContextState>();
ExecutionContext context = LoadScript(script, configureState: p =>
{
p.CallingContext = CurrentContext;
p.CallFlags = callFlags & state.CallFlags;
p.IsDynamicCall = true;
});

for (int i = args.Count - 1; i >= 0; i--)
context.EvaluationStack.Push(args[i]);
}

/// <summary>
/// The implementation of System.Runtime.CheckWitness.
/// Determines whether the specified account has witnessed the current transaction.
Expand Down Expand Up @@ -306,6 +334,8 @@ protected internal void RuntimeLog(byte[] state)
protected internal void RuntimeNotify(byte[] eventName, Array state)
{
if (eventName.Length > MaxEventName) throw new ArgumentException(null, nameof(eventName));
if (CurrentContext.GetState<ExecutionContextState>().Contract is null)
shargon marked this conversation as resolved.
Show resolved Hide resolved
throw new InvalidOperationException("Notifications are not allowed in dynamic scripts.");
using MemoryStream ms = new(MaxNotificationSize);
using BinaryWriter writer = new(ms, Utility.StrictUTF8, true);
BinarySerializer.Serialize(writer, state, MaxNotificationSize);
Expand Down
28 changes: 21 additions & 7 deletions src/Neo/SmartContract/ApplicationEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,15 @@ public partial class ApplicationEngine : ExecutionEngine
/// <summary>
/// The script hash of the calling contract. This field could be <see langword="null"/> if the current context is the entry context.
/// </summary>
public UInt160 CallingScriptHash => CurrentContext?.GetState<ExecutionContextState>().CallingScriptHash;
public UInt160 CallingScriptHash
{
get
{
if (CurrentContext is null) return null;
var state = CurrentContext.GetState<ExecutionContextState>();
return state.NativeCallingScriptHash ?? state.CallingContext?.GetState<ExecutionContextState>().ScriptHash;
}
}

/// <summary>
/// The script hash of the entry context. This field could be <see langword="null"/> if no context is loaded to the engine.
Expand Down Expand Up @@ -224,15 +232,15 @@ private ExecutionContext CallContractInternal(ContractState contract, ContractMe
invocationCounter[contract.Hash] = 1;
}

ExecutionContextState state = CurrentContext.GetState<ExecutionContextState>();
UInt160 callingScriptHash = state.ScriptHash;
ExecutionContext currentContext = CurrentContext;
ExecutionContextState state = currentContext.GetState<ExecutionContextState>();
CallFlags callingFlags = state.CallFlags;

if (args.Count != method.Parameters.Length) throw new InvalidOperationException($"Method {method} Expects {method.Parameters.Length} Arguments But Receives {args.Count} Arguments");
if (hasReturnValue ^ (method.ReturnType != ContractParameterType.Void)) throw new InvalidOperationException("The return value type does not match.");
ExecutionContext context_new = LoadContract(contract, method, flags & callingFlags);
state = context_new.GetState<ExecutionContextState>();
state.CallingScriptHash = callingScriptHash;
state.CallingContext = currentContext;

for (int i = args.Count - 1; i >= 0; i--)
context_new.EvaluationStack.Push(args[i]);
Expand All @@ -244,7 +252,7 @@ internal ContractTask CallFromNativeContract(UInt160 callingScriptHash, UInt160
{
ExecutionContext context_new = CallContractInternal(hash, method, CallFlags.All, false, args);
ExecutionContextState state = context_new.GetState<ExecutionContextState>();
state.CallingScriptHash = callingScriptHash;
state.NativeCallingScriptHash = callingScriptHash;
ContractTask task = new();
contractTasks.Add(context_new, task.GetAwaiter());
return task;
Expand All @@ -254,7 +262,7 @@ internal ContractTask<T> CallFromNativeContract<T>(UInt160 callingScriptHash, UI
{
ExecutionContext context_new = CallContractInternal(hash, method, CallFlags.All, true, args);
ExecutionContextState state = context_new.GetState<ExecutionContextState>();
state.CallingScriptHash = callingScriptHash;
state.NativeCallingScriptHash = callingScriptHash;
ContractTask<T> task = new();
contractTasks.Add(context_new, task.GetAwaiter());
return task;
Expand All @@ -273,7 +281,13 @@ protected override void ContextUnloaded(ExecutionContext context)
{
ExecutionContextState contextState = CurrentContext.GetState<ExecutionContextState>();
contextState.NotificationCount += state.NotificationCount;
if (state.PushNullWhenReturn) Push(StackItem.Null);
if (state.IsDynamicCall)
{
if (context.EvaluationStack.Count == 0)
Push(StackItem.Null);
else if (context.EvaluationStack.Count > 1)
throw new NotSupportedException("Multiple return values are not allowed in cross-contract calls.");
}
}
}
else
Expand Down
9 changes: 7 additions & 2 deletions src/Neo/SmartContract/ExecutionContextState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ public class ExecutionContextState
/// <summary>
/// The script hash of the calling contract.
/// </summary>
public UInt160 CallingScriptHash { get; set; }
public ExecutionContext CallingContext { get; set; }

/// <summary>
/// Used in native contracts only.
/// </summary>
internal UInt160 NativeCallingScriptHash { get; set; }

/// <summary>
/// The <see cref="ContractState"/> of the current context.
Expand All @@ -42,6 +47,6 @@ public class ExecutionContextState

public int NotificationCount { get; set; }

public bool PushNullWhenReturn { get; set; }
public bool IsDynamicCall { get; set; }
}
}
4 changes: 2 additions & 2 deletions tests/Neo.UnitTests/Extensions/NativeContractExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public static void UpdateContract(this DataCache snapshot, UInt160 callingScript
// Fake calling script hash
if (callingScriptHash != null)
{
engine.CurrentContext.GetState<ExecutionContextState>().CallingScriptHash = callingScriptHash;
engine.CurrentContext.GetState<ExecutionContextState>().NativeCallingScriptHash = callingScriptHash;
engine.CurrentContext.GetState<ExecutionContextState>().ScriptHash = callingScriptHash;
}

Expand All @@ -65,7 +65,7 @@ public static void DestroyContract(this DataCache snapshot, UInt160 callingScrip
// Fake calling script hash
if (callingScriptHash != null)
{
engine.CurrentContext.GetState<ExecutionContextState>().CallingScriptHash = callingScriptHash;
engine.CurrentContext.GetState<ExecutionContextState>().NativeCallingScriptHash = callingScriptHash;
engine.CurrentContext.GetState<ExecutionContextState>().ScriptHash = callingScriptHash;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public void TestNotSupportedNotification()
{
using var engine = ApplicationEngine.Create(TriggerType.Application, null, null, TestBlockchain.TheNeoSystem.GenesisBlock, settings: TestBlockchain.TheNeoSystem.Settings, gas: 1100_00000000);
engine.LoadScript(Array.Empty<byte>());
engine.CurrentContext.GetState<ExecutionContextState>().Contract = new();

// circular

Expand Down
2 changes: 2 additions & 0 deletions tests/Neo.UnitTests/SmartContract/UT_InteropService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public void Runtime_GetNotifications_Test()
// Execute

engine.LoadScript(script.ToArray());
engine.CurrentContext.GetState<ExecutionContextState>().Contract = new();
var currentScriptHash = engine.EntryScriptHash;

Assert.AreEqual(VMState.HALT, engine.Execute());
Expand Down Expand Up @@ -145,6 +146,7 @@ public void Runtime_GetNotifications_Test()
// Execute

engine.LoadScript(script.ToArray());
engine.CurrentContext.GetState<ExecutionContextState>().Contract = new();
var currentScriptHash = engine.EntryScriptHash;

Assert.AreEqual(VMState.HALT, engine.Execute());
Expand Down