From 1365ed25e49daec4d50c2e693d7ba3da2d70a8cf Mon Sep 17 00:00:00 2001 From: Alvaro Date: Tue, 28 Oct 2025 23:03:21 +0100 Subject: [PATCH 1/4] Extend ValidateExtendedTypes to check circle references --- src/Neo/SmartContract/Manifest/ContractAbi.cs | 29 ++++- .../SmartContract/Manifest/UT_ExtendedType.cs | 107 +++++++++++++++++- 2 files changed, 134 insertions(+), 2 deletions(-) diff --git a/src/Neo/SmartContract/Manifest/ContractAbi.cs b/src/Neo/SmartContract/Manifest/ContractAbi.cs index a182464205..7817ffce48 100644 --- a/src/Neo/SmartContract/Manifest/ContractAbi.cs +++ b/src/Neo/SmartContract/Manifest/ContractAbi.cs @@ -12,6 +12,7 @@ using Neo.Json; using Neo.VM; using Neo.VM.Types; +using Org.BouncyCastle.Tls; using System; using System.Collections.Generic; using System.Linq; @@ -28,7 +29,7 @@ namespace Neo.SmartContract.Manifest public class ContractAbi : IInteroperable { private IReadOnlyDictionary<(string, int), ContractMethodDescriptor>? _methodDictionary; - + private enum CheckState { UNCHECK, CHECKING, CHECK }; /// /// Gets the methods in the ABI. /// @@ -111,6 +112,26 @@ public static ContractAbi FromJson(JObject json) return abi; } + private static bool HasCircularReference(string name, IReadOnlyDictionary namedTypes, Dictionary states) + { + if (!states.TryGetValue(name, out var state)) + state = CheckState.UNCHECK; + + if (state == CheckState.CHECKING) return true; + if (state == CheckState.CHECK) return false; + + states[name] = CheckState.CHECKING; + + var next = namedTypes[name].NamedType; + if (next is not null && namedTypes.ContainsKey(next)) + { + if (HasCircularReference(next, namedTypes, states)) + return true; + } + + states[name] = CheckState.CHECK; + return false; + } internal void ValidateExtendedTypes() { ISet knownNamedTypes = NamedTypes != null @@ -119,9 +140,15 @@ internal void ValidateExtendedTypes() if (NamedTypes != null) { + var states = new Dictionary(NamedTypes.Count, StringComparer.Ordinal); foreach (var (name, type) in NamedTypes) { ExtendedType.EnsureValidNamedTypeIdentifier(name); + if (HasCircularReference(name, NamedTypes, states)) + { + throw new FormatException($"Circular reference in namedtypes starting at '{name}'"); + } + type.ValidateForNamedTypeDefinition(name, knownNamedTypes); } } diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs index 591471418b..2b53eef862 100644 --- a/tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs +++ b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs @@ -308,7 +308,7 @@ public void FromStackItem_ShouldThrow_WhenTypeMissing() } [TestMethod] - public void FromJson_Circular_Reference_ShouldThrow() + public void FromJson_Circular_Reference_A_A_ShouldThrow() { var json = (JObject)JToken.Parse(@" { @@ -326,5 +326,110 @@ public void FromJson_Circular_Reference_ShouldThrow() Assert.ThrowsExactly(() => ContractAbi.FromJson(json)); } + + [TestMethod] + public void FromJson_Circular_Reference_A_B_B_A_ShouldThrow() + { + var json = (JObject)JToken.Parse(@" + { + ""methods"": [ + { ""name"": ""_deploy"", ""parameters"": [], ""returntype"": ""Void"", ""offset"": 0, ""safe"": true} + ], + ""events"": [], + ""namedtypes"": { + ""boo"": { + ""type"": ""Array"", + ""namedtype"": ""alice"" + }, + ""alice"": { + ""type"": ""Array"", + ""namedtype"": ""boo"" + } + } + }"); + + Assert.ThrowsExactly(() => ContractAbi.FromJson(json)); + } + [TestMethod] + public void FromJson_Circular_Reference_A_B_C_A_ShouldThrow() + { + var json = (JObject)JToken.Parse(@" + { + ""methods"": [ + { ""name"": ""_deploy"", ""parameters"": [], ""returntype"": ""Void"", ""offset"": 0, ""safe"": true} + ], + ""events"": [], + ""namedtypes"": { + ""boo"": { + ""type"": ""Array"", + ""namedtype"": ""alice"" + }, + ""alice"": { + ""type"": ""Array"", + ""namedtype"": ""toc"" + }, + ""toc"": { + ""type"": ""Array"", + ""namedtype"": ""boo"" + } + } + }"); + + Assert.ThrowsExactly(() => ContractAbi.FromJson(json)); + } + [TestMethod] + public void FromJson_Circular_Reference_A_B_C_B_ShouldThrow() + { + var json = (JObject)JToken.Parse(@" + { + ""methods"": [ + { ""name"": ""_deploy"", ""parameters"": [], ""returntype"": ""Void"", ""offset"": 0, ""safe"": true} + ], + ""events"": [], + ""namedtypes"": { + ""boo"": { + ""type"": ""Array"", + ""namedtype"": ""alice"" + }, + ""alice"": { + ""type"": ""Array"", + ""namedtype"": ""toc"" + }, + ""toc"": { + ""type"": ""Array"", + ""namedtype"": ""alice"" + } + } + }"); + + Assert.ThrowsExactly(() => ContractAbi.FromJson(json)); + } + [TestMethod] + public void FromJson_No_Circular_Reference_A_B_C() + { + var json = (JObject)JToken.Parse(@" + { + ""methods"": [ + { ""name"": ""_deploy"", ""parameters"": [], ""returntype"": ""Void"", ""offset"": 0, ""safe"": true} + ], + ""events"": [], + ""namedtypes"": { + ""boo"": { + ""type"": ""Array"", + ""namedtype"": ""alice"" + }, + ""alice"": { + ""type"": ""Array"", + ""namedtype"": ""toc"" + }, + ""toc"":{ + ""type"": ""Array"", + ""value"": {""type"": ""Integer"" } + } + } + }"); + + ContractAbi.FromJson(json); + } } } From e283a3fc73adc5a6b35982a3edf3b802332366d4 Mon Sep 17 00:00:00 2001 From: Alvaro Date: Tue, 28 Oct 2025 23:05:29 +0100 Subject: [PATCH 2/4] remove unused usings --- src/Neo/SmartContract/Manifest/ContractAbi.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Neo/SmartContract/Manifest/ContractAbi.cs b/src/Neo/SmartContract/Manifest/ContractAbi.cs index 7817ffce48..43318531d2 100644 --- a/src/Neo/SmartContract/Manifest/ContractAbi.cs +++ b/src/Neo/SmartContract/Manifest/ContractAbi.cs @@ -12,7 +12,6 @@ using Neo.Json; using Neo.VM; using Neo.VM.Types; -using Org.BouncyCastle.Tls; using System; using System.Collections.Generic; using System.Linq; From c905d26a5055bdaf09a8a6c4f5cf0ff3f964603b Mon Sep 17 00:00:00 2001 From: Alvaro Date: Wed, 29 Oct 2025 18:07:19 +0100 Subject: [PATCH 3/4] Add more tests and change enum --- src/Neo/SmartContract/Manifest/ContractAbi.cs | 20 +++-- .../SmartContract/Manifest/UT_ExtendedType.cs | 78 +++++++++++++++++++ 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/src/Neo/SmartContract/Manifest/ContractAbi.cs b/src/Neo/SmartContract/Manifest/ContractAbi.cs index 43318531d2..3e16b5cc6f 100644 --- a/src/Neo/SmartContract/Manifest/ContractAbi.cs +++ b/src/Neo/SmartContract/Manifest/ContractAbi.cs @@ -28,7 +28,10 @@ namespace Neo.SmartContract.Manifest public class ContractAbi : IInteroperable { private IReadOnlyDictionary<(string, int), ContractMethodDescriptor>? _methodDictionary; - private enum CheckState { UNCHECK, CHECKING, CHECK }; + private const int STATE_UNCHECK = 0; + private const int STATE_CHECKING = 1; + private const int STATE_CHECK = 2; + /// /// Gets the methods in the ABI. /// @@ -111,15 +114,15 @@ public static ContractAbi FromJson(JObject json) return abi; } - private static bool HasCircularReference(string name, IReadOnlyDictionary namedTypes, Dictionary states) + private static bool HasCircularReference(string name, IReadOnlyDictionary namedTypes, Dictionary states) { if (!states.TryGetValue(name, out var state)) - state = CheckState.UNCHECK; + state = STATE_UNCHECK; - if (state == CheckState.CHECKING) return true; - if (state == CheckState.CHECK) return false; + if (state == STATE_CHECKING) return true; + if (state == STATE_CHECK) return false; - states[name] = CheckState.CHECKING; + states[name] = STATE_CHECKING; var next = namedTypes[name].NamedType; if (next is not null && namedTypes.ContainsKey(next)) @@ -128,9 +131,10 @@ private static bool HasCircularReference(string name, IReadOnlyDictionary knownNamedTypes = NamedTypes != null @@ -139,7 +143,7 @@ internal void ValidateExtendedTypes() if (NamedTypes != null) { - var states = new Dictionary(NamedTypes.Count, StringComparer.Ordinal); + var states = new Dictionary(NamedTypes.Count, StringComparer.Ordinal); foreach (var (name, type) in NamedTypes) { ExtendedType.EnsureValidNamedTypeIdentifier(name); diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs index 2b53eef862..1de5258f46 100644 --- a/tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs +++ b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs @@ -350,6 +350,7 @@ public void FromJson_Circular_Reference_A_B_B_A_ShouldThrow() Assert.ThrowsExactly(() => ContractAbi.FromJson(json)); } + [TestMethod] public void FromJson_Circular_Reference_A_B_C_A_ShouldThrow() { @@ -377,6 +378,7 @@ public void FromJson_Circular_Reference_A_B_C_A_ShouldThrow() Assert.ThrowsExactly(() => ContractAbi.FromJson(json)); } + [TestMethod] public void FromJson_Circular_Reference_A_B_C_B_ShouldThrow() { @@ -404,6 +406,7 @@ public void FromJson_Circular_Reference_A_B_C_B_ShouldThrow() Assert.ThrowsExactly(() => ContractAbi.FromJson(json)); } + [TestMethod] public void FromJson_No_Circular_Reference_A_B_C() { @@ -431,5 +434,80 @@ public void FromJson_No_Circular_Reference_A_B_C() ContractAbi.FromJson(json); } + + [TestMethod] + public void FromJson_Circular_Reference_A_NO_B_A_C_A() + { + // A => No namedtype + // B => A + // C => A + var json = (JObject)JToken.Parse(@" + { + ""methods"": [ + { ""name"": ""_deploy"", ""parameters"": [], ""returntype"": ""Void"", ""offset"": 0, ""safe"": true} + ], + ""events"": [], + ""namedtypes"": { + ""boo"": { + ""type"": ""Array"", + ""value"": {""type"": ""Integer"" } + }, + ""alice"": { + ""type"": ""Array"", + ""namedtype"": ""boo"" + }, + ""toc"": { + ""type"": ""Array"", + ""namedtype"": ""boo"" + } + } + }"); + + ContractAbi.FromJson(json); + } + + [TestMethod] + public void FromJson_Circular_Reference_A_VALUE_A() + { + var json = (JObject)JToken.Parse(@" + { + ""methods"": [ + { ""name"": ""_deploy"", ""parameters"": [], ""returntype"": ""Void"", ""offset"": 0, ""safe"": true} + ], + ""events"": [], + ""namedtypes"": { + ""boo"": { + ""type"": ""Array"", + ""value"": {""type"":""Array"",""namedtype"": ""boo"" } + } + } + }"); + + ContractAbi.FromJson(json); + } + + [TestMethod] + public void FromJson_Circular_Reference_A_VALUE_B_B_VALUE_A() + { + var json = (JObject)JToken.Parse(@" + { + ""methods"": [ + { ""name"": ""_deploy"", ""parameters"": [], ""returntype"": ""Void"", ""offset"": 0, ""safe"": true} + ], + ""events"": [], + ""namedtypes"": { + ""boo"": { + ""type"": ""Array"", + ""value"": {""type"":""Array"",""namedtype"": ""alice"" } + }, + ""alice"": { + ""type"": ""Array"", + ""value"": {""type"":""Array"",""namedtype"": ""boo"" } + } + } + }"); + + ContractAbi.FromJson(json); + } } } From e133f86dd01a49c57372e2c4f2102eff00b033f0 Mon Sep 17 00:00:00 2001 From: Alvaro Date: Wed, 29 Oct 2025 23:51:06 +0100 Subject: [PATCH 4/4] Add assert to valid tests --- .../SmartContract/Manifest/UT_ExtendedType.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs index 1de5258f46..40f02a502e 100644 --- a/tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs +++ b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs @@ -432,7 +432,10 @@ public void FromJson_No_Circular_Reference_A_B_C() } }"); - ContractAbi.FromJson(json); + var result = ContractAbi.FromJson(json); + Assert.IsTrue(result.NamedTypes.ContainsKey("boo")); + Assert.IsTrue(result.NamedTypes.ContainsKey("alice")); + Assert.IsTrue(result.NamedTypes.ContainsKey("toc")); } [TestMethod] @@ -463,7 +466,10 @@ public void FromJson_Circular_Reference_A_NO_B_A_C_A() } }"); - ContractAbi.FromJson(json); + var result = ContractAbi.FromJson(json); + Assert.IsTrue(result.NamedTypes.ContainsKey("boo")); + Assert.IsTrue(result.NamedTypes.ContainsKey("alice")); + Assert.IsTrue(result.NamedTypes.ContainsKey("toc")); } [TestMethod] @@ -483,7 +489,8 @@ public void FromJson_Circular_Reference_A_VALUE_A() } }"); - ContractAbi.FromJson(json); + var result = ContractAbi.FromJson(json); + Assert.IsTrue(result.NamedTypes.ContainsKey("boo")); } [TestMethod] @@ -507,7 +514,9 @@ public void FromJson_Circular_Reference_A_VALUE_B_B_VALUE_A() } }"); - ContractAbi.FromJson(json); + var result = ContractAbi.FromJson(json); + Assert.IsTrue(result.NamedTypes.ContainsKey("boo")); + Assert.IsTrue(result.NamedTypes.ContainsKey("alice")); } } }