Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize ECPoint serialization #3757

Merged
merged 7 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 32 additions & 0 deletions src/Neo.IO/ISerializableSpan.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (C) 2015-2025 The Neo Project.
//
// ISerializableSpan.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 System;

namespace Neo.IO
{
/// <summary>
/// Represents NEO objects that can be serialized.
/// </summary>
public interface ISerializableSpan
{
/// <summary>
/// The size of the object in bytes after serialization.
/// </summary>
int Size { get; }

/// <summary>
/// Gets a ReadOnlySpan that represents the current value
/// Requires keeping the data returned by GetSpan consistent with the data generated by <see cref="ISerializable"/>.
/// </summary>
ReadOnlySpan<byte> GetSpan();
}
}
17 changes: 14 additions & 3 deletions src/Neo/Cryptography/ECC/ECPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
using System;
using System.IO;
using System.Numerics;
using System.Runtime.CompilerServices;

namespace Neo.Cryptography.ECC
{
/// <summary>
/// Represents a (X,Y) coordinate pair for elliptic curve cryptography (ECC) structures.
/// </summary>
public class ECPoint : IComparable<ECPoint>, IEquatable<ECPoint>, ISerializable
public class ECPoint : IComparable<ECPoint>, IEquatable<ECPoint>, ISerializable, ISerializableSpan
{
internal ECFieldElement X, Y;
internal readonly ECCurve Curve;
Expand Down Expand Up @@ -187,10 +188,10 @@ public byte[] EncodePoint(bool commpressed)
{
if (_uncompressedPoint != null) return _uncompressedPoint;
data = new byte[65];
byte[] yBytes = Y.Value.ToByteArray(isUnsigned: true, isBigEndian: true);
var yBytes = Y.Value.ToByteArray(isUnsigned: true, isBigEndian: true);
Buffer.BlockCopy(yBytes, 0, data, 65 - yBytes.Length, yBytes.Length);
}
byte[] xBytes = X.Value.ToByteArray(isUnsigned: true, isBigEndian: true);
var xBytes = X.Value.ToByteArray(isUnsigned: true, isBigEndian: true);
Buffer.BlockCopy(xBytes, 0, data, 33 - xBytes.Length, xBytes.Length);
data[0] = commpressed ? Y.Value.IsEven ? (byte)0x02 : (byte)0x03 : (byte)0x04;
if (commpressed) _compressedPoint = data;
Expand Down Expand Up @@ -350,6 +351,16 @@ void ISerializable.Serialize(BinaryWriter writer)
writer.Write(EncodePoint(true));
}

/// <summary>
/// Gets a ReadOnlySpan that represents the current value.
/// </summary>
/// <returns>A ReadOnlySpan that represents the current value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<byte> GetSpan()
{
return EncodePoint(true).AsSpan();
}

public override string ToString()
{
return EncodePoint(true).ToHexString();
Expand Down
26 changes: 1 addition & 25 deletions src/Neo/SmartContract/KeyBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,36 +74,12 @@ public KeyBuilder Add(ReadOnlySpan<byte> key)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public KeyBuilder Add(byte[] key) => Add(key.AsSpan());

/// <summary>
/// Adds part of the key to the builder.
/// </summary>
/// <param name="key">Part of the key represented by a <see cref="UInt160"/>.</param>
/// <returns>A reference to this instance after the add operation has completed.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public KeyBuilder Add(UInt160 key) => Add(key.GetSpan());

/// <summary>
/// Adds part of the key to the builder.
/// </summary>
/// <param name="key">Part of the key represented by a <see cref="UInt256"/>.</param>
/// <returns>A reference to this instance after the add operation has completed.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public KeyBuilder Add(UInt256 key) => Add(key.GetSpan());

/// <summary>
/// Adds part of the key to the builder.
/// </summary>
/// <param name="key">Part of the key.</param>
/// <returns>A reference to this instance after the add operation has completed.</returns>
public KeyBuilder Add(ISerializable key)
{
using (BinaryWriter writer = new(_stream, Utility.StrictUTF8, true))
{
key.Serialize(writer);
writer.Flush();
}
return this;
}
public KeyBuilder Add(ISerializableSpan key) => Add(key.GetSpan());

/// <summary>
/// Adds part of the key to the builder in BigEndian.
Expand Down
2 changes: 1 addition & 1 deletion src/Neo/UInt160.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace Neo
/// Represents a 160-bit unsigned integer.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 20)]
public class UInt160 : IComparable<UInt160>, IEquatable<UInt160>, ISerializable
public class UInt160 : IComparable<UInt160>, IEquatable<UInt160>, ISerializable, ISerializableSpan
{
/// <summary>
/// The length of <see cref="UInt160"/> values.
Expand Down
2 changes: 1 addition & 1 deletion src/Neo/UInt256.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace Neo
/// Represents a 256-bit unsigned integer.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 32)]
public class UInt256 : IComparable<UInt256>, IEquatable<UInt256>, ISerializable
public class UInt256 : IComparable<UInt256>, IEquatable<UInt256>, ISerializable, ISerializableSpan
{
/// <summary>
/// The length of <see cref="UInt256"/> values.
Expand Down
2 changes: 1 addition & 1 deletion tests/Neo.Plugins.OracleService.Tests/TestUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static class TestUtils
public static readonly UInt160 MultisigScriptHash = MultisigScript.ToScriptHash();
public static readonly string MultisigAddress = MultisigScriptHash.ToAddress(ProtocolSettings.Default.AddressVersion);

public static StorageKey CreateStorageKey(this NativeContract contract, byte prefix, ISerializable key)
public static StorageKey CreateStorageKey(this NativeContract contract, byte prefix, ISerializableSpan key)
{
var k = new KeyBuilder(contract.Id, prefix);
if (key != null) k = k.Add(key);
Expand Down
10 changes: 7 additions & 3 deletions tests/Neo.UnitTests/Cryptography/ECC/UT_ECPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ public void TestEncodePoint()
[TestMethod]
public void TestEquals()
{
ECPoint point = ECCurve.Secp256k1.G;
var point = ECCurve.Secp256k1.G;
Assert.IsTrue(point.Equals(point));
Assert.IsFalse(point.Equals(null));

Expand Down Expand Up @@ -309,11 +309,15 @@ public void TestDeserialize()
[TestMethod]
public void TestSerialize()
{
MemoryStream stream = new MemoryStream();
ECPoint point = new ECPoint(null, null, ECCurve.Secp256k1);
var stream = new MemoryStream();
var point = new ECPoint(null, null, ECCurve.Secp256k1);
ISerializable serializable = point;
serializable.Serialize(new BinaryWriter(stream));
CollectionAssert.AreEqual(new byte[] { 0 }, stream.ToArray());

CollectionAssert.AreEqual(point.GetSpan().ToArray(), stream.ToArray());
point = ECCurve.Secp256r1.G;
CollectionAssert.AreEqual(point.GetSpan().ToArray(), point.ToArray());
}

[TestMethod]
Expand Down
8 changes: 4 additions & 4 deletions tests/Neo.UnitTests/SmartContract/UT_KeyBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ public void Test()
Assert.AreEqual("0100000002", key.ToArray().ToHexString());

key = new KeyBuilder(1, 2);
key = key.Add(new byte[] { 3, 4 });
key = key.Add([3, 4]);
Assert.AreEqual("01000000020304", key.ToArray().ToHexString());

key = new KeyBuilder(1, 2);
key = key.Add(new byte[] { 3, 4 });
key = key.Add([3, 4]);
key = key.Add(UInt160.Zero);
Assert.AreEqual("010000000203040000000000000000000000000000000000000000", key.ToArray().ToHexString());

Expand Down Expand Up @@ -91,7 +91,7 @@ public void TestAddUInt()
Assert.AreEqual("0100000002000102030405060708090a0b0c0d0e0f10111213", key.ToArray().ToHexString());

var key2 = new KeyBuilder(1, 2);
key2 = key2.Add((ISerializable)(new UInt160(value)));
key2 = key2.Add((ISerializableSpan)new UInt160(value));

// It must be same before and after optimization.
Assert.AreEqual(key.ToArray().ToHexString(), key2.ToArray().ToHexString());
Expand All @@ -104,7 +104,7 @@ public void TestAddUInt()
Assert.AreEqual("0100000002000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", key.ToArray().ToHexString());

key2 = new KeyBuilder(1, 2);
key2 = key2.Add((ISerializable)(new UInt256(value)));
key2 = key2.Add((ISerializableSpan)new UInt256(value));

// It must be same before and after optimization.
Assert.AreEqual(key.ToArray().ToHexString(), key2.ToArray().ToHexString());
Expand Down
16 changes: 8 additions & 8 deletions tests/Neo.UnitTests/TestUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,23 @@ namespace Neo.UnitTests
{
public static partial class TestUtils
{
public static readonly Random TestRandom = new Random(1337); // use fixed seed for guaranteed determinism
public static readonly Random TestRandom = new(1337); // use fixed seed for guaranteed determinism

public static UInt256 RandomUInt256()
{
byte[] data = new byte[32];
var data = new byte[32];
TestRandom.NextBytes(data);
return new UInt256(data);
}

public static UInt160 RandomUInt160()
{
byte[] data = new byte[20];
var data = new byte[20];
TestRandom.NextBytes(data);
return new UInt160(data);
}

public static StorageKey CreateStorageKey(this NativeContract contract, byte prefix, ISerializable key = null)
public static StorageKey CreateStorageKey(this NativeContract contract, byte prefix, ISerializableSpan key = null)
{
var k = new KeyBuilder(contract.Id, prefix);
if (key != null) k = k.Add(key);
Expand All @@ -54,9 +54,9 @@ public static StorageKey CreateStorageKey(this NativeContract contract, byte pre

public static byte[] GetByteArray(int length, byte firstByte)
{
byte[] array = new byte[length];
var array = new byte[length];
array[0] = firstByte;
for (int i = 1; i < length; i++)
for (var i = 1; i < length; i++)
{
array[i] = 0x20;
}
Expand All @@ -65,7 +65,7 @@ public static byte[] GetByteArray(int length, byte firstByte)

public static NEP6Wallet GenerateTestWallet(string password)
{
JObject wallet = new JObject();
var wallet = new JObject();
wallet["name"] = "noname";
wallet["version"] = new Version("1.0").ToString();
wallet["scrypt"] = new ScryptParameters(2, 1, 1).ToJson();
Expand Down Expand Up @@ -103,7 +103,7 @@ public static void StorageItemAdd(DataCache snapshot, int id, byte[] keyValue, b

public static void FillMemoryPool(DataCache snapshot, NeoSystem system, NEP6Wallet wallet, WalletAccount account)
{
for (int i = 0; i < system.Settings.MemoryPoolMaxTransactions; i++)
for (var i = 0; i < system.Settings.MemoryPoolMaxTransactions; i++)
{
var tx = CreateValidTx(snapshot, wallet, account);
system.MemPool.TryAdd(tx, snapshot);
Expand Down