Skip to content

Commit 63e2ba7

Browse files
shargonajara87AnnaShalevasuperboyiii
authored
JumpTable for not Gorgon (#4381)
* JumpTable Faun * Update src/Neo/SmartContract/ApplicationEngine.cs * Update src/Neo/SmartContract/ApplicationEngine.cs * Apply suggestions from code review Co-authored-by: Alvaro <amjarag@gmail.com> * Add UT por hashkey * Apply suggestion from @ajara87 * Update tests/Neo.UnitTests/SmartContract/UT_ApplicationEngine.JumpTable.cs * Not Gorgon * fix ut * fix comments * Apply suggestions from code review Co-authored-by: Anna Shaleva <shaleva.ann@gmail.com> * Apply suggestions from code review Co-authored-by: Alvaro <amjarag@gmail.com> * Update tests/Neo.UnitTests/SmartContract/UT_ApplicationEngine.JumpTable.cs Co-authored-by: Owen <38493437+superboyiii@users.noreply.github.com> * Update Neo.VM package version to 3.9.3-CI00366 --------- Co-authored-by: Alvaro <amjarag@gmail.com> Co-authored-by: Anna Shaleva <shaleva.ann@nspcc.ru> Co-authored-by: Anna Shaleva <shaleva.ann@gmail.com> Co-authored-by: Owen <38493437+superboyiii@users.noreply.github.com>
1 parent c3040ea commit 63e2ba7

3 files changed

Lines changed: 298 additions & 5 deletions

File tree

src/Neo/Neo.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.2" />
1515
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.15.0" />
1616
<PackageReference Include="Neo.Cryptography.BLS12_381" Version="3.9.0" />
17-
<PackageReference Include="Neo.VM" Version="3.9.0" />
17+
<PackageReference Include="Neo.VM" Version="3.9.3-CI00366" />
1818
</ItemGroup>
1919

2020
<ItemGroup>

src/Neo/SmartContract/ApplicationEngine.cs

Lines changed: 186 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public partial class ApplicationEngine : ExecutionEngine
3838
{
3939
protected static readonly JumpTable DefaultJumpTable = ComposeDefaultJumpTable();
4040
protected static readonly JumpTable NotEchidnaJumpTable = ComposeNotEchidnaJumpTable();
41+
protected static readonly JumpTable NotGorgonJumpTable = ComposeNotGorgonJumpTable();
4142

4243
/// <summary>
4344
/// The maximum cost that can be spent when a contract is executed in test mode.
@@ -270,11 +271,174 @@ private static JumpTable ComposeDefaultJumpTable()
270271

271272
public static JumpTable ComposeNotEchidnaJumpTable()
272273
{
273-
var jumpTable = ComposeDefaultJumpTable();
274-
jumpTable[OpCode.SUBSTR] = VulnerableSubStr;
275-
return jumpTable;
274+
var table = ComposeNotGorgonJumpTable();
275+
276+
table[OpCode.SUBSTR] = VulnerableSubStr;
277+
278+
return table;
279+
}
280+
281+
public static JumpTable ComposeNotGorgonJumpTable()
282+
{
283+
var table = ComposeDefaultJumpTable();
284+
285+
// Before https://github.com/neo-project/neo-vm/pull/543
286+
table[OpCode.HASKEY] = HasKey_Before543;
287+
table[OpCode.PICKITEM] = PickItem_Before543;
288+
table[OpCode.SETITEM] = SetItem_Before543;
289+
table[OpCode.REMOVE] = Remove_Before543;
290+
291+
return table;
276292
}
277293

294+
private static void Remove_Before543(ExecutionEngine engine, Instruction instruction)
295+
{
296+
var key = engine.Pop<PrimitiveType>();
297+
var x = engine.Pop();
298+
switch (x)
299+
{
300+
case VMArray array:
301+
var index = (int)key.GetInteger();
302+
if (index < 0 || index >= array.Count)
303+
throw new InvalidOperationException($"The index of {nameof(VMArray)} is out of range, {index}/[0, {array.Count}).");
304+
array.RemoveAt(index);
305+
break;
306+
case Map map:
307+
map.Remove(key);
308+
break;
309+
default:
310+
throw new InvalidOperationException($"Invalid type for {instruction.OpCode}: {x.Type}");
311+
}
312+
}
313+
314+
private static void SetItem_Before543(ExecutionEngine engine, Instruction instruction)
315+
{
316+
var value = engine.Pop();
317+
if (value is Struct s) value = s.Clone(engine.Limits);
318+
var key = engine.Pop<PrimitiveType>();
319+
var x = engine.Pop();
320+
switch (x)
321+
{
322+
case VMArray array:
323+
{
324+
var index = (int)key.GetInteger();
325+
if (index < 0 || index >= array.Count)
326+
throw new CatchableException($"The index of {nameof(VMArray)} is out of range, {index}/[0, {array.Count}).");
327+
array[index] = value;
328+
break;
329+
}
330+
case Map map:
331+
{
332+
map[key] = value;
333+
break;
334+
}
335+
case VM.Types.Buffer buffer:
336+
{
337+
var index = (int)key.GetInteger();
338+
if (index < 0 || index >= buffer.Size)
339+
throw new CatchableException($"The index of {nameof(Buffer)} is out of range, {index}/[0, {buffer.Size}).");
340+
if (value is not PrimitiveType p)
341+
throw new InvalidOperationException($"Only primitive type values can be set in {nameof(Buffer)} in {instruction.OpCode}.");
342+
var b = (int)p.GetInteger();
343+
if (b < sbyte.MinValue || b > byte.MaxValue)
344+
throw new InvalidOperationException($"Overflow in {instruction.OpCode}, {b} is not a byte type.");
345+
buffer.InnerBuffer.Span[index] = (byte)b;
346+
break;
347+
}
348+
default:
349+
throw new InvalidOperationException($"Invalid type for {instruction.OpCode}: {x.Type}");
350+
}
351+
}
352+
353+
private static void PickItem_Before543(ExecutionEngine engine, Instruction instruction)
354+
{
355+
var key = engine.Pop<PrimitiveType>();
356+
var x = engine.Pop();
357+
switch (x)
358+
{
359+
case VMArray array:
360+
{
361+
var index = (int)key.GetInteger();
362+
if (index < 0 || index >= array.Count)
363+
throw new CatchableException($"The index of {nameof(VMArray)} is out of range, {index}/[0, {array.Count}).");
364+
engine.Push(array[index]);
365+
break;
366+
}
367+
case Map map:
368+
{
369+
if (!map.TryGetValue(key, out var value))
370+
throw new CatchableException($"Key {key} not found in {nameof(Map)}.");
371+
engine.Push(value);
372+
break;
373+
}
374+
case PrimitiveType primitive:
375+
{
376+
var byteArray = primitive.GetSpan();
377+
var index = (int)key.GetInteger();
378+
if (index < 0 || index >= byteArray.Length)
379+
throw new CatchableException($"The index of {nameof(PrimitiveType)} is out of range, {index}/[0, {byteArray.Length}).");
380+
engine.Push((BigInteger)byteArray[index]);
381+
break;
382+
}
383+
case Buffer buffer:
384+
{
385+
var index = (int)key.GetInteger();
386+
if (index < 0 || index >= buffer.Size)
387+
throw new CatchableException($"The index of {nameof(Buffer)} is out of range, {index}/[0, {buffer.Size}).");
388+
engine.Push((BigInteger)buffer.InnerBuffer.Span[index]);
389+
break;
390+
}
391+
default:
392+
throw new InvalidOperationException($"Invalid type for {instruction.OpCode}: {x.Type}");
393+
}
394+
}
395+
396+
private static void HasKey_Before543(ExecutionEngine engine, Instruction instruction)
397+
{
398+
var key = engine.Pop<PrimitiveType>();
399+
var x = engine.Pop();
400+
// Check the type of the top item and perform the corresponding action.
401+
switch (x)
402+
{
403+
// For arrays, check if the index is within bounds and push the result onto the stack.
404+
case VMArray array:
405+
{
406+
var index = (int)key.GetInteger();
407+
if (index < 0)
408+
throw new InvalidOperationException($"The negative index {index} is invalid for OpCode {instruction.OpCode}.");
409+
engine.Push(index < array.Count);
410+
break;
411+
}
412+
// For maps, check if the key exists and push the result onto the stack.
413+
case Map map:
414+
{
415+
engine.Push(map.ContainsKey(key));
416+
break;
417+
}
418+
// For buffers, check if the index is within bounds and push the result onto the stack.
419+
case VM.Types.Buffer buffer:
420+
{
421+
var index = (int)key.GetInteger();
422+
if (index < 0)
423+
throw new InvalidOperationException($"The negative index {index} is invalid for OpCode {instruction.OpCode}.");
424+
engine.Push(index < buffer.Size);
425+
break;
426+
}
427+
// For byte strings, check if the index is within bounds and push the result onto the stack.
428+
case ByteString array:
429+
{
430+
var index = (int)key.GetInteger();
431+
if (index < 0)
432+
throw new InvalidOperationException($"The negative index {index} is invalid for OpCode {instruction.OpCode}.");
433+
engine.Push(index < array.Size);
434+
break;
435+
}
436+
default:
437+
throw new InvalidOperationException($"Invalid type for {instruction.OpCode}: {x.Type}");
438+
}
439+
}
440+
441+
278442
protected static void OnCallT(ExecutionEngine engine, Instruction instruction)
279443
{
280444
if (engine is ApplicationEngine app)
@@ -495,7 +659,25 @@ public static ApplicationEngine Create(TriggerType trigger, IVerifiable? contain
495659
var index = persistingBlock?.Index ?? NativeContract.Ledger.CurrentIndex(snapshot);
496660
settings ??= ProtocolSettings.Default;
497661
// Adjust jump table according persistingBlock
498-
var jumpTable = settings.IsHardforkEnabled(Hardfork.HF_Echidna, index) ? DefaultJumpTable : NotEchidnaJumpTable;
662+
663+
JumpTable jumpTable;
664+
665+
if (settings.IsHardforkEnabled(Hardfork.HF_Gorgon, index))
666+
{
667+
jumpTable = DefaultJumpTable;
668+
}
669+
else
670+
{
671+
if (!settings.IsHardforkEnabled(Hardfork.HF_Echidna, index))
672+
{
673+
jumpTable = NotEchidnaJumpTable;
674+
}
675+
else
676+
{
677+
jumpTable = NotGorgonJumpTable;
678+
}
679+
}
680+
499681
var engine = Provider?.Create(trigger, container, snapshot, persistingBlock, settings, gas, diagnostic, jumpTable)
500682
?? new ApplicationEngine(trigger, container, snapshot, persistingBlock, settings, gas, diagnostic, jumpTable);
501683

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright (C) 2015-2026 The Neo Project.
2+
//
3+
// UT_ApplicationEngine.JumpTable.cs file belongs to the neo project and is free
4+
// software distributed under the MIT software license, see the
5+
// accompanying file LICENSE in the main directory of the
6+
// repository or http://www.opensource.org/licenses/mit-license.php
7+
// for more details.
8+
//
9+
// Redistribution and use in source and binary forms with or without
10+
// modifications are permitted.
11+
12+
using Microsoft.VisualStudio.TestTools.UnitTesting;
13+
using Neo.Network.P2P.Payloads;
14+
using Neo.SmartContract;
15+
using Neo.VM;
16+
using System;
17+
using System.Numerics;
18+
19+
namespace Neo.UnitTests.SmartContract
20+
{
21+
public partial class UT_ApplicationEngine
22+
{
23+
[TestMethod]
24+
public void TestHasKeyWithDifferentHF()
25+
{
26+
var script = BuildHasKeyLargeIndexScript();
27+
const uint EchidnaEnable = 10u;
28+
const uint GorgonEnable = 20u;
29+
// Hardfork heights:
30+
// Echidna at 10, Gorgon at 20
31+
// - index=5 => pre-Echidna (NotEchidnaJumpTable)
32+
// - index=15 => Echidna enabled, Gorgon NOT enabled (NotGorgonJumpTable)
33+
// - index=30 => Gorgon enabled (DefaultJumpTable)
34+
var settings = ProtocolSettings.Default with
35+
{
36+
Hardforks = ProtocolSettings.Default.Hardforks
37+
.SetItem(Hardfork.HF_Echidna, EchidnaEnable)
38+
.SetItem(Hardfork.HF_Gorgon, GorgonEnable)
39+
};
40+
41+
Assert.IsFalse(settings.IsHardforkEnabled(Hardfork.HF_Echidna, 5u));
42+
Assert.IsTrue(settings.IsHardforkEnabled(Hardfork.HF_Echidna, 15u));
43+
Assert.IsTrue(settings.IsHardforkEnabled(Hardfork.HF_Echidna, 30u));
44+
Assert.IsFalse(settings.IsHardforkEnabled(Hardfork.HF_Gorgon, 15u));
45+
Assert.IsTrue(settings.IsHardforkEnabled(Hardfork.HF_Gorgon, 30u));
46+
47+
// Case A: pre-Echidna => Overflow
48+
ExecuteAndAssertFault<OverflowException>(script, settings, index: 5u);
49+
50+
// Case B: Echidna enabled but pre-Gorgon => Overflow
51+
ExecuteAndAssertFault<OverflowException>(script, settings, index: 15u);
52+
53+
// Case C: Gorgon enabled => InvalidOperationException
54+
ExecuteAndAssertFault<InvalidOperationException>(script, settings, index: 30u);
55+
}
56+
57+
private static byte[] BuildHasKeyLargeIndexScript()
58+
{
59+
// HASKEY pops: key (PrimitiveType), then x (Array/Map/Buffer/ByteString)
60+
// So push x first, then key.
61+
var largeIndex = new BigInteger((long)int.MaxValue + 1);
62+
63+
using var sb = new ScriptBuilder();
64+
65+
// Build array: [1]
66+
sb.EmitPush(1);
67+
sb.EmitPush(1);
68+
sb.Emit(OpCode.PACK);
69+
70+
// Push key and call HASKEY
71+
sb.EmitPush(largeIndex);
72+
sb.Emit(OpCode.HASKEY);
73+
74+
sb.Emit(OpCode.RET);
75+
return sb.ToArray();
76+
}
77+
78+
private static void ExecuteAndAssertFault<TException>(byte[] script, ProtocolSettings settings, uint index) where TException : Exception
79+
{
80+
var snapshotCache = TestBlockchain.GetTestSnapshotCache();
81+
var block = new Block
82+
{
83+
Header = new Header
84+
{
85+
PrevHash = UInt256.Zero,
86+
MerkleRoot = UInt256.Zero,
87+
Index = index,
88+
NextConsensus = UInt160.Zero,
89+
Witness = Witness.Empty
90+
},
91+
Transactions = []
92+
};
93+
94+
using var engine = ApplicationEngine.Create(
95+
TriggerType.Application,
96+
container: null,
97+
snapshotCache,
98+
persistingBlock: block,
99+
settings: settings,
100+
gas: 100_00000000L);
101+
102+
engine.LoadScript(script);
103+
engine.Execute();
104+
105+
Assert.AreEqual(VMState.FAULT, engine.State, $"Expected FAULT at index={index}.");
106+
Assert.IsNotNull(engine.FaultException, $"Expected FaultException at index={index}.");
107+
Assert.IsInstanceOfType(engine.FaultException, typeof(TException),
108+
$"Expected {typeof(TException).Name} at index={index}, but got {engine.FaultException.GetType().Name}.");
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)