Skip to content
Closed

NEP-25 #4043

Show file tree
Hide file tree
Changes from 84 commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
b5b0a26
NEP-25
shargon Jul 2, 2025
9fa095d
Clean
shargon Jul 2, 2025
ddba39e
Merge branch 'dev' into nep-25
Jul 19, 2025
46ea96f
Update src/Neo/SmartContract/Manifest/ExtendedType.cs
shargon Jul 29, 2025
18a250c
Update src/Neo/SmartContract/Manifest/ExtendedType.cs
shargon Jul 29, 2025
368cfdc
Use also param
shargon Jul 29, 2025
418a8ab
Merge branch 'dev' into nep-25
shargon Jul 29, 2025
d0f8d21
Rename var
shargon Jul 30, 2025
d392f50
Use the same type
shargon Jul 30, 2025
55ec5e8
Revert "Use the same type"
shargon Jul 30, 2025
7ab0c64
Add value
shargon Jul 30, 2025
b36370a
Fields
shargon Jul 30, 2025
2e0152f
Merge branch 'dev' into nep-25
shargon Jul 30, 2025
5e47abc
Merge branch 'dev' into nep-25
shargon Jul 31, 2025
8929079
Merge branch 'dev' into nep-25
shargon Aug 3, 2025
1f8d488
Merge branch 'dev' into nep-25
Jim8y Aug 6, 2025
d21d906
docs: add NEP-25 implementation documentation
Jim8y Aug 6, 2025
7c073f2
test: fix ExtendedType tests for 8-field StackItem structure
Jim8y Aug 6, 2025
a1248b2
Merge branch 'dev' into nep-25
cschuchardt88 Aug 6, 2025
b0cd9b1
Merge branch 'dev' into nep-25
shargon Aug 7, 2025
d3c8564
Move to ContractParameterDefinition
shargon Aug 7, 2025
0e85e4a
Use ContractParameterType for Key
shargon Aug 7, 2025
65f27ec
Merge branch 'dev' into nep-25
shargon Aug 12, 2025
cfd07b4
Fix UT
shargon Aug 12, 2025
f6aa6aa
Merge branch 'dev' into nep-25
shargon Aug 28, 2025
890dcd4
remove md
shargon Aug 29, 2025
5ffeb94
Anna's review
shargon Aug 29, 2025
de230cc
Merge branch 'dev' into nep-25
shargon Sep 8, 2025
a7967e5
namedtypes
shargon Sep 8, 2025
c3759a8
Merge branch 'nep-25' of https://github.com/neo-project/neo into nep-25
shargon Sep 8, 2025
8c6aa4b
allow null
shargon Sep 8, 2025
3b20380
clean
shargon Sep 8, 2025
d093162
clean
shargon Sep 8, 2025
2fa6247
Merge branch 'dev' into nep-25
shargon Sep 11, 2025
761ca46
fix build
shargon Sep 11, 2025
2ac2da1
Merge branch 'dev' into nep-25
ajara87 Sep 13, 2025
d2e9871
Merge branch 'dev' into nep-25
shargon Sep 16, 2025
14a9176
Merge branch 'dev' into nep-25
ajara87 Sep 16, 2025
7cd74e6
Merge branch 'dev' into nep-25
Jim8y Sep 22, 2025
35229d4
Merge branch 'dev' into nep-25
shargon Oct 3, 2025
8718ed4
Fix Serialize with null item
shargon Oct 3, 2025
3b56be4
Ensure the right type or null
shargon Oct 3, 2025
e2ac006
use map for serialize
shargon Oct 3, 2025
ac21144
Fix map
shargon Oct 3, 2025
242dbb5
Merge branch 'dev' into nep-25
shargon Oct 4, 2025
7c3a377
Merge branch 'dev' into nep-25
shargon Oct 6, 2025
3fb806a
Enforce NEP-25 extended type validation (#4187)
Jim8y Oct 6, 2025
20965c4
Apply suggestions from code review
shargon Oct 6, 2025
b9cfdb6
Update src/Neo/SmartContract/Manifest/ContractAbi.cs
shargon Oct 6, 2025
c0fd6f2
Update src/Neo/SmartContract/Manifest/ContractEventDescriptor.cs
shargon Oct 6, 2025
b6a80d1
Update src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs
shargon Oct 6, 2025
c45eecf
Update src/Neo/SmartContract/Manifest/ContractParameterDefinition.cs
shargon Oct 6, 2025
51adba8
Update src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs
shargon Oct 6, 2025
3b0ab01
Update src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs
shargon Oct 6, 2025
4ca1f65
Update src/Neo/SmartContract/Manifest/ContractParameterDefinition.cs
shargon Oct 6, 2025
0118b4c
Apply suggestions from code review
shargon Oct 7, 2025
09c4839
Update tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs
shargon Oct 9, 2025
41782d5
Merge branch 'dev' into nep-25
ajara87 Oct 11, 2025
afa0632
Update tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs
shargon Oct 11, 2025
d327be8
Update src/Neo/SmartContract/Manifest/ExtendedType.cs
shargon Oct 11, 2025
a8408f4
Merge branch 'dev' into nep-25
shargon Oct 11, 2025
169ea7b
@ajara87 suggestion
shargon Oct 11, 2025
60eac04
Merge branch 'dev' into nep-25
ajara87 Oct 11, 2025
c4bf05a
fix wrong review of @cschuchardt88
shargon Oct 12, 2025
ad1ffde
Add Ut without the key
shargon Oct 12, 2025
c49499d
Clean code
shargon Oct 13, 2025
4e1efa0
Remove void restriction
shargon Oct 13, 2025
054e5de
Clean enum
shargon Oct 13, 2025
aca22c0
Merge branch 'dev' into nep-25
ajara87 Oct 14, 2025
81b7b5a
Merge branch 'dev' into nep-25
NGDAdmin Oct 14, 2025
7be35bd
Merge branch 'dev' into nep-25
shargon Oct 16, 2025
a4a3d3d
Merge branch 'dev' into nep-25
shargon Oct 18, 2025
a23ee2d
- Styling
shargon Oct 18, 2025
c719313
Roman's review: Ensure no circular named type
shargon Oct 18, 2025
56ea443
Merge branch 'dev' into nep-25
ajara87 Oct 23, 2025
533d472
Merge branch 'dev' into nep-25
shargon Oct 23, 2025
fefdb95
Merge branch 'dev' into nep-25
ajara87 Oct 23, 2025
f9e1526
Merge branch 'dev' into nep-25
shargon Oct 24, 2025
88b861b
Circular reference change
shargon Oct 24, 2025
a799ca6
Merge branch 'nep-25' of https://github.com/neo-project/neo into nep-25
shargon Oct 24, 2025
c575449
Merge branch 'dev' into nep-25
ajara87 Oct 26, 2025
9b4cccc
UT for circular reference (#4251)
ajara87 Oct 26, 2025
2d3e1fe
Merge branch 'dev' into nep-25
ajara87 Oct 28, 2025
20aa730
Merge branch 'dev' into nep-25
ajara87 Oct 29, 2025
0c5d4c6
[NEP25] - Validate circular reference (#4258)
ajara87 Oct 30, 2025
ba009ea
Allow Fields with 0 length
shargon Oct 31, 2025
3fd4f14
Merge branch 'dev' into nep-25
ajara87 Nov 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 104 additions & 17 deletions src/Neo/SmartContract/Manifest/ContractAbi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
using System.Linq;
using Array = Neo.VM.Types.Array;

#nullable enable

namespace Neo.SmartContract.Manifest
{
/// <summary>
Expand All @@ -25,32 +27,51 @@ namespace Neo.SmartContract.Manifest
/// <remarks>For more details, see NEP-14.</remarks>
public class ContractAbi : IInteroperable
{
private IReadOnlyDictionary<(string, int), ContractMethodDescriptor> methodDictionary;
private IReadOnlyDictionary<(string, int), ContractMethodDescriptor>? _methodDictionary;

/// <summary>
/// Gets the methods in the ABI.
/// </summary>
public ContractMethodDescriptor[] Methods { get; set; }
public ContractMethodDescriptor[] Methods { get; set; } = [];

/// <summary>
/// Gets the events in the ABI.
/// </summary>
public ContractEventDescriptor[] Events { get; set; }
public ContractEventDescriptor[] Events { get; set; } = [];

/// <summary>
/// An object with each member having a name (a string consisting of one or more identifiers joined by dots) and a value of ExtendedType object.
/// </summary>
public Dictionary<string, ExtendedType>? NamedTypes { get; set; }

void IInteroperable.FromStackItem(StackItem stackItem)
{
Struct @struct = (Struct)stackItem;
Methods = ((Array)@struct[0]).Select(p => p.ToInteroperable<ContractMethodDescriptor>()).ToArray();
Events = ((Array)@struct[1]).Select(p => p.ToInteroperable<ContractEventDescriptor>()).ToArray();
var data = (Struct)stackItem;
Methods = [.. ((Array)data[0]).Select(p => p.ToInteroperable<ContractMethodDescriptor>())];
Events = [.. ((Array)data[1]).Select(p => p.ToInteroperable<ContractEventDescriptor>())];

if (data.Count >= 3 && !data[2].IsNull)
NamedTypes = ((Map)data[2]).ToDictionary(p => p.Key.GetString()!, p => p.Value.ToInteroperable<ExtendedType>());
else
NamedTypes = null;

ValidateExtendedTypes();
}

public StackItem ToStackItem(IReferenceCounter referenceCounter)
{
return new Struct(referenceCounter)
var ret = new Struct(referenceCounter)
{
new Array(referenceCounter, Methods.Select(p => p.ToStackItem(referenceCounter))),
new Array(referenceCounter, Events.Select(p => p.ToStackItem(referenceCounter))),
new Array(referenceCounter, Events.Select(p => p.ToStackItem(referenceCounter)))
};

if (NamedTypes != null)
{
ret.Add(new Map(NamedTypes.ToDictionary(p => (PrimitiveType)p.Key, p => (StackItem)p.Value.ToStackItem(referenceCounter)), referenceCounter));
}
Comment on lines +72 to +75
Copy link
Member

Choose a reason for hiding this comment

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

I have a concern: manifest serialization format doesn't depend on hardforks, and we don't change native Management in any way. It means that there's a tiny possibility of a statediff during an update to this version: if someone deploys contract with extended types, then deployment transaction will fail on old nodes and will succeed on updated nodes.

This fact may probably be ignored, but at least we should know about it.

Copy link
Member Author

Choose a reason for hiding this comment

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

If they don't update the node, the state difference will come later, isn't it?

Copy link
Member

Choose a reason for hiding this comment

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

the state difference will come later,

At Faun height, due to other Faun-specific updates, right. But my concern is that with the current implementation state difference may happen even before Faun if someone submits extended manifest to the chain on contract deploy/update.

Copy link
Member Author

Choose a reason for hiding this comment

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

It was resolved in #4283


return ret;
}

/// <summary>
Expand All @@ -60,15 +81,70 @@ public StackItem ToStackItem(IReferenceCounter referenceCounter)
/// <returns>The converted ABI.</returns>
public static ContractAbi FromJson(JObject json)
{
Dictionary<string, ExtendedType>? namedTypes = null;
var knownNamedTypes = new HashSet<string>(StringComparer.Ordinal);
if (json!["namedtypes"] is JObject namedTypesJson)
{
foreach (var key in namedTypesJson.Properties.Keys)
{
knownNamedTypes.Add(key);
}

namedTypes = new(namedTypesJson.Properties.Count, StringComparer.Ordinal);
foreach (var (name, token) in namedTypesJson.Properties)
{
if (token is not JObject valueObject)
throw new FormatException("Named type definition must be a JSON object.");
namedTypes[name] = ExtendedType.FromJson(valueObject);
}
}

ContractAbi abi = new()
{
Methods = ((JArray)json!["methods"])?.Select(u => ContractMethodDescriptor.FromJson((JObject)u)).ToArray() ?? [],
Events = ((JArray)json!["events"])?.Select(u => ContractEventDescriptor.FromJson((JObject)u)).ToArray() ?? []
Methods = ((JArray)json!["methods"]!)?.Select(u => ContractMethodDescriptor.FromJson((JObject)u!, knownNamedTypes)).ToArray() ?? [],
Events = ((JArray)json!["events"]!)?.Select(u => ContractEventDescriptor.FromJson((JObject)u!, knownNamedTypes)).ToArray() ?? [],
NamedTypes = namedTypes
};
if (abi.Methods.Length == 0) throw new FormatException("Methods in ContractAbi is empty");

abi.ValidateExtendedTypes();
return abi;
}

internal void ValidateExtendedTypes()
{
ISet<string> knownNamedTypes = NamedTypes != null
? new HashSet<string>(NamedTypes.Keys, StringComparer.Ordinal)
: new HashSet<string>(StringComparer.Ordinal);

if (NamedTypes != null)
{
foreach (var (name, type) in NamedTypes)
{
ExtendedType.EnsureValidNamedTypeIdentifier(name);
type.ValidateForNamedTypeDefinition(name, knownNamedTypes);
}
}

foreach (var method in Methods)
{
foreach (var parameter in method.Parameters)
{
parameter.ExtendedType?.ValidateForParameterOrReturn(parameter.Type, knownNamedTypes);
}

method.ExtendedReturnType?.ValidateForParameterOrReturn(method.ReturnType, knownNamedTypes);
}

foreach (var ev in Events)
{
foreach (var parameter in ev.Parameters)
{
parameter.ExtendedType?.ValidateForParameterOrReturn(parameter.Type, knownNamedTypes);
}
}
}

/// <summary>
/// Gets the method with the specified name.
/// </summary>
Expand All @@ -81,15 +157,17 @@ public static ContractAbi FromJson(JObject json)
/// The method that matches the specified name and number of parameters.
/// If <paramref name="pcount"/> is set to -1, the first method with the specified name will be returned.
/// </returns>
public ContractMethodDescriptor GetMethod(string name, int pcount)
public ContractMethodDescriptor? GetMethod(string name, int pcount)
{
if (pcount < -1 || pcount > ushort.MaxValue)
throw new ArgumentOutOfRangeException(nameof(pcount), $"`pcount` must be between [-1, {ushort.MaxValue}]");
if (pcount >= 0)
{
methodDictionary ??= Methods.ToDictionary(p => (p.Name, p.Parameters.Length));
methodDictionary.TryGetValue((name, pcount), out var method);
return method;
_methodDictionary ??= Methods.ToDictionary(p => (p.Name, p.Parameters.Length));
if (_methodDictionary.TryGetValue((name, pcount), out var method))
return method;

return null;
}
else
{
Expand All @@ -103,11 +181,20 @@ public ContractMethodDescriptor GetMethod(string name, int pcount)
/// <returns>The ABI represented by a JSON object.</returns>
public JObject ToJson()
{
return new JObject()
var ret = new JObject()
{
["methods"] = new JArray(Methods.Select(u => u.ToJson()).ToArray()),
["events"] = new JArray(Events.Select(u => u.ToJson()).ToArray())
["methods"] = new JArray([.. Methods.Select(u => u.ToJson())]),
["events"] = new JArray([.. Events.Select(u => u.ToJson())])
};

if (NamedTypes != null)
{
ret["namedtypes"] = new JObject(NamedTypes.ToDictionary(u => u.Key, u => (JToken?)u.Value.ToJson()));
}

return ret;
}
}
}

#nullable disable
6 changes: 4 additions & 2 deletions src/Neo/SmartContract/Manifest/ContractEventDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Neo.VM;
using Neo.VM.Types;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Array = Neo.VM.Types.Array;
Expand Down Expand Up @@ -54,13 +55,14 @@ public virtual StackItem ToStackItem(IReferenceCounter referenceCounter)
/// Converts the event from a JSON object.
/// </summary>
/// <param name="json">The event represented by a JSON object.</param>
/// <param name="knownNamedTypes">Set of named type identifiers declared in the manifest, if any.</param>
/// <returns>The converted event.</returns>
public static ContractEventDescriptor FromJson(JObject json)
public static ContractEventDescriptor FromJson(JObject json, ISet<string> knownNamedTypes = null)
{
ContractEventDescriptor descriptor = new()
{
Name = json["name"].GetString(),
Parameters = ((JArray)json["parameters"]).Select(u => ContractParameterDefinition.FromJson((JObject)u)).ToArray(),
Parameters = ((JArray)json["parameters"]).Select(u => ContractParameterDefinition.FromJson((JObject)u, knownNamedTypes)).ToArray(),
};
if (string.IsNullOrEmpty(descriptor.Name)) throw new FormatException("Name in ContractEventDescriptor is empty");
_ = descriptor.Parameters.ToDictionary(p => p.Name);
Expand Down
58 changes: 43 additions & 15 deletions src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Neo.VM;
using Neo.VM.Types;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

Expand All @@ -28,6 +29,11 @@ public class ContractMethodDescriptor : ContractEventDescriptor, IEquatable<Cont
/// </summary>
public ContractParameterType ReturnType { get; set; }

/// <summary>
/// NEP-25 extended return type
/// </summary>
public ExtendedType ExtendedReturnType { get; set; }

/// <summary>
/// The position of the method in the contract script.
/// </summary>
Expand All @@ -42,46 +48,63 @@ public class ContractMethodDescriptor : ContractEventDescriptor, IEquatable<Cont
public override void FromStackItem(StackItem stackItem)
{
base.FromStackItem(stackItem);
Struct @struct = (Struct)stackItem;
ReturnType = (ContractParameterType)(byte)@struct[2].GetInteger();
Offset = (int)@struct[3].GetInteger();
Safe = @struct[4].GetBoolean();
var item = (Struct)stackItem;
ReturnType = (ContractParameterType)(byte)item[2].GetInteger();
Offset = (int)item[3].GetInteger();
Safe = item[4].GetBoolean();

if (item.Count >= 6)
{
ExtendedReturnType = new ExtendedType();
ExtendedReturnType.FromStackItem(item[5]);
ExtendedReturnType.ValidateForParameterOrReturn(ReturnType, null);
}
else
{
ExtendedReturnType = null;
}
}

public override StackItem ToStackItem(IReferenceCounter referenceCounter)
{
Struct @struct = (Struct)base.ToStackItem(referenceCounter);
@struct.Add((byte)ReturnType);
@struct.Add(Offset);
@struct.Add(Safe);
return @struct;
var item = (Struct)base.ToStackItem(referenceCounter);
item.Add((byte)ReturnType);
item.Add(Offset);
item.Add(Safe);
if (ExtendedReturnType != null)
{
item.Add(ExtendedReturnType.ToStackItem(referenceCounter));
}
return item;
}

/// <summary>
/// Converts the method from a JSON object.
/// </summary>
/// <param name="json">The method represented by a JSON object.</param>
/// <param name="knownNamedTypes">Set of named type identifiers declared in the manifest, if any.</param>
/// <returns>The converted method.</returns>
public new static ContractMethodDescriptor FromJson(JObject json)
public new static ContractMethodDescriptor FromJson(JObject json, ISet<string> knownNamedTypes = null)
{
ContractMethodDescriptor descriptor = new()
{
Name = json["name"].GetString(),
Parameters = ((JArray)json["parameters"]).Select(u => ContractParameterDefinition.FromJson((JObject)u)).ToArray(),
Parameters = ((JArray)json["parameters"]).Select(u => ContractParameterDefinition.FromJson((JObject)u, knownNamedTypes)).ToArray(),
ReturnType = Enum.Parse<ContractParameterType>(json["returntype"].GetString()),
Offset = json["offset"].GetInt32(),
Safe = json["safe"].GetBoolean()
Safe = json["safe"].GetBoolean(),
ExtendedReturnType = json["extendedreturntype"] != null ? ExtendedType.FromJson((JObject)json["extendedreturntype"]) : null
};

if (string.IsNullOrEmpty(descriptor.Name))
throw new FormatException("Name in ContractMethodDescriptor is empty");

_ = descriptor.Parameters.ToDictionary(p => p.Name);

if (!Enum.IsDefined(typeof(ContractParameterType), descriptor.ReturnType))
throw new FormatException($"ReturnType({descriptor.ReturnType}) in ContractMethodDescriptor is not valid");
if (descriptor.Offset < 0)
throw new FormatException($"Offset({descriptor.Offset}) in ContractMethodDescriptor is not valid");
descriptor.ExtendedReturnType?.ValidateForParameterOrReturn(descriptor.ReturnType, knownNamedTypes);
return descriptor;
}

Expand All @@ -95,6 +118,10 @@ public override JObject ToJson()
json["returntype"] = ReturnType.ToString();
json["offset"] = Offset;
json["safe"] = Safe;
if (ExtendedReturnType != null)
{
json["extendedreturntype"] = ExtendedReturnType.ToJson();
}
return json;
}

Expand All @@ -106,7 +133,8 @@ public bool Equals(ContractMethodDescriptor other)
base.Equals(other) && // Already check null
ReturnType == other.ReturnType
&& Offset == other.Offset
&& Safe == other.Safe;
&& Safe == other.Safe
&& Equals(ExtendedReturnType, other.ExtendedReturnType);
}

public override bool Equals(object other)
Expand All @@ -119,7 +147,7 @@ public override bool Equals(object other)

public override int GetHashCode()
{
return HashCode.Combine(ReturnType, Offset, Safe, base.GetHashCode());
return HashCode.Combine(ReturnType, Offset, Safe, ExtendedReturnType?.GetHashCode() ?? -1, base.GetHashCode());
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
Loading