Skip to content

Commit

Permalink
Add hardfork HF_Echidna (#3454)
Browse files Browse the repository at this point in the history
* add hardofork HF_Echidna

* Add entries to `Designation` event (#3397)

* Add entries to Designation event

* Change to HF_Echidna

* Add UT

* Add count

* [Neo Core StdLib] Add Base64url (#3453)

* add base64url

* active in

* update placehold hf height

* fix hf issue and move methods to proper place.

* fix test

* use identifymodel instead.

* add hardofork HF_Echidna

* Add entries to `Designation` event (#3397)

* Add entries to Designation event

* Change to HF_Echidna

* Add UT

* Add count

* [Neo Core StdLib] Add Base64url (#3453)

* add base64url

* active in

* update placehold hf height

* fix hf issue and move methods to proper place.

* fix test

* use identifymodel instead.

* add hardofork HF_Echidna

* Add entries to `Designation` event (#3397)

* Add entries to Designation event

* Change to HF_Echidna

* Add UT

* Add count

* [Neo Core StdLib] Add Base64url (#3453)

* add base64url

* active in

* update placehold hf height

* fix hf issue and move methods to proper place.

* fix test

* use identifymodel instead.

* format

* Fixed typo

* Added back #3397

* Fixed tests

* fixed global.json

* Update src/Neo/Neo.csproj

* Update src/Neo/Neo.csproj

* [`Fix`]: integer overflow in `JumpTable.SubStr ` (#3496)

* fix: int overflow in SubStr

* fix: int overflow in SubStr

* format

* Versioning change

* Clean

* Rename

* Show change

* Space

* remove duplicated lines in gitignroe

---------

Co-authored-by: Jimmy <[email protected]>
Co-authored-by: Shargon <[email protected]>

* Fix NEO callstates (#3599)

* Allow callstates to use HF

* Rename to method

* Other rename

* Change the way

* Reduce changes

* Reduce changes

* Adapt name always

* Avoid string when only is lower the first char

* UT

* Test all

* Update src/Neo/ProtocolSettings.cs

Co-authored-by: Christopher Schuchardt <[email protected]>

* Update src/Neo/ProtocolSettings.cs

Co-authored-by: Christopher Schuchardt <[email protected]>

* Reuse Load from stream

* Unify

* Fix default logic

* Change ContractMethod to allowMultiple

* Use LowerInvariant

* Move CheckingHardfork

* Remove optional arg

* Fix build

* Avoid file not found error

---------

Co-authored-by: Christopher Schuchardt <[email protected]>

* fix tests error (#3636)

* fux build error

* Update src/Neo/SmartContract/ApplicationEngine.cs

---------

Co-authored-by: Shargon <[email protected]>

* NeoToken: accept candidate registration via onNEP17Payment (#3597)

Solves two problems:
 * inability to estimate GAS needed for registerCandidate in a regular way
   because of its very high fee (more than what normal RPC servers allow)
 * inability to have MaxBlockSystemFee lower than the registration price
   which is very high on its own (more than practically possible to execute)

Fixes #3552.

Signed-off-by: Roman Khimov <[email protected]>

* specify the argument exception information.

* Fix Ut (#3635)

* NeoToken: add NEP-27 to supported standards list starting from Echidna (#3643)

#3597 introduces `onNEP17Payment`
handler to native NeoToke contract starting from Echidna hardfork. We
need to update the list of supported standards respectively.

Signed-off-by: Anna Shaleva <[email protected]>

* ut: fix HF_Echidna unit tests (#3646)

* Fix UT

* Update src/Neo/ProtocolSettings.cs

Co-authored-by: nan01ab <[email protected]>

* Update src/Neo/ProtocolSettings.cs

Co-authored-by: nan01ab <[email protected]>

* Update src/Neo/ProtocolSettings.cs

Co-authored-by: Christopher Schuchardt <[email protected]>

---------

Co-authored-by: Jimmy <[email protected]>
Co-authored-by: nan01ab <[email protected]>
Co-authored-by: Christopher Schuchardt <[email protected]>

* [Core Add] Add support to Ed25519 (#3507)

* fix unnecessary change

* Clean using

---------

Co-authored-by: Fernando Diaz Toledano <[email protected]>

* Fix `HF_Echidna` comments (#3679)

* Fix obsolete

* Fix https://github.com/neo-project/neo/pull/3454/files#r1912152270

* Fix comment

* Update RoleManagement.cs

* Unset HF_Echidna

* Revert getTransaction

* Revert verifyWithECDsa

* format

---------

Signed-off-by: Roman Khimov <[email protected]>
Signed-off-by: Anna Shaleva <[email protected]>
Co-authored-by: Shargon <[email protected]>
Co-authored-by: Christopher Schuchardt <[email protected]>
Co-authored-by: nan01ab <[email protected]>
Co-authored-by: Roman Khimov <[email protected]>
Co-authored-by: Anna Shaleva <[email protected]>
Co-authored-by: Vitor Nazário Coelho <[email protected]>
  • Loading branch information
7 people authored Jan 23, 2025
1 parent 02fc4cb commit befe7f6
Show file tree
Hide file tree
Showing 35 changed files with 948 additions and 129 deletions.
2 changes: 1 addition & 1 deletion src/Neo.VM/JumpTable/JumpTable.Splice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public virtual void SubStr(ExecutionEngine engine, Instruction instruction)
if (index < 0)
throw new InvalidOperationException($"The index can not be negative for {nameof(OpCode.SUBSTR)}, index: {index}.");
var x = engine.Pop().GetSpan();
if (index + count > x.Length)
if (checked(index + count) > x.Length)
throw new InvalidOperationException($"The index + count is out of range for {nameof(OpCode.SUBSTR)}, index: {index}, count: {count}, {index + count}/[0, {x.Length}].");
Types.Buffer result = new(count, false);
x.Slice(index, count).CopyTo(result.InnerBuffer.Span);
Expand Down
99 changes: 99 additions & 0 deletions src/Neo/Cryptography/Ed25519.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (C) 2015-2025 The Neo Project.
//
// Ed25519.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Security;
using System;

namespace Neo.Cryptography
{
public class Ed25519
{
internal const int PublicKeySize = 32;
private const int PrivateKeySize = 32;
internal const int SignatureSize = 64;

/// <summary>
/// Generates a new Ed25519 key pair.
/// </summary>
/// <returns>A byte array containing the private key.</returns>
public static byte[] GenerateKeyPair()
{
var keyPairGenerator = new Ed25519KeyPairGenerator();
keyPairGenerator.Init(new Ed25519KeyGenerationParameters(new SecureRandom()));
var keyPair = keyPairGenerator.GenerateKeyPair();
return ((Ed25519PrivateKeyParameters)keyPair.Private).GetEncoded();
}

/// <summary>
/// Derives the public key from a given private key.
/// </summary>
/// <param name="privateKey">The private key as a byte array.</param>
/// <returns>The corresponding public key as a byte array.</returns>
/// <exception cref="ArgumentException">Thrown when the private key size is invalid.</exception>
public static byte[] GetPublicKey(byte[] privateKey)
{
if (privateKey.Length != PrivateKeySize)
throw new ArgumentException("Invalid private key size", nameof(privateKey));

var privateKeyParams = new Ed25519PrivateKeyParameters(privateKey, 0);
return privateKeyParams.GeneratePublicKey().GetEncoded();
}

/// <summary>
/// Signs a message using the provided private key.
/// Parameters are in the same order as the sample in the Ed25519 specification
/// Ed25519.sign(privkey, pubkey, msg) with pubkey omitted
/// ref. https://datatracker.ietf.org/doc/html/rfc8032.
/// </summary>
/// <param name="privateKey">The private key used for signing.</param>
/// <param name="message">The message to be signed.</param>
/// <returns>The signature as a byte array.</returns>
/// <exception cref="ArgumentException">Thrown when the private key size is invalid.</exception>
public static byte[] Sign(byte[] privateKey, byte[] message)
{
if (privateKey.Length != PrivateKeySize)
throw new ArgumentException("Invalid private key size", nameof(privateKey));

var signer = new Ed25519Signer();
signer.Init(true, new Ed25519PrivateKeyParameters(privateKey, 0));
signer.BlockUpdate(message, 0, message.Length);
return signer.GenerateSignature();
}

/// <summary>
/// Verifies an Ed25519 signature for a given message using the provided public key.
/// Parameters are in the same order as the sample in the Ed25519 specification
/// Ed25519.verify(public, msg, signature)
/// ref. https://datatracker.ietf.org/doc/html/rfc8032.
/// </summary>
/// <param name="publicKey">The 32-byte public key used for verification.</param>
/// <param name="message">The message that was signed.</param>
/// <param name="signature">The 64-byte signature to verify.</param>
/// <returns>True if the signature is valid for the given message and public key; otherwise, false.</returns>
/// <exception cref="ArgumentException">Thrown when the signature or public key size is invalid.</exception>
public static bool Verify(byte[] publicKey, byte[] message, byte[] signature)
{
if (signature.Length != SignatureSize)
throw new ArgumentException("Invalid signature size", nameof(signature));

if (publicKey.Length != PublicKeySize)
throw new ArgumentException("Invalid public key size", nameof(publicKey));

var verifier = new Ed25519Signer();
verifier.Init(false, new Ed25519PublicKeyParameters(publicKey, 0));
verifier.BlockUpdate(message, 0, message.Length);
return verifier.VerifySignature(signature);
}
}
}
3 changes: 2 additions & 1 deletion src/Neo/Hardfork.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public enum Hardfork : byte
HF_Aspidochelone,
HF_Basilisk,
HF_Cockatrice,
HF_Domovoi
HF_Domovoi,
HF_Echidna
}
}
2 changes: 2 additions & 0 deletions src/Neo/Neo.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.1" />
<PackageReference Include="System.IO.Hashing" Version="9.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
66 changes: 59 additions & 7 deletions src/Neo/ProtocolSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;

namespace Neo
Expand Down Expand Up @@ -123,19 +124,69 @@ public record ProtocolSettings

public static ProtocolSettings Custom { get; set; }

/// <summary>
/// Searches for a file in the given path. If not found, checks in the executable directory.
/// </summary>
/// <param name="fileName">The name of the file to search for.</param>
/// <param name="path">The primary path to search in.</param>
/// <returns>Full path of the file if found, null otherwise.</returns>
public static string FindFile(string fileName, string path)
{
// Check if the given path is relative
if (!Path.IsPathRooted(path))
{
// Combine with the executable directory if relative
var executablePath = AppContext.BaseDirectory;
path = Path.Combine(executablePath, path);
}

// Check if file exists in the specified (resolved) path
var fullPath = Path.Combine(path, fileName);
if (File.Exists(fullPath))
{
return fullPath;
}

// Check if file exists in the executable directory
var executableDir = AppContext.BaseDirectory;
fullPath = Path.Combine(executableDir, fileName);
if (File.Exists(fullPath))
{
return fullPath;
}

// File not found in either location
return null;
}

/// <summary>
/// Loads the <see cref="ProtocolSettings"/> from the specified stream.
/// </summary>
/// <param name="stream">The stream of the settings.</param>
/// <returns>The loaded <see cref="ProtocolSettings"/>.</returns>
public static ProtocolSettings Load(Stream stream)
{
var config = new ConfigurationBuilder().AddJsonStream(stream).Build();
var section = config.GetSection("ProtocolConfiguration");
return Load(section);
}

/// <summary>
/// Loads the <see cref="ProtocolSettings"/> at the specified path.
/// </summary>
/// <param name="path">The path of the settings file.</param>
/// <param name="optional">Indicates whether the file is optional.</param>
/// <returns>The loaded <see cref="ProtocolSettings"/>.</returns>
public static ProtocolSettings Load(string path, bool optional = true)
public static ProtocolSettings Load(string path)
{
IConfigurationRoot config = new ConfigurationBuilder().AddJsonFile(path, optional).Build();
IConfigurationSection section = config.GetSection("ProtocolConfiguration");
var settings = Load(section);
CheckingHardfork(settings);
return settings;
path = FindFile(path, Environment.CurrentDirectory);

if (path is null)
{
return Default;
}

using var stream = File.OpenRead(path);
return Load(stream);
}

/// <summary>
Expand Down Expand Up @@ -165,6 +216,7 @@ public static ProtocolSettings Load(IConfigurationSection section)
? EnsureOmmitedHardforks(section.GetSection("Hardforks").GetChildren().ToDictionary(p => Enum.Parse<Hardfork>(p.Key, true), p => uint.Parse(p.Value))).ToImmutableDictionary()
: Default.Hardforks
};
CheckingHardfork(Custom);
return Custom;
}

Expand Down
38 changes: 37 additions & 1 deletion src/Neo/SmartContract/ApplicationEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ namespace Neo.SmartContract
public partial class ApplicationEngine : ExecutionEngine
{
protected static readonly JumpTable DefaultJumpTable = ComposeDefaultJumpTable();
protected static readonly JumpTable NotEchidnaJumpTable = ComposeNotEchidnaJumpTable();

/// <summary>
/// The maximum cost that can be spent when a contract is executed in test mode.
Expand Down Expand Up @@ -215,6 +216,13 @@ private static JumpTable ComposeDefaultJumpTable()
return table;
}

public static JumpTable ComposeNotEchidnaJumpTable()
{
var jumpTable = ComposeDefaultJumpTable();
jumpTable[OpCode.SUBSTR] = VulnerableSubStr;
return jumpTable;
}

protected static void OnCallT(ExecutionEngine engine, Instruction instruction)
{
if (engine is ApplicationEngine app)
Expand Down Expand Up @@ -400,13 +408,41 @@ internal override void UnloadContext(ExecutionContext context)
/// <returns>The engine instance created.</returns>
public static ApplicationEngine Create(TriggerType trigger, IVerifiable container, DataCache snapshot, Block persistingBlock = null, ProtocolSettings settings = null, long gas = TestModeGas, IDiagnostic diagnostic = null)
{
var index = persistingBlock?.Index ?? (snapshot == null ? 0 : NativeContract.Ledger.CurrentIndex(snapshot));

// Adjust jump table according persistingBlock
var jumpTable = ApplicationEngine.DefaultJumpTable;

var jumpTable = settings == null || settings.IsHardforkEnabled(Hardfork.HF_Echidna, index) ? DefaultJumpTable : NotEchidnaJumpTable;
return Provider?.Create(trigger, container, snapshot, persistingBlock, settings, gas, diagnostic, jumpTable)
?? new ApplicationEngine(trigger, container, snapshot, persistingBlock, settings, gas, diagnostic, jumpTable);
}

/// <summary>
/// Extracts a substring from the specified buffer and pushes it onto the evaluation stack.
/// <see cref="OpCode.SUBSTR"/>
/// </summary>
/// <param name="engine">The execution engine.</param>
/// <param name="instruction">The instruction being executed.</param>
/// <remarks>Pop 3, Push 1</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void VulnerableSubStr(ExecutionEngine engine, Instruction instruction)
{
var count = (int)engine.Pop().GetInteger();
if (count < 0)
throw new InvalidOperationException($"The count can not be negative for {nameof(OpCode.SUBSTR)}, count: {count}.");
var index = (int)engine.Pop().GetInteger();
if (index < 0)
throw new InvalidOperationException($"The index can not be negative for {nameof(OpCode.SUBSTR)}, index: {index}.");
var x = engine.Pop().GetSpan();
// Note: here it's the main change
if (index + count > x.Length)
throw new InvalidOperationException($"The index + count is out of range for {nameof(OpCode.SUBSTR)}, index: {index}, count: {count}, {index + count}/[0, {x.Length}].");

VM.Types.Buffer result = new(count, false);
x.Slice(index, count).CopyTo(result.InnerBuffer.Span);
engine.Push(result);
}

public override void LoadContext(ExecutionContext context)
{
// Set default execution context state
Expand Down
2 changes: 1 addition & 1 deletion src/Neo/SmartContract/Contract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public static Contract CreateMultiSigContract(int m, IReadOnlyCollection<ECPoint
public static byte[] CreateMultiSigRedeemScript(int m, IReadOnlyCollection<ECPoint> publicKeys)
{
if (!(1 <= m && m <= publicKeys.Count && publicKeys.Count <= 1024))
throw new ArgumentException();
throw new ArgumentException($"Invalid multisig parameters: m={m}, publicKeys.Count={publicKeys.Count}");
using ScriptBuilder sb = new();
sb.EmitPush(m);
foreach (ECPoint publicKey in publicKeys.OrderBy(p => p))
Expand Down
3 changes: 2 additions & 1 deletion src/Neo/SmartContract/Native/ContractMethodAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
namespace Neo.SmartContract.Native
{
[DebuggerDisplay("{Name}")]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false)]
// We allow multiple attributes because the fees or requiredCallFlags may change between hard forks.
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true)]
internal class ContractMethodAttribute : Attribute, IHardforkActivable
{
public string Name { get; init; }
Expand Down
3 changes: 2 additions & 1 deletion src/Neo/SmartContract/Native/ContractMethodMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ internal class ContractMethodMetadata : IHardforkActivable

public ContractMethodMetadata(MemberInfo member, ContractMethodAttribute attribute)
{
Name = attribute.Name ?? member.Name.ToLower()[0] + member.Name[1..];
Name = attribute.Name ?? member.Name;
Name = Name.ToLowerInvariant()[0] + Name[1..];
Handler = member switch
{
MethodInfo m => m,
Expand Down
31 changes: 31 additions & 0 deletions src/Neo/SmartContract/Native/CryptoLib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

using Neo.Cryptography;
using Neo.Cryptography.ECC;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using System;
using System.Collections.Generic;

Expand Down Expand Up @@ -115,5 +117,34 @@ public static bool VerifyWithECDsaV0(byte[] message, byte[] pubkey, byte[] signa
return false;
}
}

/// <summary>
/// Verifies that a digital signature is appropriate for the provided key and message using the Ed25519 algorithm.
/// </summary>
/// <param name="message">The signed message.</param>
/// <param name="publicKey">The Ed25519 public key to be used.</param>
/// <param name="signature">The signature to be verified.</param>
/// <returns><see langword="true"/> if the signature is valid; otherwise, <see langword="false"/>.</returns>
[ContractMethod(Hardfork.HF_Echidna, CpuFee = 1 << 15)]
public static bool VerifyWithEd25519(byte[] message, byte[] publicKey, byte[] signature)
{
if (signature.Length != Ed25519.SignatureSize)
return false;

if (publicKey.Length != Ed25519.PublicKeySize)
return false;

try
{
var verifier = new Ed25519Signer();
verifier.Init(false, new Ed25519PublicKeyParameters(publicKey, 0));
verifier.BlockUpdate(message, 0, message.Length);
return verifier.VerifySignature(signature);
}
catch (Exception)
{
return false;
}
}
}
}
2 changes: 1 addition & 1 deletion src/Neo/SmartContract/Native/FungibleToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ protected FungibleToken() : base()
Factor = BigInteger.Pow(10, Decimals);
}

protected override void OnManifestCompose(ContractManifest manifest)
protected override void OnManifestCompose(IsHardforkEnabledDelegate hfChecker, uint blockHeight, ContractManifest manifest)
{
manifest.SupportedStandards = new[] { "NEP-17" };
}
Expand Down
Loading

0 comments on commit befe7f6

Please sign in to comment.