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

Add ITraceDebugPlugin infrastructure #1724

Closed
wants to merge 14 commits into from
8 changes: 8 additions & 0 deletions src/neo/Ledger/Blockchain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -427,10 +427,18 @@ private void Persist(Block block)
clonedSnapshot.Transactions.Add(tx.Hash, state);
clonedSnapshot.Transactions.Commit();

// For now, only support a first trace debug plugin that returns true to ShouldTrace
var traceDebugPlugin = Plugin.TraceDebugPlugins
devhawk marked this conversation as resolved.
Show resolved Hide resolved
.Where(p => p.ShouldTrace(block.Header, tx))
.FirstOrDefault();

using var traceDebugSink = traceDebugPlugin?.GetSink(block.Header, tx);

using (ApplicationEngine engine = new ApplicationEngine(TriggerType.Application, tx, clonedSnapshot, tx.SystemFee))
{
engine.LoadScript(tx.Script);
state.VMState = engine.Execute();
traceDebugSink?.Results(state.VMState, engine.GasConsumed, engine.ResultStack);
if (state.VMState == VMState.HALT)
{
clonedSnapshot.Commit();
Expand Down
10 changes: 10 additions & 0 deletions src/neo/Plugins/ITraceDebugPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Neo.Network.P2P.Payloads;

namespace Neo.Plugins
{
public interface ITraceDebugPlugin
{
bool ShouldTrace(Header header, Transaction tx);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is not enough with GetSink ? it's needed ShouldTrace?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea here is that you don't likely want to trace every tx. For example, if I deploy a contract to mainnet, I can also deploy a mainnet node with the debugger plugin to capture every run of my contract for later analysis if need be. But I don't want to capture every tx - just mine. This API allows me to cleanly separate the logic that decides if a given tx should be traced vs. the logic that does the tracing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this logic can be done in the plugin, if you don't want to trace this TX, just ignore it, isn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it could be done other ways. I feel this approach provides more flexibility in case we want to support multiple tracers for a single transaction.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think @erikzhang ?

ITraceDebugSink GetSink(Header header, Transaction tx);
}
}
17 changes: 17 additions & 0 deletions src/neo/Plugins/ITraceDebugSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using Neo.Ledger;
using Neo.SmartContract;
using Neo.VM;

namespace Neo.Plugins
{
public interface ITraceDebugSink : IDisposable
{
void Trace(VMState vmState, IReadOnlyCollection<ExecutionContext> stackFrames,
IEnumerable<(UInt160 scriptHash, byte[] key, StorageItem item)> storages);
void Log(LogEventArgs args);
void Notify(NotifyEventArgs args);
void Results(VMState vmState, long gasConsumed, IReadOnlyCollection<Neo.VM.Types.StackItem> results);
}
}
2 changes: 2 additions & 0 deletions src/neo/Plugins/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public abstract class Plugin : IDisposable
internal static readonly List<IPersistencePlugin> PersistencePlugins = new List<IPersistencePlugin>();
internal static readonly List<IP2PPlugin> P2PPlugins = new List<IP2PPlugin>();
internal static readonly List<IMemoryPoolTxObserverPlugin> TxObserverPlugins = new List<IMemoryPoolTxObserverPlugin>();
internal static readonly List<ITraceDebugPlugin> TraceDebugPlugins = new List<ITraceDebugPlugin>();

public static readonly string PluginsDirectory = Combine(GetDirectoryName(Assembly.GetEntryAssembly().Location), "Plugins");
private static readonly FileSystemWatcher configWatcher;
Expand Down Expand Up @@ -54,6 +55,7 @@ protected Plugin()
if (this is IP2PPlugin p2p) P2PPlugins.Add(p2p);
if (this is IPersistencePlugin persistence) PersistencePlugins.Add(persistence);
if (this is IMemoryPoolTxObserverPlugin txObserver) TxObserverPlugins.Add(txObserver);
if (this is ITraceDebugPlugin traceDebug) TraceDebugPlugins.Add(traceDebug);

Configure();
}
Expand Down
8 changes: 7 additions & 1 deletion src/neo/SmartContract/ApplicationEngine.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,12 @@ internal void RuntimeLog(byte[] state)
{
if (state.Length > MaxNotificationSize) throw new ArgumentException();
string message = Utility.StrictUTF8.GetString(state);
Log?.Invoke(this, new LogEventArgs(ScriptContainer, CurrentScriptHash, message));
if (Log != null || traceDebugSink != null)
{
var args = new LogEventArgs(ScriptContainer, CurrentScriptHash, message);
Log?.Invoke(this, args);
traceDebugSink?.Log(args);
}
}

internal void RuntimeNotify(byte[] eventName, Array state)
Expand All @@ -157,6 +162,7 @@ internal void SendNotification(UInt160 hash, string eventName, Array state)
{
NotifyEventArgs notification = new NotifyEventArgs(ScriptContainer, hash, eventName, (Array)state.DeepCopy());
Notify?.Invoke(this, notification);
traceDebugSink?.Notify(notification);
notifications.Add(notification);
}

Expand Down
43 changes: 42 additions & 1 deletion src/neo/SmartContract/ApplicationEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Neo.Ledger;
using Neo.Network.P2P.Payloads;
using Neo.Persistence;
using Neo.Plugins;
using Neo.VM;
using Neo.VM.Types;
using System;
Expand Down Expand Up @@ -31,10 +32,12 @@ private class InvocationState
private static Dictionary<uint, InteropDescriptor> services;
private readonly long gas_amount;
private readonly bool testMode;
private bool preExecTrace = true;
private readonly List<NotifyEventArgs> notifications = new List<NotifyEventArgs>();
private readonly List<IDisposable> disposables = new List<IDisposable>();
private readonly Dictionary<UInt160, int> invocationCounter = new Dictionary<UInt160, int>();
private readonly Dictionary<ExecutionContext, InvocationState> invocationStates = new Dictionary<ExecutionContext, InvocationState>();
private readonly ITraceDebugSink traceDebugSink;

public static IReadOnlyDictionary<uint, InteropDescriptor> Services => services;
public TriggerType Trigger { get; }
Expand All @@ -47,10 +50,11 @@ private class InvocationState
public UInt160 EntryScriptHash => EntryContext?.GetState<ExecutionContextState>().ScriptHash;
public IReadOnlyList<NotifyEventArgs> Notifications => notifications;

public ApplicationEngine(TriggerType trigger, IVerifiable container, StoreView snapshot, long gas, bool testMode = false)
public ApplicationEngine(TriggerType trigger, IVerifiable container, StoreView snapshot, long gas, bool testMode = false, ITraceDebugSink traceDebugSink = null)
devhawk marked this conversation as resolved.
Show resolved Hide resolved
{
this.gas_amount = GasFree + gas;
this.testMode = testMode;
this.traceDebugSink = traceDebugSink;
this.Trigger = trigger;
this.ScriptContainer = container;
this.Snapshot = snapshot;
Expand Down Expand Up @@ -221,12 +225,49 @@ protected override void OnSysCall(uint method)
Push(Convert(returnValue));
}

internal void Trace()
{
if (traceDebugSink != null)
{
var storages = InvocationStack
.Select(ec => ec.GetScriptHash())
.Distinct()
.SelectMany(scriptHash =>
{
var contractState = Snapshot.Contracts.TryGet(scriptHash);
return contractState != null
? Snapshot.Storages
.Find(StorageKey.CreateSearchPrefix(contractState.Id, default))
.Select(t => (scriptHash, t.Key.Key, t.Value))
: Enumerable.Empty<(UInt160, byte[], StorageItem)>();
});

traceDebugSink.Trace(State, InvocationStack, storages);
}
}

protected override void PreExecuteInstruction()
{
if (preExecTrace)
{
Trace();
preExecTrace = false;
}

if (CurrentContext.InstructionPointer < CurrentContext.Script.Length)
AddGas(OpCodePrices[CurrentContext.CurrentInstruction.OpCode]);
}

protected override void PostExecuteInstruction(Instruction instruction)
{
Trace();
}

protected override void OnStateChanged()
{
Trace();
}

private static Block CreateDummyBlock(StoreView snapshot)
{
var currentBlock = snapshot.Blocks[snapshot.CurrentBlockHash];
Expand Down