Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ public class CompilationContext

public bool Success => _diagnostics.All(p => p.Severity != DiagnosticSeverity.Error);
public IReadOnlyList<Diagnostic> Diagnostics => _diagnostics;
// TODO: basename should not work when multiple contracts exit in one project
public string? ContractName => _displayName ?? Options.BaseName ?? _className;
internal string ClassName => _className ?? _targetContract.Name;
private string? Source { get; set; }

internal IEnumerable<IFieldSymbol> StaticFieldSymbols => _staticFields.OrderBy(p => p.Value).Select(p => p.Key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ namespace Neo.Optimizer
{
static class OptimizedScriptBuilder
{
private static readonly IReadOnlyDictionary<OpCode, (OpCode LongOp, int SizeDelta)> ShortToLongJumpMap =
Copy link
Contributor

@Wi1l-B0t Wi1l-B0t Oct 24, 2025

Choose a reason for hiding this comment

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

This is a part of 'Expose policy governance APIs' ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed, this pr focus on only exposing poliocy govergance API now

new Dictionary<OpCode, (OpCode, int)>
{
{ OpCode.JMP, (OpCode.JMP_L, 3) },
{ OpCode.JMPIF, (OpCode.JMPIF_L, 3) },
{ OpCode.JMPIFNOT, (OpCode.JMPIFNOT_L, 3) },
{ OpCode.JMPEQ, (OpCode.JMPEQ_L, 3) },
{ OpCode.JMPNE, (OpCode.JMPNE_L, 3) },
{ OpCode.JMPGT, (OpCode.JMPGT_L, 3) },
{ OpCode.JMPGE, (OpCode.JMPGE_L, 3) },
{ OpCode.JMPLT, (OpCode.JMPLT_L, 3) },
{ OpCode.JMPLE, (OpCode.JMPLE_L, 3) },
{ OpCode.CALL, (OpCode.CALL_L, 3) },
{ OpCode.ENDTRY, (OpCode.ENDTRY_L, 3) },
};

/// <summary>
/// Build script with instruction dictionary and jump
/// </summary>
Expand All @@ -37,19 +53,27 @@ public static Script BuildScriptWithJumpTargets(
Dictionary<int, Instruction>? oldAddressToInstruction = null)
{
List<byte> simplifiedScript = new();
int instructionIndex = 0;
foreach (DictionaryEntry item in simplifiedInstructionsToAddress)
{
(Instruction i, int a) = ((Instruction)item.Key, (int)item.Value!);
simplifiedScript.Add((byte)i.OpCode);
int operandSizeLength = OperandSizePrefixTable[(int)i.OpCode];
simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(i.Operand.Length)[0..operandSizeLength]).ToList();
if (jumpSourceToTargets.TryGetValue(i, out Instruction? dst))
bool operandEmitted = false;
OpCode opcodeToEmit = i.OpCode;
bool upgradedToLong = false;
int delta = 0;
jumpSourceToTargets.TryGetValue(i, out Instruction? maybeTarget);
bool hasJumpTarget = maybeTarget is not null;
if (hasJumpTarget)
{
int delta;
if (simplifiedInstructionsToAddress.Contains(dst)) // target instruction not deleted
Instruction dst = maybeTarget!;
if (simplifiedInstructionsToAddress.Contains(dst))
{
delta = (int)simplifiedInstructionsToAddress[dst]! - a;
}
else if (i.OpCode == OpCode.PUSHA || i.OpCode == OpCode.ENDTRY || i.OpCode == OpCode.ENDTRY_L)
{
delta = 0; // TODO: decide a good target
}
else
{
if (oldAddressToInstruction != null)
Expand All @@ -58,15 +82,36 @@ public static Script BuildScriptWithJumpTargets(
throw new BadScriptException($"Target instruction of {i.OpCode} at old address {oldAddress} is deleted");
throw new BadScriptException($"Target instruction of {i.OpCode} at new address {a} is deleted");
}
if (i.OpCode == OpCode.JMP || conditionalJump.Contains(i.OpCode) || i.OpCode == OpCode.CALL || i.OpCode == OpCode.ENDTRY)
if (sbyte.MinValue <= delta && delta <= sbyte.MaxValue)
simplifiedScript.Add(BitConverter.GetBytes(delta)[0]);
else
// TODO: build with _L version
throw new NotImplementedException($"Need {i.OpCode}_L for delta={delta}");
if (i.OpCode == OpCode.PUSHA || i.OpCode == OpCode.JMP_L || conditionalJump_L.Contains(i.OpCode) || i.OpCode == OpCode.CALL_L || i.OpCode == OpCode.ENDTRY_L)

if (ShortToLongJumpMap.TryGetValue(opcodeToEmit, out (OpCode LongOp, int SizeDelta) map) && (delta < sbyte.MinValue || delta > sbyte.MaxValue))
{
opcodeToEmit = map.LongOp;
upgradedToLong = true;
AdjustInstructionAddresses(simplifiedInstructionsToAddress, instructionIndex + 1, map.SizeDelta);
if (simplifiedInstructionsToAddress.Contains(dst))
delta = (int)simplifiedInstructionsToAddress[dst]! - a;
}
}

simplifiedScript.Add((byte)opcodeToEmit);
int operandSizeLength = OperandSizePrefixTable[(int)opcodeToEmit];
simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(i.Operand.Length)[0..operandSizeLength]).ToList();
if (hasJumpTarget)
{
if (opcodeToEmit == OpCode.JMP || conditionalJump.Contains(opcodeToEmit) || opcodeToEmit == OpCode.CALL || opcodeToEmit == OpCode.ENDTRY)
{
simplifiedScript.Add(BitConverter.GetBytes(delta)[0]);
operandEmitted = true;
}
else if (opcodeToEmit == OpCode.PUSHA || opcodeToEmit == OpCode.JMP_L || conditionalJump_L.Contains(opcodeToEmit) || opcodeToEmit == OpCode.CALL_L || opcodeToEmit == OpCode.ENDTRY_L)
{
simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(delta)).ToList();
continue;
operandEmitted = true;
}
else if (upgradedToLong)
{
throw new NotImplementedException($"Long form emission missing handler for {opcodeToEmit}.");
}
}
if (trySourceToTargets.TryGetValue(i, out (Instruction dst1, Instruction dst2) dsts))
{
Expand All @@ -82,15 +127,26 @@ public static Script BuildScriptWithJumpTargets(
simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(delta1)).ToList();
simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(delta2)).ToList();
}
continue;
operandEmitted = true;
}
if (i.Operand.Length != 0)
if (!operandEmitted && i.Operand.Length != 0)
simplifiedScript = simplifiedScript.Concat(i.Operand.ToArray()).ToList();
instructionIndex++;
}
Script script = new(simplifiedScript.ToArray());
return script;
}

private static void AdjustInstructionAddresses(System.Collections.Specialized.OrderedDictionary instructionsToAddress, int startIndex, int sizeDelta)
{
if (sizeDelta == 0) return;
for (int idx = startIndex; idx < instructionsToAddress.Count; idx++)
{
int current = (int)instructionsToAddress[idx]!;
instructionsToAddress[idx] = current + sizeDelta;
}
}

/// <summary>
/// Typically used when you delete the oldTarget from script
/// and the newTarget is the first following instruction undeleted in script
Expand Down
13 changes: 13 additions & 0 deletions src/Neo.Compiler.CSharp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,19 @@ private static int ProcessSources(Options options, string folder, string[] sourc

private static int ProcessOutputs(Options options, string folder, List<CompilationContext> contexts)
{
if (!string.IsNullOrEmpty(options.BaseName) && contexts.Count > 1)
{
string[] uniqueContracts = contexts
.Select(c => c.ClassName)
.Distinct(StringComparer.InvariantCulture)
.ToArray();
if (uniqueContracts.Length > 1)
{
Console.Error.WriteLine("The --base-name option can only be used when compiling a single contract. Contracts found: {0}", string.Join(", ", uniqueContracts));
return 1;
}
}

int result = 0;
List<Exception> exceptions = new();
foreach (CompilationContext context in contexts)
Expand Down
33 changes: 30 additions & 3 deletions src/Neo.Compiler.CSharp/SecurityAnalyzer/ReEntrancyAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Neo.SmartContract.Testing.Coverage;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

Expand All @@ -36,8 +37,6 @@ public class ReEntrancyVulnerabilityPair
public readonly Dictionary<BasicBlock, HashSet<int>> callOtherContractInstructions;
public readonly Dictionary<BasicBlock, HashSet<int>> writeStorageInstructions;
public JToken? DebugInfo { get; init; }
// TODO: use debugInfo to GetWarningInfo with source codes

public ReEntrancyVulnerabilityPair(
Dictionary<BasicBlock, HashSet<BasicBlock>> vulnerabilityPairs,
Dictionary<BasicBlock, HashSet<int>> callOtherContractInstructions,
Expand Down Expand Up @@ -243,17 +242,45 @@ private class SourceLocation
if (sequencePoint.Document >= 0 && sequencePoint.Document < debugInfo.Documents.Count)
{
var fileName = debugInfo.Documents[sequencePoint.Document];
var snippet = TryGetSourceLine(fileName, sequencePoint.Start.Line);
return new SourceLocation
{
FileName = System.IO.Path.GetFileName(fileName),
Line = sequencePoint.Start.Line,
Column = sequencePoint.Start.Column,
CodeSnippet = null // Could be enhanced to read actual source code
CodeSnippet = snippet
};
}
}
}
return null;
}

private static string? TryGetSourceLine(string fileName, int zeroBasedLineNumber)
{
string? path = fileName;
if (!Path.IsPathRooted(path))
{
path = Path.Combine(Environment.CurrentDirectory, path);
}

try
{
if (path != null && File.Exists(path))
{
string[] lines = File.ReadAllLines(path);
foreach (int candidate in new[] { zeroBasedLineNumber, zeroBasedLineNumber - 1 })
{
if (candidate >= 0 && candidate < lines.Length)
return lines[candidate].Trim();
}
}
}
catch
{
// ignore IO issues and fall back to raw offsets
}
return null;
}
}
}
31 changes: 28 additions & 3 deletions src/Neo.Compiler.CSharp/SecurityAnalyzer/WriteInTryAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Neo.SmartContract.Testing.Coverage;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

Expand All @@ -28,8 +29,6 @@ public class WriteInTryVulnerability
// key block writes storage; value blocks in try
public readonly Dictionary<BasicBlock, HashSet<int>> vulnerabilities;
public JToken? DebugInfo { get; init; }
// TODO: use debugInfo to GetWarningInfo with source codes

public WriteInTryVulnerability(Dictionary<BasicBlock, HashSet<int>> vulnerabilities, JToken? debugInfo = null)
{
this.vulnerabilities = vulnerabilities;
Expand Down Expand Up @@ -211,19 +210,45 @@ private class SourceLocation
if (sequencePoint.Document >= 0 && sequencePoint.Document < debugInfo.Documents.Count)
{
var fileName = debugInfo.Documents[sequencePoint.Document];
var snippet = TryGetSourceLine(fileName, sequencePoint.Start.Line);
return new SourceLocation
{
FileName = System.IO.Path.GetFileName(fileName),
Line = sequencePoint.Start.Line,
Column = sequencePoint.Start.Column,
CodeSnippet = null // Could be enhanced to read actual source code
CodeSnippet = snippet
};
}
}
}
return null;
}

private static string? TryGetSourceLine(string fileName, int zeroBasedLineNumber)
{
string? path = fileName;
if (!Path.IsPathRooted(path))
path = Path.Combine(Environment.CurrentDirectory, path);

try
{
if (path != null && File.Exists(path))
Copy link
Member

Choose a reason for hiding this comment

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

The method are duplicated, and it's better to use !string.IsNullOrEmpty

Copy link
Member

Choose a reason for hiding this comment

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

I think it's possible to simplify the method.

    private static string? TryGetSourceSnippet(string fileName, int startZeroBasedLineNumber, int endZeroBasedLineNumber)
    {
        if (string.IsNullOrWhiteSpace(fileName)) return null;
        var path = Path.IsPathRooted(fileName) ? fileName : Path.Combine(Environment.CurrentDirectory, fileName);

        try
        {
            if (!File.Exists(path))
                return null;

             string[] lines = File.ReadAllLines(path);
             int start = Math.Max(0, startZeroBasedLineNumber);
             int end = Math.Min(lines.Length - 1, Math.Max(startZeroBasedLineNumber, endZeroBasedLineNumber));
             if (start > end) return null;
             return string.Join(Environment.NewLine, lines[start..(end + 1)]).Trim();
        }
        catch { }
        return null;
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no longer needed, this pr now focus on only one task

{
string[] lines = File.ReadAllLines(path);
foreach (int candidate in new[] { zeroBasedLineNumber, zeroBasedLineNumber - 1 })
{
if (candidate >= 0 && candidate < lines.Length)
return lines[candidate].Trim();
}
}
}
catch
{
// ignore IO failures, diagnostics will fall back to instruction offsets
}
return null;
}

public static HashSet<BasicBlock> FindAllBasicBlocksWritingStorageInTryCatchFinally
(TryCatchFinallySingleCoverage c, HashSet<TryCatchFinallySingleCoverage> visitedTrys, HashSet<BasicBlock> allBasicBlocksWritingStorage)
{
Expand Down
10 changes: 10 additions & 0 deletions src/Neo.SmartContract.Framework/Native/Policy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#pragma warning disable CS0626

using Neo.SmartContract.Framework.Attributes;
using Neo.SmartContract.Framework;

namespace Neo.SmartContract.Framework.Native
{
Expand All @@ -26,5 +27,14 @@ public class Policy
public static extern bool IsBlocked(UInt160 account);
public static extern uint GetAttributeFee(TransactionAttributeType attributeType);
public static extern void SetAttributeFee(TransactionAttributeType attributeType, uint value);
public static extern void SetFeePerByte(long value);
public static extern void SetExecFeeFactor(uint value);
public static extern void SetStoragePrice(uint value);
public static extern bool BlockAccount(UInt160 account);
public static extern bool UnblockAccount(UInt160 account);
public static extern uint GetMaxValidUntilBlockIncrement();
public static extern void SetMaxValidUntilBlockIncrement(uint value);
public static extern uint GetMaxTraceableBlocks();
public static extern void SetMaxTraceableBlocks(uint value);
}
}
2 changes: 2 additions & 0 deletions src/Neo.SmartContract.Framework/Native/RoleManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#pragma warning disable CS0626

using Neo.SmartContract.Framework.Attributes;
using Neo.SmartContract.Framework;

namespace Neo.SmartContract.Framework.Native
{
Expand All @@ -21,5 +22,6 @@ public class RoleManagement
[ContractHash]
public static extern UInt160 Hash { get; }
public static extern ECPoint[] GetDesignatedByRole(Role role, uint index);
public static extern void DesignateAsRole(Role role, ECPoint[] nodes);
}
}
Loading
Loading