diff --git a/README.md b/README.md index 291bf3a..9293398 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# NEO Platform - C# Blockchain SDK +# NEO Platform [![License](https://img.shields.io/github/license/Rapid-Loop/neo-platform)](https://github.com/Rapid-Loop/neo-platform/blob/master/LICENSE) [![Stars](https://img.shields.io/github/stars/Rapid-Loop/neo-platform)](https://github.com/Rapid-Loop/neo-platform/stargazers) @@ -7,105 +7,115 @@ [![Last Commit](https://img.shields.io/github/last-commit/Rapid-Loop/neo-platform)](https://github.com/Rapid-Loop/neo-platform/commits/master) [![Verify Pull Request](https://github.com/Rapid-Loop/neo-platform/actions/workflows/verify.yml/badge.svg)](https://github.com/Rapid-Loop/neo-platform/actions/workflows/verify.yml) -**Official Rapid Loop fork and enterprise-enhanced rebuild of the NEO Blockchain Smart Economy Platform.** +**Modular Neo blockchain platform for C# / .NET** — maintained by [Rapid Loop](https://github.com/Rapid-Loop). -A modern, high-performance C# library for building on the **Neo blockchain** — designed for developers and enterprises who need reliability, scalability, and seamless integration. +A high-performance library set for building on **Neo**: core types, virtual machine, wallets, storage, and hosting integration. Aimed at developers and enterprises that need reliable packaging and a modern .NET experience. + +This repository is **compatible with Neo protocol semantics**. It is Rapid Loop’s platform packaging and engineering work — **not** an “official Neo Foundation” product unless separately stated by Neo. + +**Neo docs:** [developers.neo.org](https://developers.neo.org/) · **Neo site:** [neo.org](https://neo.org) --- -## 🌟 About This Project +## About -**neo-platform** is Rapid Loop's actively maintained fork of the Neo blockchain C# ecosystem. We are rebuilding and enhancing the Neo platform with enterprise-grade improvements for: +**neo-platform** is Rapid Loop’s actively developed Neo C# stack: modular packages, enterprise-friendly configuration and logging hooks, and a cleaner project layout for real systems. -- Better performance and scalability -- Modern developer experience -- Seamless integration with existing business systems -- Enhanced security and tooling +Use it when you need: -Whether you're building decentralized applications (dApps), smart contracts, wallets, or enterprise blockchain solutions, this SDK provides the core building blocks. +- Core blockchain types and serialization +- Neo VM execution +- Wallet formats (e.g. NEP-6) and account management +- RocksDB-backed blockchain storage +- .NET Generic Host / CLI integration -**Official Neo Website**: [neo.org](https://neo.org) +Led by a [neo-project](https://github.com/neo-project) contributor with core Neo development experience. Rapid Loop commercial work (integration, review, support) is separate from open-source contribution — see [Rapid-Loop](https://github.com/Rapid-Loop). --- -## ✨ Key Features +## Packages -- **Full Neo Compatibility** — Core blockchain, VM, cryptography, and wallet functionality -- **Enterprise Ready** — Robust configuration, logging, and integration tools -- **Modular Architecture** — Clean separation across packages: - - `Neo.Core` — Core blockchain logic - - `Neo.VM` — Virtual Machine - - `Neo.Cryptography` — Secure cryptographic primitives - - `Neo.Wallet` — Wallet management - - And more... -- **High Performance** — Optimized for real-world usage -- **Comprehensive Testing** — Full test suite included -- **NuGet Ready** — Easy package distribution +| Project | Role | +|---------|------| +| `Neo.Core` | Protocol types, crypto, P2P messages, serialization, VM opcodes / limits | +| `Neo.VM` | Execution engine, stack types, middleware pipeline | +| `Neo.Wallet` | Wallet abstractions, NEP-6 / dev wallets | +| `Neo.IO` | I/O helpers (hashing, bloom filter) | +| `Neo.Configuration` | Options and JSON converters for protocol / store settings | +| `Neo.Platform.Storage` | Blockchain store, snapshots, backups (RocksDB) | +| `Neo.Platform.Hosting` | Hosting, configuration, logging, command-line wiring | +| `Neo.Platform.CLI` | Sample / entry CLI host | +| `Neo.Localization` | Localized VM messages | --- -## 📁 Repository Structure +## Repository layout -```bash +```text neo-platform/ -├── src/ # Source code (multiple projects) -│ ├── Neo.Core/ -│ ├── Neo.VM/ -│ ├── Neo.Cryptography/ -│ ├── Neo.Wallet/ -│ └── ... -├── tests/ # Unit and integration tests -├── pkgs/ # Package distribution -├── .github/workflows/ # CI/CD pipelines -├── .images/ # Logos and assets -└── All.slnx # Solution file +├── src/ # Library and host projects +├── tests/ # Unit and integration tests +├── pkgs/ # NuGet package output +├── .github/workflows/ # CI +├── .images/ # Package / branding assets +└── All.slnx # Solution ``` --- -## 🚀 Quick Start +## Quick start + +### Clone -### 1. Clone the repository ```bash git clone https://github.com/Rapid-Loop/neo-platform.git cd neo-platform ``` -### 2. Build the solution +### Build + ```bash dotnet build All.slnx ``` -### 3. Run tests +### Test + ```bash -dotnet test +dotnet test All.slnx ``` --- -## 🛠️ Tech Stack +## Tech stack -- **Language**: `C# (.NET)` -- **Build System**: `dotnet` -- **CI/CD**: `GitHub Actions` +- **Language:** C# (.NET) +- **Build:** `dotnet` / `All.slnx` +- **CI:** GitHub Actions --- -## 📖 Documentation +## Documentation -- [Neo Official Documentation](https://developers.neo.org/) -- Check individual project folders for more details. +- [Neo developer documentation](https://developers.neo.org/) +- Public APIs use XML documentation comments (`///`) in source under `src/` +- Contribution guide: [CONTRIBUTING.md](CONTRIBUTING.md) --- -## 🤝 Contributing +## Contributing -We welcome contributions! See our [Contributing Guidelines](CONTRIBUTING.md) (create this file if needed). +Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) and the [Code of Conduct](CODE_OF_CONDUCT.md). --- -## 📜 License +## License -This project is licensed under the **BSD-2-Clause License** — see the [LICENSE](LICENSE) file for details. +**BSD-2-Clause** — see [LICENSE](LICENSE). --- + +## Contact + +- **Org:** [github.com/Rapid-Loop](https://github.com/Rapid-Loop) +- **Website:** [rapidloop.net](https://rapidloop.net) +- **Email:** [github@rapidloop.net](mailto:github@rapidloop.net) diff --git a/src/Neo.Configuration/BlockchainBackupOptions.cs b/src/Neo.Configuration/BlockchainBackupOptions.cs index 06cd81d..effd294 100644 --- a/src/Neo.Configuration/BlockchainBackupOptions.cs +++ b/src/Neo.Configuration/BlockchainBackupOptions.cs @@ -26,10 +26,20 @@ namespace Neo.Configuration { + /// + /// Configuration options for blockchain store backups. + /// public class BlockchainBackupOptions : JsonModel { + /// + /// The directory path where backup files are stored. + /// Defaults to data/backups under the application base directory. + /// public string BackupPath { get; init; } = Path.Combine(AppContext.BaseDirectory, "data", "backups"); + /// + /// The maximum number of backup files to retain. Defaults to 3. + /// public int MaxBackups { get; init; } = 3; } } diff --git a/src/Neo.Configuration/BlockchainStoreOptions.cs b/src/Neo.Configuration/BlockchainStoreOptions.cs index adaaf5d..5954230 100644 --- a/src/Neo.Configuration/BlockchainStoreOptions.cs +++ b/src/Neo.Configuration/BlockchainStoreOptions.cs @@ -26,10 +26,20 @@ namespace Neo.Configuration { + /// + /// Configuration options for the blockchain store database. + /// public class BlockchainStoreOptions : JsonModel { + /// + /// The directory path of the blockchain database. + /// Defaults to data/chain under the application base directory. + /// public string DatabasePath { get; init; } = Path.Combine(AppContext.BaseDirectory, "data", "chain"); + /// + /// Whether to create the database if it does not already exist. Defaults to . + /// public bool CreateIfMissing { get; init; } = true; } } diff --git a/src/Neo.Configuration/Json/Converters/JsonIPEndPointConverter.cs b/src/Neo.Configuration/Json/Converters/JsonIPEndPointConverter.cs index 5a8658e..4a3213e 100644 --- a/src/Neo.Configuration/Json/Converters/JsonIPEndPointConverter.cs +++ b/src/Neo.Configuration/Json/Converters/JsonIPEndPointConverter.cs @@ -28,8 +28,22 @@ namespace Neo.Configuration.Json.Converters { + /// + /// Converts values to and from JSON strings. + /// Supports IP address and hostname forms (for example, 127.0.0.1:20333). + /// public class JsonIPEndPointConverter : JsonConverter { + /// + /// Reads an from a JSON string token. + /// + /// The reader positioned at the JSON value. + /// The type to convert. + /// The serializer options. + /// + /// The parsed endpoint; when the string is empty or cannot be resolved, . + /// + /// Thrown when the token is not a JSON string. public override IPEndPoint? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.String) @@ -71,6 +85,12 @@ public class JsonIPEndPointConverter : JsonConverter return default; } + /// + /// Writes an as a JSON string, or JSON null when the value is . + /// + /// The writer to which the value is written. + /// The endpoint to write. + /// The serializer options. public override void Write(Utf8JsonWriter writer, IPEndPoint? value, JsonSerializerOptions options) { if (value is null) diff --git a/src/Neo.Configuration/Json/Converters/JsonStringAddressConverter.cs b/src/Neo.Configuration/Json/Converters/JsonStringAddressConverter.cs index 5ac9861..5da5e09 100644 --- a/src/Neo.Configuration/Json/Converters/JsonStringAddressConverter.cs +++ b/src/Neo.Configuration/Json/Converters/JsonStringAddressConverter.cs @@ -28,8 +28,20 @@ namespace Neo.Configuration.Json.Converters { + /// + /// Converts script hashes to and from NEO address JSON strings + /// using address version. + /// public class JsonStringAddressConverter : JsonConverter { + /// + /// Reads a from a NEO address string. + /// + /// The reader positioned at the JSON value. + /// The type to convert. + /// The serializer options. + /// The script hash, or when the string is empty. + /// Thrown when the token is not a JSON string. public override UInt160? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.String) @@ -44,6 +56,12 @@ public class JsonStringAddressConverter : JsonConverter return valueString.ToScriptHash(ProtocolSettings.Default.AddressVersion); } + /// + /// Writes a as a NEO address string, or JSON null when the value is . + /// + /// The writer to which the value is written. + /// The script hash to write. + /// The serializer options. public override void Write(Utf8JsonWriter writer, UInt160? value, JsonSerializerOptions options) { // TODO: Remove `ProtocolSettings.Default` and get from active ProtocolSettings. diff --git a/src/Neo.Configuration/Json/Converters/JsonStringByteArrayConverter.cs b/src/Neo.Configuration/Json/Converters/JsonStringByteArrayConverter.cs index 1d1d3d9..62994ec 100644 --- a/src/Neo.Configuration/Json/Converters/JsonStringByteArrayConverter.cs +++ b/src/Neo.Configuration/Json/Converters/JsonStringByteArrayConverter.cs @@ -27,8 +27,19 @@ namespace Neo.Configuration.Json.Converters { + /// + /// Converts values to and from UTF-8 encoded JSON strings. + /// public class JsonStringByteArrayConverter : JsonConverter { + /// + /// Reads a byte array from a UTF-8 JSON string. + /// + /// The reader positioned at the JSON value. + /// The type to convert. + /// The serializer options. + /// The UTF-8 bytes of the string, or when the string is empty. + /// Thrown when the token is not a JSON string. public override byte[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.String) @@ -42,6 +53,12 @@ public class JsonStringByteArrayConverter : JsonConverter return Encoding.UTF8.GetBytes(valueString); } + /// + /// Writes a byte array as a UTF-8 JSON string, or JSON null when the value is . + /// + /// The writer to which the value is written. + /// The bytes to write. + /// The serializer options. public override void Write(Utf8JsonWriter writer, byte[]? value, JsonSerializerOptions options) { if (value is null) diff --git a/src/Neo.Configuration/Json/Converters/JsonStringECPointConverter.cs b/src/Neo.Configuration/Json/Converters/JsonStringECPointConverter.cs index 3cf8b9a..014d256 100644 --- a/src/Neo.Configuration/Json/Converters/JsonStringECPointConverter.cs +++ b/src/Neo.Configuration/Json/Converters/JsonStringECPointConverter.cs @@ -27,8 +27,21 @@ namespace Neo.Configuration.Json.Converters { + /// + /// Converts values to and from hex-encoded JSON strings on the secp256r1 curve. + /// public class JsonStringECPointConverter : JsonConverter { + /// + /// Reads an from a hex-encoded JSON string. + /// + /// The reader positioned at the JSON value. + /// The type to convert. + /// The serializer options. + /// The parsed point, or when the string is empty. + /// + /// Thrown when the token is not a JSON string, or the string is not a valid secp256r1 point. + /// public override ECPoint? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.String) @@ -45,6 +58,12 @@ public class JsonStringECPointConverter : JsonConverter return value; } + /// + /// Writes an as a hex-encoded JSON string, or JSON null when the value is . + /// + /// The writer to which the value is written. + /// The point to write. + /// The serializer options. public override void Write(Utf8JsonWriter writer, ECPoint? value, JsonSerializerOptions options) { if (value is null) diff --git a/src/Neo.Configuration/Json/Converters/JsonStringHexFormatConverter.cs b/src/Neo.Configuration/Json/Converters/JsonStringHexFormatConverter.cs index 477e1a4..027dacc 100644 --- a/src/Neo.Configuration/Json/Converters/JsonStringHexFormatConverter.cs +++ b/src/Neo.Configuration/Json/Converters/JsonStringHexFormatConverter.cs @@ -26,8 +26,19 @@ namespace Neo.Configuration.Json.Converters { + /// + /// Converts values to and from lowercase hexadecimal JSON strings. + /// public class JsonStringHexFormatConverter : JsonConverter { + /// + /// Reads a byte array from a hexadecimal JSON string. + /// + /// The reader positioned at the JSON value. + /// The type to convert. + /// The serializer options. + /// The decoded bytes, or when the string is empty. + /// Thrown when the token is not a JSON string. public override byte[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.String) @@ -41,6 +52,12 @@ public class JsonStringHexFormatConverter : JsonConverter return Convert.FromHexString(valueString); } + /// + /// Writes a byte array as a lowercase hexadecimal JSON string, or JSON null when the value is . + /// + /// The writer to which the value is written. + /// The bytes to write. + /// The serializer options. public override void Write(Utf8JsonWriter writer, byte[]? value, JsonSerializerOptions options) { if (value is null) diff --git a/src/Neo.Configuration/Json/Converters/JsonStringUInt160Converter.cs b/src/Neo.Configuration/Json/Converters/JsonStringUInt160Converter.cs index 150215f..991a92f 100644 --- a/src/Neo.Configuration/Json/Converters/JsonStringUInt160Converter.cs +++ b/src/Neo.Configuration/Json/Converters/JsonStringUInt160Converter.cs @@ -27,8 +27,21 @@ namespace Neo.Configuration.Json.Converters { + /// + /// Converts values to and from hex-encoded JSON strings. + /// public class JsonStringUInt160Converter : JsonConverter { + /// + /// Reads a from a hex-encoded JSON string. + /// + /// The reader positioned at the JSON value. + /// The type to convert. + /// The serializer options. + /// The parsed script hash, or when the string is empty. + /// + /// Thrown when the token is not a JSON string, or the string is not a valid . + /// public override UInt160? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.String) @@ -45,6 +58,12 @@ public class JsonStringUInt160Converter : JsonConverter return scriptHash; } + /// + /// Writes a as a hex-encoded JSON string, or JSON null when the value is . + /// + /// The writer to which the value is written. + /// The script hash to write. + /// The serializer options. public override void Write(Utf8JsonWriter writer, UInt160? value, JsonSerializerOptions options) { if (value is null) diff --git a/src/Neo.Configuration/Json/JsonDefaults.cs b/src/Neo.Configuration/Json/JsonDefaults.cs index 6a34d92..504c068 100644 --- a/src/Neo.Configuration/Json/JsonDefaults.cs +++ b/src/Neo.Configuration/Json/JsonDefaults.cs @@ -27,8 +27,15 @@ namespace Neo.Configuration.Json { + /// + /// Provides default used by configuration models. + /// public static class JsonDefaults { + /// + /// The default serializer options for Neo configuration JSON, including camel-case naming + /// and converters for enums, , , and . + /// public static readonly JsonSerializerOptions SerializerOptions = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, diff --git a/src/Neo.Configuration/Json/JsonModel.cs b/src/Neo.Configuration/Json/JsonModel.cs index 2b499a7..4bcddec 100644 --- a/src/Neo.Configuration/Json/JsonModel.cs +++ b/src/Neo.Configuration/Json/JsonModel.cs @@ -27,23 +27,52 @@ namespace Neo.Configuration.Json { + /// + /// Base type for configuration models that can be serialized to and deserialized from JSON. + /// public abstract class JsonModel { protected JsonSerializerOptions _jsonSerializerOptions = JsonDefaults.SerializerOptions; + /// + /// Serializes this model to a JSON string using the specified options, or the instance defaults when omitted. + /// + /// Optional serializer options; when , instance defaults are used. + /// The JSON representation of this model. [return: NotNull] public virtual string? ToString(JsonSerializerOptions? options = default) => ToJson(options); + /// + /// Serializes this model to a JSON string. + /// + /// Optional serializer options; when , instance defaults are used. + /// The JSON representation of this model. public virtual string ToJson(JsonSerializerOptions? options) => JsonSerializer.Serialize(this, options ?? _jsonSerializerOptions); + /// + /// Deserializes a JSON string into a instance. + /// + /// The model type to deserialize. + /// The JSON string to deserialize. + /// Optional serializer options; when , is used. + /// The deserialized model, or if deserialization yields no value. public static TModel? FromJson(string jsonString, JsonSerializerOptions? options = default) where TModel : notnull, JsonModel => JsonSerializer.Deserialize( jsonString, options ?? JsonDefaults.SerializerOptions); + /// + /// Deserializes the contents of a JSON file into a instance. + /// + /// The model type to deserialize. + /// The JSON file to read. + /// Optional serializer options; when , is used. + /// The deserialized model, or if deserialization yields no value. + /// Thrown when does not exist. + /// Thrown when the file is empty. public static TModel? FromJson(FileInfo file, JsonSerializerOptions? options = default) where TModel : notnull, JsonModel { diff --git a/src/Neo.Configuration/ProtocolSettingsOptions.cs b/src/Neo.Configuration/ProtocolSettingsOptions.cs index 5d1103f..079355c 100644 --- a/src/Neo.Configuration/ProtocolSettingsOptions.cs +++ b/src/Neo.Configuration/ProtocolSettingsOptions.cs @@ -32,30 +32,71 @@ namespace Neo.Configuration { + /// + /// JSON-bindable configuration options that map to . + /// public class ProtocolSettingsOptions : JsonModel, IMap { + /// + /// The magic number of the NEO network. + /// public uint Network { get; set; } + /// + /// The address version of the NEO system. + /// public byte AddressVersion { get; set; } + /// + /// The time in milliseconds between two blocks. + /// public uint MillisecondsPerBlock { get; set; } + /// + /// The maximum number of transactions that can be contained in a block. + /// public uint MaxTransactionsPerBlock { get; set; } + /// + /// The maximum number of transactions that can be contained in the memory pool. + /// public int MemoryPoolMaxTransactions { get; set; } + /// + /// The maximum number of blocks that can be traced in a smart contract. + /// public uint MaxTraceableBlocks { get; set; } + /// + /// The amount of GAS to distribute during initialization, in datoshi (1 GAS = 1e8 datoshi). + /// public ulong InitialGasDistribution { get; set; } + /// + /// The number of validators in the NEO system. + /// public int ValidatorsCount { get; set; } + /// + /// The default seed node endpoints, or if none are configured. + /// public IPEndPoint[]? SeedList { get; set; } + /// + /// The block heights from which hard forks are activated, or if none are configured. + /// public IDictionary? HardForks { get; set; } + /// + /// The public keys of the standby committee members, or if none are configured. + /// public ECPoint[]? StandbyCommittee { get; set; } + /// + /// Converts these options into a instance. + /// Nullable collections are replaced with empty defaults. + /// + /// A new populated from this instance. public ProtocolSettings ToObject() => new() { diff --git a/src/Neo.Core/Blockchain/Block.cs b/src/Neo.Core/Blockchain/Block.cs index 439a857..e8d6402 100644 --- a/src/Neo.Core/Blockchain/Block.cs +++ b/src/Neo.Core/Blockchain/Block.cs @@ -31,8 +31,14 @@ namespace Neo.Core.Blockchain { + /// + /// A blockchain block that includes a header and its transactions. + /// public class Block : BlockHeader, IEquatable, INeoSerializable, IInventory, IVerifiable { + /// + /// Gets the hash of this block. + /// public override UInt256 Hash => this.ToArray().ToTxHash(); /// @@ -40,17 +46,32 @@ public class Block : BlockHeader, IEquatable, INeoSerializable, IInventor /// public Transaction[] Transactions { get; set; } = []; + /// + /// Gets the inventory type for this block. + /// public InventoryType InventoryType => InventoryType.Block; + /// + /// Gets the serialized size of this block in bytes. + /// public override int Size => base.Size + Transactions.GetSerializedSize(); + /// + /// Returns a hash code for this block based on its hash. + /// + /// A hash code for the current block. public override int GetHashCode() { return Hash.GetHashCode(); } + /// + /// Determines whether the specified is equal to the current instance. + /// + /// The block to compare with the current instance. + /// if the blocks have the same hash; otherwise, . public bool Equals([NotNullWhen(true)] Block? other) { if (ReferenceEquals(other, this)) return true; @@ -58,6 +79,11 @@ public bool Equals([NotNullWhen(true)] Block? other) return Hash == other.Hash; } + /// + /// Determines whether the specified object is equal to the current block. + /// + /// The object to compare with the current instance. + /// if the objects are equal; otherwise, . public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -65,6 +91,11 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as Block); } + /// + /// Deserializes this block from the specified stream and validates the Merkle root. + /// + /// The stream to read from. + /// The computed Merkle root does not match the header. public override void Deserialize(Stream reader) { base.Deserialize(reader); @@ -78,6 +109,10 @@ public override void Deserialize(Stream reader) throw new FormatException("The computed Merkle root does not match the expected value."); } + /// + /// Serializes this block to the specified stream, recomputing the Merkle root first. + /// + /// The stream to write to. public override void Serialize(Stream writer) { var txHashes = Transactions diff --git a/src/Neo.Core/Blockchain/BlockHeader.cs b/src/Neo.Core/Blockchain/BlockHeader.cs index 8f7cfc0..0265490 100644 --- a/src/Neo.Core/Blockchain/BlockHeader.cs +++ b/src/Neo.Core/Blockchain/BlockHeader.cs @@ -29,6 +29,9 @@ namespace Neo.Core.Blockchain { + /// + /// A blockchain block header without transaction bodies. + /// public class BlockHeader : IEquatable, INeoSerializable, IVerifiable { /// @@ -36,6 +39,9 @@ public class BlockHeader : IEquatable, INeoSerializable, IVerifiabl /// public uint Version { get; set; } + /// + /// Gets the hash of this block header. + /// public virtual UInt256 Hash => this.ToArray().ToTxHash(); /// @@ -83,6 +89,9 @@ public class BlockHeader : IEquatable, INeoSerializable, IVerifiabl /// public Witness[] Witnesses => [Witness]; + /// + /// Gets the serialized size of this block header in bytes. + /// public virtual int Size => sizeof(uint) + // Version UInt256.Length + // PrevHash @@ -94,11 +103,20 @@ public class BlockHeader : IEquatable, INeoSerializable, IVerifiabl UInt160.Length + // NextConsensus Witnesses.GetSerializedSize(); + /// + /// Returns a hash code for this header based on its hash. + /// + /// A hash code for the current header. public override int GetHashCode() { return Hash.GetHashCode(); } + /// + /// Determines whether the specified is equal to the current instance. + /// + /// The header to compare with the current instance. + /// if the headers have the same hash; otherwise, . public bool Equals([NotNullWhen(true)] BlockHeader? other) { if (ReferenceEquals(other, this)) return true; @@ -106,6 +124,11 @@ public bool Equals([NotNullWhen(true)] BlockHeader? other) return Hash == other.Hash; } + /// + /// Determines whether the specified object is equal to the current header. + /// + /// The object to compare with the current instance. + /// if the objects are equal; otherwise, . public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -113,6 +136,11 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as BlockHeader); } + /// + /// Deserializes this header from the specified stream. + /// + /// The stream to read from. + /// Version is not 0, or the witness count is not 1. public virtual void Deserialize(Stream reader) { Version = reader.Read(); @@ -133,6 +161,10 @@ public virtual void Deserialize(Stream reader) Witness = witnesses[0]; } + /// + /// Serializes this header to the specified stream. + /// + /// The stream to write to. public virtual void Serialize(Stream writer) { writer.Write(Version); diff --git a/src/Neo.Core/Blockchain/Interface/IInventory.cs b/src/Neo.Core/Blockchain/Interface/IInventory.cs index 5d13a21..1fb604e 100644 --- a/src/Neo.Core/Blockchain/Interface/IInventory.cs +++ b/src/Neo.Core/Blockchain/Interface/IInventory.cs @@ -22,8 +22,14 @@ namespace Neo.Core.Blockchain.Interface { + /// + /// An inventory item that can be relayed on the network (transaction, block, or extensible payload). + /// public interface IInventory { + /// + /// Gets the inventory type of this item. + /// InventoryType InventoryType { get; } } } diff --git a/src/Neo.Core/Blockchain/Interface/IVerifiable.cs b/src/Neo.Core/Blockchain/Interface/IVerifiable.cs index b59fbc5..5135bd3 100644 --- a/src/Neo.Core/Blockchain/Interface/IVerifiable.cs +++ b/src/Neo.Core/Blockchain/Interface/IVerifiable.cs @@ -22,6 +22,9 @@ namespace Neo.Core.Blockchain.Interface { + /// + /// An object that can be cryptographically verified via its hash and witnesses. + /// public interface IVerifiable { /// diff --git a/src/Neo.Core/Blockchain/Signer.cs b/src/Neo.Core/Blockchain/Signer.cs index 1bad251..09eaa88 100644 --- a/src/Neo.Core/Blockchain/Signer.cs +++ b/src/Neo.Core/Blockchain/Signer.cs @@ -30,6 +30,9 @@ namespace Neo.Core.Blockchain { + /// + /// A transaction signer describing an account, witness scopes, and related permissions. + /// public class Signer : INeoSerializable, IEquatable { /// @@ -60,8 +63,16 @@ public class Signer : INeoSerializable, IEquatable /// public WitnessRule[] Rules { get; set; } = []; + /// + /// Gets the serialized size of this signer in bytes. + /// + /// Always thrown; size calculation is not implemented. public int Size => throw new NotImplementedException(); + /// + /// Returns a hash code for this signer. + /// + /// A hash code for the current signer. public override int GetHashCode() { unchecked @@ -78,12 +89,23 @@ public override int GetHashCode() } } + /// + /// Determines whether the specified object is equal to the current signer. + /// + /// The object to compare with the current instance. + /// if the objects are equal; otherwise, . public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; if (obj is null) return false; return Equals(obj as Signer); } + + /// + /// Determines whether the specified is equal to the current instance. + /// + /// The signer to compare with the current instance. + /// if the signers are equal; otherwise, . public bool Equals([NotNullWhen(true)] Signer? other) { if (ReferenceEquals(other, this)) return true; @@ -106,6 +128,11 @@ public bool Equals([NotNullWhen(true)] Signer? other) return true; } + /// + /// Deserializes this signer from the specified stream. + /// + /// The stream to read from. + /// combines with other flags. public void Deserialize(Stream reader) { Account.Deserialize(reader); @@ -124,6 +151,10 @@ public void Deserialize(Stream reader) Rules = reader.ReadObjects(); } + /// + /// Serializes this signer to the specified stream. + /// + /// The stream to write to. public void Serialize(Stream writer) { Account.Serialize(writer); diff --git a/src/Neo.Core/Blockchain/Transaction.cs b/src/Neo.Core/Blockchain/Transaction.cs index 1f287d2..13161b2 100644 --- a/src/Neo.Core/Blockchain/Transaction.cs +++ b/src/Neo.Core/Blockchain/Transaction.cs @@ -30,9 +30,19 @@ namespace Neo.Core.Blockchain { + /// + /// A NEO transaction that can be verified, inventoried, and serialized. + /// public class Transaction : IEquatable, IInventory, INeoSerializable, IVerifiable { + /// + /// The maximum allowed size of a serialized transaction in bytes. + /// public const int MaxTransactionSize = 102400; + + /// + /// The maximum number of attributes allowed on a transaction. + /// public const int MaxTransactionAttributes = 16; /// @@ -40,6 +50,9 @@ public class Transaction : IEquatable, IInventory, INeoSerializable /// public byte Version { get; set; } + /// + /// Gets the hash of this transaction. + /// public UInt256 Hash => this.ToArray().ToTxHash(); /// @@ -82,6 +95,9 @@ public class Transaction : IEquatable, IInventory, INeoSerializable /// public Signer[] Signers { get; set; } = []; + /// + /// The witnesses of the transaction. + /// public Witness[] Witnesses { get; set; } = []; /// @@ -90,8 +106,14 @@ public class Transaction : IEquatable, IInventory, INeoSerializable /// Note: The sender will pay the fees of the transaction. public UInt160 Sender => Signers.FirstOrDefault()?.Account ?? UInt160.Zero; + /// + /// Gets the inventory type for this transaction. + /// public InventoryType InventoryType => InventoryType.TX; + /// + /// Gets the serialized size of this transaction in bytes. + /// public int Size => sizeof(byte) + //Version sizeof(uint) + //Nonce @@ -103,11 +125,20 @@ public class Transaction : IEquatable, IInventory, INeoSerializable Script.GetSerializedSize() + Witnesses.GetSerializedSize(); + /// + /// Returns a hash code for this transaction based on its hash. + /// + /// A hash code for the current transaction. public override int GetHashCode() { return Hash.GetHashCode(); } + /// + /// Determines whether the specified object is equal to the current transaction. + /// + /// The object to compare with the current instance. + /// if the objects are equal; otherwise, . public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -115,6 +146,11 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as Transaction); } + /// + /// Determines whether the specified is equal to the current instance. + /// + /// The transaction to compare with the current instance. + /// if the transactions have the same hash; otherwise, . public bool Equals([NotNullWhen(true)] Transaction? other) { if (ReferenceEquals(other, this)) return true; @@ -122,6 +158,10 @@ public bool Equals([NotNullWhen(true)] Transaction? other) return Hash == other.Hash; } + /// + /// Deserializes this transaction from the specified stream. + /// + /// The stream to read from. public void Deserialize(Stream reader) { Version = reader.Read(); @@ -135,6 +175,10 @@ public void Deserialize(Stream reader) Witnesses = reader.ReadObjects(); } + /// + /// Serializes this transaction to the specified stream. + /// + /// The stream to write to. public void Serialize(Stream writer) { writer.Write(Version); diff --git a/src/Neo.Core/Blockchain/TransactionAttribute.cs b/src/Neo.Core/Blockchain/TransactionAttribute.cs index cd09cd5..e966e0b 100644 --- a/src/Neo.Core/Blockchain/TransactionAttribute.cs +++ b/src/Neo.Core/Blockchain/TransactionAttribute.cs @@ -27,6 +27,9 @@ namespace Neo.Core.Blockchain { + /// + /// Base type for optional transaction attributes carried on a . + /// public abstract class TransactionAttribute : INeoSerializable { /// @@ -39,8 +42,16 @@ public abstract class TransactionAttribute : INeoSerializable /// public abstract bool AllowMultiple { get; } + /// + /// Gets the serialized size of this attribute in bytes. + /// public virtual int Size => sizeof(TransactionAttributeType); + /// + /// Deserializes this attribute from the specified stream. + /// + /// The stream to read from. + /// The attribute type is not defined. public virtual void Deserialize(Stream reader) { var type = reader.Read(); @@ -49,6 +60,10 @@ public virtual void Deserialize(Stream reader) throw new FormatException($"[{nameof(TransactionAttribute)}] Invalid {nameof(Type)} \'{type:d}\'."); } + /// + /// Serializes this attribute to the specified stream. + /// + /// The stream to write to. public virtual void Serialize(Stream writer) { writer.Write(Type); diff --git a/src/Neo.Core/Blockchain/TransactionAttributeType.cs b/src/Neo.Core/Blockchain/TransactionAttributeType.cs index 83d67fd..9f767bf 100644 --- a/src/Neo.Core/Blockchain/TransactionAttributeType.cs +++ b/src/Neo.Core/Blockchain/TransactionAttributeType.cs @@ -22,6 +22,9 @@ namespace Neo.Core.Blockchain { + /// + /// Identifies the type of a . + /// public enum TransactionAttributeType : byte { /// diff --git a/src/Neo.Core/Blockchain/Witness.cs b/src/Neo.Core/Blockchain/Witness.cs index 7346b6b..3392568 100644 --- a/src/Neo.Core/Blockchain/Witness.cs +++ b/src/Neo.Core/Blockchain/Witness.cs @@ -26,11 +26,24 @@ namespace Neo.Core.Blockchain { + /// + /// A cryptographic witness consisting of invocation and verification scripts. + /// public class Witness : INeoSerializable { + /// + /// The maximum allowed length of an invocation script in bytes. + /// public const int MaxInvocationScript = 1024; + + /// + /// The maximum allowed length of a verification script in bytes. + /// public const int MaxVerificationScript = 1024; + /// + /// Gets an empty witness with no scripts. + /// public static Witness Empty => new(); /// @@ -48,16 +61,27 @@ public class Witness : INeoSerializable /// public UInt160 ScriptHash => VerificationScript.ToScriptHash(); + /// + /// Gets the serialized size of this witness in bytes. + /// public int Size => InvocationScript.GetSerializedSize() + VerificationScript.GetSerializedSize(); + /// + /// Deserializes this witness from the specified stream. + /// + /// The stream to read from. public void Deserialize(Stream reader) { InvocationScript = reader.ReadDynamic(); VerificationScript = reader.ReadDynamic(); } + /// + /// Serializes this witness to the specified stream. + /// + /// The stream to write to. public void Serialize(Stream writer) { writer.Write(InvocationScript); diff --git a/src/Neo.Core/Blockchain/WitnessCondition.cs b/src/Neo.Core/Blockchain/WitnessCondition.cs index 5a1b949..add1fb9 100644 --- a/src/Neo.Core/Blockchain/WitnessCondition.cs +++ b/src/Neo.Core/Blockchain/WitnessCondition.cs @@ -28,6 +28,9 @@ namespace Neo.Core.Blockchain { + /// + /// Base type for conditions evaluated as part of a . + /// public abstract class WitnessCondition : INeoSerializable { /// @@ -35,8 +38,16 @@ public abstract class WitnessCondition : INeoSerializable /// public abstract WitnessConditionType Type { get; } + /// + /// Gets the serialized size of this condition in bytes. + /// public virtual int Size => Unsafe.SizeOf(); + /// + /// Deserializes this condition from the specified stream. + /// + /// The stream to read from. + /// The type byte does not match . public void Deserialize(Stream reader) { var type = reader.Read(); @@ -45,6 +56,10 @@ public void Deserialize(Stream reader) throw new FormatException($"[{nameof(WitnessCondition)}::{nameof(Type)}] Value \'{type}\' does not match \'{Type}\'."); } + /// + /// Serializes this condition to the specified stream. + /// + /// The stream to write to. public void Serialize(Stream writer) { writer.Write(Type); diff --git a/src/Neo.Core/Blockchain/WitnessContract.cs b/src/Neo.Core/Blockchain/WitnessContract.cs index 30cdb81..9ff080c 100644 --- a/src/Neo.Core/Blockchain/WitnessContract.cs +++ b/src/Neo.Core/Blockchain/WitnessContract.cs @@ -117,6 +117,13 @@ public static byte[] CreateMultiSigRedeemScript(int m, params ECPoint[] publicKe return sb.ToArray(); } + /// + /// Determines whether the specified redeem script is a multi-signature contract script. + /// + /// The redeem script to inspect. + /// + /// if the script ends with a multi-signature system call; otherwise, . + /// public static bool IsMultiSigContract(byte[] redeemScriptBytes) { using var sb = new ScriptBuilder(); diff --git a/src/Neo.Core/Blockchain/WitnessRule.cs b/src/Neo.Core/Blockchain/WitnessRule.cs index 802f90b..ff9426d 100644 --- a/src/Neo.Core/Blockchain/WitnessRule.cs +++ b/src/Neo.Core/Blockchain/WitnessRule.cs @@ -29,6 +29,9 @@ namespace Neo.Core.Blockchain { + /// + /// A witness rule pairing an action with a . + /// public class WitnessRule : IEquatable, INeoSerializable { /// @@ -41,10 +44,17 @@ public class WitnessRule : IEquatable, INeoSerializable /// public WitnessCondition Condition { get; set; } = default!; + /// + /// Gets the serialized size of this rule in bytes. + /// public int Size => Unsafe.SizeOf() + Condition.Size; + /// + /// Returns a hash code for this rule. + /// + /// A hash code for the current rule. public override int GetHashCode() { unchecked @@ -58,6 +68,11 @@ public override int GetHashCode() } } + /// + /// Determines whether the specified object is equal to the current rule. + /// + /// The object to compare with the current instance. + /// if the objects are equal; otherwise, . public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -65,6 +80,11 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as WitnessRule); } + /// + /// Determines whether the specified is equal to the current instance. + /// + /// The rule to compare with the current instance. + /// if the rules are equal; otherwise, . public bool Equals([NotNullWhen(true)] WitnessRule? other) { if (ReferenceEquals(other, this)) return true; @@ -72,12 +92,20 @@ public bool Equals([NotNullWhen(true)] WitnessRule? other) return Action == other.Action && Condition == other.Condition; } + /// + /// Deserializes this rule from the specified stream. + /// + /// The stream to read from. public void Deserialize(Stream reader) { Action = reader.Read(); Condition.Deserialize(reader); } + /// + /// Serializes this rule to the specified stream. + /// + /// The stream to write to. public void Serialize(Stream writer) { writer.Write(Action); diff --git a/src/Neo.Core/CoreUtilities.cs b/src/Neo.Core/CoreUtilities.cs index 5e94015..6ebea33 100644 --- a/src/Neo.Core/CoreUtilities.cs +++ b/src/Neo.Core/CoreUtilities.cs @@ -25,32 +25,86 @@ namespace Neo.Core { + /// + /// Shared helpers for encoding and Unix time conversion. + /// public static class CoreUtilities { + /// + /// Gets a UTF-8 encoding that throws on invalid sequences. + /// public static Encoding StrictUtf8Encoding => Encoding.GetEncoding("utf-8", EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback); + /// + /// Converts a to Unix time in milliseconds. + /// + /// The date and time to convert. + /// The UTC offset of . + /// The Unix timestamp in milliseconds. public static long ToUnixTimeMilliseconds(DateTime dateTime, TimeSpan offset = default) => new DateTimeOffset(dateTime, offset) .ToUnixTimeMilliseconds(); + /// + /// Converts a duration from the Unix epoch to milliseconds. + /// + /// The time span from the Unix epoch. + /// The UTC offset applied to the conversion. + /// The Unix timestamp in milliseconds. public static long ToUnixTimeMilliseconds(TimeSpan timeSpan, TimeSpan offset = default) => new DateTimeOffset(timeSpan.Ticks, offset) .ToUnixTimeMilliseconds(); + /// + /// Converts a to Unix time in seconds. + /// + /// The date and time to convert. + /// The UTC offset of . + /// The Unix timestamp in seconds. public static uint ToUnixTimeSeconds(DateTime dateTime, TimeSpan offset = default) => (uint)new DateTimeOffset(dateTime, offset) .ToUnixTimeSeconds(); + /// + /// Converts a duration from the Unix epoch to seconds. + /// + /// The time span from the Unix epoch. + /// The UTC offset applied to the conversion. + /// The Unix timestamp in seconds. public static uint ToUnixTimeSeconds(TimeSpan timeSpan, TimeSpan offset = default) => (uint)new DateTimeOffset(timeSpan.Ticks, offset) .ToUnixTimeSeconds(); + /// + /// Converts a Unix timestamp in milliseconds to a . + /// + /// The Unix timestamp in milliseconds. + /// + /// to return local time; otherwise UTC. + /// + /// The converted date and time. public static DateTime FromUnixTimeMilliseconds(ulong milliseconds, bool isLocalDateTime = false) => FromUnixTimeMilliseconds((long)milliseconds, isLocalDateTime); + /// + /// Converts a Unix timestamp in seconds to a . + /// + /// The Unix timestamp in seconds. + /// + /// to return local time; otherwise UTC. + /// + /// The converted date and time. public static DateTime FromUnixTimeSeconds(uint seconds, bool isLocalDateTime = false) => FromUnixTimeMilliseconds(seconds * TimeSpan.MillisecondsPerSecond, isLocalDateTime); + /// + /// Converts a Unix timestamp in milliseconds to a . + /// + /// The Unix timestamp in milliseconds. + /// + /// to return local time; otherwise UTC. + /// + /// The converted date and time. public static DateTime FromUnixTimeMilliseconds(long milliseconds, bool isLocalDateTime = false) { var dateTimeOffset = DateTimeOffset.FromUnixTimeMilliseconds(milliseconds); diff --git a/src/Neo.Core/Cryptography/ECC/ECCurve.cs b/src/Neo.Core/Cryptography/ECC/ECCurve.cs index f4692a9..67c9fd7 100644 --- a/src/Neo.Core/Cryptography/ECC/ECCurve.cs +++ b/src/Neo.Core/Cryptography/ECC/ECCurve.cs @@ -26,20 +26,38 @@ namespace Neo.Core.Cryptography.ECC { + /// + /// Parameters and helpers for a named elliptic curve used by Neo. + /// public class ECCurve { + /// + /// The secp256r1 (NIST P-256) curve used by Neo. + /// public static readonly ECCurve SecP256r1 = new( BigInteger.Parse("115792089210356248762697446949407573530086143415290314195533631308867097853951"), BigInteger.Parse("41058363725152142129326129780047268409114441015993725554835256314039467401291"), ECCurveName.SecP256r1 ); + /// + /// Gets the name of this curve. + /// public ECCurveName Name => _curveName; + /// + /// Gets the corresponding for platform crypto APIs. + /// public CECCurve ECCurveType => _curve; + /// + /// Gets the prime field modulus of the curve. + /// public BigInteger P => _p; + /// + /// Gets the curve equation coefficient b. + /// public BigInteger B => _b; private readonly BigInteger _p; diff --git a/src/Neo.Core/Cryptography/ECC/ECCurveName.cs b/src/Neo.Core/Cryptography/ECC/ECCurveName.cs index 56bcfe2..f774905 100644 --- a/src/Neo.Core/Cryptography/ECC/ECCurveName.cs +++ b/src/Neo.Core/Cryptography/ECC/ECCurveName.cs @@ -22,8 +22,14 @@ namespace Neo.Core.Cryptography.ECC { + /// + /// Identifies a supported elliptic curve. + /// public enum ECCurveName : byte { + /// + /// The NIST P-256 / secp256r1 curve. + /// SecP256r1 = 0, } } diff --git a/src/Neo.Core/Cryptography/ECC/ECPoint.cs b/src/Neo.Core/Cryptography/ECC/ECPoint.cs index a77c2a9..ebdb80c 100644 --- a/src/Neo.Core/Cryptography/ECC/ECPoint.cs +++ b/src/Neo.Core/Cryptography/ECC/ECPoint.cs @@ -32,24 +32,52 @@ namespace Neo.Core.Cryptography.ECC { + /// + /// An elliptic curve public key point on a supported . + /// [TypeConverter(typeof(ECPointTypeConverter))] public class ECPoint : IEquatable, IComparable, IComparable, INeoSerializable { + /// + /// The length in bytes of an uncompressed public key encoding (including prefix). + /// public const int UncompressedLength = 65; + + /// + /// The length in bytes of a compressed public key encoding (including prefix). + /// public const int CompressedLength = 33; + /// + /// Gets the X coordinate of this point. + /// public BigInteger X => _x; + /// + /// Gets the Y coordinate of this point. + /// public BigInteger Y => _y; + /// + /// Gets a value indicating whether this point is the point at infinity. + /// public bool IsInfinity => _x == BigInteger.Zero && _y == BigInteger.Zero; + /// + /// Gets the length of the uncompressed encoding in bytes. + /// public int Length => _uncompressed.Length; + /// + /// Gets the serialized size of this point in bytes. + /// public int Size => sizeof(ECCurveName) + _uncompressed.Length; + /// + /// Gets the curve this point belongs to. + /// public ECCurve Curve => _curve; private ECCurve _curve; @@ -71,6 +99,12 @@ .. y.ToByteArray(true, true), ]; } + /// + /// Derives the public key point from a private key on the specified curve. + /// + /// The private key bytes. + /// The elliptic curve. + /// The corresponding public key point. public static ECPoint FromPrivateKey(ReadOnlySpan privateKeySpan, ECCurve curve) { using var ecdsa = ECDsa.Create(); @@ -90,6 +124,13 @@ public static ECPoint FromPrivateKey(ReadOnlySpan privateKeySpan, ECCurve return new(x, y, curve); } + /// + /// Parses a hex-encoded public key on the specified curve. + /// + /// A compressed or uncompressed hex public key. + /// The elliptic curve. + /// The parsed point. + /// is not a valid public key encoding. public static ECPoint Parse(string value, ECCurve curve) { if ((value.Length != UncompressedLength * 2 || value.Length != CompressedLength * 2) && @@ -112,6 +153,13 @@ public static ECPoint Parse(string value, ECCurve curve) return new(x, y, curve); } + /// + /// Attempts to parse a hex-encoded public key on the specified curve. + /// + /// A compressed or uncompressed hex public key. + /// The elliptic curve. + /// When this method returns, the parsed point if successful; otherwise, . + /// if parsing succeeded; otherwise, . public static bool TryParse(string value, ECCurve curve, [MaybeNullWhen(false)] out ECPoint? result) { try @@ -126,6 +174,11 @@ public static bool TryParse(string value, ECCurve curve, [MaybeNullWhen(false)] } } + /// + /// Determines whether the specified object is equal to the current point. + /// + /// The object to compare with the current instance. + /// if the objects are equal; otherwise, . public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -133,17 +186,30 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as ECPoint); } + /// + /// Returns a hash code for this point. + /// + /// A hash code for the current point. public override int GetHashCode() { return _uncompressed.ToHashCode(); } + /// + /// Returns the uncompressed hex encoding of this point. + /// + /// A lower-case hex string of the uncompressed public key. [return: MaybeNull] public override string? ToString() { return Convert.ToHexStringLower(Encode(false)); } + /// + /// Determines whether the specified is equal to the current instance. + /// + /// The point to compare with the current instance. + /// if the points are equal; otherwise, . public bool Equals(ECPoint? other) { if (ReferenceEquals(other, this)) return true; @@ -152,12 +218,26 @@ public bool Equals(ECPoint? other) return _x == other._x && _y == other._y; } + /// + /// Compares this instance to a specified object. + /// + /// An object to compare, or . + /// + /// A signed integer that indicates the relative order of this instance and . + /// public int CompareTo(object? obj) { if (ReferenceEquals(obj, this)) return 0; return CompareTo(obj as ECPoint); } + /// + /// Compares this instance to another . + /// + /// An to compare. + /// + /// A signed integer that indicates the relative order of this instance and . + /// public int CompareTo(ECPoint? other) { if (other is null) return 1; @@ -166,6 +246,11 @@ public int CompareTo(ECPoint? other) return _y.CompareTo(other._y); } + /// + /// Serializes this point to the specified stream. + /// + /// The stream to write to. + /// The stream does not support writing. public void Serialize(Stream writer) { if (writer.CanWrite == false) @@ -175,6 +260,12 @@ public void Serialize(Stream writer) writer.Write(Encode(false)); } + /// + /// Deserializes this point from the specified stream. + /// + /// The stream to read from. + /// The stream does not support reading. + /// The curve name is not supported. public void Deserialize(Stream reader) { if (reader.CanRead == false) @@ -193,6 +284,11 @@ public void Deserialize(Stream reader) _y = new(_uncompressed.AsSpan()[33..UncompressedLength], true, true); } + /// + /// Encodes this point in compressed or uncompressed form. + /// + /// for compressed encoding; otherwise uncompressed. + /// The encoded public key bytes. public byte[] Encode(bool shouldCompress = true) { return shouldCompress ? @@ -200,6 +296,9 @@ public byte[] Encode(bool shouldCompress = true) _uncompressed; } + /// + /// Determines whether two values are equal. + /// public static bool operator ==(ECPoint? left, ECPoint? right) { if (left is null) @@ -207,26 +306,41 @@ public byte[] Encode(bool shouldCompress = true) return left.Equals(right); } + /// + /// Determines whether two values are not equal. + /// public static bool operator !=(ECPoint? left, ECPoint? right) { return !(left == right); } + /// + /// Determines whether one is less than another. + /// public static bool operator <(ECPoint? left, ECPoint? right) { return left is null ? right is not null : left.CompareTo(right) < 0; } + /// + /// Determines whether one is less than or equal to another. + /// public static bool operator <=(ECPoint? left, ECPoint? right) { return left is null || left.CompareTo(right) <= 0; } + /// + /// Determines whether one is greater than another. + /// public static bool operator >(ECPoint? left, ECPoint? right) { return left is not null && left.CompareTo(right) > 0; } + /// + /// Determines whether one is greater than or equal to another. + /// public static bool operator >=(ECPoint? left, ECPoint? right) { return left is null ? right is null : left.CompareTo(right) >= 0; diff --git a/src/Neo.Core/Cryptography/MerkleTree.cs b/src/Neo.Core/Cryptography/MerkleTree.cs index 4f9aa8f..356f7ba 100644 --- a/src/Neo.Core/Cryptography/MerkleTree.cs +++ b/src/Neo.Core/Cryptography/MerkleTree.cs @@ -28,6 +28,9 @@ namespace Neo.Core.Cryptography { + /// + /// Builds Merkle trees over hash leaves and computes Merkle roots. + /// public class MerkleTree { private readonly byte[] _root = []; @@ -41,6 +44,11 @@ private MerkleTree(IEnumerable leaves) _root = BuildTree(_leaves); } + /// + /// Computes the Merkle root of the specified hash leaves. + /// + /// The leaf hashes; an empty array yields . + /// The Merkle root of . public static UInt256 ComputeRoot(UInt256[] hashes) { if (hashes.Length == 0) return UInt256.Zero; diff --git a/src/Neo.Core/Cryptography/ProtectedString.cs b/src/Neo.Core/Cryptography/ProtectedString.cs index dca2459..041bda4 100644 --- a/src/Neo.Core/Cryptography/ProtectedString.cs +++ b/src/Neo.Core/Cryptography/ProtectedString.cs @@ -30,6 +30,9 @@ namespace Neo.Core.Cryptography { + /// + /// Stores a string encrypted in memory using AES-GCM with process-local entropy. + /// public class ProtectedString : IEquatable, IComparable, IComparable, IDisposable { // Sizes required for standard AES-GCM compliance @@ -42,26 +45,45 @@ public class ProtectedString : IEquatable, IComparable, ICompar private int _cachedHashCode = 0; + /// + /// Initializes an empty protected string. + /// public ProtectedString() { } + /// + /// Initializes a protected string from the specified plaintext. + /// + /// The plaintext string to protect. public ProtectedString(string plaintext) { var plaintextBytes = Encoding.UTF8.GetBytes(plaintext); _protectedMemory = ProtectData(plaintextBytes); } + /// + /// Clears the protected memory and releases resources. + /// public void Dispose() { CryptographicOperations.ZeroMemory(_protectedMemory.Span); GC.SuppressFinalize(this); } + /// + /// Determines whether the specified object is equal to the current instance. + /// + /// The object to compare with the current instance. + /// if the objects are equal; otherwise, . public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; return Equals(obj as ProtectedString); } + /// + /// Returns a hash code for this instance based on the protected payload. + /// + /// A hash code for the current instance. public override int GetHashCode() { if (_cachedHashCode != 0) return _cachedHashCode; @@ -71,21 +93,44 @@ public override int GetHashCode() (hash * 31) ^ b); } + /// + /// Decrypts and returns the plaintext string. + /// + /// The unprotected plaintext. public override string ToString() => UnprotectData(_protectedMemory.Span); + /// + /// Compares this instance to a specified object. + /// + /// An object to compare, or . + /// + /// A signed integer that indicates the relative order of this instance and . + /// public int CompareTo(object? obj) { if (ReferenceEquals(obj, this)) return 0; return CompareTo(obj as ProtectedString); } + /// + /// Compares this instance to another . + /// + /// A to compare. + /// + /// A signed integer that indicates the relative order of this instance and . + /// public int CompareTo(ProtectedString? other) { if (other is null) return 1; return string.Compare(this, other); } + /// + /// Determines whether the specified is equal to the current instance. + /// + /// The instance to compare with the current instance. + /// if the plaintext values are equal; otherwise, . public bool Equals([NotNullWhen(true)] ProtectedString? other) { if (other is null) return false; @@ -124,6 +169,11 @@ private static Memory ProtectData(byte[] data) return result; } + /// + /// Decrypts a protected binary payload produced by this type. + /// + /// The encrypted payload (nonce, ciphertext, and tag). + /// The decrypted UTF-8 string. public static string UnprotectData(ReadOnlySpan protectedData) { if (protectedData.Length == 0) return string.Empty; @@ -140,17 +190,31 @@ public static string UnprotectData(ReadOnlySpan protectedData) return Encoding.UTF8.GetString(plaintext); } + /// + /// Implicitly converts a plaintext string to a . + /// + /// The plaintext string. public static implicit operator ProtectedString(string value) => new(value); + /// + /// Implicitly converts a to its plaintext string. + /// + /// The protected string. public static implicit operator string(ProtectedString value) => value.ToString(); + /// + /// Determines whether two values are equal. + /// public static bool operator ==(ProtectedString a, ProtectedString b) { return a.Equals(b); } + /// + /// Determines whether two values are not equal. + /// public static bool operator !=(ProtectedString a, ProtectedString b) => !(a == b); } diff --git a/src/Neo.Core/Cryptography/RipeMD160.cs b/src/Neo.Core/Cryptography/RipeMD160.cs index d0ed9c7..006d563 100644 --- a/src/Neo.Core/Cryptography/RipeMD160.cs +++ b/src/Neo.Core/Cryptography/RipeMD160.cs @@ -25,6 +25,9 @@ namespace Neo.Core.Cryptography { + /// + /// Computes the RIPEMD-160 cryptographic hash for the input data. + /// public class RipeMD160 : HashAlgorithm { private readonly byte[] _buffer; @@ -41,6 +44,11 @@ private RipeMD160() InitializeState(); } + /// + /// Computes the RIPEMD-160 hash of the specified data. + /// + /// The data to hash. + /// The 20-byte hash value. public static byte[] HashData(byte[] data) { using var hasher = new RipeMD160(); @@ -50,6 +58,9 @@ public static byte[] HashData(byte[] data) #pragma warning restore CA1850 } + /// + /// Initializes this instance for a new hash computation. + /// public override void Initialize() { InitializeState(); diff --git a/src/Neo.Core/Cryptography/SCrypt.cs b/src/Neo.Core/Cryptography/SCrypt.cs index e5dc78a..7cd1f82 100644 --- a/src/Neo.Core/Cryptography/SCrypt.cs +++ b/src/Neo.Core/Cryptography/SCrypt.cs @@ -25,11 +25,23 @@ namespace Neo.Core.Cryptography { + /// + /// SCrypt password-based key derivation and salt generation. + /// public static class SCrypt { /// /// Generates a derived key using SCrypt. /// + /// The password bytes. + /// The salt bytes. + /// CPU/memory cost parameter; must be a power of two greater than 1. + /// Block size parameter; must be at least 1. + /// Parallelization parameter; must be at least 1. + /// The desired derived key length in bytes. + /// The derived key. + /// or is empty. + /// A parameter is outside the allowed range. public static byte[] Generate(byte[] password, byte[] salt, int n, int r, int p, int keyLength = 32) { ArgumentOutOfRangeException.ThrowIfZero(password.Length, nameof(password)); @@ -44,8 +56,15 @@ public static byte[] Generate(byte[] password, byte[] salt, int n, int r, int p, } /// - /// Convenience overload for string password. + /// Generates a derived key from a string password using SCrypt. /// + /// The password string (UTF-8 encoded). + /// The salt bytes. + /// CPU/memory cost parameter; must be a power of two greater than 1. + /// Block size parameter; must be at least 1. + /// Parallelization parameter; must be at least 1. + /// The desired derived key length in bytes. + /// The derived key. public static byte[] Generate(string password, byte[] salt, int n = 16384, int r = 8, int p = 1, int keyLength = 32) { var passBytes = CoreUtilities.StrictUtf8Encoding.GetBytes(password); @@ -55,6 +74,8 @@ public static byte[] Generate(string password, byte[] salt, int n = 16384, int r /// /// Generates a cryptographically secure random salt. /// + /// The salt length in bytes. + /// A new random salt. public static byte[] GenerateSalt(int length = 16) { var salt = new byte[length]; @@ -63,8 +84,21 @@ public static byte[] GenerateSalt(int length = 16) } } + /// + /// Low-level scrypt encoder helpers used by . + /// internal static class ScryptEncoder { + /// + /// Derives a key using the scrypt password-based key derivation function. + /// + /// Password bytes. + /// Salt bytes. + /// CPU/memory cost parameter (must be a power of two). + /// Block size parameter. + /// Parallelization parameter. + /// Length of the derived key in bytes. + /// The derived key. public unsafe static byte[] CryptoScrypt(byte[] password, byte[] salt, int n, int r, int p, int keyLength = 32) { var ba = new byte[128 * r * p + 63]; diff --git a/src/Neo.Core/Extensions/BigIntegerExtensions.cs b/src/Neo.Core/Extensions/BigIntegerExtensions.cs index bef70b5..5bcae81 100644 --- a/src/Neo.Core/Extensions/BigIntegerExtensions.cs +++ b/src/Neo.Core/Extensions/BigIntegerExtensions.cs @@ -25,8 +25,17 @@ namespace Neo.Core.Extensions { + /// + /// Provides extension methods for . + /// public static class BigIntegerExtensions { + /// + /// Computes the integer square root of a non-negative value (floor of the true square root). + /// + /// The non-negative value whose square root is computed. + /// The largest integer whose square is less than or equal to . + /// Thrown when is negative. public static BigInteger Sqrt(this BigInteger value) { if (value < 0) @@ -57,9 +66,13 @@ public static BigInteger Sqrt(this BigInteger value) } /// - /// Calculates the modular multiplicative inverse of 'a' modulo 'm'. - /// Throws ArithmeticException if the inverse does not exist. + /// Calculates the modular multiplicative inverse of modulo . /// + /// The value for which the modular inverse is computed. + /// The positive modulus. + /// The modular multiplicative inverse of modulo . + /// Thrown when is not positive. + /// Thrown when the inverse does not exist because the values are not coprime. public static BigInteger ModInverse(this BigInteger a, BigInteger m) { if (m <= 0) diff --git a/src/Neo.Core/Extensions/ByteExtensions.cs b/src/Neo.Core/Extensions/ByteExtensions.cs index ffd404d..123728c 100644 --- a/src/Neo.Core/Extensions/ByteExtensions.cs +++ b/src/Neo.Core/Extensions/ByteExtensions.cs @@ -29,11 +29,18 @@ namespace Neo.Core.Extensions { + /// + /// Provides extension methods for arrays. + /// public static class ByteExtensions { /// /// Returns a new byte array containing the bitwise XOR of the two arrays. /// + /// The first operand. + /// The second operand; must be the same length as . + /// A new array containing the bitwise XOR of and . + /// Thrown when the arrays have different lengths. public static byte[] Xor(this byte[] x, byte[] y) { ArgumentOutOfRangeException.ThrowIfNotEqual(y.Length, x.Length, nameof(y)); @@ -85,23 +92,60 @@ public static T AsSerializable(this byte[] data, int startIndex = 0) return newObject; } + /// + /// Gets the serialized size of a byte array, including its compact-size length prefix. + /// + /// The byte array. + /// The number of bytes required to serialize . public static int GetSerializedSize(this byte[] data) => data.Length.GetCompactSize() + data.Length; + /// + /// Computes the script hash of the byte array (Hash160). + /// + /// The data to hash. + /// A script hash of the data. public static UInt160 ToScriptHash(this byte[] data) => new(data.ToHash160()); + /// + /// Computes the RIPEMD-160 hash of the byte array. + /// + /// The data to hash. + /// The 20-byte RIPEMD-160 digest. public static byte[] ToRipeMD160(this byte[] data) => RipeMD160.HashData(data); + /// + /// Computes the SHA-256 hash of the byte array. + /// + /// The data to hash. + /// The 32-byte SHA-256 digest. public static byte[] ToSha256(this byte[] data) => SHA256.HashData(data); + /// + /// Computes Hash160 (SHA-256 followed by RIPEMD-160) of the byte array. + /// + /// The data to hash. + /// The 20-byte Hash160 digest. public static byte[] ToHash160(this byte[] data) => data.ToSha256().ToRipeMD160(); + /// + /// Computes a transaction-style hash of the byte array as a . + /// + /// The data to hash. + /// A containing the SHA-256 digest. public static UInt256 ToTxHash(this byte[] data) => new(data.ToSha256()); + + /// + /// Computes a stable integer hash code over the byte array contents. + /// + /// The data to hash. + /// The initial hash seed. + /// An integer hash code derived from the array contents. public static int ToHashCode(this byte[] data, int seed = 397) => data.Aggregate(seed, (hash, b) => diff --git a/src/Neo.Core/Extensions/DictionaryExtensions.cs b/src/Neo.Core/Extensions/DictionaryExtensions.cs index abf6997..e49db36 100644 --- a/src/Neo.Core/Extensions/DictionaryExtensions.cs +++ b/src/Neo.Core/Extensions/DictionaryExtensions.cs @@ -26,8 +26,20 @@ namespace Neo.Core.Extensions { + /// + /// Provides extension methods for dictionary types. + /// public static class DictionaryExtensions { + /// + /// Gets the value associated with the specified key, or adds a new value created by the factory. + /// + /// The type of dictionary keys. + /// The type of dictionary values. + /// The dictionary to query or update. + /// The key to look up. + /// A factory used to create the value when the key is missing. + /// The existing or newly created value for . public static TValue GetOrAdd(this IDictionary source, TKey key, Func valueFactory) { if (source.TryGetValue(key, out var value) == false) @@ -38,12 +50,28 @@ public static TValue GetOrAdd(this IDictionary sourc return value; } + /// + /// Computes a stable integer hash code over the dictionary entries. + /// + /// The type of dictionary keys. + /// The type of dictionary values. + /// The dictionary to hash. + /// The initial hash seed. + /// An integer hash code derived from the dictionary contents. public static int ToHashCode(this IDictionary source, int seed = 397) => source.Aggregate(seed, (hash, b) => unchecked((hash * 31) + ((b.Key?.GetHashCode() ?? 0) ^ (b.Value?.GetHashCode() ?? 0))) ); + /// + /// Computes a stable integer hash code over the read-only dictionary entries. + /// + /// The type of dictionary keys. + /// The type of dictionary values. + /// The dictionary to hash. + /// The initial hash seed. + /// An integer hash code derived from the dictionary contents. public static int ToHashCode(this IReadOnlyDictionary source, int seed = 397) => source.Aggregate(seed, (hash, b) => diff --git a/src/Neo.Core/Extensions/EnumerableExtensions.cs b/src/Neo.Core/Extensions/EnumerableExtensions.cs index 18572f6..55d3d6b 100644 --- a/src/Neo.Core/Extensions/EnumerableExtensions.cs +++ b/src/Neo.Core/Extensions/EnumerableExtensions.cs @@ -25,8 +25,18 @@ namespace Neo.Core.Extensions { + /// + /// Provides extension methods for . + /// public static class EnumerableExtensions { + /// + /// Computes a stable integer hash code over the sequence elements. + /// + /// The type of the sequence elements. + /// The sequence to hash. + /// The initial hash seed. + /// An integer hash code derived from the sequence contents. public static int ToHashCode(this IEnumerable source, int seed = 397) => source.Aggregate(seed, (hash, b) => diff --git a/src/Neo.Core/Extensions/INeoSerializableExtensions.cs b/src/Neo.Core/Extensions/INeoSerializableExtensions.cs index 2ce42bb..ab58498 100644 --- a/src/Neo.Core/Extensions/INeoSerializableExtensions.cs +++ b/src/Neo.Core/Extensions/INeoSerializableExtensions.cs @@ -26,6 +26,9 @@ namespace Neo.Core.Extensions { + /// + /// Provides extension methods for types. + /// public static class INeoSerializableExtensions { /// @@ -41,6 +44,11 @@ public static byte[] ToArray(this INeoSerializable source) return ms.ToArray(); } + /// + /// Gets the serialized size of an array of objects, including the compact-size length prefix. + /// + /// The array of serializable objects. + /// The number of bytes required to serialize the array. public static int GetSerializedSize(this INeoSerializable[] source) => source.Length.GetCompactSize() + source.Sum(static s => s.Size); diff --git a/src/Neo.Core/Extensions/IntegerExtensions.cs b/src/Neo.Core/Extensions/IntegerExtensions.cs index 7a8c2b0..3cc6cc6 100644 --- a/src/Neo.Core/Extensions/IntegerExtensions.cs +++ b/src/Neo.Core/Extensions/IntegerExtensions.cs @@ -25,8 +25,17 @@ namespace Neo.Core.Extensions { + /// + /// Provides extension methods for integer types. + /// public static class IntegerExtensions { + /// + /// Gets the number of bytes required to encode the value as a Bitcoin-style compact size integer. + /// + /// The integer type. + /// The value to measure. + /// The encoded size in bytes (1, 3, 5, or 9). public static byte GetCompactSize(this T value) where T : unmanaged, IBinaryInteger => Convert.ToUInt64(value) switch diff --git a/src/Neo.Core/Extensions/ListExtensions.cs b/src/Neo.Core/Extensions/ListExtensions.cs index 4222ec6..16e28f8 100644 --- a/src/Neo.Core/Extensions/ListExtensions.cs +++ b/src/Neo.Core/Extensions/ListExtensions.cs @@ -25,14 +25,31 @@ namespace Neo.Core.Extensions { + /// + /// Provides extension methods for list types. + /// public static class ListExtensions { + /// + /// Computes a stable integer hash code over the list elements. + /// + /// The type of the list elements. + /// The list to hash. + /// The initial hash seed. + /// An integer hash code derived from the list contents. public static int ToHashCode(this IList source, int seed = 397) => source.Aggregate(seed, (hash, b) => unchecked((hash * 31) ^ (b?.GetHashCode() ?? 0)) ); + /// + /// Computes a stable integer hash code over the read-only list elements. + /// + /// The type of the list elements. + /// The list to hash. + /// The initial hash seed. + /// An integer hash code derived from the list contents. public static int ToHashCode(this IReadOnlyList source, int seed = 397) => source.Aggregate(seed, (hash, b) => diff --git a/src/Neo.Core/Extensions/MemoryExtensions.cs b/src/Neo.Core/Extensions/MemoryExtensions.cs index e924f46..87a49f3 100644 --- a/src/Neo.Core/Extensions/MemoryExtensions.cs +++ b/src/Neo.Core/Extensions/MemoryExtensions.cs @@ -26,30 +26,54 @@ namespace Neo.Core.Extensions { + /// + /// Provides extension methods for and . + /// public static class MemoryExtensions { + /// + /// Gets the serialized size of a memory region of unmanaged values, including the compact-size length prefix. + /// + /// The unmanaged element type. + /// The memory region to measure. + /// The number of bytes required to serialize the memory contents. public static int GetSerializedSize(this Memory memory) where TSource : unmanaged => memory.Length.GetCompactSize() + (memory.Length * Unsafe.SizeOf()); + /// + /// Gets the serialized size of a read-only memory region of unmanaged values, including the compact-size length prefix. + /// + /// The unmanaged element type. + /// The memory region to measure. + /// The number of bytes required to serialize the memory contents. public static int GetSerializedSize(this ReadOnlyMemory readOnlyMemory) where TSource : unmanaged => readOnlyMemory.Length.GetCompactSize() + (readOnlyMemory.Length * Unsafe.SizeOf()); /// - /// Converts a byte array to an object. + /// Converts a byte memory region to an object. /// /// The type to convert to. - /// The byte array to be converted. - /// + /// The bytes to be converted. + /// The offset into the byte data from which to begin deserialization. /// The converted object. public static T? AsSerializable(this ReadOnlyMemory memory, int startIndex = 0) where T : class?, INeoSerializable? => memory.ToArray() .AsSerializable(startIndex); + /// + /// Applies an accumulator function over a memory region. + /// + /// The type of the memory elements. + /// The type of the accumulator value. + /// The memory region to aggregate. + /// The initial accumulator value. + /// An accumulator function to be invoked on each element. + /// The final accumulator value. public static TAccumulate Aggregate(this Memory source, TAccumulate seed, Func func) { var val = seed; @@ -65,6 +89,15 @@ public static TAccumulate Aggregate(this Memory s return val; } + /// + /// Applies an accumulator function over a read-only memory region. + /// + /// The type of the memory elements. + /// The type of the accumulator value. + /// The memory region to aggregate. + /// The initial accumulator value. + /// An accumulator function to be invoked on each element. + /// The final accumulator value. public static TAccumulate Aggregate(this ReadOnlyMemory source, TAccumulate seed, Func func) { var val = seed; @@ -78,18 +111,34 @@ public static TAccumulate Aggregate(this ReadOnlyMemory + /// Computes a stable integer hash code over the byte memory contents. + /// + /// The data to hash. + /// The initial hash seed. + /// An integer hash code derived from the memory contents. public static int ToHashCode(this Memory data, int seed) => data.Aggregate(seed, (hash, b) => unchecked((hash * 31) ^ b) ); + /// + /// Computes a stable integer hash code over the byte memory contents, seeded by the memory length. + /// + /// The data to hash. + /// An integer hash code derived from the memory contents. public static int ToHashCode(this Memory data) => data.Aggregate(data.Length, (hash, b) => (hash * 31) ^ b ); + /// + /// Computes a stable integer hash code over the read-only byte memory contents, seeded by the memory length. + /// + /// The data to hash. + /// An integer hash code derived from the memory contents. public static int ToHashCode(this ReadOnlyMemory data) => data.Aggregate(data.Length, (hash, b) => diff --git a/src/Neo.Core/Extensions/ObjectExtensions.cs b/src/Neo.Core/Extensions/ObjectExtensions.cs index edb2a39..188bb58 100644 --- a/src/Neo.Core/Extensions/ObjectExtensions.cs +++ b/src/Neo.Core/Extensions/ObjectExtensions.cs @@ -24,8 +24,17 @@ namespace Neo.Core.Extensions { + /// + /// Provides extension methods for unmanaged object arrays. + /// public static class ObjectExtensions { + /// + /// Gets the serialized size of an unmanaged array, including its compact-size length prefix. + /// + /// The unmanaged element type. + /// The array to measure. + /// The number of bytes required to serialize the array. public static int GetSerializedSize(this TSource[] source) where TSource : unmanaged => source.Length.GetCompactSize() + diff --git a/src/Neo.Core/Extensions/SpanExtensions.cs b/src/Neo.Core/Extensions/SpanExtensions.cs index 6d24ebe..ebf1d86 100644 --- a/src/Neo.Core/Extensions/SpanExtensions.cs +++ b/src/Neo.Core/Extensions/SpanExtensions.cs @@ -28,30 +28,54 @@ namespace Neo.Core.Extensions { + /// + /// Provides extension methods for and . + /// public static class SpanExtensions { + /// + /// Gets the serialized size of a span of unmanaged values, including the compact-size length prefix. + /// + /// The unmanaged element type. + /// The span to measure. + /// The number of bytes required to serialize the span contents. public static int GetSerializedSize(this Span span) where TSource : unmanaged => span.Length.GetCompactSize() + (span.Length * Unsafe.SizeOf()); + /// + /// Gets the serialized size of a read-only span of unmanaged values, including the compact-size length prefix. + /// + /// The unmanaged element type. + /// The span to measure. + /// The number of bytes required to serialize the span contents. public static int GetSerializedSize(this ReadOnlySpan readOnlySpan) where TSource : unmanaged => readOnlySpan.Length.GetCompactSize() + (readOnlySpan.Length * Unsafe.SizeOf()); /// - /// Converts a byte array to an object. + /// Converts a byte span to an object. /// /// The type to convert to. - /// The byte array to be converted. - /// + /// The bytes to be converted. + /// The offset into the byte data from which to begin deserialization. /// The converted object. public static T? AsSerializable(this ReadOnlySpan span, int startIndex = 0) where T : class?, INeoSerializable? => span.ToArray() .AsSerializable(startIndex); + /// + /// Applies an accumulator function over a span. + /// + /// The type of the span elements. + /// The type of the accumulator value. + /// The span to aggregate. + /// The initial accumulator value. + /// An accumulator function to be invoked on each element. + /// The final accumulator value. public static TAccumulate Aggregate(this Span source, TAccumulate seed, Func func) { var val = seed; @@ -67,6 +91,15 @@ public static TAccumulate Aggregate(this Span sou return val; } + /// + /// Applies an accumulator function over a read-only span. + /// + /// The type of the span elements. + /// The type of the accumulator value. + /// The span to aggregate. + /// The initial accumulator value. + /// An accumulator function to be invoked on each element. + /// The final accumulator value. public static TAccumulate Aggregate(this ReadOnlySpan source, TAccumulate seed, Func func) { var val = seed; @@ -80,12 +113,23 @@ public static TAccumulate Aggregate(this ReadOnlySpan + /// Computes a stable integer hash code over the byte span contents. + /// + /// The data to hash. + /// The initial hash seed. + /// An integer hash code derived from the span contents. public static int ToHashCode(this ReadOnlySpan data, int seed = 397) => data.Aggregate(seed, (hash, b) => unchecked((hash * 31) ^ b) ); + /// + /// Compresses the span using LZ4, prefixing the output with the original length as a little-endian . + /// + /// The data to compress. + /// A span containing the original length followed by the LZ4-compressed payload. public static Span ToLz4Compress(this ReadOnlySpan data) { var maxLength = LZ4Codec.MaximumOutputSize(data.Length); @@ -98,6 +142,14 @@ public static Span ToLz4Compress(this ReadOnlySpan data) return buffer[..(sizeof(uint) + length)]; } + /// + /// Decompresses an LZ4 payload previously produced by . + /// + /// The compressed data, beginning with a little-endian original length. + /// The decompressed bytes. + /// + /// Thrown when the decompressed length does not match the length prefix. + /// public static Span ToLz4Decompress(this ReadOnlySpan data) { var length = BinaryPrimitives.ReadInt32LittleEndian(data); diff --git a/src/Neo.Core/Extensions/StreamExtensions.cs b/src/Neo.Core/Extensions/StreamExtensions.cs index 6da5a33..da3e8c3 100644 --- a/src/Neo.Core/Extensions/StreamExtensions.cs +++ b/src/Neo.Core/Extensions/StreamExtensions.cs @@ -28,8 +28,18 @@ namespace Neo.Core.Extensions { + /// + /// Provides extension methods for reading and writing Neo serialization formats on . + /// public static class StreamExtensions { + /// + /// Writes an integer value using Bitcoin-style compact size encoding. + /// + /// The integer type to write. + /// The stream to write to. + /// The value to encode. + /// Thrown when the stream does not support writing. public static void WriteCompact(this Stream stream, T value) where T : unmanaged, IBinaryInteger { @@ -58,6 +68,13 @@ public static void WriteCompact(this Stream stream, T value) } } + /// + /// Writes the raw bytes of an unmanaged value to the stream. + /// + /// The unmanaged type to write. + /// The stream to write to. + /// The value to write. + /// Thrown when the stream does not support writing. public static void Write(this Stream stream, T value) where T : unmanaged { @@ -70,6 +87,13 @@ public static void Write(this Stream stream, T value) stream.Write(span); } + /// + /// Writes the raw bytes of an unmanaged value to the stream by reference. + /// + /// The unmanaged type to write. + /// The stream to write to. + /// The value to write. + /// Thrown when the stream does not support writing. public static void Write(this Stream stream, ref T value) where T : unmanaged { @@ -82,6 +106,13 @@ public static void Write(this Stream stream, ref T value) stream.Write(span); } + /// + /// Writes an unmanaged array with a compact-size length prefix followed by the raw element bytes. + /// + /// The unmanaged element type. + /// The stream to write to. + /// The array to write. + /// Thrown when the stream does not support writing. public static void Write(this Stream stream, T[] values) where T : unmanaged { @@ -95,10 +126,23 @@ public static void Write(this Stream stream, T[] values) stream.Write(span); } + /// + /// Writes an unmanaged span with a compact-size length prefix followed by the raw element bytes. + /// + /// The unmanaged element type. + /// The stream to write to. + /// The span to write. public static void Write(this Stream stream, Span values) where T : unmanaged => stream.Write((ReadOnlySpan)values); + /// + /// Writes an unmanaged read-only span with a compact-size length prefix followed by the raw element bytes. + /// + /// The unmanaged element type. + /// The stream to write to. + /// The span to write. + /// Thrown when the stream does not support writing. public static void Write(this Stream stream, ReadOnlySpan values) where T : unmanaged { @@ -111,6 +155,12 @@ public static void Write(this Stream stream, ReadOnlySpan values) stream.Write(span); } + /// + /// Writes a UTF-8 string with a compact-size byte-length prefix. + /// + /// The stream to write to. + /// The string to write. + /// Thrown when the stream does not support writing. public static void Write(this Stream stream, string value) { if (stream.CanWrite == false) @@ -127,6 +177,12 @@ public static void Write(this Stream stream, string value) stream.Write(byteSpan); } + /// + /// Writes a character array as UTF-8 with compact-size prefixes for both byte length and character count. + /// + /// The stream to write to. + /// The character array to write. + /// Thrown when the stream does not support writing. public static void Write(this Stream stream, char[] value) { if (stream.CanWrite == false) @@ -144,6 +200,13 @@ public static void Write(this Stream stream, char[] value) stream.Write(byteSpan); } + /// + /// Writes an array of objects with a compact-size length prefix. + /// + /// The serializable element type. + /// The stream to write to. + /// The objects to serialize. + /// Thrown when the stream does not support writing. public static void WriteObjects(this Stream stream, T[] values) where T : INeoSerializable { @@ -156,6 +219,14 @@ public static void WriteObjects(this Stream stream, T[] values) t.Serialize(stream); } + /// + /// Reads an integer value encoded with Bitcoin-style compact size encoding. + /// + /// The integer type to read. + /// The stream to read from. + /// The decoded integer value. + /// Thrown when the stream does not support reading. + /// Thrown when the stream ends before the value can be read. public static T ReadCompact(this Stream stream) where T : unmanaged, IBinaryInteger { @@ -180,6 +251,14 @@ public static T ReadCompact(this Stream stream) return T.CreateChecked(value); // Throws on overflow for the target type } + /// + /// Reads the raw bytes of an unmanaged value into the provided reference. + /// + /// The unmanaged type to read. + /// The stream to read from. + /// A reference that receives the read value. + /// A reference to . + /// Thrown when the stream does not support reading. public static ref T Read(this Stream stream, ref T result) where T : unmanaged { @@ -194,6 +273,13 @@ public static ref T Read(this Stream stream, ref T result) return ref result; } + /// + /// Reads the raw bytes of an unmanaged value from the stream. + /// + /// The unmanaged type to read. + /// The stream to read from. + /// The value read from the stream. + /// Thrown when the stream does not support reading. public static T Read(this Stream stream) where T : unmanaged { @@ -209,6 +295,12 @@ public static T Read(this Stream stream) return result; } + /// + /// Reads a character array previously written by . + /// + /// The stream to read from. + /// The decoded character array. + /// Thrown when the stream does not support reading. public static char[] ReadCharArray(this Stream stream) { if (stream.CanRead == false) @@ -228,6 +320,12 @@ public static char[] ReadCharArray(this Stream stream) return results; } + /// + /// Reads a UTF-8 string previously written by . + /// + /// The stream to read from. + /// The decoded string. + /// Thrown when the stream does not support reading. public static string ReadString(this Stream stream) { if (stream.CanRead == false) @@ -242,6 +340,14 @@ public static string ReadString(this Stream stream) return encoding.GetString(bytes); } + /// + /// Reads an unmanaged array previously written with a compact-size length prefix. + /// + /// The unmanaged element type. + /// The stream to read from. + /// The decoded array. + /// Thrown when the stream does not support reading. + /// Thrown when is . public static T[] ReadDynamic(this Stream stream) where T : unmanaged { @@ -261,6 +367,14 @@ public static T[] ReadDynamic(this Stream stream) return results; } + /// + /// Reads an array of objects previously written by . + /// + /// The serializable element type. + /// The stream to read from. + /// The deserialized object array. + /// Thrown when the stream does not support reading. + /// Thrown when is . public static T[] ReadObjects(this Stream stream) where T : INeoSerializable { diff --git a/src/Neo.Core/Extensions/StringExtensions.cs b/src/Neo.Core/Extensions/StringExtensions.cs index 2d6a531..efa87bd 100644 --- a/src/Neo.Core/Extensions/StringExtensions.cs +++ b/src/Neo.Core/Extensions/StringExtensions.cs @@ -25,6 +25,9 @@ namespace Neo.Core.Extensions { + /// + /// Provides extension methods for and character spans. + /// public static class StringExtensions { /// @@ -42,6 +45,13 @@ public static ReadOnlySpan TrimStartIgnoreCase(this ReadOnlySpan val return value; } + /// + /// Pads a string on both sides so that it is centered within the specified total width. + /// + /// The string to pad; when , an empty string is returned. + /// The total width of the resulting string. + /// The character used for padding. + /// The centered string, or the original string when it is already at least characters. public static string PadCenter(this string str, int totalWidth, char paddingChar) { if (str is null) return string.Empty; @@ -59,6 +69,9 @@ public static string PadCenter(this string str, int totalWidth, char paddingChar /// The address to convert. /// The address version. /// The converted script hash. + /// + /// Thrown when the address is not a valid Base58Check address for the specified version. + /// public static UInt160 ToScriptHash(this string address, byte version) { var data = Base58.DecodeCheck(address); @@ -72,6 +85,11 @@ public static UInt160 ToScriptHash(this string address, byte version) return new(data.AsSpan(1)); } + /// + /// Gets the serialized size of a UTF-8 string, including its compact-size length prefix. + /// + /// The string to measure. + /// The number of bytes required to serialize the string. public static int GetSerializedSize(this string source) { var byteCount = CoreUtilities.StrictUtf8Encoding.GetByteCount(source); diff --git a/src/Neo.Core/Extensions/TryCatchExtensions.cs b/src/Neo.Core/Extensions/TryCatchExtensions.cs index a4b7d87..0c36ae9 100644 --- a/src/Neo.Core/Extensions/TryCatchExtensions.cs +++ b/src/Neo.Core/Extensions/TryCatchExtensions.cs @@ -24,8 +24,19 @@ namespace Neo.Core.Extensions { + /// + /// Provides fluent try/catch helpers for chaining operations with optional error handling. + /// public static class TryCatchExtensions { + /// + /// Invokes a function and returns its result, or the default value when any exception is thrown. + /// + /// The type of the receiver. + /// The type of the result. + /// The receiver passed to . + /// The function to invoke. + /// The function result, or if an exception is thrown. public static TResult? TryCatch(this TSource obj, Func func) { try @@ -38,6 +49,15 @@ public static class TryCatchExtensions } } + /// + /// Invokes an action and optionally handles a specific exception type, then returns the receiver. + /// + /// The type of the receiver. + /// The exception type to catch. + /// The receiver passed to the action. + /// The action to invoke. + /// An optional handler invoked when is thrown. + /// The original receiver. public static TSource TryCatch(this TSource obj, Action action, Action? onError = default) where TException : Exception { @@ -53,6 +73,19 @@ public static TSource TryCatch(this TSource obj, Action + /// Invokes a function and optionally maps a specific exception type to a result. + /// + /// The type of the receiver. + /// The exception type to catch. + /// The type of the result. + /// The receiver passed to the function. + /// The function to invoke. + /// + /// An optional handler that produces a result when is thrown. + /// When omitted, the exception is rethrown. + /// + /// The function result, or the value produced by . public static TResult TryCatch(this TSource obj, Func func, Func? onError = default) where TException : Exception { @@ -67,6 +100,14 @@ public static TResult TryCatch(this TSource obj, F } } + /// + /// Invokes an action and rethrows exceptions of type . + /// + /// The type of the receiver. + /// The exception type that is rethrown. + /// The receiver passed to the action. + /// The action to invoke. + /// The original receiver when the action completes successfully. public static TSource TryCatchThrow(this TSource obj, Action action) where TException : Exception { @@ -82,6 +123,15 @@ public static TSource TryCatchThrow(this TSource obj, Actio } } + /// + /// Invokes a function and rethrows exceptions of type . + /// + /// The type of the receiver. + /// The exception type that is rethrown. + /// The type of the result. + /// The receiver passed to the function. + /// The function to invoke. + /// The function result when it completes successfully. public static TResult TryCatchThrow(this TSource obj, Func func) where TException : Exception { @@ -95,6 +145,17 @@ public static TResult TryCatchThrow(this TSource o } } + /// + /// Invokes an action and, on failure, optionally wraps with a new message. + /// + /// The type of the receiver. + /// The exception type to catch and optionally wrap. + /// The receiver passed to the action. + /// The action to invoke. + /// + /// When provided, a new is thrown with this message and the original exception as the inner exception. + /// + /// The original receiver when the action completes successfully. public static TSource TryCatchThrow(this TSource obj, Action action, string? errorMessage = default) where TException : Exception, new() { @@ -119,6 +180,18 @@ public static TSource TryCatchThrow(this TSource obj, Actio } } + /// + /// Invokes a function and, on failure, optionally wraps with a new message. + /// + /// The type of the receiver. + /// The exception type to catch and optionally wrap. + /// The type of the result. + /// The receiver passed to the function. + /// The function to invoke. + /// + /// When provided, a new is thrown with this message and the original exception as the inner exception. + /// + /// The function result when it completes successfully. public static TResult? TryCatchThrow(this TSource obj, Func func, string? errorMessage = default) where TException : Exception { diff --git a/src/Neo.Core/Extensions/TypeExtensions.cs b/src/Neo.Core/Extensions/TypeExtensions.cs index 61e7ed7..53038ff 100644 --- a/src/Neo.Core/Extensions/TypeExtensions.cs +++ b/src/Neo.Core/Extensions/TypeExtensions.cs @@ -25,8 +25,19 @@ namespace Neo.Core.Extensions { + /// + /// Provides extension methods for . + /// public static class TypeExtensions { + /// + /// Creates a new instance of the specified type using its public or non-public parameterless constructor. + /// + /// The type to instantiate. + /// A newly constructed instance of . + /// + /// Thrown when the type does not have a parameterless constructor. + /// public static object CreateInitializedObject(this Type source) { // Try to get the private/public parameterless constructor. diff --git a/src/Neo.Core/Extensions/UInt160Extensions.cs b/src/Neo.Core/Extensions/UInt160Extensions.cs index 326b0aa..b6239da 100644 --- a/src/Neo.Core/Extensions/UInt160Extensions.cs +++ b/src/Neo.Core/Extensions/UInt160Extensions.cs @@ -24,6 +24,9 @@ namespace Neo.Core.Extensions { + /// + /// Provides extension methods for . + /// public static class UInt160Extensions { /// diff --git a/src/Neo.Core/Factory/RandomNumberFactory.cs b/src/Neo.Core/Factory/RandomNumberFactory.cs index 5d2b031..3d1bd37 100644 --- a/src/Neo.Core/Factory/RandomNumberFactory.cs +++ b/src/Neo.Core/Factory/RandomNumberFactory.cs @@ -27,11 +27,24 @@ namespace Neo.Core.Factory { + /// + /// Factory methods for generating random numbers and byte sequences. + /// public static class RandomNumberFactory { + /// + /// Returns a non-negative random less than . + /// + /// A random value in the range [0, ). public static sbyte NextSByte() => NextSByte(0, sbyte.MaxValue); + /// + /// Returns a non-negative random that is less than the specified maximum. + /// + /// The exclusive upper bound of the random number to be generated. + /// A random value in the range [0, ). + /// is negative. public static sbyte NextSByte(sbyte maxValue) { if (maxValue < 0) @@ -40,6 +53,13 @@ public static sbyte NextSByte(sbyte maxValue) return NextSByte(0, maxValue); } + /// + /// Returns a random within the specified range. + /// + /// The inclusive lower bound of the random number returned. + /// The exclusive upper bound of the random number returned. + /// A random value in the range [, ). + /// is greater than . public static sbyte NextSByte(sbyte minValue, sbyte maxValue) { if (minValue == maxValue) return maxValue; @@ -50,12 +70,28 @@ public static sbyte NextSByte(sbyte minValue, sbyte maxValue) return (sbyte)(NextUInt32((uint)(maxValue - minValue)) + minValue); } + /// + /// Returns a random less than . + /// + /// A random value in the range [0, ). public static byte NextByte() => NextByte(0, byte.MaxValue); + /// + /// Returns a random that is less than the specified maximum. + /// + /// The exclusive upper bound of the random number to be generated. + /// A random value in the range [0, ). public static byte NextByte(byte maxValue) => NextByte(0, maxValue); + /// + /// Returns a random within the specified range. + /// + /// The inclusive lower bound of the random number returned. + /// The exclusive upper bound of the random number returned. + /// A random value in the range [, ). + /// is greater than . public static byte NextByte(byte minValue, byte maxValue) { if (minValue == maxValue) return maxValue; @@ -66,9 +102,19 @@ public static byte NextByte(byte minValue, byte maxValue) return (byte)(NextUInt32((uint)(maxValue - minValue)) + minValue); } + /// + /// Returns a non-negative random less than . + /// + /// A random value in the range [0, ). public static short NextInt16() => NextInt16(0, short.MaxValue); + /// + /// Returns a non-negative random that is less than the specified maximum. + /// + /// The exclusive upper bound of the random number to be generated. + /// A random value in the range [0, ). + /// is negative. public static short NextInt16(short maxValue) { if (maxValue < 0) @@ -77,6 +123,13 @@ public static short NextInt16(short maxValue) return NextInt16(0, maxValue); } + /// + /// Returns a random within the specified range. + /// + /// The inclusive lower bound of the random number returned. + /// The exclusive upper bound of the random number returned. + /// A random value in the range [, ). + /// is greater than . public static short NextInt16(short minValue, short maxValue) { if (minValue == maxValue) return maxValue; @@ -87,12 +140,28 @@ public static short NextInt16(short minValue, short maxValue) return (short)(NextUInt32((uint)(maxValue - minValue)) + minValue); } + /// + /// Returns a random less than . + /// + /// A random value in the range [0, ). public static ushort NextUInt16() => NextUInt16(0, ushort.MaxValue); + /// + /// Returns a random that is less than the specified maximum. + /// + /// The exclusive upper bound of the random number to be generated. + /// A random value in the range [0, ). public static ushort NextUInt16(ushort maxValue) => NextUInt16(0, maxValue); + /// + /// Returns a random within the specified range. + /// + /// The inclusive lower bound of the random number returned. + /// The exclusive upper bound of the random number returned. + /// A random value in the range [, ). + /// is greater than . public static ushort NextUInt16(ushort minValue, ushort maxValue) { if (minValue == maxValue) return maxValue; @@ -103,9 +172,19 @@ public static ushort NextUInt16(ushort minValue, ushort maxValue) return (ushort)(NextUInt32((uint)(maxValue - minValue)) + minValue); } + /// + /// Returns a non-negative random less than . + /// + /// A random value in the range [0, ). public static int NextInt32() => NextInt32(0, int.MaxValue); + /// + /// Returns a non-negative random that is less than the specified maximum. + /// + /// The exclusive upper bound of the random number to be generated. + /// A random value in the range [0, ). + /// is negative. public static int NextInt32(int maxValue) { ArgumentOutOfRangeException.ThrowIfNegative(maxValue); @@ -113,6 +192,13 @@ public static int NextInt32(int maxValue) return NextInt32(0, maxValue); } + /// + /// Returns a random within the specified range. + /// + /// The inclusive lower bound of the random number returned. + /// The exclusive upper bound of the random number returned. + /// A random value in the range [, ). + /// is greater than . public static int NextInt32(int minValue, int maxValue) { if (minValue == maxValue) return maxValue; @@ -122,9 +208,18 @@ public static int NextInt32(int minValue, int maxValue) return (int)NextUInt32((uint)(maxValue - minValue)) + minValue; } + /// + /// Returns a random using cryptographic randomness. + /// + /// A random 32-bit unsigned integer. public static uint NextUInt32() => BinaryPrimitives.ReadUInt32LittleEndian(NextBytes(4, true)); + /// + /// Returns a random that is less than the specified maximum. + /// + /// The exclusive upper bound of the random number to be generated. + /// A random value in the range [0, ). public static uint NextUInt32(uint maxValue) { var randomProduct = (ulong)maxValue * NextUInt32(); @@ -144,6 +239,13 @@ public static uint NextUInt32(uint maxValue) return (uint)(randomProduct >> 32); } + /// + /// Returns a random within the specified range. + /// + /// The inclusive lower bound of the random number returned. + /// The exclusive upper bound of the random number returned. + /// A random value in the range [, ). + /// is greater than . public static uint NextUInt32(uint minValue, uint maxValue) { if (minValue == maxValue) return maxValue; @@ -153,14 +255,30 @@ public static uint NextUInt32(uint minValue, uint maxValue) return NextUInt32(maxValue - minValue) + minValue; } + /// + /// Returns a non-negative random less than . + /// + /// A random value in the range [0, ). public static long NextInt64() => NextInt64(0L, long.MaxValue); + /// + /// Returns a non-negative random that is less than the specified maximum. + /// + /// The exclusive upper bound of the random number to be generated. + /// A random value in the range [0, ). public static long NextInt64(long maxValue) { return NextInt64(0L, maxValue); } + /// + /// Returns a random within the specified range. + /// + /// The inclusive lower bound of the random number returned. + /// The exclusive upper bound of the random number returned. + /// A random value in the range [, ). + /// is greater than . public static long NextInt64(long minValue, long maxValue) { if (minValue == maxValue) return maxValue; @@ -170,9 +288,18 @@ public static long NextInt64(long minValue, long maxValue) return (long)NextUInt64((ulong)(maxValue - minValue)) + minValue; } + /// + /// Returns a random using cryptographic randomness. + /// + /// A random 64-bit unsigned integer. public static ulong NextUInt64() => BinaryPrimitives.ReadUInt64LittleEndian(NextBytes(8, true)); + /// + /// Returns a random that is less than the specified maximum. + /// + /// The exclusive upper bound of the random number to be generated. + /// A random value in the range [0, ). public static ulong NextUInt64(ulong maxValue) { var randomProduct = Math.BigMul(maxValue, NextUInt64(), out var lowPart); @@ -190,6 +317,13 @@ public static ulong NextUInt64(ulong maxValue) return randomProduct; } + /// + /// Returns a random within the specified range. + /// + /// The inclusive lower bound of the random number returned. + /// The exclusive upper bound of the random number returned. + /// A random value in the range [, ). + /// is greater than . public static ulong NextUInt64(ulong minValue, ulong maxValue) { if (minValue == maxValue) return maxValue; @@ -199,6 +333,13 @@ public static ulong NextUInt64(ulong minValue, ulong maxValue) return NextUInt64(maxValue - minValue) + minValue; } + /// + /// Returns a random within the specified range. + /// + /// The inclusive lower bound of the random number returned. + /// The exclusive upper bound of the random number returned. + /// A random value in the range [, ). + /// is greater than . public static BigInteger NextBigInteger(BigInteger minValue, BigInteger maxValue) { if (minValue == maxValue) return maxValue; @@ -208,6 +349,12 @@ public static BigInteger NextBigInteger(BigInteger minValue, BigInteger maxValue return NextBigInteger(maxValue - minValue) + minValue; } + /// + /// Returns a non-negative random that is less than the specified maximum. + /// + /// The exclusive upper bound of the random number to be generated. + /// A random value in the range [0, ). + /// is negative. public static BigInteger NextBigInteger(BigInteger maxValue) { if (maxValue.Sign < 0) @@ -236,6 +383,12 @@ public static BigInteger NextBigInteger(BigInteger maxValue) return randomProduct >> maxValueBits; } + /// + /// Returns a non-negative random with the specified bit length. + /// + /// The number of bits in the generated value. + /// A random non-negative integer with at most bits. + /// is negative. public static BigInteger NextBigInteger(int sizeInBits) { if (sizeInBits < 0) @@ -255,6 +408,15 @@ public static BigInteger NextBigInteger(int sizeInBits) return new BigInteger(b); } + /// + /// Returns a random byte array of the specified length. + /// + /// The number of random bytes to generate. + /// + /// to use ; otherwise . + /// + /// A new array filled with random bytes. + /// is negative. public static byte[] NextBytes(int length, bool cryptography = false) { ArgumentOutOfRangeException.ThrowIfLessThan(length, 0, nameof(length)); diff --git a/src/Neo.Core/Interfaces/IMap.cs b/src/Neo.Core/Interfaces/IMap.cs index 7d2c8b8..8c14d98 100644 --- a/src/Neo.Core/Interfaces/IMap.cs +++ b/src/Neo.Core/Interfaces/IMap.cs @@ -22,9 +22,17 @@ namespace Neo.Core.Interfaces { + /// + /// Maps an instance to a destination representation of type . + /// + /// The type of the mapped result. public interface IMap where TDestination : notnull { + /// + /// Converts this instance to a value. + /// + /// The mapped destination object. TDestination ToObject(); } } diff --git a/src/Neo.Core/Json/LowerCaseNamingPolicy.cs b/src/Neo.Core/Json/LowerCaseNamingPolicy.cs index 7ff7d93..9c97099 100644 --- a/src/Neo.Core/Json/LowerCaseNamingPolicy.cs +++ b/src/Neo.Core/Json/LowerCaseNamingPolicy.cs @@ -24,8 +24,16 @@ namespace Neo.Core.Json { + /// + /// A JSON naming policy that converts property names to lower case. + /// public class LowerCaseNamingPolicy : JsonNamingPolicy { + /// + /// Converts the specified property name to lower case using the invariant culture. + /// + /// The property name to convert. + /// The lower-case property name. public override string ConvertName(string name) => name.ToLowerInvariant(); } diff --git a/src/Neo.Core/Net/Message/ArchivalNodeCapabilityMessage.cs b/src/Neo.Core/Net/Message/ArchivalNodeCapabilityMessage.cs index 177c925..126cc26 100644 --- a/src/Neo.Core/Net/Message/ArchivalNodeCapabilityMessage.cs +++ b/src/Neo.Core/Net/Message/ArchivalNodeCapabilityMessage.cs @@ -26,10 +26,19 @@ namespace Neo.Core.Net.Message { + /// + /// Advertises that the node stores full block history for archival synchronization. + /// public class ArchivalNodeCapabilityMessage : NodeCapabilityMessage { + /// + /// Gets the capability type (). + /// public override NodeCapabilityType Type => NodeCapabilityType.ArchivalNode; + /// + /// Gets the serialized size of this capability in bytes. + /// public override int Size => base.Size + 1; diff --git a/src/Neo.Core/Net/Message/DisableCompressionCapabilityMessage.cs b/src/Neo.Core/Net/Message/DisableCompressionCapabilityMessage.cs index ba4dd32..c4c0972 100644 --- a/src/Neo.Core/Net/Message/DisableCompressionCapabilityMessage.cs +++ b/src/Neo.Core/Net/Message/DisableCompressionCapabilityMessage.cs @@ -26,10 +26,19 @@ namespace Neo.Core.Net.Message { + /// + /// Advertises that the node does not support P2P payload compression. + /// public class DisableCompressionCapabilityMessage : NodeCapabilityMessage { + /// + /// Gets the capability type (). + /// public override NodeCapabilityType Type => NodeCapabilityType.DisableCompression; + /// + /// Gets the serialized size of this capability in bytes. + /// public override int Size => base.Size + 1; diff --git a/src/Neo.Core/Net/Message/FullNodeCapabilityMessage.cs b/src/Neo.Core/Net/Message/FullNodeCapabilityMessage.cs index 671cc10..2da701c 100644 --- a/src/Neo.Core/Net/Message/FullNodeCapabilityMessage.cs +++ b/src/Neo.Core/Net/Message/FullNodeCapabilityMessage.cs @@ -25,20 +25,39 @@ namespace Neo.Core.Net.Message { + /// + /// Advertises that the node has complete current state at a given start height. + /// public class FullNodeCapabilityMessage : NodeCapabilityMessage { + /// + /// Gets the capability type (). + /// public override NodeCapabilityType Type => NodeCapabilityType.FullNode; + /// + /// Gets the block height at which this node reports full state. + /// public uint StartHeight { get; private set; } + /// + /// Gets the serialized size of this capability in bytes. + /// public override int Size => base.Size + sizeof(uint); + /// + /// Initializes an empty full-node capability for deserialization. + /// public FullNodeCapabilityMessage() { } + /// + /// Initializes a full-node capability with the specified start height. + /// + /// The reported full-state block height. public FullNodeCapabilityMessage(uint startHeight) { StartHeight = startHeight; diff --git a/src/Neo.Core/Net/Message/NodeCapabilityMessage.cs b/src/Neo.Core/Net/Message/NodeCapabilityMessage.cs index 8e8e3e7..1edab40 100644 --- a/src/Neo.Core/Net/Message/NodeCapabilityMessage.cs +++ b/src/Neo.Core/Net/Message/NodeCapabilityMessage.cs @@ -34,8 +34,14 @@ namespace Neo.Core.Net.Message /// public abstract class NodeCapabilityMessage : INeoSerializable { + /// + /// Gets the capability type of this message. + /// public abstract NodeCapabilityType Type { get; } + /// + /// Gets the serialized size of this capability in bytes. + /// public virtual int Size => sizeof(NodeCapabilityType); @@ -73,6 +79,10 @@ public void Deserialize(Stream reader) DeserializeWithoutType(reader); } + /// + /// Serializes this capability to the specified stream. + /// + /// The stream to write to. public void Serialize(Stream writer) { writer.Write(Type); diff --git a/src/Neo.Core/Net/Message/PingMessage.cs b/src/Neo.Core/Net/Message/PingMessage.cs index eb79aa2..8afec89 100644 --- a/src/Neo.Core/Net/Message/PingMessage.cs +++ b/src/Neo.Core/Net/Message/PingMessage.cs @@ -33,20 +33,43 @@ namespace Neo.Core.Net.Message /// public class PingMessage : INeoSerializable { + /// + /// Gets the last known block index of the sender. + /// public uint LastBlockIndex { get; private set; } + /// + /// Gets the Unix timestamp (seconds) when this payload was created. + /// public uint Timestamp { get; private set; } + /// + /// Gets the random nonce used to correlate Ping and Pong. + /// public uint Nonce { get; private set; } + /// + /// Gets the serialized size of this payload in bytes. + /// public int Size => sizeof(uint) + // LastBlockIndex sizeof(uint) + // Timestamp sizeof(uint); // Nonce + /// + /// Creates a ping/pong payload with a random nonce. + /// + /// The sender's last known block index. + /// A new . public static PingMessage Create(uint lastBlockIndex) => Create(lastBlockIndex, RandomNumberFactory.NextUInt32()); + /// + /// Creates a ping/pong payload with the specified nonce. + /// + /// The sender's last known block index. + /// The correlation nonce. + /// A new . public static PingMessage Create(uint lastBlockIndex, uint nonce) => new() { @@ -55,6 +78,10 @@ public static PingMessage Create(uint lastBlockIndex, uint nonce) => Nonce = nonce, }; + /// + /// Deserializes this payload from the specified stream. + /// + /// The stream to read from. public void Deserialize(Stream reader) { LastBlockIndex = reader.Read(); @@ -62,6 +89,10 @@ public void Deserialize(Stream reader) Nonce = reader.Read(); } + /// + /// Serializes this payload to the specified stream. + /// + /// The stream to write to. public void Serialize(Stream writer) { writer.Write(LastBlockIndex); diff --git a/src/Neo.Core/Net/Message/ProtocolMessage.cs b/src/Neo.Core/Net/Message/ProtocolMessage.cs index b457bd7..764718b 100644 --- a/src/Neo.Core/Net/Message/ProtocolMessage.cs +++ b/src/Neo.Core/Net/Message/ProtocolMessage.cs @@ -43,12 +43,24 @@ public class ProtocolMessage : INeoSerializable private const int CompressionMinSize = 128; private const int CompressionThreshold = 64; + /// + /// Gets the message flags (for example, compressed). + /// public ProtocolMessageFlags Flags { get; private set; } + /// + /// Gets the protocol command of this frame. + /// public ProtocolMessageCommand Command { get; private set; } + /// + /// Gets the decoded payload object when known (Version, Ping, or Pong); otherwise, . + /// public INeoSerializable? Message => _protocolMessage; + /// + /// Gets a value indicating whether the payload is LZ4-compressed on the wire. + /// public bool Compressed => Flags.HasFlag(ProtocolMessageFlags.Compressed); /// @@ -56,6 +68,9 @@ public class ProtocolMessage : INeoSerializable /// public ReadOnlyMemory PayloadBytes => _compressedPayloadBytes; + /// + /// Gets the serialized size of this frame in bytes. + /// public int Size => sizeof(ProtocolMessageFlags) + sizeof(ProtocolMessageCommand) + @@ -195,6 +210,10 @@ public byte[] ToArray(bool allowCompression = true) return ms.ToArray(); } + /// + /// Deserializes this frame from the specified stream. + /// + /// The stream to read from. public void Deserialize(Stream reader) { Flags = reader.Read(); @@ -205,6 +224,10 @@ public void Deserialize(Stream reader) DecodePayload(); } + /// + /// Serializes this frame to the specified stream. + /// + /// The stream to write to. public void Serialize(Stream writer) { writer.Write(Flags); diff --git a/src/Neo.Core/Net/Message/ServerCapabilityMessage.cs b/src/Neo.Core/Net/Message/ServerCapabilityMessage.cs index 0b0c202..a634073 100644 --- a/src/Neo.Core/Net/Message/ServerCapabilityMessage.cs +++ b/src/Neo.Core/Net/Message/ServerCapabilityMessage.cs @@ -25,20 +25,39 @@ namespace Neo.Core.Net.Message { + /// + /// Advertises that the node listens for TCP peers on a specific port. + /// public class ServerCapabilityMessage : NodeCapabilityMessage { + /// + /// Gets the capability type (). + /// public override NodeCapabilityType Type => NodeCapabilityType.TcpServer; + /// + /// Gets the TCP listen port. + /// public ushort Port { get; private set; } + /// + /// Gets the serialized size of this capability in bytes. + /// public override int Size => base.Size + sizeof(ushort); + /// + /// Initializes an empty server capability for deserialization. + /// public ServerCapabilityMessage() { } + /// + /// Initializes a server capability with the specified port. + /// + /// The TCP listen port. public ServerCapabilityMessage(ushort port) { Port = port; diff --git a/src/Neo.Core/Net/Message/VersionMessage.cs b/src/Neo.Core/Net/Message/VersionMessage.cs index a127805..f135013 100644 --- a/src/Neo.Core/Net/Message/VersionMessage.cs +++ b/src/Neo.Core/Net/Message/VersionMessage.cs @@ -30,26 +30,56 @@ namespace Neo.Core.Net.Message { + /// + /// Payload for the P2P Version handshake message. + /// public class VersionMessage : INeoSerializable { + /// + /// The maximum number of capabilities allowed in a version payload. + /// public const int MaxCapabilities = 32; private readonly static string s_version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0"; + /// + /// Gets the network magic of the sending node. + /// public uint Network { get; private set; } + /// + /// Gets the protocol version of the sending node. + /// public uint Version { get; private set; } = 0u; + /// + /// Gets the Unix timestamp (seconds) when this payload was created. + /// public uint Timestamp { get; private set; } = CoreUtilities.ToUnixTimeSeconds(DateTime.UtcNow); + /// + /// Gets the random nonce identifying the sending node. + /// public uint Nonce { get; private set; } = RandomNumberFactory.NextUInt32(); + /// + /// Gets a value indicating whether the peer allows payload compression. + /// public bool AllowCompression => !Capabilities.Any(static a => a.Type == NodeCapabilityType.DisableCompression); + /// + /// Gets the user agent string of the sending node. + /// public string UserAgent { get; private set; } = $"/RapidLoop:{s_version}/"; + /// + /// Gets the node capabilities advertised by the sender. + /// public NodeCapabilityMessage[] Capabilities { get; private set; } = []; + /// + /// Gets the serialized size of this payload in bytes. + /// public int Size => sizeof(uint) + // Network sizeof(uint) + // Version @@ -58,6 +88,12 @@ public class VersionMessage : INeoSerializable UserAgent.GetSerializedSize() + // UserAgent Capabilities.GetSerializedSize(); // Node Capabilities + /// + /// Creates a version payload for the specified network and capabilities. + /// + /// The network magic number. + /// The node capabilities to advertise. + /// A new . public static VersionMessage Create(uint network, params NodeCapabilityMessage[] capabilities) => new() { @@ -65,6 +101,13 @@ public static VersionMessage Create(uint network, params NodeCapabilityMessage[] Capabilities = capabilities, }; + /// + /// Creates a version payload with an explicit node nonce. + /// + /// The network magic number. + /// The local node nonce. + /// The node capabilities to advertise. + /// A new . public static VersionMessage Create(uint network, uint nonce, params NodeCapabilityMessage[] capabilities) => new() { @@ -73,6 +116,11 @@ public static VersionMessage Create(uint network, uint nonce, params NodeCapabil Capabilities = capabilities, }; + /// + /// Deserializes this payload from the specified stream. + /// + /// The stream to read from. + /// Capability count exceeds the limit or capabilities are duplicated. public void Deserialize(Stream reader) { Network = reader.Read(); @@ -94,6 +142,10 @@ public void Deserialize(Stream reader) throw new FormatException("Duplicate node capabilities."); } + /// + /// Serializes this payload to the specified stream. + /// + /// The stream to write to. public void Serialize(Stream writer) { writer.Write(Network); diff --git a/src/Neo.Core/Net/NodeCapabilityType.cs b/src/Neo.Core/Net/NodeCapabilityType.cs index 47cafeb..2180a17 100644 --- a/src/Neo.Core/Net/NodeCapabilityType.cs +++ b/src/Neo.Core/Net/NodeCapabilityType.cs @@ -22,6 +22,9 @@ namespace Neo.Core.Net { + /// + /// Identifies a node capability advertised in the P2P version handshake. + /// public enum NodeCapabilityType : byte { #region Servers diff --git a/src/Neo.Core/Net/NodeConnection.cs b/src/Neo.Core/Net/NodeConnection.cs index 90808e6..ba00bba 100644 --- a/src/Neo.Core/Net/NodeConnection.cs +++ b/src/Neo.Core/Net/NodeConnection.cs @@ -77,16 +77,34 @@ public class NodeConnection : IAsyncDisposable /// public event EventHandler? Disconnected; + /// + /// Gets a value indicating whether the underlying socket is connected. + /// public bool Connected => _socket.Connected; + /// + /// Gets the remote endpoint of this connection, if available. + /// public EndPoint? RemoteEndPoint => _socket.RemoteEndPoint; + /// + /// Gets the local endpoint of this connection, if available. + /// public EndPoint? LocalEndPoint => _socket.LocalEndPoint; + /// + /// Gets the current handshake state. + /// public NodeHandshakeState HandshakeState => _handshake.State; + /// + /// Gets a value indicating whether the Version/Verack handshake has completed. + /// public bool IsReady => _handshake.IsReady; + /// + /// Gets the remote Version payload after handshake processing, if available. + /// public VersionMessage? RemoteVersion => _handshake.RemoteVersion; /// @@ -123,6 +141,14 @@ public uint LocalBlockIndex private long _localBlockIndex; private long _remoteLastBlockIndex; + /// + /// Initializes a new connection over an already-connected socket. + /// + /// The connected TCP socket. + /// The protocol settings for this network. + /// The local node nonce used for self-connection detection. + /// Optional local capabilities advertised in Version. + /// Optional logger factory; defaults to a null logger. public NodeConnection( Socket socket, ProtocolSettings protocolSettings, @@ -232,6 +258,10 @@ void OnDisconnected(object? sender, EventArgs e) => } } + /// + /// Cancels outstanding work, closes the socket, and releases resources. + /// + /// A task that completes when dispose work finishes. public async ValueTask DisposeAsync() { try diff --git a/src/Neo.Core/Net/NodeHandshake.cs b/src/Neo.Core/Net/NodeHandshake.cs index b1ef853..3171b27 100644 --- a/src/Neo.Core/Net/NodeHandshake.cs +++ b/src/Neo.Core/Net/NodeHandshake.cs @@ -30,15 +30,26 @@ namespace Neo.Core.Net /// Pure Neo N3 handshake state machine (no I/O). /// Local already sent Version; then: receive Version → send Verack → receive Verack → Ready. /// + /// The local network magic expected from the remote Version. + /// The local node nonce; matching remote nonce is treated as a self-connection. public sealed class NodeHandshake(uint network, uint localNonce) { private readonly uint _network = network; private readonly uint _localNonce = localNonce; + /// + /// Gets the current handshake state. + /// public NodeHandshakeState State { get; private set; } = NodeHandshakeState.WaitingForVersion; + /// + /// Gets the remote Version payload after it has been accepted. + /// public VersionMessage? RemoteVersion { get; private set; } + /// + /// Gets a value indicating whether the handshake has completed successfully. + /// public bool IsReady => State == NodeHandshakeState.Ready; /// diff --git a/src/Neo.Core/Net/NodeServerListener.cs b/src/Neo.Core/Net/NodeServerListener.cs index f1eec4b..6d03d21 100644 --- a/src/Neo.Core/Net/NodeServerListener.cs +++ b/src/Neo.Core/Net/NodeServerListener.cs @@ -44,8 +44,14 @@ public class NodeServerListener : IAsyncDisposable /// public const int DefaultBacklog = 128; + /// + /// Gets the currently tracked peer connections. + /// public NodeConnection[] Clients => [.. _connections.Keys]; + /// + /// Gets the protocol settings used for accepted and outbound connections. + /// public ProtocolSettings ProtocolSettings => _protocolSettings; /// @@ -61,6 +67,9 @@ public class NodeServerListener : IAsyncDisposable _serverSocket.LocalEndPoint as IPEndPoint ?? throw new InvalidOperationException($"Listener is not bound. Call {nameof(Start)} first."); + /// + /// Gets a value indicating whether the accept loop has been started. + /// public bool IsActive => _isActive != 0; /// @@ -84,6 +93,12 @@ _serverSocket.LocalEndPoint as IPEndPoint private int _isActive; private int _disposed; + /// + /// Initializes a new listener for the specified endpoint. + /// + /// The local endpoint to bind when starting. + /// Optional protocol settings; defaults to . + /// Optional logger factory; defaults to a null logger. public NodeServerListener( IPEndPoint endPoint, ProtocolSettings? protocolSettings = default, @@ -100,6 +115,10 @@ public NodeServerListener( _capabilities = CreateLocalCapabilities(endPoint); } + /// + /// Stops listening, disconnects peers, and releases resources. + /// + /// A task that completes when dispose work finishes. public async ValueTask DisposeAsync() { if (Interlocked.Exchange(ref _disposed, 1) != 0) @@ -143,11 +162,20 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); } + /// + /// Binds the listener and starts accepting connections with . + /// public void Start() { Start(DefaultBacklog); } + /// + /// Binds the listener and starts accepting connections. + /// + /// The maximum length of the pending connections queue. + /// is less than or equal to zero. + /// The listener has been disposed. public void Start(int backlog) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(backlog); diff --git a/src/Neo.Core/Net/ProtocolMessageFlags.cs b/src/Neo.Core/Net/ProtocolMessageFlags.cs index 36af10f..f74e90c 100644 --- a/src/Neo.Core/Net/ProtocolMessageFlags.cs +++ b/src/Neo.Core/Net/ProtocolMessageFlags.cs @@ -24,6 +24,9 @@ namespace Neo.Core.Net { + /// + /// Flags carried in a Neo P2P protocol message frame. + /// [Flags] public enum ProtocolMessageFlags : byte { diff --git a/src/Neo.Core/ProtocolSettings.cs b/src/Neo.Core/ProtocolSettings.cs index 8121922..b06881b 100644 --- a/src/Neo.Core/ProtocolSettings.cs +++ b/src/Neo.Core/ProtocolSettings.cs @@ -30,6 +30,9 @@ namespace Neo.Core { + /// + /// Network and chain protocol parameters for a NEO network. + /// public class ProtocolSettings { /// @@ -126,6 +129,11 @@ public class ProtocolSettings InitialGasDistribution = 52_000_000_00000000ul, }; + /// + /// Gets the highest hard fork that is active at the specified block height. + /// + /// The block height to evaluate. + /// The active hard fork at . public HardFork GetActiveHardFork(long blockHeight) => HardForks .OrderBy(static o => o.Key) diff --git a/src/Neo.Core/Serialization/INeoSerializable.cs b/src/Neo.Core/Serialization/INeoSerializable.cs index 1bfd8e9..1cf1e2f 100644 --- a/src/Neo.Core/Serialization/INeoSerializable.cs +++ b/src/Neo.Core/Serialization/INeoSerializable.cs @@ -24,6 +24,9 @@ namespace Neo.Core.Serialization { + /// + /// Defines binary serialization and deserialization for Neo wire and storage types. + /// public interface INeoSerializable { /// diff --git a/src/Neo.Core/Storage/IReadOnlyStore.cs b/src/Neo.Core/Storage/IReadOnlyStore.cs index c897a3c..4e71c52 100644 --- a/src/Neo.Core/Storage/IReadOnlyStore.cs +++ b/src/Neo.Core/Storage/IReadOnlyStore.cs @@ -26,14 +26,41 @@ namespace Neo.Core.Storage { + /// + /// A read-only key/value store of binary entries. + /// public interface IReadOnlyStore : IDisposable { + /// + /// Determines whether the store contains the specified key. + /// + /// The key to locate. + /// if the key exists; otherwise, . bool ContainsKey(ReadOnlySpan key); + /// + /// Gets the value associated with the specified key. + /// + /// The key of the value to get. + /// The value for the key, or if not found. byte[]? Get(ReadOnlySpan key); + /// + /// Attempts to get the value associated with the specified key. + /// + /// The key of the value to get. + /// When this method returns, the value if found; otherwise, . + /// if the key was found; otherwise, . bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value); + /// + /// Seeks key/value pairs starting at the specified key or prefix. + /// + /// The key or prefix at which to begin seeking. + /// + /// to seek in reverse from the end; otherwise forward. + /// + /// An enumerable of matching key/value pairs. IEnumerable> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false); } } diff --git a/src/Neo.Core/Storage/IStore.cs b/src/Neo.Core/Storage/IStore.cs index 795279d..f662a0f 100644 --- a/src/Neo.Core/Storage/IStore.cs +++ b/src/Neo.Core/Storage/IStore.cs @@ -24,16 +24,40 @@ namespace Neo.Core.Storage { + /// + /// A mutable key/value store that supports snapshots, backups, and checkpoints. + /// public interface IStore : IReadOnlyStore { + /// + /// Creates a snapshot of the current store state. + /// + /// A disposable store snapshot. IStoreSnapshot CreateSnapshot(); + /// + /// Creates a backup handle for this store. + /// + /// A disposable backup handle. IStoreBackup CreateBackup(); + /// + /// Creates a checkpoint of the store under the specified directory. + /// + /// The directory that receives the checkpoint. void CreateCheckpoint(string checkpointDirectory); + /// + /// Puts or replaces a value for the specified key. + /// + /// The key to write. + /// The value to associate with . void Put(ReadOnlySpan key, ReadOnlySpan value); + /// + /// Deletes the value for the specified key, if present. + /// + /// The key to delete. void Delete(ReadOnlySpan key); } } diff --git a/src/Neo.Core/Storage/IStoreBackup.cs b/src/Neo.Core/Storage/IStoreBackup.cs index fc6f5fb..645f231 100644 --- a/src/Neo.Core/Storage/IStoreBackup.cs +++ b/src/Neo.Core/Storage/IStoreBackup.cs @@ -24,10 +24,27 @@ namespace Neo.Core.Storage { + /// + /// Creates, purges, and restores store backups. + /// public interface IStoreBackup : IDisposable { + /// + /// Creates a backup of the store. + /// void Backup(); + + /// + /// Removes older backups, retaining the specified number of most recent ones. + /// + /// The number of backups to keep. void Purge(uint numberOfBackupsToKeep); + + /// + /// Restores the store from a backup path. + /// + /// The path of the backup to restore. + /// Optional write-ahead log path used during restore. void Restore(string restorePath, string? walPath = null); } } diff --git a/src/Neo.Core/Storage/IStoreSnapshot.cs b/src/Neo.Core/Storage/IStoreSnapshot.cs index 5c5083c..9fb4b79 100644 --- a/src/Neo.Core/Storage/IStoreSnapshot.cs +++ b/src/Neo.Core/Storage/IStoreSnapshot.cs @@ -24,10 +24,19 @@ namespace Neo.Core.Storage { + /// + /// A transactional snapshot of an . + /// public interface IStoreSnapshot : IStore, IDisposable { + /// + /// Gets the underlying store that produced this snapshot. + /// IStore Store { get; } + /// + /// Commits pending changes made on this snapshot to the underlying store. + /// void Commit(); } } diff --git a/src/Neo.Core/Text/Base58.cs b/src/Neo.Core/Text/Base58.cs index 64c67b6..4adce51 100644 --- a/src/Neo.Core/Text/Base58.cs +++ b/src/Neo.Core/Text/Base58.cs @@ -28,6 +28,9 @@ namespace Neo.Core.Text { + /// + /// Provides Base58 and Base58Check encoding and decoding. + /// public static class Base58 { private const string Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; diff --git a/src/Neo.Core/Types/Converter/ECPointTypeConverter.cs b/src/Neo.Core/Types/Converter/ECPointTypeConverter.cs index e98c0f2..fa189ae 100644 --- a/src/Neo.Core/Types/Converter/ECPointTypeConverter.cs +++ b/src/Neo.Core/Types/Converter/ECPointTypeConverter.cs @@ -27,11 +27,20 @@ namespace Neo.Core.Types.Converter { + /// + /// Converts between values and strings for design-time and configuration scenarios. + /// internal class ECPointTypeConverter : TypeConverter { + /// + /// Returns whether this converter can convert an object of the given type to an . + /// public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string) && base.CanConvertFrom(context, sourceType); + /// + /// Converts a string (or other supported value) to an on secp256r1. + /// public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { if (value is string pointString) @@ -43,6 +52,9 @@ public override bool CanConvertFrom(ITypeDescriptorContext? context, Type source return base.ConvertFrom(context, culture, value); } + /// + /// Converts an to the requested destination type (typically a compressed hex string). + /// public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) => CanConvertTo(context, destinationType) ? value switch diff --git a/src/Neo.Core/Types/Converter/IPAddressTypeConverter.cs b/src/Neo.Core/Types/Converter/IPAddressTypeConverter.cs index 295601b..037ee0c 100644 --- a/src/Neo.Core/Types/Converter/IPAddressTypeConverter.cs +++ b/src/Neo.Core/Types/Converter/IPAddressTypeConverter.cs @@ -27,11 +27,29 @@ namespace Neo.Core.Types.Converter { + /// + /// Converts string values to instances. + /// public class IPAddressTypeConverter : TypeConverter { + /// + /// Returns whether this converter can convert an object of the given type to an . + /// + /// An that provides a format context. + /// A that represents the type to convert from. + /// + /// if conversion from is supported; otherwise, the base result. + /// public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string) && base.CanConvertFrom(context, sourceType); + /// + /// Converts the given object to an . + /// + /// An that provides a format context. + /// The to use as the current culture. + /// The object to convert. + /// An , or the base conversion result. public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { if (value is string ipString) diff --git a/src/Neo.Core/Types/Converter/IPEndPointTypeConverter.cs b/src/Neo.Core/Types/Converter/IPEndPointTypeConverter.cs index 9002655..cbc6b8a 100644 --- a/src/Neo.Core/Types/Converter/IPEndPointTypeConverter.cs +++ b/src/Neo.Core/Types/Converter/IPEndPointTypeConverter.cs @@ -28,11 +28,29 @@ namespace Neo.Core.Types.Converter { + /// + /// Converts string values to instances, including host name resolution. + /// public class IPEndPointTypeConverter : TypeConverter { + /// + /// Returns whether this converter can convert an object of the given type to an . + /// + /// An that provides a format context. + /// A that represents the type to convert from. + /// + /// if conversion from is supported; otherwise, the base result. + /// public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string) && base.CanConvertFrom(context, sourceType); + /// + /// Converts the given object to an . + /// + /// An that provides a format context. + /// The to use as the current culture. + /// The object to convert. + /// An , or the base conversion result. public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { if (value is string endPointString) diff --git a/src/Neo.Core/UInt160.cs b/src/Neo.Core/UInt160.cs index b05b696..c970ccf 100644 --- a/src/Neo.Core/UInt160.cs +++ b/src/Neo.Core/UInt160.cs @@ -51,6 +51,9 @@ public class UInt160 : IComparable, IComparable, IEquatable, I [FieldOffset(8)] private ulong _value2; [FieldOffset(16)] private uint _value3; + /// + /// Gets the serialized size of this value in bytes. + /// public int Size => Length; /// @@ -71,12 +74,26 @@ public UInt160(ReadOnlySpan value) value.CopyTo(span); } + /// + /// Compares this instance to a specified object. + /// + /// An object to compare, or . + /// + /// A signed integer that indicates the relative order of this instance and . + /// public int CompareTo(object? obj) { if (ReferenceEquals(obj, this)) return 0; return CompareTo(obj as UInt160); } + /// + /// Compares this instance to another . + /// + /// An to compare. + /// + /// A signed integer that indicates the relative order of this instance and . + /// public int CompareTo(UInt160? other) { if (other is null) return 1; @@ -87,6 +104,10 @@ public int CompareTo(UInt160? other) return _value1.CompareTo(other._value1); } + /// + /// Deserializes this value from the specified stream. + /// + /// The stream to read from. public void Deserialize(Stream reader) { _value1 = reader.Read(); @@ -94,12 +115,22 @@ public void Deserialize(Stream reader) _value3 = reader.Read(); } + /// + /// Determines whether the specified object is equal to the current . + /// + /// The object to compare with the current instance. + /// if the objects are equal; otherwise, . public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; return Equals(obj as UInt160); } + /// + /// Determines whether the specified is equal to the current instance. + /// + /// The to compare with the current instance. + /// if the values are equal; otherwise, . public bool Equals(UInt160? other) { if (other == null) return false; @@ -108,6 +139,10 @@ public bool Equals(UInt160? other) _value3 == other._value3; } + /// + /// Returns a hash code for this instance. + /// + /// A hash code for the current . public override int GetHashCode() { unchecked @@ -122,6 +157,10 @@ public override int GetHashCode() } } + /// + /// Serializes this value to the specified stream. + /// + /// The stream to write to. public void Serialize(Stream writer) { writer.Write(_value1); @@ -129,6 +168,10 @@ public void Serialize(Stream writer) writer.Write(_value3); } + /// + /// Returns the hex string representation of this value with a 0x prefix. + /// + /// A hex string of this . public override string ToString() { return "0x" + Convert.ToHexStringLower([.. this.ToArray().Reverse()]); @@ -173,16 +216,27 @@ public static UInt160 Parse(string value) return new UInt160([.. Convert.FromHexString(data).Reverse()]); } + /// + /// Implicitly converts a hex string to a . + /// + /// A hex string representation of a . public static implicit operator UInt160(string s) { return Parse(s); } + /// + /// Implicitly converts a 20-byte array to a . + /// + /// The little-endian 20-byte value. public static implicit operator UInt160(byte[] b) { return new UInt160(b); } + /// + /// Determines whether two values are equal. + /// public static bool operator ==(UInt160? left, UInt160? right) { if (left is null || right is null) @@ -190,6 +244,9 @@ public static implicit operator UInt160(byte[] b) return left.Equals(right); } + /// + /// Determines whether two values are not equal. + /// public static bool operator !=(UInt160? left, UInt160? right) { if (left is null || right is null) @@ -197,21 +254,33 @@ public static implicit operator UInt160(byte[] b) return !left.Equals(right); } + /// + /// Determines whether one is greater than another. + /// public static bool operator >(UInt160 left, UInt160 right) { return left.CompareTo(right) > 0; } + /// + /// Determines whether one is greater than or equal to another. + /// public static bool operator >=(UInt160 left, UInt160 right) { return left.CompareTo(right) >= 0; } + /// + /// Determines whether one is less than another. + /// public static bool operator <(UInt160 left, UInt160 right) { return left.CompareTo(right) < 0; } + /// + /// Determines whether one is less than or equal to another. + /// public static bool operator <=(UInt160 left, UInt160 right) { return left.CompareTo(right) <= 0; diff --git a/src/Neo.Core/UInt256.cs b/src/Neo.Core/UInt256.cs index 9b4b2d5..37fb8a7 100644 --- a/src/Neo.Core/UInt256.cs +++ b/src/Neo.Core/UInt256.cs @@ -52,6 +52,9 @@ public class UInt256 : IComparable, IComparable, IEquatable, I [FieldOffset(16)] private ulong _value3; [FieldOffset(24)] private ulong _value4; + /// + /// Gets the serialized size of this value in bytes. + /// public int Size => Length; /// @@ -73,12 +76,26 @@ public UInt256(ReadOnlySpan value) value.CopyTo(span); } + /// + /// Compares this instance to a specified object. + /// + /// An object to compare, or . + /// + /// A signed integer that indicates the relative order of this instance and . + /// public int CompareTo(object? obj) { if (ReferenceEquals(obj, this)) return 0; return CompareTo(obj as UInt256); } + /// + /// Compares this instance to another . + /// + /// An to compare. + /// + /// A signed integer that indicates the relative order of this instance and . + /// public int CompareTo(UInt256? other) { if (other is null) return 1; @@ -91,6 +108,10 @@ public int CompareTo(UInt256? other) return _value1.CompareTo(other._value1); } + /// + /// Deserializes this value from the specified stream. + /// + /// The stream to read from. public void Deserialize(Stream reader) { _value1 = reader.Read(); @@ -99,12 +120,22 @@ public void Deserialize(Stream reader) _value4 = reader.Read(); } + /// + /// Determines whether the specified object is equal to the current . + /// + /// The object to compare with the current instance. + /// if the objects are equal; otherwise, . public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; return Equals(obj as UInt256); } + /// + /// Determines whether the specified is equal to the current instance. + /// + /// The to compare with the current instance. + /// if the values are equal; otherwise, . public bool Equals(UInt256? other) { if (other is null) return false; @@ -114,6 +145,10 @@ public bool Equals(UInt256? other) && _value4 == other._value4; } + /// + /// Returns a hash code for this instance. + /// + /// A hash code for the current . public override int GetHashCode() { unchecked @@ -129,6 +164,10 @@ public override int GetHashCode() } } + /// + /// Serializes this value to the specified stream. + /// + /// The stream to write to. public void Serialize(Stream writer) { writer.Write(_value1); @@ -137,6 +176,10 @@ public void Serialize(Stream writer) writer.Write(_value4); } + /// + /// Returns the hex string representation of this value with a 0x prefix. + /// + /// A hex string of this . public override string ToString() { return "0x" + Convert.ToHexStringLower([.. this.ToArray().Reverse()]); @@ -180,16 +223,27 @@ public static UInt256 Parse(string value) return new UInt256([.. Convert.FromHexString(data).Reverse()]); } + /// + /// Implicitly converts a hex string to a . + /// + /// A hex string representation of a . public static implicit operator UInt256(string s) { return Parse(s); } + /// + /// Implicitly converts a 32-byte array to a . + /// + /// The little-endian 32-byte value. public static implicit operator UInt256(byte[] b) { return new UInt256(b); } + /// + /// Determines whether two values are equal. + /// public static bool operator ==(UInt256? left, UInt256? right) { if (left is null || right is null) @@ -197,26 +251,41 @@ public static implicit operator UInt256(byte[] b) return left.Equals(right); } + /// + /// Determines whether two values are not equal. + /// public static bool operator !=(UInt256? left, UInt256? right) { return !(left == right); } + /// + /// Determines whether one is greater than another. + /// public static bool operator >(UInt256 left, UInt256 right) { return left.CompareTo(right) > 0; } + /// + /// Determines whether one is greater than or equal to another. + /// public static bool operator >=(UInt256 left, UInt256 right) { return left.CompareTo(right) >= 0; } + /// + /// Determines whether one is less than another. + /// public static bool operator <(UInt256 left, UInt256 right) { return left.CompareTo(right) < 0; } + /// + /// Determines whether one is less than or equal to another. + /// public static bool operator <=(UInt256 left, UInt256 right) { return left.CompareTo(right) <= 0; diff --git a/src/Neo.Core/VM/Attributes/MethodDescriptorAttribute.cs b/src/Neo.Core/VM/Attributes/MethodDescriptorAttribute.cs index 94d2286..c1f8a50 100644 --- a/src/Neo.Core/VM/Attributes/MethodDescriptorAttribute.cs +++ b/src/Neo.Core/VM/Attributes/MethodDescriptorAttribute.cs @@ -25,6 +25,9 @@ namespace Neo.Core.VM.Attributes { + /// + /// Describes an interoperable service method exposed to the VM, including pricing, permissions, and type metadata. + /// [AttributeUsage( AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Event, AllowMultiple = true @@ -51,8 +54,14 @@ public class MethodDescriptorAttribute : Attribute /// public required CallFlags CallFlags { get; init; } = CallFlags.None; + /// + /// Indicates whether the method is safe to invoke without changing chain state. + /// public required bool Safe { get; init; } + /// + /// The return type of the interoperable service method. + /// public required MethodParameterType ReturnType { get; init; } = MethodParameterType.Any; } } diff --git a/src/Neo.Core/VM/Attributes/OpCodePriceAttribute.cs b/src/Neo.Core/VM/Attributes/OpCodePriceAttribute.cs index a62ee88..017ac36 100644 --- a/src/Neo.Core/VM/Attributes/OpCodePriceAttribute.cs +++ b/src/Neo.Core/VM/Attributes/OpCodePriceAttribute.cs @@ -30,8 +30,14 @@ namespace Neo.Core.VM.Attributes [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] public class OpCodePriceAttribute : Attribute { + /// + /// The hard fork from which this price applies. + /// public required HardFork HardFork { get; init; } + /// + /// The gas cost of the opcode at the specified hard fork. + /// public required long Cost { get; init; } } } diff --git a/src/Neo.Core/VM/ExecutionEngineLimits.cs b/src/Neo.Core/VM/ExecutionEngineLimits.cs index e59475a..51a39c9 100644 --- a/src/Neo.Core/VM/ExecutionEngineLimits.cs +++ b/src/Neo.Core/VM/ExecutionEngineLimits.cs @@ -29,6 +29,9 @@ namespace Neo.Core.VM /// public sealed class ExecutionEngineLimits { + /// + /// The default maximum amount of gas that can be consumed by a script execution. + /// public const long MaxGas = 20_00000000; /// diff --git a/src/Neo.Core/VM/HardFork.cs b/src/Neo.Core/VM/HardFork.cs index 171bdf4..4e6bf5c 100644 --- a/src/Neo.Core/VM/HardFork.cs +++ b/src/Neo.Core/VM/HardFork.cs @@ -22,15 +22,49 @@ namespace Neo.Core.VM { + /// + /// Identifies protocol hard forks that may change VM behavior, gas costs, or features. + /// public enum HardFork : byte { + /// + /// The initial protocol state before any named hard fork. + /// Genesis = 0, + + /// + /// The Aspidochelone hard fork. + /// Aspidochelone, + + /// + /// The Basilisk hard fork. + /// Basilisk, + + /// + /// The Cockatrice hard fork. + /// Cockatrice, + + /// + /// The Domovoi hard fork. + /// Domovoi, + + /// + /// The Echidna hard fork. + /// Echidna, + + /// + /// The Faun hard fork. + /// Faun, + + /// + /// The Gorgon hard fork. + /// Gorgon, } } diff --git a/src/Neo.Core/VM/Interface/IVMComponent.cs b/src/Neo.Core/VM/Interface/IVMComponent.cs index 7eb9fd1..672c6af 100644 --- a/src/Neo.Core/VM/Interface/IVMComponent.cs +++ b/src/Neo.Core/VM/Interface/IVMComponent.cs @@ -24,9 +24,19 @@ namespace Neo.Core.VM.Interface { + /// + /// Represents a disposable VM component that tracks size and reference count. + /// public interface IVMComponent : IDisposable { + /// + /// Gets the size of the component in bytes for stack and limit accounting. + /// int Size { get; } + + /// + /// Gets the current reference count of the component. + /// int RefCount { get; } } } diff --git a/src/Neo.Core/VM/MethodDescriptor.cs b/src/Neo.Core/VM/MethodDescriptor.cs index 041a358..01b6824 100644 --- a/src/Neo.Core/VM/MethodDescriptor.cs +++ b/src/Neo.Core/VM/MethodDescriptor.cs @@ -31,6 +31,9 @@ namespace Neo.Core.VM { + /// + /// Describes an interoperable service method that can be invoked from the VM via . + /// public class MethodDescriptor : IEquatable { /// @@ -49,7 +52,7 @@ public class MethodDescriptor : IEquatable public IReadOnlyList Parameters => _targetMethodInfo.GetParameters(); /// - /// the method info of the interoperable service. + /// The method info of the interoperable service. /// public MethodInfo TargetMethodInfo => _targetMethodInfo; @@ -63,6 +66,16 @@ public class MethodDescriptor : IEquatable private readonly MethodInfo _targetMethodInfo; private readonly Delegate _targetMethod; + /// + /// Initializes a new instance of the class. + /// + /// The instance that owns the target method, property, or event. + /// The name of the method, property, or event to bind. + /// + /// Optional system method name used for the call address. + /// When omitted, a name of the form System.{Type}.{Method} is generated. + /// + /// Thrown when the named target cannot be found on the instance type. public MethodDescriptor(object targetInstance, string targetMethodName, string? systemMethodName = default) { var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; @@ -87,6 +100,11 @@ public MethodDescriptor(object targetInstance, string targetMethodName, string? _hash = CreateCallAddress(_systemMethodName); } + /// + /// Determines whether this descriptor refers to the same interoperable service as another. + /// + /// The other descriptor to compare. + /// if both descriptors share the same hash; otherwise, . public bool Equals(MethodDescriptor? other) { if (ReferenceEquals(other, this)) return true; @@ -94,6 +112,7 @@ public bool Equals(MethodDescriptor? other) return _hash == other._hash; } + /// public override bool Equals(object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -101,6 +120,7 @@ public override bool Equals(object? obj) return Equals(obj as MethodDescriptor); } + /// public override int GetHashCode() { checked @@ -109,9 +129,18 @@ public override int GetHashCode() } } + /// + /// Creates a 32-bit call address from a system method name by hashing it with SHA-256. + /// + /// The interoperable service name to hash. + /// The first four little-endian bytes of the SHA-256 hash. public static uint CreateCallAddress(string systemMethodName) => BinaryPrimitives.ReadUInt32LittleEndian(Encoding.ASCII.GetBytes(systemMethodName).ToSha256()); + /// + /// Converts the descriptor to its interoperable service hash. + /// + /// The descriptor to convert. public static implicit operator uint(MethodDescriptor descriptor) { return descriptor._hash; diff --git a/src/Neo.Core/VM/OpCodeInst.cs b/src/Neo.Core/VM/OpCodeInst.cs index e69d26c..bd5511c 100644 --- a/src/Neo.Core/VM/OpCodeInst.cs +++ b/src/Neo.Core/VM/OpCodeInst.cs @@ -36,18 +36,47 @@ namespace Neo.Core.VM { + /// + /// Represents a single decoded instruction within a Neo VM script, including its opcode and operand. + /// [DebuggerDisplay("OpCode={OpCode}, OperandSize={OperandSize}")] public sealed class OpCodeInst : IEnumerable { private const int OpCodeSize = 1; + /// + /// Gets a prebuilt instruction containing only . + /// public static OpCodeInst Ret => new((byte[])[(byte)OpCode.RET]); + /// + /// Gets the byte offset of this instruction within the script. + /// public int Position { get; private init; } + + /// + /// Gets the opcode of this instruction. + /// public OpCode OpCode { get; private init; } + + /// + /// Gets the operand bytes associated with this instruction. + /// public ReadOnlyMemory Operand { get; private init; } + + /// + /// Gets the total operand size in bytes, including any size prefix. + /// public int OperandSize { get; private init; } + + /// + /// Gets the size of the operand length prefix in bytes, or zero when the operand size is fixed. + /// public int OperandPrefixSize { get; private init; } + + /// + /// Gets the total size of this instruction in bytes, including the opcode and operand. + /// public int Size => OperandPrefixSize > 0 ? OpCodeSize + Operand.Length : OpCodeSize + OperandSize; @@ -57,6 +86,14 @@ public sealed class OpCodeInst : IEnumerable private readonly ReadOnlyMemory _script; + /// + /// Decodes the instruction at the specified position in a script. + /// + /// The script that contains the instruction. + /// The byte offset at which the instruction begins. + /// Thrown when the script is empty. + /// Thrown when the opcode or operand prefix is invalid. + /// Thrown when the operand extends past the end of the script. public OpCodeInst(ReadOnlyMemory script, int start = 0) { if (script.IsEmpty) @@ -104,6 +141,10 @@ static OpCodeInst() } } + /// + /// Enumerates this instruction and all subsequent instructions in the script. + /// + /// An enumerator over instructions starting at the current position. public IEnumerator GetEnumerator() { var nip = Position + OperandSize + OpCodeSize; @@ -114,9 +155,14 @@ public IEnumerator GetEnumerator() yield return instruct = new OpCodeInst(_script, ip); } + /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + /// + /// Returns a human-readable representation of the instruction, including position, opcode, and decoded operand. + /// + /// A formatted instruction string. public override string ToString() { var sb = new StringBuilder(); @@ -124,6 +170,15 @@ public override string ToString() return sb.ToString(); } + /// + /// Interprets a portion of the operand as an unmanaged value of type . + /// + /// The unmanaged type to read from the operand. + /// The byte offset within the operand at which to begin reading. + /// The value read from the operand. + /// + /// Thrown when does not fit in the operand at the specified index. + /// public T AsToken(uint index = 0) where T : unmanaged { @@ -138,6 +193,10 @@ public T AsToken(uint index = 0) return Unsafe.As(ref bytes[index]); } + /// + /// Produces a short human-readable decoding of the operand for display and debugging. + /// + /// A string describing the operand, or an empty string when no decoding is available. public string DecodeOperand() { var operand = Operand[OperandPrefixSize..].ToArray(); diff --git a/src/Neo.Core/VM/ScriptBuilder.cs b/src/Neo.Core/VM/ScriptBuilder.cs index 9f93595..7c3a6d9 100644 --- a/src/Neo.Core/VM/ScriptBuilder.cs +++ b/src/Neo.Core/VM/ScriptBuilder.cs @@ -42,6 +42,9 @@ public class ScriptBuilder : IDisposable /// public int Length => (int)_stream.Length; + /// + /// Releases the underlying script buffer. + /// public void Dispose() { _stream.Dispose(); @@ -200,6 +203,12 @@ public ScriptBuilder EmitSysCall(uint api) return Emit(OpCode.SYSCALL, BitConverter.GetBytes(api)); } + /// + /// Emits instructions that create an array from the specified items. + /// + /// The type of the array elements. + /// The items to place into the array; when null or empty, emits . + /// A reference to this instance after the emit operation has completed. public ScriptBuilder EmitCreateArray(IReadOnlyList? list = null) { if (list is null || list.Count == 0) @@ -213,6 +222,13 @@ public ScriptBuilder EmitCreateArray(IReadOnlyList? list = null) return Emit(OpCode.PACK); } + /// + /// Emits instructions that create a map from the specified key/value pairs. + /// + /// The type of the map keys. + /// The type of the map values. + /// The key/value pairs to place into the map. + /// A reference to this instance after the emit operation has completed. public ScriptBuilder EmitCreateMap(IEnumerable> map) where TKey : notnull where TValue : notnull @@ -233,6 +249,13 @@ public ScriptBuilder EmitCreateMap(IEnumerable + /// Emits instructions that create a map from the specified dictionary. + /// + /// The type of the map keys. + /// The type of the map values. + /// The dictionary to place into the map. + /// A reference to this instance after the emit operation has completed. public ScriptBuilder EmitCreateMap(IReadOnlyDictionary map) where TKey : notnull where TValue : notnull @@ -251,6 +274,12 @@ public ScriptBuilder EmitCreateMap(IReadOnlyDictionary + /// Emits instructions that create a struct from the specified items. + /// + /// The type of the struct elements. + /// The items to place into the struct. + /// A reference to this instance after the emit operation has completed. public ScriptBuilder EmitCreateStruct(IReadOnlyList array) where T : notnull { @@ -265,6 +294,11 @@ public ScriptBuilder EmitCreateStruct(IReadOnlyList array) return Emit(OpCode.PACKSTRUCT); } + /// + /// Emits one or more opcodes without operands. + /// + /// The opcodes to emit. + /// A reference to this instance after the emit operation has completed. public ScriptBuilder Emit(params OpCode[] ops) { foreach (var op in ops) @@ -273,6 +307,15 @@ public ScriptBuilder Emit(params OpCode[] ops) return this; } + /// + /// Emits a push instruction for a supported object value. + /// + /// + /// The value to push. Supported types include booleans, numeric types, strings, byte arrays, + /// enums, instances, and . + /// + /// A reference to this instance after the emit operation has completed. + /// Thrown when has an unsupported type. public ScriptBuilder EmitPush(object? obj) { switch (obj) @@ -332,6 +375,12 @@ public ScriptBuilder EmitPush(object? obj) return this; } + /// + /// Emits argument pushes followed by an for the specified API. + /// + /// The interoperable service hash to call. + /// The arguments to push onto the stack before the call. + /// A reference to this instance after the emit operation has completed. public ScriptBuilder EmitSysCall(uint method, params object[] args) { for (var i = args.Length - 1; i >= 0; i--) @@ -340,6 +389,10 @@ public ScriptBuilder EmitSysCall(uint method, params object[] args) return EmitSysCall(method); } + /// + /// Emits an instruction. + /// + /// A reference to this instance after the emit operation has completed. public ScriptBuilder EmitReturn() => Emit(OpCode.RET); diff --git a/src/Neo.Core/VM/SmartContract/MethodParameterType.cs b/src/Neo.Core/VM/SmartContract/MethodParameterType.cs index b909e1b..89bdc2f 100644 --- a/src/Neo.Core/VM/SmartContract/MethodParameterType.cs +++ b/src/Neo.Core/VM/SmartContract/MethodParameterType.cs @@ -22,6 +22,9 @@ namespace Neo.Core.VM.SmartContract { + /// + /// Describes the expected parameter or return type of a smart-contract method. + /// public enum MethodParameterType : byte { /// diff --git a/src/Neo.Core/VM/Specs/GasTable.cs b/src/Neo.Core/VM/Specs/GasTable.cs index d3a969c..5402c02 100644 --- a/src/Neo.Core/VM/Specs/GasTable.cs +++ b/src/Neo.Core/VM/Specs/GasTable.cs @@ -28,6 +28,9 @@ namespace Neo.Core.VM.Specs { + /// + /// Provides opcode and interop method gas costs resolved by hard fork. + /// public static class GasTable { private readonly static Dictionary> s_gasCosts = []; @@ -61,8 +64,11 @@ static GasTable() } /// - /// Get gas cost for an under a specific . + /// Gets the gas cost for an under a specific . /// + /// The opcode whose gas cost is requested. + /// The hard fork used to resolve the cost; falls back to earlier forks when needed. + /// The gas cost for the opcode at the specified hard fork. public static long GetGasCost(OpCode opcode, HardFork fork = HardFork.Genesis) { for (var current = fork; current >= HardFork.Genesis && Enum.IsDefined(current); current--) @@ -77,6 +83,15 @@ public static long GetGasCost(OpCode opcode, HardFork fork = HardFork.Genesis) return s_gasCosts[HardFork.Genesis][opcode]; // fallback Genesis MUST exist } + /// + /// Gets the gas cost for an interoperable service method under a specific . + /// + /// The method descriptor whose execute price is requested. + /// The hard fork used to resolve the price; falls back to earlier forks when needed. + /// + /// The method's when available; + /// otherwise the gas cost of . + /// public static long GetGasCost(MethodDescriptor method, HardFork fork = HardFork.Genesis) { var attributes = method.TargetMethodInfo.GetCustomAttributes(); @@ -93,8 +108,10 @@ public static long GetGasCost(MethodDescriptor method, HardFork fork = HardFork. } /// - /// Get all gas costs for a specific (useful for debugging). + /// Gets all gas costs defined for a specific (useful for debugging). /// + /// The hard fork used to resolve the cost table; falls back to earlier forks when needed. + /// A read-only dictionary of opcodes to gas costs for the resolved hard fork. public static IReadOnlyDictionary GetAllCosts(HardFork fork = HardFork.Genesis) { for (var current = fork; current >= HardFork.Genesis && Enum.IsDefined(current); current--) diff --git a/src/Neo.IO/Data/BloomFIlter.cs b/src/Neo.IO/Data/BloomFIlter.cs index 75fbda5..474cfac 100644 --- a/src/Neo.IO/Data/BloomFIlter.cs +++ b/src/Neo.IO/Data/BloomFIlter.cs @@ -26,6 +26,9 @@ namespace Neo.IO.Data { + /// + /// A probabilistic set that supports membership tests with a configurable false-positive rate. + /// public class BloomFilter { private readonly BitArray _bits; @@ -33,10 +36,19 @@ public class BloomFilter private readonly int _hashCount; private readonly uint _tweak; + /// + /// Gets the number of hash functions used by this filter. + /// public int HashCount => _hashCount; + /// + /// Gets the size of the underlying bit array in bits. + /// public int BitSize => _bitSize; + /// + /// Gets the tweak value mixed into hash seeds for this filter. + /// public uint Tweak => _tweak; /// @@ -62,6 +74,10 @@ public BloomFilter(int expectedElements, double falsePositiveRate = 0.01, uint t _tweak = tweak; } + /// + /// Adds the specified data to the filter. + /// + /// The element data to insert. public void Add(ReadOnlySpan data) { for (var i = 0; i < _hashCount; i++) @@ -73,6 +89,14 @@ public void Add(ReadOnlySpan data) } } + /// + /// Tests whether the specified data may be present in the filter. + /// + /// The element data to test. + /// + /// if the element is definitely not present; + /// otherwise if it is probably present. + /// public bool Contains(ReadOnlySpan data) { for (var i = 0; i < _hashCount; i++) @@ -87,17 +111,38 @@ public bool Contains(ReadOnlySpan data) return true; // Probably present } + /// + /// Adds the specified data to the filter. + /// + /// The element data to insert. public void Add(byte[] data) => Add(data.AsSpan()); + /// + /// Tests whether the specified data may be present in the filter. + /// + /// The element data to test. + /// + /// if the element is definitely not present; + /// otherwise if it is probably present. + /// public bool Contains(byte[] data) => Contains(data.AsSpan()); + /// + /// Estimates the false-positive probability after the specified number of insertions. + /// + /// The number of elements that have been inserted. + /// The approximate false-positive probability in the range [0, 1]. public double GetFalsePositiveProbability(int insertedCount) { return Math.Pow(1 - Math.Exp(-_hashCount * insertedCount / (double)_bitSize), _hashCount); } + /// + /// Copies the underlying bit array into the provided buffer. + /// + /// The destination buffer that receives the bit array contents. public void GetBits(byte[] bytes) => _bits.CopyTo(bytes, 0); } diff --git a/src/Neo.IO/Hashing/Murmur128.cs b/src/Neo.IO/Hashing/Murmur128.cs index 52ae2fe..047bd5c 100644 --- a/src/Neo.IO/Hashing/Murmur128.cs +++ b/src/Neo.IO/Hashing/Murmur128.cs @@ -28,6 +28,9 @@ namespace Neo.IO.Hashing { + /// + /// Implements the 128-bit MurmurHash3 non-cryptographic hash algorithm. + /// public class Murmur128 : NonCryptographicHashAlgorithm { private const uint DefaultSeed = 0xdeadc0deu; @@ -47,9 +50,21 @@ private Murmur128(uint seed = DefaultSeed) : base(16) Reset(); } + /// + /// Computes the 128-bit MurmurHash3 of the specified data. + /// + /// The input data to hash. + /// The hash seed. Defaults to a fixed non-zero seed. + /// A 16-byte array containing the hash digest. public static byte[] Hash(byte[] data, uint seed = DefaultSeed) => Hash(data.AsSpan(), seed); + /// + /// Computes the 128-bit MurmurHash3 of the specified data. + /// + /// The input data to hash. + /// The hash seed. Defaults to a fixed non-zero seed. + /// A 16-byte array containing the hash digest. public static byte[] Hash(ReadOnlySpan data, uint seed = DefaultSeed) { var hasher = new Murmur128(seed); @@ -57,24 +72,51 @@ public static byte[] Hash(ReadOnlySpan data, uint seed = DefaultSeed) return hasher.GetCurrentHash(); } + /// + /// Computes the 128-bit MurmurHash3 of the UTF-8 encoding of the specified text. + /// + /// The text to hash. + /// The hash seed. Defaults to a fixed non-zero seed. + /// A 16-byte array containing the hash digest. public static byte[] Hash(string text, uint seed = DefaultSeed) { var bytes = Encoding.UTF8.GetBytes(text); return Hash(bytes, seed); } + /// + /// Computes the 128-bit MurmurHash3 of the specified data and returns it as a . + /// + /// The input data to hash. + /// The hash seed. Defaults to a fixed non-zero seed. + /// The hash digest as a . public static UInt128 HashToUInt128(byte[] data, uint seed = DefaultSeed) => HashToUInt128(data.AsSpan(), seed); + /// + /// Computes the 128-bit MurmurHash3 of the specified data and returns it as a . + /// + /// The input data to hash. + /// The hash seed. Defaults to a fixed non-zero seed. + /// The hash digest as a . public static UInt128 HashToUInt128(ReadOnlySpan data, uint seed = DefaultSeed) => BitConverter.ToUInt128(Hash(data, seed), 0); + /// + /// Computes the 128-bit MurmurHash3 of the UTF-8 encoding of the specified text and returns it as a . + /// + /// The text to hash. + /// The hash seed. Defaults to a fixed non-zero seed. + /// The hash digest as a . public static UInt128 HashToUInt128(string text, uint seed = DefaultSeed) { var bytes = Encoding.UTF8.GetBytes(text); return HashToUInt128(bytes, seed); } + /// + /// Resets the hasher to its initial state for the configured seed. + /// public override void Reset() { _h1 = _seed; @@ -82,6 +124,10 @@ public override void Reset() _length = 0; } + /// + /// Appends data to the hash computation. + /// + /// The data to include in the hash. public override void Append(ReadOnlySpan source) { _length += (uint)source.Length; diff --git a/src/Neo.IO/Hashing/Murmur32.cs b/src/Neo.IO/Hashing/Murmur32.cs index 16ecca2..93989ec 100644 --- a/src/Neo.IO/Hashing/Murmur32.cs +++ b/src/Neo.IO/Hashing/Murmur32.cs @@ -26,6 +26,9 @@ namespace Neo.IO.Hashing { + /// + /// Implements the 32-bit MurmurHash3 non-cryptographic hash algorithm. + /// public sealed class Murmur32 : NonCryptographicHashAlgorithm { private const uint DefaultSeed = 0xdeadc0deu; @@ -54,9 +57,21 @@ private Murmur32(uint seed = DefaultSeed) : base(sizeof(uint)) // 4 bytes outout _hash = _seed; } + /// + /// Computes the 32-bit MurmurHash3 of the specified data. + /// + /// The input data to hash. + /// The hash seed. Defaults to a fixed non-zero seed. + /// A 4-byte array containing the hash digest. public static byte[] Hash(byte[] data, uint seed = DefaultSeed) => Hash(data.AsSpan(), seed); + /// + /// Computes the 32-bit MurmurHash3 of the specified data. + /// + /// The input data to hash. + /// The hash seed. Defaults to a fixed non-zero seed. + /// A 4-byte array containing the hash digest. public static byte[] Hash(ReadOnlySpan data, uint seed = DefaultSeed) { var hasher = new Murmur32(seed); @@ -64,24 +79,52 @@ public static byte[] Hash(ReadOnlySpan data, uint seed = DefaultSeed) return hasher.GetCurrentHash(); } + /// + /// Computes the 32-bit MurmurHash3 of the UTF-8 encoding of the specified text. + /// + /// The text to hash. + /// The hash seed. Defaults to a fixed non-zero seed. + /// A 4-byte array containing the hash digest. public static byte[] Hash(string text, uint seed = DefaultSeed) { var bytes = Encoding.UTF8.GetBytes(text); return Hash(bytes, seed); } + /// + /// Computes the 32-bit MurmurHash3 of the specified data and returns it as a . + /// + /// The input data to hash. + /// The hash seed. Defaults to a fixed non-zero seed. + /// The hash digest as a . public static uint HashToUInt32(byte[] data, uint seed = DefaultSeed) => HashToUInt32(data.AsSpan(), seed); + /// + /// Computes the 32-bit MurmurHash3 of the specified data and returns it as a . + /// + /// The input data to hash. + /// The hash seed. Defaults to a fixed non-zero seed. + /// The hash digest as a . public static uint HashToUInt32(ReadOnlySpan data, uint seed = DefaultSeed) => BitConverter.ToUInt32(Hash(data, seed), 0); + /// + /// Computes the 32-bit MurmurHash3 of the UTF-8 encoding of the specified text and returns it as a . + /// + /// The text to hash. + /// The hash seed. Defaults to a fixed non-zero seed. + /// The hash digest as a . public static uint HashToUInt32(string text, uint seed = DefaultSeed) { var bytes = Encoding.UTF8.GetBytes(text); return HashToUInt32(bytes, seed); } + /// + /// Appends data to the hash computation. + /// + /// The data to include in the hash. public override void Append(ReadOnlySpan source) { _length += (uint)source.Length; @@ -120,6 +163,9 @@ public override void Append(ReadOnlySpan source) } } + /// + /// Resets the hasher to its initial state. + /// public override void Reset() { _hash = 0; diff --git a/src/Neo.Localization/ResourceFactory.cs b/src/Neo.Localization/ResourceFactory.cs index 1e4ebc8..54a0369 100644 --- a/src/Neo.Localization/ResourceFactory.cs +++ b/src/Neo.Localization/ResourceFactory.cs @@ -27,8 +27,14 @@ namespace Neo.Localization { + /// + /// Provides a shared for Neo localization resources. + /// public static class ResourceFactory { + /// + /// Gets the default string localizer factory configured with the Resources path. + /// public static readonly ResourceManagerStringLocalizerFactory Instance = Create(config => config.ResourcesPath = "Resources"); private static ResourceManagerStringLocalizerFactory Create(Action configure) diff --git a/src/Neo.Localization/VM/VirtualMachineLocalizer.cs b/src/Neo.Localization/VM/VirtualMachineLocalizer.cs index 9be48d2..440bd51 100644 --- a/src/Neo.Localization/VM/VirtualMachineLocalizer.cs +++ b/src/Neo.Localization/VM/VirtualMachineLocalizer.cs @@ -25,10 +25,19 @@ namespace Neo.Localization.VM { + /// + /// Resolves localized virtual machine messages from resource files. + /// public static class VirtualMachineLocalizer { private static readonly IStringLocalizer s_localizer = ResourceFactory.Instance.Create(typeof(VirtualMachineLocalizer)); + /// + /// Gets a localized virtual machine message by name, optionally formatting it with the provided arguments. + /// + /// The resource key of the message, typically from . + /// Optional format arguments for the message template. + /// The localized message string. [return: NotNullIfNotNull(nameof(s_localizer))] public static string GetMessage(string messageName, params object[] args) => s_localizer[messageName, args]; diff --git a/src/Neo.Localization/VM/VirtualMachineMessageNames.cs b/src/Neo.Localization/VM/VirtualMachineMessageNames.cs index 49a5325..eaf0826 100644 --- a/src/Neo.Localization/VM/VirtualMachineMessageNames.cs +++ b/src/Neo.Localization/VM/VirtualMachineMessageNames.cs @@ -22,12 +22,26 @@ namespace Neo.Localization.VM { + /// + /// Resource keys for localized virtual machine messages. + /// public static class VirtualMachineMessageNames { private const string MessageSuffixString = "Message"; + /// + /// Resource key for the message emitted when VM execution starts. + /// public const string ExecuteStartup = nameof(ExecuteStartup) + MessageSuffixString; + + /// + /// Resource key for the message emitted when an opcode is executed. + /// public const string ExecuteOpCode = nameof(ExecuteOpCode) + MessageSuffixString; + + /// + /// Resource key for the message emitted when VM execution completes successfully. + /// public const string ExecuteSuccessfully = nameof(ExecuteSuccessfully) + MessageSuffixString; } } diff --git a/src/Neo.Platform.CLI/Commands/EmptyHandler.cs b/src/Neo.Platform.CLI/Commands/EmptyHandler.cs index ccb8028..d85cfe0 100644 --- a/src/Neo.Platform.CLI/Commands/EmptyHandler.cs +++ b/src/Neo.Platform.CLI/Commands/EmptyHandler.cs @@ -27,8 +27,17 @@ namespace Neo.Platform.CLI.Commands { + /// + /// A no-op command-line action that always succeeds with exit code 0. + /// public class EmptyHandler : AsynchronousCommandLineAction { + /// + /// Invokes the handler without performing any work. + /// + /// The parse result for the current command invocation. + /// A token that can cancel the operation. + /// A completed task with exit code 0. public override Task InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default) { return Task.FromResult(0); diff --git a/src/Neo.Platform.CLI/Commands/ProgramRootCommand.cs b/src/Neo.Platform.CLI/Commands/ProgramRootCommand.cs index b4e2d1a..f820629 100644 --- a/src/Neo.Platform.CLI/Commands/ProgramRootCommand.cs +++ b/src/Neo.Platform.CLI/Commands/ProgramRootCommand.cs @@ -24,8 +24,14 @@ namespace Neo.Platform.CLI.Commands { + /// + /// Root command for the Neo Platform CLI. + /// internal class ProgramRootCommand : RootCommand { + /// + /// Initializes the root command and registers subcommands. + /// public ProgramRootCommand() : base("Neo Platform Command-line Tool") { var showCommand = new ShowCommand(); diff --git a/src/Neo.Platform.CLI/Commands/ShowCommand.cs b/src/Neo.Platform.CLI/Commands/ShowCommand.cs index c1515fa..6d5ba5a 100644 --- a/src/Neo.Platform.CLI/Commands/ShowCommand.cs +++ b/src/Neo.Platform.CLI/Commands/ShowCommand.cs @@ -28,17 +28,33 @@ namespace Neo.Platform.CLI.Commands { + /// + /// CLI command that displays configuration samples and related information. + /// internal class ShowCommand : Command { + /// + /// Initializes the show command. + /// public ShowCommand() : base("show", "Shows many configurations") { } + /// + /// Handles the show command by writing sample configuration output. + /// + /// The factory used to create the command logger. public class Handler(ILoggerFactory loggerFactory) : AsynchronousCommandLineAction { private readonly ILogger _logger = loggerFactory.CreateLogger(); + /// + /// Invokes the show command handler. + /// + /// The parse result for the current command invocation. + /// A token that can cancel the operation. + /// A completed task with exit code 0. public override Task InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default) { _logger.LogInformation("Hello World!!!!"); diff --git a/src/Neo.Platform.Hosting/BlockchainStoreNames.cs b/src/Neo.Platform.Hosting/BlockchainStoreNames.cs index bc8c31d..0ebda99 100644 --- a/src/Neo.Platform.Hosting/BlockchainStoreNames.cs +++ b/src/Neo.Platform.Hosting/BlockchainStoreNames.cs @@ -24,16 +24,39 @@ namespace Neo.Platform.Hosting { + /// + /// Configuration key names for blockchain store and backup options. + /// public static class BlockchainStoreNames { + /// + /// The root configuration section for store settings. + /// public static readonly string SectionKey = "Store"; + /// + /// Configuration key for . + /// public static readonly string DatabasePathKey = $"{SectionKey}:{nameof(BlockchainStoreOptions.DatabasePath)}"; + + /// + /// Configuration key for . + /// public static readonly string CreateIfMissingKey = $"{SectionKey}:{nameof(BlockchainStoreOptions.CreateIfMissing)}"; + /// + /// Configuration section for store backup settings. + /// public static readonly string BackupSectionKey = $"{SectionKey}:Backup"; + /// + /// Configuration key for . + /// public static readonly string BackupPathKey = $"{BackupSectionKey}:{nameof(BlockchainBackupOptions.BackupPath)}"; + + /// + /// Configuration key for . + /// public static readonly string MaxBackupsKey = $"{BackupSectionKey}:{nameof(BlockchainBackupOptions.MaxBackups)}"; } } diff --git a/src/Neo.Platform.Hosting/Builder/PlatformCommandLineBuilder.cs b/src/Neo.Platform.Hosting/Builder/PlatformCommandLineBuilder.cs index 43876ef..dbb7d24 100644 --- a/src/Neo.Platform.Hosting/Builder/PlatformCommandLineBuilder.cs +++ b/src/Neo.Platform.Hosting/Builder/PlatformCommandLineBuilder.cs @@ -30,6 +30,16 @@ namespace Neo.Platform.Hosting.Builder { + /// + /// Fluent builder that wires System.CommandLine parsing with a Microsoft.Extensions host. + /// + /// + /// The root command to parse. When , a new is used. + /// + /// + /// Command-line arguments to parse when is called. + /// When , an empty argument list is used. + /// public class PlatformCommandLineBuilder(Command? rootCommand = default, string[]? args = default) { internal Command Command { get; } = rootCommand ?? new RootCommand(); @@ -42,6 +52,19 @@ public class PlatformCommandLineBuilder(Command? rootCommand = default, string[] internal void AddMiddleware(Action middleware) => _middleware.Add(middleware); + /// + /// Configures host creation middleware that runs after parsing. + /// + /// + /// Factory that creates an from unmatched tokens. + /// When , a new is created. + /// + /// Optional callback that further configures the host builder. + /// The same builder for chaining. + /// + /// The middleware stores the on the host builder properties, + /// registers it for DI, and enables . + /// public PlatformCommandLineBuilder UseHost( Func? hostBuilderFactory, Action? configure = default) @@ -68,24 +91,43 @@ public PlatformCommandLineBuilder UseHost( return this; } + /// + /// Enables or disables the default System.CommandLine exception handler. + /// + /// to enable the handler; otherwise, . + /// The same builder for chaining. public PlatformCommandLineBuilder EnableDefaultExceptionHandler(bool value = true) { InvocationConfiguration.EnableDefaultExceptionHandler = value; return this; } + /// + /// Sets the process termination timeout used during command invocation. + /// + /// The termination timeout. + /// The same builder for chaining. public PlatformCommandLineBuilder SetProcessTerminationTimeout(TimeSpan timeout) { InvocationConfiguration.ProcessTerminationTimeout = timeout; return this; } + /// + /// Enables or disables POSIX-style option bundling for the parser. + /// + /// to enable bundling; otherwise, . + /// The same builder for chaining. public PlatformCommandLineBuilder EnablePosixBundling(bool value = true) { ParserConfiguration.EnablePosixBundling = value; return this; } + /// + /// Parses the configured arguments, runs middleware, builds the host, and returns a runnable command line. + /// + /// A that can start the host and invoke the command. public PlatformCommandLine Build() { var parseResults = Command.Parse(args ?? [], ParserConfiguration); diff --git a/src/Neo.Platform.Hosting/Configuration/ConfigurationBuilderExtensions.cs b/src/Neo.Platform.Hosting/Configuration/ConfigurationBuilderExtensions.cs index ebffc0c..a7f7d03 100644 --- a/src/Neo.Platform.Hosting/Configuration/ConfigurationBuilderExtensions.cs +++ b/src/Neo.Platform.Hosting/Configuration/ConfigurationBuilderExtensions.cs @@ -25,8 +25,20 @@ namespace Neo.Platform.Hosting.Configuration { + /// + /// Extension methods for adding Neo platform configuration sources. + /// public static class ConfigurationBuilderExtensions { + /// + /// Adds Neo platform defaults and NEO_-prefixed environment variables + /// as a configuration source. + /// + /// The configuration builder. + /// + /// Optional key/value pairs that override platform defaults before environment variables are applied. + /// + /// The same for chaining. public static IConfigurationBuilder AddNeoPlatformConfiguration(this IConfigurationBuilder builder, IEnumerable>? initialData = default) { builder.Add(new NeoEnvironmentConfigurationSource() { InitialData = initialData, }); diff --git a/src/Neo.Platform.Hosting/Configuration/NeoEnvironmentConfigurationSource.cs b/src/Neo.Platform.Hosting/Configuration/NeoEnvironmentConfigurationSource.cs index 9f7a9be..2aac8b3 100644 --- a/src/Neo.Platform.Hosting/Configuration/NeoEnvironmentConfigurationSource.cs +++ b/src/Neo.Platform.Hosting/Configuration/NeoEnvironmentConfigurationSource.cs @@ -26,10 +26,21 @@ namespace Neo.Platform.Hosting.Configuration { + /// + /// Configuration source that supplies Neo environment-related settings. + /// internal class NeoEnvironmentConfigurationSource : IConfigurationSource { + /// + /// Optional initial key/value pairs merged into the configuration provider. + /// public IEnumerable>? InitialData { get; set; } + /// + /// Builds a configuration provider for this source. + /// + /// The configuration builder. + /// A instance. public IConfigurationProvider Build(IConfigurationBuilder builder) => new NeoEnvironmentConfigurationProvider(this); } diff --git a/src/Neo.Platform.Hosting/Factory/NeoHostingFactory.cs b/src/Neo.Platform.Hosting/Factory/NeoHostingFactory.cs index e9256c1..ad0df28 100644 --- a/src/Neo.Platform.Hosting/Factory/NeoHostingFactory.cs +++ b/src/Neo.Platform.Hosting/Factory/NeoHostingFactory.cs @@ -25,14 +25,28 @@ namespace Neo.Platform.Hosting.Factory { + /// + /// Helper factory methods used when constructing Neo hosting environments. + /// internal static class NeoHostingFactory { private static readonly uint s_networkSeed = 810960196u; // DEV0 Magic Code private static readonly string s_globalString = "Global\\"; + /// + /// Computes a development network magic value for the given index. + /// + /// Development network index embedded in the high nibble of the magic. + /// The derived network magic number. public static uint GetDevNetwork(uint index) => s_networkSeed & ~(0xfu << 24) | (index << 24); + /// + /// Resolves a file path relative to a root directory when the path is not rooted. + /// + /// File name or path to resolve. + /// Root directory used when is relative. + /// A for the resolved path. public static FileInfo ResolveFileName(string filename, string rootPath) { if (Path.IsPathRooted(filename)) @@ -42,6 +56,11 @@ public static FileInfo ResolveFileName(string filename, string rootPath) return new(Path.Combine(rootPath, filename)); } + /// + /// Creates a named system mutex, generating a global random name when none is supplied. + /// + /// Optional mutex name; when null or empty a random global name is used. + /// A newly created that is initially owned. public static Mutex CreateMutex(string? name = default) { if (string.IsNullOrEmpty(name)) diff --git a/src/Neo.Platform.Hosting/HostEnvironmentExtensions.cs b/src/Neo.Platform.Hosting/HostEnvironmentExtensions.cs index 2adefa1..1701496 100644 --- a/src/Neo.Platform.Hosting/HostEnvironmentExtensions.cs +++ b/src/Neo.Platform.Hosting/HostEnvironmentExtensions.cs @@ -25,26 +25,53 @@ namespace Neo.Platform.Hosting { + /// + /// Extension methods for checking Neo hosting environment names. + /// public static class HostEnvironmentExtensions { + /// + /// Determines whether the host is running in the localnet environment. + /// + /// The host environment. + /// if the environment is localnet; otherwise, . + /// is . public static bool IsLocalNet(this IHostEnvironment hostEnvironment) { ArgumentNullException.ThrowIfNull(hostEnvironment, nameof(hostEnvironment)); return hostEnvironment.IsEnvironment(NeoHostingEnvironmentNames.LocalNet); } + /// + /// Determines whether the host is running in the testnet environment. + /// + /// The host environment. + /// if the environment is testnet; otherwise, . + /// is . public static bool IsTestNet(this IHostEnvironment hostEnvironment) { ArgumentNullException.ThrowIfNull(hostEnvironment, nameof(hostEnvironment)); return hostEnvironment.IsEnvironment(NeoHostingEnvironmentNames.TestNet); } + /// + /// Determines whether the host is running in the mainnet environment. + /// + /// The host environment. + /// if the environment is mainnet; otherwise, . + /// is . public static bool IsMainNet(this IHostEnvironment hostEnvironment) { ArgumentNullException.ThrowIfNull(hostEnvironment, nameof(hostEnvironment)); return hostEnvironment.IsEnvironment(NeoHostingEnvironmentNames.MainNet); } + /// + /// Determines whether the host is running in the privatenet environment. + /// + /// The host environment. + /// if the environment is privatenet; otherwise, . + /// is . public static bool IsPrivateNet(this IHostEnvironment hostEnvironment) { ArgumentNullException.ThrowIfNull(hostEnvironment, nameof(hostEnvironment)); diff --git a/src/Neo.Platform.Hosting/HostingExtensions.cs b/src/Neo.Platform.Hosting/HostingExtensions.cs index de711ab..4ed5f65 100644 --- a/src/Neo.Platform.Hosting/HostingExtensions.cs +++ b/src/Neo.Platform.Hosting/HostingExtensions.cs @@ -38,9 +38,22 @@ namespace Neo.Platform.Hosting { + /// + /// Extension methods that configure Neo platform hosting defaults on . + /// public static class HostingExtensions { - + /// + /// Registers a command action for a matching System.CommandLine command type. + /// + /// The command type expected on the parse result. + /// The action type resolved from DI and assigned to the command. + /// The host builder. + /// The same for chaining. + /// + /// Requires a in whose + /// selected command is exactly of type . + /// public static IHostBuilder UseCommandAction(this IHostBuilder builder) where TCommand : Command where TCommandAction : CommandLineAction @@ -61,6 +74,16 @@ parseResult.CommandResult.Command is Command command && return builder; } + /// + /// Configures Neo platform host defaults for configuration, logging, lifetime, options, and type converters. + /// + /// The host builder. + /// The same for chaining. + /// + /// Loads JSON configuration from the application base and content root (including environment-specific files), + /// registers Neo platform logging, enables development service-provider validation in Development, + /// and applies and . + /// public static IHostBuilder UseNeoPlatformConfiguration(this IHostBuilder builder) { // Host Configuration @@ -163,6 +186,12 @@ public static IHostBuilder UseNeoPlatformConfiguration(this IHostBuilder builder return builder; } + /// + /// Binds protocol settings from configuration and registers them as a singleton + /// service. + /// + /// The host builder. + /// The same for chaining. public static IHostBuilder UseNeoPlatformOptions(this IHostBuilder builder) => builder.ConfigureServices ( @@ -174,6 +203,12 @@ public static IHostBuilder UseNeoPlatformOptions(this IHostBuilder builder) => } ); + /// + /// Registers as the host lifetime and optionally configures its options. + /// + /// The host builder. + /// Optional callback that configures . + /// The same for chaining. public static IHostBuilder UseNeoPlatformLifetime(this IHostBuilder builder, Action? configure = default) => builder.ConfigureServices( services => diff --git a/src/Neo.Platform.Hosting/Logging/LoggingBuilderExtensions.cs b/src/Neo.Platform.Hosting/Logging/LoggingBuilderExtensions.cs index 201c77d..bffde18 100644 --- a/src/Neo.Platform.Hosting/Logging/LoggingBuilderExtensions.cs +++ b/src/Neo.Platform.Hosting/Logging/LoggingBuilderExtensions.cs @@ -28,8 +28,16 @@ namespace Neo.Platform.Hosting.Logging { + /// + /// Extension methods for registering the Neo platform console logger. + /// public static class LoggingBuilderExtensions { + /// + /// Adds the Neo platform logger provider to the logging pipeline. + /// + /// The logging builder. + /// The same for chaining. public static ILoggingBuilder AddNeoPlatform(this ILoggingBuilder builder) { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); @@ -39,6 +47,12 @@ public static ILoggingBuilder AddNeoPlatform(this ILoggingBuilder builder) return builder; } + /// + /// Adds the Neo platform logger provider and configures its options. + /// + /// The logging builder. + /// A callback that configures . + /// The same for chaining. public static ILoggingBuilder AddNeoPlatform(this ILoggingBuilder builder, Action configure) { builder.AddNeoPlatform(); diff --git a/src/Neo.Platform.Hosting/Logging/NeoPlatformLogger.cs b/src/Neo.Platform.Hosting/Logging/NeoPlatformLogger.cs index 7272c7e..57447a6 100644 --- a/src/Neo.Platform.Hosting/Logging/NeoPlatformLogger.cs +++ b/src/Neo.Platform.Hosting/Logging/NeoPlatformLogger.cs @@ -26,6 +26,9 @@ namespace Neo.Platform.Hosting.Logging { + /// + /// Console logger implementation used by the Neo platform host. + /// internal sealed class NeoPlatformLogger( string name, Func config) : ILogger @@ -36,12 +39,15 @@ internal sealed class NeoPlatformLogger( private readonly string _name = name; private readonly Func _getConfig = config; + /// public IDisposable? BeginScope(TState state) where TState : notnull => default!; + /// public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; + /// public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (IsEnabled(logLevel) == false) @@ -90,12 +96,19 @@ private void WriteDateTime(bool isError = false) Write(format, DateTimeNow); } + /// + /// Writes a formatted message to standard output without a trailing newline. + /// public static void Write([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params object?[] args) { var message = string.Format(format, args); Console.Out.Write(message); } + + /// + /// Writes a formatted message to standard error without a trailing newline. + /// public static void ErrorWrite([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params object?[] args) { var message = string.Format(format, args); @@ -103,18 +116,33 @@ public static void ErrorWrite([StringSyntax(StringSyntaxAttribute.CompositeForma Console.Error.Write(message); } + /// + /// Writes a newline to standard output. + /// public static void WriteLine() => Write(Environment.NewLine); + /// + /// Writes a formatted message to standard output followed by a newline. + /// public static void WriteLine([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params object?[] args) => Write(string.Format(format, args) + Environment.NewLine); + /// + /// Writes a newline to standard error. + /// public static void ErrorWriteLine() => ErrorWrite(Environment.NewLine); + /// + /// Writes a formatted message to standard error followed by a newline. + /// public static void ErrorWriteLine([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params object?[] args) => ErrorWrite(string.Format(format, args) + Environment.NewLine); + /// + /// Writes an information-level message with platform coloring. + /// public void InfoMessage([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params object?[] args) { WriteDateTime(); @@ -125,6 +153,9 @@ public void InfoMessage([StringSyntax(StringSyntaxAttribute.CompositeFormat)] st ResetColor(); } + /// + /// Writes a warning-level message with platform coloring. + /// public void WarningMessage([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params object?[] args) { WriteDateTime(); @@ -135,6 +166,9 @@ public void WarningMessage([StringSyntax(StringSyntaxAttribute.CompositeFormat)] ResetColor(); } + /// + /// Writes a debug-level message with platform coloring. + /// public void DebugMessage([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params object?[] args) { SetTerminalForegroundColor(ConsoleColor.DarkGray); @@ -144,6 +178,9 @@ public void DebugMessage([StringSyntax(StringSyntaxAttribute.CompositeFormat)] s ResetColor(); } + /// + /// Writes a trace-level message with platform coloring. + /// public void TraceMessage([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params object?[] args) { SetTerminalForegroundColor(ConsoleColor.DarkGray); @@ -153,6 +190,11 @@ public void TraceMessage([StringSyntax(StringSyntaxAttribute.CompositeFormat)] s ResetColor(); } + /// + /// Writes exception details to standard error, optionally including a stack trace. + /// + /// Exception to report. + /// When , includes stack trace output. public void ErrorMessage(Exception exception, bool showStackTrace = true) { var stackTrace = exception.InnerException?.StackTrace ?? exception.StackTrace; @@ -174,6 +216,9 @@ public void ErrorMessage(Exception exception, bool showStackTrace = true) ResetColor(); } + /// + /// Writes an error-level formatted message to standard error. + /// public void ErrorMessage([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params object?[] args) { SetTerminalForegroundColor(ConsoleColor.Red); @@ -185,16 +230,25 @@ public void ErrorMessage([StringSyntax(StringSyntaxAttribute.CompositeFormat)] s ResetColor(); } + /// + /// Sets the console foreground color. + /// public static void SetTerminalForegroundColor(ConsoleColor consoleColor) { Console.ForegroundColor = consoleColor; } + /// + /// Sets the console background color. + /// public static void SetTerminalBackgroundColor(ConsoleColor consoleColor) { Console.BackgroundColor = consoleColor; } + /// + /// Resets console colors to the default. + /// public static void ResetColor() { Console.ResetColor(); diff --git a/src/Neo.Platform.Hosting/Logging/NeoPlatformLoggerOptions.cs b/src/Neo.Platform.Hosting/Logging/NeoPlatformLoggerOptions.cs index 90672d6..6050a93 100644 --- a/src/Neo.Platform.Hosting/Logging/NeoPlatformLoggerOptions.cs +++ b/src/Neo.Platform.Hosting/Logging/NeoPlatformLoggerOptions.cs @@ -24,15 +24,33 @@ namespace Neo.Platform.Hosting.Logging { + /// + /// Options that control Neo platform console log formatting. + /// public sealed class NeoPlatformLoggerOptions { + /// + /// The default timestamp format string used by . + /// public const string DefaultDateTimeFormatString = "yyyy-MM-dd HH:mm:ss.ffff"; + /// + /// Gets or sets a value indicating whether log timestamps use UTC. + /// Defaults to . + /// public bool UseUtcTimestamp { get; set; } = true; + /// + /// Gets or sets the .NET date/time format string used for log timestamps. + /// Defaults to . + /// [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] public string TimestampFormat { get; set; } = DefaultDateTimeFormatString; + /// + /// Gets or sets a value indicating whether exception stack traces are written to the console. + /// Defaults to . + /// public bool ShowExceptionStackTrace { get; set; } = true; } } diff --git a/src/Neo.Platform.Hosting/Logging/NeoPlatformLoggerProvider.cs b/src/Neo.Platform.Hosting/Logging/NeoPlatformLoggerProvider.cs index 6882d01..c109d02 100644 --- a/src/Neo.Platform.Hosting/Logging/NeoPlatformLoggerProvider.cs +++ b/src/Neo.Platform.Hosting/Logging/NeoPlatformLoggerProvider.cs @@ -28,6 +28,9 @@ namespace Neo.Platform.Hosting.Logging { + /// + /// that creates instances. + /// [UnsupportedOSPlatform("browser")] [ProviderAlias("NeoPlatform")] internal sealed class NeoPlatformLoggerProvider : ILoggerProvider @@ -37,6 +40,10 @@ internal sealed class NeoPlatformLoggerProvider : ILoggerProvider private NeoPlatformLoggerOptions _currentConfig; + /// + /// Initializes a new provider that tracks option changes from the monitor. + /// + /// Options monitor for logger configuration. public NeoPlatformLoggerProvider( IOptionsMonitor config) { @@ -44,6 +51,11 @@ public NeoPlatformLoggerProvider( _onChangeToken = config.OnChange(updatedConfig => _currentConfig = updatedConfig); } + /// + /// Creates or returns a cached logger for the specified category. + /// + /// Logger category name. + /// An for the category. public ILogger CreateLogger(string categoryName) => _loggers.GetOrAdd( categoryName, @@ -54,6 +66,9 @@ public ILogger CreateLogger(string categoryName) => private NeoPlatformLoggerOptions GetCurrentConfig() => _currentConfig; + /// + /// Clears cached loggers and disposes option change subscriptions. + /// public void Dispose() { _loggers.Clear(); diff --git a/src/Neo.Platform.Hosting/Middleware/CommandLineMiddleware.cs b/src/Neo.Platform.Hosting/Middleware/CommandLineMiddleware.cs index 53204f4..16910e2 100644 --- a/src/Neo.Platform.Hosting/Middleware/CommandLineMiddleware.cs +++ b/src/Neo.Platform.Hosting/Middleware/CommandLineMiddleware.cs @@ -24,5 +24,10 @@ namespace Neo.Platform.Hosting.Middleware { + /// + /// Continues the middleware chain + /// with the current . + /// + /// The parse result to pass to the next middleware. public delegate void PlatformCommandLineNextDelegate(ParseResult parseResult); } diff --git a/src/Neo.Platform.Hosting/NeoHostingEnvironments.cs b/src/Neo.Platform.Hosting/NeoHostingEnvironments.cs index f234eb9..e0942a1 100644 --- a/src/Neo.Platform.Hosting/NeoHostingEnvironments.cs +++ b/src/Neo.Platform.Hosting/NeoHostingEnvironments.cs @@ -22,14 +22,29 @@ namespace Neo.Platform.Hosting { + /// + /// Well-known hosting environment name strings for Neo platform hosts. + /// internal static class NeoHostingEnvironmentNames { + /// + /// Local development network environment name. + /// public static readonly string LocalNet = "localnet"; + /// + /// Public test network environment name. + /// public static readonly string TestNet = "testnet"; + /// + /// Public main network environment name. + /// public static readonly string MainNet = "mainnet"; + /// + /// Private network environment name. + /// public static readonly string PrivateNet = "privatenet"; } diff --git a/src/Neo.Platform.Hosting/NeoPlatformLifetime.cs b/src/Neo.Platform.Hosting/NeoPlatformLifetime.cs index e8c02ad..710e236 100644 --- a/src/Neo.Platform.Hosting/NeoPlatformLifetime.cs +++ b/src/Neo.Platform.Hosting/NeoPlatformLifetime.cs @@ -30,16 +30,34 @@ namespace Neo.Platform.Hosting { + /// + /// Host lifetime that logs Neo platform start and shutdown status messages. + /// + /// Lifetime options, including whether status messages are suppressed. + /// The current hosting environment. + /// Application lifetime notifications used to log status. + /// Optional logger factory. When omitted, a null logger is used. public class NeoPlatformLifetime( IOptions options, IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory? loggerFactory = default) : IHostLifetime, IDisposable { + /// + /// Gets the configured lifetime options. + /// public NeoPlatformLifetimeOptions Options { get; } = options?.Value ?? throw new ArgumentNullException(nameof(options)); + + /// + /// Gets the hosting environment. + /// public IHostEnvironment Environment { get; } = environment ?? throw new ArgumentNullException(nameof(environment)); + + /// + /// Gets the application lifetime used for start and stop notifications. + /// public IHostApplicationLifetime ApplicationLifetime { get; } = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime)); @@ -49,6 +67,9 @@ public class NeoPlatformLifetime( private readonly ILogger _logger = (loggerFactory ?? NullLoggerFactory.Instance) .CreateLogger("Neo.Platform.Hosting.Lifetime"); + /// + /// Releases registrations for application started and stopping callbacks. + /// public void Dispose() { _appStartedReg.Dispose(); @@ -57,12 +78,22 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Called when the host is stopping. This implementation performs no work. + /// + /// A token that can cancel the stop operation. + /// A completed task. public Task StopAsync(CancellationToken cancellationToken) { // Nothing to do return Task.CompletedTask; } + /// + /// Registers status logging for application start and stop when messages are not suppressed. + /// + /// A token that can cancel waiting for start. + /// A completed task. public Task WaitForStartAsync(CancellationToken cancellationToken) { if (Options.SuppressStatusMessages == false) diff --git a/src/Neo.Platform.Hosting/NeoPlatformLifetimeOptions.cs b/src/Neo.Platform.Hosting/NeoPlatformLifetimeOptions.cs index 9a3e252..aeb9404 100644 --- a/src/Neo.Platform.Hosting/NeoPlatformLifetimeOptions.cs +++ b/src/Neo.Platform.Hosting/NeoPlatformLifetimeOptions.cs @@ -24,6 +24,13 @@ namespace Neo.Platform.Hosting { + /// + /// Options that configure . + /// + /// + /// Inherits console lifetime settings such as status message suppression from + /// . + /// public class NeoPlatformLifetimeOptions : ConsoleLifetimeOptions { } diff --git a/src/Neo.Platform.Hosting/PlatformCommandLine.cs b/src/Neo.Platform.Hosting/PlatformCommandLine.cs index 953c3b9..36cf5b4 100644 --- a/src/Neo.Platform.Hosting/PlatformCommandLine.cs +++ b/src/Neo.Platform.Hosting/PlatformCommandLine.cs @@ -28,8 +28,22 @@ namespace Neo.Platform.Hosting { + /// + /// Runs a System.CommandLine parse result against a configured . + /// + /// The parsed command-line result to invoke. + /// The host that supplies services for the application lifetime. + /// Invocation settings for System.CommandLine. public sealed class PlatformCommandLine(ParseResult parseResult, IHost host, InvocationConfiguration invocationConfiguration) { + /// + /// Starts the host, invokes the command-line action, then stops the host. + /// + /// A token that can cancel host start, command invocation, or host stop. + /// + /// The process exit code from the command invocation, or the exception + /// if invocation fails. + /// public async Task InvokeAsync(CancellationToken cancellationToken = default) { await host.StartAsync(cancellationToken); diff --git a/src/Neo.Platform.Hosting/ProtocolSettingNames.cs b/src/Neo.Platform.Hosting/ProtocolSettingNames.cs index a34e65b..302fd67 100644 --- a/src/Neo.Platform.Hosting/ProtocolSettingNames.cs +++ b/src/Neo.Platform.Hosting/ProtocolSettingNames.cs @@ -24,20 +24,69 @@ namespace Neo.Platform.Hosting { + /// + /// Configuration key names for Neo protocol settings. + /// public static class ProtocolSettingNames { + /// + /// The root configuration section for protocol settings. + /// public static readonly string SectionKey = "Protocol"; + /// + /// Configuration key for . + /// public static readonly string NetworkKey = $"{SectionKey}:{nameof(ProtocolSettings.Network)}"; + + /// + /// Configuration key for . + /// public static readonly string AddressVersionKey = $"{SectionKey}:{nameof(ProtocolSettings.AddressVersion)}"; + + /// + /// Configuration key for . + /// public static readonly string StandbyCommitteeKey = $"{SectionKey}:{nameof(ProtocolSettings.StandbyCommittee)}"; + + /// + /// Configuration key for . + /// public static readonly string ValidatorsCountKey = $"{SectionKey}:{nameof(ProtocolSettings.ValidatorsCount)}"; + + /// + /// Configuration key for . + /// public static readonly string HardForksKey = $"{SectionKey}:{nameof(ProtocolSettings.HardForks)}"; + + /// + /// Configuration key for . + /// public static readonly string SeedListKey = $"{SectionKey}:{nameof(ProtocolSettings.SeedList)}"; + + /// + /// Configuration key for . + /// public static readonly string MillisecondsPerBlockKey = $"{SectionKey}:{nameof(ProtocolSettings.MillisecondsPerBlock)}"; + + /// + /// Configuration key for . + /// public static readonly string MaxTransactionsPerBlockKey = $"{SectionKey}:{nameof(ProtocolSettings.MaxTransactionsPerBlock)}"; + + /// + /// Configuration key for . + /// public static readonly string MemoryPoolMaxTransactionsKey = $"{SectionKey}:{nameof(ProtocolSettings.MemoryPoolMaxTransactions)}"; + + /// + /// Configuration key for . + /// public static readonly string MaxTraceableBlocksKey = $"{SectionKey}:{nameof(ProtocolSettings.MaxTraceableBlocks)}"; + + /// + /// Configuration key for . + /// public static readonly string InitialGasDistributionKey = $"{SectionKey}:{nameof(ProtocolSettings.InitialGasDistribution)}"; } } diff --git a/src/Neo.Platform.Storage/BlockchainStore.cs b/src/Neo.Platform.Storage/BlockchainStore.cs index 2f20416..c0cd2e1 100644 --- a/src/Neo.Platform.Storage/BlockchainStore.cs +++ b/src/Neo.Platform.Storage/BlockchainStore.cs @@ -35,10 +35,19 @@ namespace Neo.Platform.Storage { + /// + /// RocksDB-backed blockchain key/value store implementing . + /// public sealed class BlockchainStore : IEnumerable>, IStore { + /// + /// Gets the store options used to open the database. + /// public BlockchainStoreOptions StoreOptions => _storeOptions.Value; + /// + /// Gets the backup options used when creating backups. + /// public BlockchainBackupOptions BackupOptions => _backupOptions.Value; internal RocksDb Database => _db; @@ -58,6 +67,12 @@ public sealed class BlockchainStore : IEnumerable>, private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; + /// + /// Opens or creates a RocksDB database at the path specified by . + /// + /// Store options, including the database path. + /// Optional backup options. When omitted, default backup options are used. + /// Optional logger factory. When omitted, a null logger is used. public BlockchainStore( IOptions options, IOptions? backupOptions = default, @@ -128,6 +143,9 @@ public BlockchainStore( _db = RocksDb.Open(dbOptions, _storeOptions.Value.DatabasePath, s_columnFamilies); } + /// + /// Releases the underlying RocksDB database and related native resources. + /// public void Dispose() { _db.Dispose(); @@ -135,6 +153,13 @@ public void Dispose() _bloomFilter.Dispose(); } + /// + /// Creates a RocksDB checkpoint in the specified directory. + /// + /// The directory that will receive the checkpoint. + /// + /// is or empty. + /// public void CreateCheckpoint(string checkpointDirectory) { if (string.IsNullOrEmpty(checkpointDirectory)) @@ -149,9 +174,17 @@ public void CreateCheckpoint(string checkpointDirectory) _logger.LogCheckpointMessage(logLevel, $"Created checkpoint: \'{checkpointDirectory}\'"); } + /// + /// Creates a backup handle for this store. + /// + /// An bound to this database. public IStoreBackup CreateBackup() => new BlockchainStoreBackup(_db, _backupOptions, _loggerFactory); + /// + /// Creates a consistent snapshot of the current store state. + /// + /// An for transactional reads and writes. public IStoreSnapshot CreateSnapshot() { var logLevel = LogLevel.Debug; @@ -162,6 +195,11 @@ public IStoreSnapshot CreateSnapshot() return new BlockchainStoreSnapshot(this, _db, _loggerFactory); } + /// + /// Writes under . + /// + /// The storage key. + /// The value to store. public void Put(ReadOnlySpan key, ReadOnlySpan value) { _db.Put(key, value); @@ -172,6 +210,10 @@ public void Put(ReadOnlySpan key, ReadOnlySpan value) _logger.LogWriteMessage(logLevel, $"Put key: 0x{Convert.ToHexStringLower(key)} value: 0x{Convert.ToHexStringLower(value)}"); } + /// + /// Deletes the value associated with if it may exist. + /// + /// The storage key to delete. public void Delete(ReadOnlySpan key) { if (_db.KeyMayExist(key)) @@ -185,6 +227,11 @@ public void Delete(ReadOnlySpan key) } } + /// + /// Determines whether is present in the store. + /// + /// The storage key. + /// if the key exists; otherwise, . public bool ContainsKey(ReadOnlySpan key) { if (_db.KeyMayExist(key)) @@ -192,6 +239,11 @@ public bool ContainsKey(ReadOnlySpan key) return false; } + /// + /// Gets the value for , or if it does not exist. + /// + /// The storage key. + /// The stored value, or when missing. public byte[]? Get(ReadOnlySpan key) { if (_db.KeyMayExist(key)) @@ -210,6 +262,15 @@ public bool ContainsKey(ReadOnlySpan key) return default; } + /// + /// Attempts to get the value for . + /// + /// The storage key. + /// + /// When this method returns , contains the stored value; + /// otherwise, . + /// + /// if the key was found; otherwise, . public bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value) { if (_db.KeyMayExist(key)) @@ -232,6 +293,14 @@ public bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value return false; } + /// + /// Iterates key/value pairs starting at or before . + /// + /// The seek key or prefix. + /// + /// When , iterates backward; otherwise, forward. + /// + /// An enumerable of key/value pairs in seek order. public IEnumerable> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false) { using var iter = _db.NewIterator(); @@ -255,6 +324,10 @@ public IEnumerable> Seek(ReadOnlyMemory keyOr } } + /// + /// Returns a non-generic enumerator over all key/value pairs. + /// + /// An enumerator for the store contents. public IEnumerator GetEnumerator() => GetEnumerator(); diff --git a/src/Neo.Platform.Storage/BlockchainStoreBackup.cs b/src/Neo.Platform.Storage/BlockchainStoreBackup.cs index 1ae73f4..0826d9f 100644 --- a/src/Neo.Platform.Storage/BlockchainStoreBackup.cs +++ b/src/Neo.Platform.Storage/BlockchainStoreBackup.cs @@ -33,8 +33,14 @@ namespace Neo.Platform.Storage { + /// + /// RocksDB backup engine wrapper that implements . + /// public class BlockchainStoreBackup : IStoreBackup { + /// + /// Gets the backup options used by this instance. + /// public BlockchainBackupOptions BackupOptions => _backupOptions; private readonly BlockchainBackupOptions _backupOptions; @@ -45,6 +51,12 @@ public class BlockchainStoreBackup : IStoreBackup private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; + /// + /// Initializes a new backup engine for the specified database. + /// + /// The open RocksDB instance to back up. + /// Backup options, including the backup directory path. + /// Optional logger factory. When omitted, a null logger is used. public BlockchainStoreBackup( RocksDb db, IOptions options, @@ -63,12 +75,18 @@ public BlockchainStoreBackup( _backupEngine = BackupEngine.Open(new(), _backupOptions.BackupPath); } + /// + /// Releases the underlying RocksDB backup engine. + /// public void Dispose() { _backupEngine.Dispose(); GC.SuppressFinalize(this); } + /// + /// Creates a new backup of the database. + /// public void Backup() { _backupEngine.CreateNewBackup(_db); @@ -79,6 +97,19 @@ public void Backup() _logger.LogBackupMessage(logLevel, $"Created new backup of the store."); } + /// + /// Restores the latest backup to the specified path. + /// + /// The directory that will receive the restored database. + /// + /// Optional WAL directory. When omitted, is used. + /// + /// + /// is or empty. + /// + /// + /// or does not exist. + /// public void Restore(string restorePath, string? walPath = default) { if (string.IsNullOrEmpty(restorePath)) @@ -99,9 +130,17 @@ public void Restore(string restorePath, string? walPath = default) _logger.LogRestoreMessage(logLevel, $"Restored latest backup to path \'{restorePath}\'."); } + /// + /// Lists available backups managed by the backup engine. + /// + /// An enumerable of backup metadata. public IEnumerable ListBackups() => _backupEngine.AsEnumerable(); + /// + /// Deletes older backups, keeping only the newest backups up to the specified count. + /// + /// The number of most recent backups to retain. public void Purge(uint numberOfBackupsToKeep) { _backupEngine.PurgeOldBackups(numberOfBackupsToKeep); diff --git a/src/Neo.Platform.Storage/BlockchainStoreSnapshot.cs b/src/Neo.Platform.Storage/BlockchainStoreSnapshot.cs index 93d5d5f..4640d98 100644 --- a/src/Neo.Platform.Storage/BlockchainStoreSnapshot.cs +++ b/src/Neo.Platform.Storage/BlockchainStoreSnapshot.cs @@ -32,8 +32,14 @@ namespace Neo.Platform.Storage { + /// + /// Point-in-time RocksDB snapshot that stages writes in a batch until . + /// public class BlockchainStoreSnapshot : IEnumerable>, IStoreSnapshot { + /// + /// Gets the store that produced this snapshot. + /// public IStore Store => _store; private readonly IStore _store; @@ -45,6 +51,12 @@ public class BlockchainStoreSnapshot : IEnumerable> private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; + /// + /// Creates a new snapshot over the given store and RocksDB instance. + /// + /// The parent store associated with this snapshot. + /// The open RocksDB database. + /// Optional logger factory. When omitted, a null logger is used. public BlockchainStoreSnapshot( IStore store, RocksDb db, @@ -61,6 +73,9 @@ public BlockchainStoreSnapshot( _writeBatch = new(); } + /// + /// Releases the RocksDB snapshot, read options, and write batch. + /// public void Dispose() { _snapshot.Dispose(); @@ -69,6 +84,9 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Applies all staged write-batch changes to the database. + /// public void Commit() { var logLevel = LogLevel.Debug; @@ -79,6 +97,11 @@ public void Commit() _db.Write(_writeBatch); } + /// + /// Determines whether is present as of this snapshot. + /// + /// The storage key. + /// if the key exists; otherwise, . public bool ContainsKey(ReadOnlySpan key) { if (_db.KeyMayExist(key)) @@ -86,12 +109,24 @@ public bool ContainsKey(ReadOnlySpan key) return false; } + /// + /// Creates a backup handle for the underlying store. + /// + /// An from the parent store. public IStoreBackup CreateBackup() => _store.CreateBackup(); + /// + /// Creates a checkpoint of the underlying store. + /// + /// The directory that will receive the checkpoint. public void CreateCheckpoint(string checkpointDirectory) => _store.CreateCheckpoint(checkpointDirectory); + /// + /// Creates a nested snapshot over the same database. + /// + /// A new . public IStoreSnapshot CreateSnapshot() { var logLevel = LogLevel.Debug; @@ -102,6 +137,10 @@ public IStoreSnapshot CreateSnapshot() return new BlockchainStoreSnapshot(this, _db, _loggerFactory); } + /// + /// Stages deletion of in the write batch when the key may exist. + /// + /// The storage key to delete. public void Delete(ReadOnlySpan key) { if (_db.KeyMayExist(key)) @@ -115,6 +154,11 @@ public void Delete(ReadOnlySpan key) } } + /// + /// Gets the value for as of this snapshot, or if missing. + /// + /// The storage key. + /// The stored value, or when missing. public byte[]? Get(ReadOnlySpan key) { if (_db.KeyMayExist(key)) @@ -133,6 +177,10 @@ public void Delete(ReadOnlySpan key) return default; } + /// + /// Enumerates all key/value pairs visible in this snapshot. + /// + /// An enumerator over snapshot contents. public IEnumerator> GetEnumerator() { using var iter = _db.NewIterator(_readOptions); @@ -140,6 +188,11 @@ public IEnumerator> GetEnumerator() yield return new(iter.KeyToArray(), iter.ValueToArray()); } + /// + /// Stages a put of under in the write batch. + /// + /// The storage key. + /// The value to store. public void Put(ReadOnlySpan key, ReadOnlySpan value) { _writeBatch.Put(key, value); @@ -150,6 +203,14 @@ public void Put(ReadOnlySpan key, ReadOnlySpan value) _logger.LogWriteMessage(logLevel, $"Put key: {Convert.ToHexStringLower(key)} value: {Convert.ToHexStringLower(value)}"); } + /// + /// Iterates key/value pairs from this snapshot starting at or before . + /// + /// The seek key or prefix. + /// + /// When , iterates backward; otherwise, forward. + /// + /// An enumerable of key/value pairs in seek order. public IEnumerable> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false) { using var iter = _db.NewIterator(_readOptions); @@ -173,6 +234,15 @@ public IEnumerable> Seek(ReadOnlyMemory keyOr } } + /// + /// Attempts to get the value for as of this snapshot. + /// + /// The storage key. + /// + /// When this method returns , contains the stored value; + /// otherwise, . + /// + /// if the key was found; otherwise, . public bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value) { if (_db.KeyMayExist(key)) diff --git a/src/Neo.Platform.Storage/ColumnFamilyNames.cs b/src/Neo.Platform.Storage/ColumnFamilyNames.cs index a3dcb72..411d6ba 100644 --- a/src/Neo.Platform.Storage/ColumnFamilyNames.cs +++ b/src/Neo.Platform.Storage/ColumnFamilyNames.cs @@ -22,8 +22,14 @@ namespace Neo.Platform.Storage { + /// + /// Well-known RocksDB column family names used by the blockchain store. + /// public static class ColumnFamilyNames { + /// + /// The default column family name. + /// public static readonly string Default = "default"; } } diff --git a/src/Neo.Platform.Storage/Extensions/StorageKeyBuilderExtensions.cs b/src/Neo.Platform.Storage/Extensions/StorageKeyBuilderExtensions.cs index 4fec16f..ff8e251 100644 --- a/src/Neo.Platform.Storage/Extensions/StorageKeyBuilderExtensions.cs +++ b/src/Neo.Platform.Storage/Extensions/StorageKeyBuilderExtensions.cs @@ -25,11 +25,27 @@ namespace Neo.Platform.Storage.Extensions { + /// + /// Extension methods that append values to a . + /// public static class StorageKeyBuilderExtensions { + /// + /// Serializes and appends the resulting bytes to the builder. + /// + /// The key builder. + /// The serializable value to append. + /// The same for chaining. public static StorageKeyBuilder Append(this StorageKeyBuilder builder, INeoSerializable data) => builder.Append(data.ToArray()); + /// + /// Appends a prefix byte, then serializes and appends . + /// + /// The key builder. + /// The prefix byte to append first. + /// The serializable value to append after the prefix. + /// The same for chaining. public static StorageKeyBuilder Append(this StorageKeyBuilder builder, byte prefix, INeoSerializable data) => builder.Append(prefix).Append(data.ToArray()); } diff --git a/src/Neo.Platform.Storage/Logging/LoggingExtensions.cs b/src/Neo.Platform.Storage/Logging/LoggingExtensions.cs index adb23de..dc3a012 100644 --- a/src/Neo.Platform.Storage/Logging/LoggingExtensions.cs +++ b/src/Neo.Platform.Storage/Logging/LoggingExtensions.cs @@ -25,8 +25,14 @@ namespace Neo.Platform.Storage.Logging { + /// + /// Source-generated logging helpers for blockchain store operations. + /// internal static partial class LoggerExtensions { + /// + /// Logs a store fault message. + /// [LoggerMessage( EventId = StoreEventId.Fault, EventName = nameof(StoreEventId.Fault), @@ -38,6 +44,9 @@ public static partial void LogFaultMessage( string message ); + /// + /// Logs a store fault with an exception. + /// [LoggerMessage( EventId = StoreEventId.Fault, Message = "" @@ -48,6 +57,9 @@ public static partial void LogFaultMessage( Exception exception ); + /// + /// Logs a store read message. + /// [LoggerMessage( EventId = StoreEventId.Read, EventName = nameof(StoreEventId.Read), @@ -59,6 +71,9 @@ public static partial void LogReadMessage( string message ); + /// + /// Logs a store write message. + /// [LoggerMessage( EventId = StoreEventId.Write, EventName = nameof(StoreEventId.Write), @@ -70,6 +85,9 @@ public static partial void LogWriteMessage( string message ); + /// + /// Logs a store delete message. + /// [LoggerMessage( EventId = StoreEventId.Delete, EventName = nameof(StoreEventId.Delete), @@ -81,6 +99,9 @@ public static partial void LogDeleteMessage( string message ); + /// + /// Logs a store snapshot message. + /// [LoggerMessage( EventId = StoreEventId.Snapshot, EventName = nameof(StoreEventId.Snapshot), @@ -92,6 +113,9 @@ public static partial void LogSnapshotMessage( string message ); + /// + /// Logs a store commit message. + /// [LoggerMessage( EventId = StoreEventId.Commit, EventName = nameof(StoreEventId.Commit), @@ -103,6 +127,9 @@ public static partial void LogCommitMessage( string message ); + /// + /// Logs a store backup message. + /// [LoggerMessage( EventId = StoreEventId.Backup, EventName = nameof(StoreEventId.Backup), @@ -114,6 +141,9 @@ public static partial void LogBackupMessage( string message ); + /// + /// Logs a store restore message. + /// [LoggerMessage( EventId = StoreEventId.Restore, EventName = nameof(StoreEventId.Restore), @@ -125,6 +155,9 @@ public static partial void LogRestoreMessage( string message ); + /// + /// Logs a store checkpoint message. + /// [LoggerMessage( EventId = StoreEventId.Checkpoint, EventName = nameof(StoreEventId.Checkpoint), diff --git a/src/Neo.Platform.Storage/Logging/StoreEventId.cs b/src/Neo.Platform.Storage/Logging/StoreEventId.cs index b659bfa..b5f6999 100644 --- a/src/Neo.Platform.Storage/Logging/StoreEventId.cs +++ b/src/Neo.Platform.Storage/Logging/StoreEventId.cs @@ -22,16 +22,28 @@ namespace Neo.Platform.Storage.Logging { + /// + /// Event id constants for blockchain store logging. + /// internal static class StoreEventId { + /// Event id for store fault logs. public const int Fault = 100; + /// Event id for store read logs. public const int Read = 200; + /// Event id for store write logs. public const int Write = 300; + /// Event id for store delete logs. public const int Delete = 400; + /// Event id for store snapshot logs. public const int Snapshot = 500; + /// Event id for store commit logs. public const int Commit = 600; + /// Event id for store restore logs. public const int Restore = 700; + /// Event id for store backup logs. public const int Backup = 800; + /// Event id for store checkpoint logs. public const int Checkpoint = 900; } } diff --git a/src/Neo.Platform.Storage/StorageKeyBuilder.cs b/src/Neo.Platform.Storage/StorageKeyBuilder.cs index 81583b8..b23f1c3 100644 --- a/src/Neo.Platform.Storage/StorageKeyBuilder.cs +++ b/src/Neo.Platform.Storage/StorageKeyBuilder.cs @@ -35,12 +35,28 @@ namespace Neo.Platform.Storage { + /// + /// Builds binary storage keys by appending typed values into a rented buffer. + /// + /// + /// Keys begin with a little-endian contract id and an optional prefix byte + /// (). Dispose the builder when finished to return pooled memory. + /// public class StorageKeyBuilder : IDisposable, IComparable, IComparable, IEquatable, IEnumerable { + /// + /// The fixed prefix length: 4-byte little-endian id plus 1 prefix byte. + /// public const int PrefixLength = sizeof(int) + sizeof(byte); + /// + /// Gets the number of bytes currently written to the key. + /// public int Length => _byteCount; + /// + /// Gets the maximum capacity of the key buffer, including the fixed prefix. + /// public int MaxLength => _maxLength; private readonly IMemoryOwner _memoryOwner; @@ -72,6 +88,9 @@ private StorageKeyBuilder(ReadOnlySpan data, int maxLength = -1) data.CopyTo(_memoryOwner.Memory.Span); } + /// + /// Clears and returns the rented key buffer to the shared pool. + /// public void Dispose() { _memoryOwner.Memory.Span.Clear(); @@ -79,6 +98,11 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Determines whether the specified object is a with the same key bytes. + /// + /// The object to compare. + /// if the objects represent the same key; otherwise, . public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -86,6 +110,11 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as StorageKeyBuilder); } + /// + /// Determines whether another builder has the same key bytes. + /// + /// The other builder. + /// if the keys are equal; otherwise, . public bool Equals([NotNullWhen(true)] StorageKeyBuilder? other) { if (ReferenceEquals(other, this)) return true; @@ -96,6 +125,14 @@ public bool Equals([NotNullWhen(true)] StorageKeyBuilder? other) .SequenceEqual(other._memoryOwner.Memory[..other._byteCount].Span); } + /// + /// Compares this key to another object. + /// + /// The object to compare. + /// + /// A value less than zero if this key is less than ; + /// zero if equal; greater than zero if greater. + /// public int CompareTo(object? obj) { if (ReferenceEquals(obj, this)) return 0; @@ -103,6 +140,14 @@ public int CompareTo(object? obj) return CompareTo(obj as StorageKeyBuilder); } + /// + /// Compares this key to another builder by length, then by byte sequence. + /// + /// The other builder. + /// + /// A value less than zero if this key is less than ; + /// zero if equal; greater than zero if greater. + /// public int CompareTo(StorageKeyBuilder? other) { if (ReferenceEquals(other, this)) return 0; @@ -116,13 +161,27 @@ public int CompareTo(StorageKeyBuilder? other) .SequenceCompareTo(other._memoryOwner.Memory[..other._byteCount].Span); } + /// + /// Returns a hash code for the current key bytes. + /// + /// A hash code for the key content. public override int GetHashCode() => _memoryOwner.Memory[.._byteCount] .ToHashCode(); + /// + /// Returns a lowercase hexadecimal representation of the key bytes. + /// + /// The key as a hex string. public override string ToString() => Convert.ToHexStringLower(_memoryOwner.Memory[.._byteCount].Span); + /// + /// Concatenates the key bytes of two builders into a new builder. + /// + /// The left-hand key. + /// The right-hand key. + /// A new builder containing the concatenated bytes. public static StorageKeyBuilder operator +(StorageKeyBuilder a, StorageKeyBuilder b) => new ( @@ -133,21 +192,52 @@ .. b._memoryOwner.Memory[..b._byteCount].Span a._maxLength + b._maxLength ); + /// + /// Determines whether two builders have equal key bytes. + /// + /// The left-hand builder. + /// The right-hand builder. + /// if the keys are equal; otherwise, . public static bool operator ==(StorageKeyBuilder left, StorageKeyBuilder right) => left.Equals(right); + /// + /// Determines whether two builders have different key bytes. + /// + /// The left-hand builder. + /// The right-hand builder. + /// if the keys differ; otherwise, . public static bool operator !=(StorageKeyBuilder left, StorageKeyBuilder right) => !(left == right); + /// + /// Creates a new key builder for the specified contract id and prefix. + /// + /// The contract id written as a little-endian . + /// An optional prefix byte written after the id. + /// A new instance. public static StorageKeyBuilder Create(int id, byte prefix = 0) => new(id, prefix); + /// + /// Appends a single byte to the key. + /// + /// The byte to append. + /// This builder for chaining. public StorageKeyBuilder Append(byte data) { _memoryOwner.Memory.Span[_byteCount++] = data; return this; } + /// + /// Appends a 16-bit signed integer to the key. + /// + /// The value to append. + /// + /// When , writes big-endian; otherwise little-endian. + /// + /// This builder for chaining. public StorageKeyBuilder Append(short data, bool isBigEndian = false) { if (isBigEndian) @@ -158,6 +248,14 @@ public StorageKeyBuilder Append(short data, bool isBigEndian = false) return this; } + /// + /// Appends a 16-bit unsigned integer to the key. + /// + /// The value to append. + /// + /// When , writes big-endian; otherwise little-endian. + /// + /// This builder for chaining. public StorageKeyBuilder Append(ushort data, bool isBigEndian = false) { if (isBigEndian) @@ -168,6 +266,14 @@ public StorageKeyBuilder Append(ushort data, bool isBigEndian = false) return this; } + /// + /// Appends a 32-bit signed integer to the key. + /// + /// The value to append. + /// + /// When , writes big-endian; otherwise little-endian. + /// + /// This builder for chaining. public StorageKeyBuilder Append(int data, bool isBigEndian = false) { if (isBigEndian) @@ -178,6 +284,14 @@ public StorageKeyBuilder Append(int data, bool isBigEndian = false) return this; } + /// + /// Appends a 32-bit unsigned integer to the key. + /// + /// The value to append. + /// + /// When , writes big-endian; otherwise little-endian. + /// + /// This builder for chaining. public StorageKeyBuilder Append(uint data, bool isBigEndian = false) { if (isBigEndian) @@ -188,6 +302,14 @@ public StorageKeyBuilder Append(uint data, bool isBigEndian = false) return this; } + /// + /// Appends a 64-bit signed integer to the key. + /// + /// The value to append. + /// + /// When , writes big-endian; otherwise little-endian. + /// + /// This builder for chaining. public StorageKeyBuilder Append(long data, bool isBigEndian = false) { if (isBigEndian) @@ -198,6 +320,14 @@ public StorageKeyBuilder Append(long data, bool isBigEndian = false) return this; } + /// + /// Appends a 64-bit unsigned integer to the key. + /// + /// The value to append. + /// + /// When , writes big-endian; otherwise little-endian. + /// + /// This builder for chaining. public StorageKeyBuilder Append(ulong data, bool isBigEndian = false) { if (isBigEndian) @@ -208,6 +338,14 @@ public StorageKeyBuilder Append(ulong data, bool isBigEndian = false) return this; } + /// + /// Appends a 128-bit signed integer to the key. + /// + /// The value to append. + /// + /// When , writes big-endian; otherwise little-endian. + /// + /// This builder for chaining. public StorageKeyBuilder Append(Int128 data, bool isBigEndian = false) { if (isBigEndian) @@ -218,6 +356,14 @@ public StorageKeyBuilder Append(Int128 data, bool isBigEndian = false) return this; } + /// + /// Appends a 128-bit unsigned integer to the key. + /// + /// The value to append. + /// + /// When , writes big-endian; otherwise little-endian. + /// + /// This builder for chaining. public StorageKeyBuilder Append(UInt128 data, bool isBigEndian = false) { if (isBigEndian) @@ -228,6 +374,14 @@ public StorageKeyBuilder Append(UInt128 data, bool isBigEndian = false) return this; } + /// + /// Appends a double-precision floating-point value to the key. + /// + /// The value to append. + /// + /// When , writes big-endian; otherwise little-endian. + /// + /// This builder for chaining. public StorageKeyBuilder Append(double data, bool isBigEndian = false) { if (isBigEndian) @@ -238,6 +392,14 @@ public StorageKeyBuilder Append(double data, bool isBigEndian = false) return this; } + /// + /// Appends a single-precision floating-point value to the key. + /// + /// The value to append. + /// + /// When , writes big-endian; otherwise little-endian. + /// + /// This builder for chaining. public StorageKeyBuilder Append(float data, bool isBigEndian = false) { if (isBigEndian) @@ -248,6 +410,14 @@ public StorageKeyBuilder Append(float data, bool isBigEndian = false) return this; } + /// + /// Appends a half-precision floating-point value to the key. + /// + /// The value to append. + /// + /// When , writes big-endian; otherwise little-endian. + /// + /// This builder for chaining. public StorageKeyBuilder Append(Half data, bool isBigEndian = false) { if (isBigEndian) @@ -258,9 +428,22 @@ public StorageKeyBuilder Append(Half data, bool isBigEndian = false) return this; } + /// + /// Appends a as its unsigned byte representation. + /// + /// The value to append. + /// + /// When , writes big-endian; otherwise little-endian. + /// + /// This builder for chaining. public StorageKeyBuilder Append(BigInteger data, bool isBigEndian = false) => Append(data.ToByteArray(true, isBigEndian)); + /// + /// Appends raw bytes to the key. + /// + /// The bytes to append. + /// This builder for chaining. public StorageKeyBuilder Append(ReadOnlySpan data) { data.CopyTo(_memoryOwner.Memory.Span[_byteCount..]); @@ -268,6 +451,12 @@ public StorageKeyBuilder Append(ReadOnlySpan data) return this; } + /// + /// Appends a as its four 32-bit integer parts (little-endian). + /// + /// The value to append. + /// Reserved for API consistency; currently unused. + /// This builder for chaining. public StorageKeyBuilder Append(decimal data, bool isBigEndian = false) { Span span = stackalloc int[4]; @@ -280,90 +469,247 @@ public StorageKeyBuilder Append(decimal data, bool isBigEndian = false) return this; } + /// + /// Appends a as its 16-byte representation. + /// + /// The value to append. + /// Controls the byte order of the GUID encoding. + /// This builder for chaining. public StorageKeyBuilder Append(Guid data, bool isBigEndian = false) => Append(data.ToByteArray(isBigEndian)); + /// + /// Appends a sequence of bytes to the key. + /// + /// The sequence to append. + /// This builder for chaining. public StorageKeyBuilder Append(ReadOnlySequence data) => Append(data.ToArray()); + /// + /// Appends an array segment of bytes to the key. + /// + /// The segment to append. + /// This builder for chaining. public StorageKeyBuilder Append(ArraySegment data) => Append(data.AsSpan()); + /// + /// Appends a as Unix milliseconds (UTC adjusted by ). + /// + /// The timestamp to append. + /// Optional offset applied when converting to Unix time. + /// This builder for chaining. public StorageKeyBuilder Append(DateTime stamp, TimeSpan offset = default) => Append((ulong)CoreUtilities.ToUnixTimeMilliseconds(stamp, offset)); + /// + /// Appends a as Unix milliseconds (adjusted by ). + /// + /// The time span to append. + /// Optional offset applied when converting to Unix time. + /// This builder for chaining. public StorageKeyBuilder Append(TimeSpan span, TimeSpan offset = default) => Append((ulong)CoreUtilities.ToUnixTimeMilliseconds(span, offset)); + /// + /// Appends a as Unix milliseconds. + /// + /// The timestamp to append. + /// This builder for chaining. public StorageKeyBuilder Append(DateTimeOffset stamp) => Append((ulong)stamp.ToUnixTimeMilliseconds()); + /// + /// Appends a value as its time-of-day span encoding. + /// + /// The time to append. + /// This builder for chaining. public StorageKeyBuilder Append(TimeOnly time) => Append(time.ToTimeSpan()); + /// + /// Appends a combined with an optional as Unix milliseconds. + /// + /// The date to append. + /// The time of day to combine with the date. Defaults to midnight. + /// This builder for chaining. public StorageKeyBuilder Append(DateOnly date, TimeOnly time = default) => Append(date.ToDateTime(time)); + /// + /// Appends a sequence of characters encoded as UTF-8. + /// + /// The characters to encode and append. + /// + /// When , uses strict UTF-8 encoding; otherwise standard UTF-8. + /// + /// This builder for chaining. public StorageKeyBuilder Append(ReadOnlySequence data, bool isStrictUtf8 = true) => Append(data.ToArray(), isStrictUtf8); + /// + /// Appends characters encoded as UTF-8. + /// + /// The characters to encode and append. + /// + /// When , uses strict UTF-8 encoding; otherwise standard UTF-8. + /// + /// This builder for chaining. public StorageKeyBuilder Append(ReadOnlyMemory data, bool isStrictUtf8 = true) => Append(data.ToArray(), isStrictUtf8); + /// + /// Appends characters encoded as UTF-8. + /// + /// The characters to encode and append. + /// + /// When , uses strict UTF-8 encoding; otherwise standard UTF-8. + /// + /// This builder for chaining. public StorageKeyBuilder Append(ReadOnlySpan data, bool isStrictUtf8 = true) => Append(data.ToArray(), isStrictUtf8); + /// + /// Appends a character array encoded as UTF-8. + /// + /// The characters to encode and append. + /// + /// When , uses strict UTF-8 encoding; otherwise standard UTF-8. + /// + /// This builder for chaining. public StorageKeyBuilder Append(char[] data, bool isStrictUtf8 = true) => isStrictUtf8 ? Append(CoreUtilities.StrictUtf8Encoding.GetBytes(data)) : Append(Encoding.UTF8.GetBytes(data)); + /// + /// Appends a single character encoded as UTF-8. + /// + /// The character to encode and append. + /// + /// When , uses strict UTF-8 encoding; otherwise standard UTF-8. + /// + /// This builder for chaining. public StorageKeyBuilder Append(char data, bool isStrictUtf8 = true) => isStrictUtf8 ? Append(CoreUtilities.StrictUtf8Encoding.GetBytes(data.ToString())) : Append(Encoding.UTF8.GetBytes(data.ToString())); + /// + /// Appends a signed byte as an unsigned byte. + /// + /// The value to append. + /// This builder for chaining. public StorageKeyBuilder Append(sbyte data) => Append((byte)data); + /// + /// Appends a Boolean as a single byte (1 for , 0 for ). + /// + /// The value to append. + /// This builder for chaining. public StorageKeyBuilder Append(bool data) => Append(data ? (byte)1 : (byte)0); + /// + /// Appends a byte array to the key. + /// + /// The bytes to append. + /// This builder for chaining. public StorageKeyBuilder Append(byte[] data) => Append(data.AsSpan()); + /// + /// Appends a memory region of bytes to the key. + /// + /// The bytes to append. + /// This builder for chaining. public StorageKeyBuilder Append(ReadOnlyMemory data) => Append(data.Span); + /// + /// Appends the key bytes of another builder. + /// + /// The builder whose current bytes are appended. + /// This builder for chaining. public StorageKeyBuilder Append(StorageKeyBuilder other) => Append(other._memoryOwner.Memory[..other._byteCount].Span); + /// + /// Appends a as its 20-byte representation. + /// + /// The value to append. + /// This builder for chaining. public StorageKeyBuilder Append(UInt160 data) => Append(data.ToArray()); + /// + /// Appends a as its 32-byte representation. + /// + /// The value to append. + /// This builder for chaining. public StorageKeyBuilder Append(UInt256 data) => Append(data.ToArray()); + /// + /// Appends a string encoded as UTF-8. + /// + /// The string to encode and append. + /// + /// When , uses strict UTF-8 encoding; otherwise standard UTF-8. + /// + /// This builder for chaining. public StorageKeyBuilder Append(string data, bool isStrictUtf8 = true) => isStrictUtf8 ? Append(CoreUtilities.StrictUtf8Encoding.GetBytes(data)) : Append(Encoding.UTF8.GetBytes(data)); + /// + /// Appends a prefix byte followed by raw bytes. + /// + /// The prefix byte. + /// The bytes to append after the prefix. + /// This builder for chaining. public StorageKeyBuilder Append(byte prefix, ReadOnlySpan data) => Append(prefix).Append(data); + /// + /// Appends a prefix byte followed by a . + /// + /// The prefix byte. + /// The value to append after the prefix. + /// This builder for chaining. public StorageKeyBuilder Append(byte prefix, UInt160 data) => Append(prefix).Append(data); + /// + /// Appends a prefix byte followed by a . + /// + /// The prefix byte. + /// The value to append after the prefix. + /// This builder for chaining. public StorageKeyBuilder Append(byte prefix, UInt256 data) => Append(prefix).Append(data); + /// + /// Copies the current key bytes into a new array. + /// + /// A new array containing the key bytes written so far. public byte[] ToArray() => _memoryOwner.Memory[.._byteCount].ToArray(); + /// + /// Creates a new builder with a copy of the current key bytes. + /// + /// A cloned . public StorageKeyBuilder Clone() => new(_memoryOwner.Memory[.._byteCount].Span, _maxLength); + /// + /// Returns an enumerator that iterates over the key bytes. + /// + /// An enumerator for the key content. public IEnumerator GetEnumerator() => GetEnumerator(); diff --git a/src/Neo.Platform.Storage/StoreExtensions.cs b/src/Neo.Platform.Storage/StoreExtensions.cs index be335de..e0619fd 100644 --- a/src/Neo.Platform.Storage/StoreExtensions.cs +++ b/src/Neo.Platform.Storage/StoreExtensions.cs @@ -28,16 +28,48 @@ namespace Neo.Platform.Storage { + /// + /// Extension methods for reading and writing values from stores. + /// public static class StoreExtensions { + /// + /// Serializes and stores it under . + /// + /// The serializable value type. + /// The store to write to. + /// The storage key. + /// The value to serialize and store. public static void Put(this IStore store, ReadOnlySpan key, T value) where T : INeoSerializable => store.Put(key, value.ToArray()); + /// + /// Reads and deserializes a value of type for . + /// + /// The serializable result type. + /// The store to read from. + /// The storage key. + /// + /// The deserialized value, or if the key is not present. + /// public static TResult? Get(this IReadOnlyStore store, ReadOnlySpan key) where TResult : class?, INeoSerializable => store.Get(key)?.AsSerializable(); + /// + /// Attempts to read and deserialize a value of type for . + /// + /// The serializable value type. + /// The store to read from. + /// The storage key. + /// + /// When this method returns , contains the deserialized value; + /// otherwise, . + /// + /// + /// if the key was found and deserialized; otherwise, . + /// public static bool TryGet(this IReadOnlyStore store, ReadOnlySpan key, [NotNullWhen(true)] out T? value) where T : class?, INeoSerializable { diff --git a/src/Neo.VM/Builder/VirtualMachineBuilder.cs b/src/Neo.VM/Builder/VirtualMachineBuilder.cs index d2ba4f2..915babe 100644 --- a/src/Neo.VM/Builder/VirtualMachineBuilder.cs +++ b/src/Neo.VM/Builder/VirtualMachineBuilder.cs @@ -29,6 +29,9 @@ namespace Neo.VM.Builder { + /// + /// Fluent builder for constructing a . + /// public sealed class VirtualMachineBuilder { private VirtualTable? _vtable; @@ -42,15 +45,29 @@ public sealed class VirtualMachineBuilder private VirtualMachineBuilder() { } + /// + /// Creates a new instance. + /// + /// A new builder. public static VirtualMachineBuilder Create() => new(); + /// + /// Configures the middleware pipeline used during execution. + /// + /// The pipeline to use. + /// This builder for chaining. public VirtualMachineBuilder UsePipeline(VirtualMachinePipeline pipeline) { _pipeline = pipeline; return this; } + /// + /// Configures the middleware pipeline via a nested . + /// + /// An action that registers middleware on the pipeline builder. + /// This builder for chaining. public VirtualMachineBuilder UsePipeline(Action config) { var pb = VirtualMachinePipelineBuilder.Create(); @@ -59,36 +76,66 @@ public VirtualMachineBuilder UsePipeline(Action c return this; } + /// + /// Sets the execution limits for the engine. + /// + /// The limits to apply. + /// This builder for chaining. public VirtualMachineBuilder UseLimits(ExecutionEngineLimits limits) { _engineLimits = limits; return this; } + /// + /// Sets the block being persisted, used to resolve the active hard fork. + /// + /// The block under persistence. + /// This builder for chaining. public VirtualMachineBuilder UseBlock(Block persistingBlock) { _persistingBlock = persistingBlock; return this; } + /// + /// Sets the protocol settings used by the engine. + /// + /// The protocol settings. + /// This builder for chaining. public VirtualMachineBuilder UseProtocolSettings(ProtocolSettings protocolSettings) { _protocolSettings = protocolSettings; return this; } + /// + /// Sets the opcode dispatch table used by the engine. + /// + /// The virtual table of opcode handlers. + /// This builder for chaining. public VirtualMachineBuilder UseVirtualTable(VirtualTable vtable) { _vtable = vtable; return this; } + /// + /// Sets the transaction container, which supplies the system-fee gas limit. + /// + /// The transaction to execute against. + /// This builder for chaining. public VirtualMachineBuilder UseTransaction(Transaction tx) { _tx = tx; return this; } + /// + /// Builds a from the configured options. + /// Unspecified options use engine defaults. + /// + /// A new engine instance. public VirtualMachineEngine Build() => new ( diff --git a/src/Neo.VM/Builder/VirtualMachinePipelineBuilder.cs b/src/Neo.VM/Builder/VirtualMachinePipelineBuilder.cs index d4cabec..f9961af 100644 --- a/src/Neo.VM/Builder/VirtualMachinePipelineBuilder.cs +++ b/src/Neo.VM/Builder/VirtualMachinePipelineBuilder.cs @@ -39,12 +39,18 @@ public sealed class VirtualMachinePipelineBuilder private VirtualMachinePipelineBuilder() { } + /// + /// Creates a new instance. + /// + /// A new builder. public static VirtualMachinePipelineBuilder Create() => new(); /// /// Registers a middleware instance. /// + /// The middleware to register. + /// This builder for chaining. public VirtualMachinePipelineBuilder Use(IEngineMiddleware middleware) { ArgumentNullException.ThrowIfNull(middleware); @@ -55,6 +61,8 @@ public VirtualMachinePipelineBuilder Use(IEngineMiddleware middleware) /// /// Registers a middleware by type (requires parameterless constructor). /// + /// The middleware type to instantiate and register. + /// This builder for chaining. public VirtualMachinePipelineBuilder UseMiddleware() where TMiddleware : IEngineMiddleware, new() { @@ -64,6 +72,8 @@ public VirtualMachinePipelineBuilder UseMiddleware() /// /// Registers multiple middleware's at once. /// + /// The middleware instances to register, in execution order. + /// This builder for chaining. public VirtualMachinePipelineBuilder Use(IEnumerable middlewares) { foreach (var middleware in middlewares) @@ -75,6 +85,7 @@ public VirtualMachinePipelineBuilder Use(IEnumerable middlewa /// /// Builds the final optimized pipeline with separate chains for each hook. /// + /// A pipeline ready for use by . public VirtualMachinePipeline Build() { var preExecution = BuildExecutionChain(mw => mw.PreExecution); diff --git a/src/Neo.VM/Core/ExecutionContext.cs b/src/Neo.VM/Core/ExecutionContext.cs index 5ef9d8b..78c95af 100644 --- a/src/Neo.VM/Core/ExecutionContext.cs +++ b/src/Neo.VM/Core/ExecutionContext.cs @@ -28,59 +28,68 @@ namespace Neo.VM.Core { + /// + /// Represents a single invocation frame: script, instruction pointer, gas budget, and evaluation stack. + /// public class ExecutionContext { /// - /// The script being executed (bytecode) + /// Gets the script being executed (bytecode), including a trailing RET appended at construction. /// public ReadOnlyMemory Script => _script; /// - /// The pointer indicating the current instruction. + /// Gets the pointer indicating the current instruction. /// public int InstructionPointer { get; internal set; } /// - /// Returns the current . + /// Gets the current at . /// public OpCodeInst CurrentInstruction => new(_script[InstructionPointer..]); /// - /// Returns the next . + /// Gets the next after the current instruction. /// public OpCodeInst NextInstruction => new(_script[(InstructionPointer + CurrentInstruction.Size)..]); /// - /// Current stack frame + /// Gets the current stack frame that owns the evaluation stack and locals. /// public StackFrame Frame { get; } /// - /// Gas remaining for this execution + /// Gets the amount of gas consumed by this context so far. /// public long GasConsumed => _maxGasConsumed; /// - /// Whether this context is currently executing + /// Gets a value indicating whether this context is still eligible to execute instructions. /// public bool IsExecuting { get; internal set; } = true; /// - /// Parent context (for nested calls) + /// Gets the parent context for nested calls, if any. /// public ExecutionContext? Parent => _parentContext; /// - /// Custom state / context data (e.g., contract storage, runtime) + /// Gets a dictionary of custom state objects keyed by type (for example, runtime services). /// public Dictionary State { get; } = []; + /// + /// Gets the hard fork under which this context executes. + /// public HardFork HardFork => _fork; + /// + /// Gets the block height associated with this context. + /// public uint BlockHeight => _blockHeight; /// - /// Invocation stack depth + /// Gets the depth of this context in the invocation chain. /// public int Depth => _contextDepth; @@ -94,6 +103,15 @@ public class ExecutionContext private long _initialGasLeft; private long _maxGasConsumed; + /// + /// Initializes a new execution context for the specified script. + /// + /// The bytecode to execute. A trailing RET is appended automatically. + /// The hard fork that determines gas costs and opcode behavior. + /// The block height associated with this execution. + /// The gas budget available to this context. + /// The invocation depth of this context. + /// The parent context for nested calls, or for a root context. public ExecutionContext(byte[] script, HardFork fork = HardFork.Genesis, uint blockHeight = 0, long initialGas = 1_000000L, int depth = 0, ExecutionContext? parent = null) { script = [.. script, (byte)OpCode.RET]; @@ -110,16 +128,19 @@ public ExecutionContext(byte[] script, HardFork fork = HardFork.Genesis, uint bl } /// - /// Check if execution should continue + /// Determines whether execution should continue for this context. /// + /// if the context is active, has remaining script bytes, and has gas remaining. public bool ShouldContinue() { return IsExecuting && InstructionPointer < Script.Length && _initialGasLeft > 0; } /// - /// Consume gas for an operation + /// Attempts to consume the gas cost for the specified opcode. /// + /// The opcode whose gas cost should be charged. + /// if gas was available and deducted; otherwise and execution is stopped. public bool ConsumeGas(OpCode opcode) { var cost = GasTable.GetGasCost(opcode, HardFork); @@ -136,62 +157,87 @@ public bool ConsumeGas(OpCode opcode) } /// - /// Push value onto current frame's evaluation stack + /// Pushes a value onto the current frame's evaluation stack. /// + /// The stack item to push. + /// Whether to increment the item's reference count. + /// Whether to increment reference counts of child items. public void Push(VMObject item, bool addReferenceItem = true, bool addReferenceChildren = true) { Frame.Push(item, addReferenceItem, addReferenceChildren); } /// - /// Pop value from current frame's evaluation stack + /// Pops a value from the current frame's evaluation stack. /// + /// Whether to release the item's reference count. + /// Whether to release reference counts of child items. + /// The popped stack item. public VMObject Pop(bool releaseReferenceItem = true, bool releaseReferenceChildren = true) { return Frame.Pop(releaseReferenceItem, releaseReferenceChildren); } /// - /// Peek value from current frame's evaluation stack without removing + /// Peeks at the top of the current frame's evaluation stack without removing it. /// + /// The top stack item. public VMObject Peek() { return Frame.Peek(); } /// - /// Peek value from current frame's evaluation stack without removing by an index + /// Peeks at an item on the evaluation stack by depth without removing it. /// + /// Zero-based depth from the top of the stack. + /// The stack item at the specified depth. public VMObject Peek(int index) { return Frame.Peek(index); } + /// + /// Inserts an item into the evaluation stack at the specified depth. + /// + /// Zero-based depth from the top of the stack. + /// The item to insert. public void Insert(int index, VMObject item) { Frame.Insert(index, item); } + /// + /// Swaps two items on the evaluation stack by depth. + /// + /// The first depth index. + /// The second depth index. public void Swap(int fromIndex, int toIndex) { Frame.Swap(fromIndex, toIndex); } + /// + /// Reverses the order of the top items on the evaluation stack. + /// + /// The number of items from the top to reverse. public void Reverse(int n) { Frame.Reverse(n); } /// - /// Pop value from current frame's evaluation stack at an index + /// Removes and returns the evaluation-stack item at the specified depth. /// + /// Zero-based depth from the top of the stack. + /// The removed stack item. public VMObject RemoveAt(int index) { return Frame.RemoveAt(index); } /// - /// Safely clear current frame's evaluation stack and release all references + /// Clears the current frame's evaluation stack and releases all references. /// public void Clear() { @@ -199,7 +245,7 @@ public void Clear() } /// - /// Clean up this execution context + /// Releases stack resources and marks this context as no longer executing. /// public void Cleanup() { diff --git a/src/Neo.VM/Core/StackFrame.cs b/src/Neo.VM/Core/StackFrame.cs index 69a1b21..6bd09e1 100644 --- a/src/Neo.VM/Core/StackFrame.cs +++ b/src/Neo.VM/Core/StackFrame.cs @@ -26,40 +26,50 @@ namespace Neo.VM.Core { + /// + /// Holds the evaluation stack, locals, static fields, and arguments for a single call frame. + /// + /// The parent frame for nested calls, or for a root frame. public class StackFrame(StackFrame? parent = default) : IDisposable { /// - /// The evaluation stack for this frame + /// Gets the evaluation stack for this frame. /// public Stack EvaluationStack { get; } = []; /// - /// Local variables for this frame (indexed) + /// Gets the local variables for this frame (indexed). /// public List LocalVariables { get; } = []; + /// + /// Gets the static fields associated with this frame. + /// public List StaticFields { get; } = []; /// - /// The list used to store the arguments of the current method. + /// Gets the list used to store the arguments of the current method. /// public List Arguments { get; } = []; /// - /// Alternative stack (used by some NeoVM instructions) + /// Gets the alternative stack (used by some NeoVM instructions). /// public Stack AltStack { get; } = []; /// - /// Parent frame (for nested calls) + /// Gets the parent frame for nested calls. /// public StackFrame? Parent { get; } = parent; /// - /// Whether this frame is currently executing + /// Gets a value indicating whether this frame is still active. /// public bool IsActive { get; private set; } = true; + /// + /// Releases stack resources held by this frame. + /// public void Dispose() { Cleanup(); @@ -67,8 +77,11 @@ public void Dispose() } /// - /// Push item onto evaluation stack with reference counting + /// Pushes an item onto the evaluation stack with optional reference counting. /// + /// The item to push. + /// Whether to increment the item's reference count. + /// Whether to increment reference counts of child items. public void Push(VMObject item, bool addReferenceItem = true, bool addReferenceChildren = true) { if (addReferenceItem) @@ -87,8 +100,12 @@ public void Push(VMObject item, bool addReferenceItem = true, bool addReferenceC } /// - /// Pop item from evaluation stack and release reference + /// Pops an item from the evaluation stack and optionally releases references. /// + /// Whether to release the item's reference count. + /// Whether to release reference counts of child items. + /// The popped item. + /// Thrown when the evaluation stack is empty. public VMObject Pop(bool releaseReferenceItem = true, bool releaseReferenceChildren = true) { if (EvaluationStack.Count == 0) @@ -112,14 +129,21 @@ public VMObject Pop(bool releaseReferenceItem = true, bool releaseReferenceChild } /// - /// Peek from top of evaluation stack without removing by an index + /// Peeks at an evaluation-stack item by depth without removing it. /// + /// Zero-based depth from the top of the stack. + /// The stack item at the specified depth. public VMObject Peek(int index = 0) { var list = new List(EvaluationStack); // Copy to list (top is at the end) return list[index]; } + /// + /// Inserts an item into the evaluation stack at the specified depth. + /// + /// Zero-based depth from the top of the stack. + /// The item to insert. public void Insert(int index, VMObject item) { var list = new List(EvaluationStack); // Copy to list (top is at the end) @@ -136,6 +160,11 @@ public void Insert(int index, VMObject item) item.AddReference(); } + /// + /// Swaps two items on the evaluation stack by depth. + /// + /// The first depth index. + /// The second depth index. public void Swap(int fromIndex, int toIndex) { if (fromIndex == toIndex) return; @@ -151,6 +180,11 @@ public void Swap(int fromIndex, int toIndex) list.Clear(); } + /// + /// Reverses the order of the top items on the evaluation stack. + /// + /// The number of items from the top to reverse. + /// Thrown when is outside the stack bounds. public void Reverse(int n) { var list = new List(EvaluationStack); // Copy to list (top is at the end) @@ -171,6 +205,11 @@ public void Reverse(int n) } + /// + /// Removes and returns the evaluation-stack item at the specified depth. + /// + /// Zero-based depth from the top of the stack. + /// The removed item. public VMObject RemoveAt(int index) { var list = new List(EvaluationStack); // Copy to list (top is at the end) @@ -189,7 +228,7 @@ public VMObject RemoveAt(int index) } /// - /// Safely clear a evaluation stack and release all references + /// Clears the evaluation stack and releases all references. /// public void Clear() { @@ -200,6 +239,10 @@ public void Clear() } } + /// + /// Ensures the local-variable list has at least the specified number of slots, filling with . + /// + /// The required number of local slots. public void InitLocalVariables(int count) { while (LocalVariables.Count < count) @@ -207,8 +250,10 @@ public void InitLocalVariables(int count) } /// - /// Set local variable at index (with proper ref counting) + /// Sets the local variable at the specified index, updating reference counts. /// + /// The local-variable index. + /// The value to store. public void SetLocalVariable(int index, VMObject value) { // Expand locals list if needed @@ -223,12 +268,21 @@ public void SetLocalVariable(int index, VMObject value) LocalVariables[index] = value; } + /// + /// Ensures the static-field list has at least the specified number of slots, filling with . + /// + /// The required number of static-field slots. public void InitStaticFields(int count) { while (StaticFields.Count < count) StaticFields.Add(VMNull.Instance); } + /// + /// Sets the static field at the specified index, updating reference counts. + /// + /// The static-field index. + /// The value to store. public void SetStaticField(int index, VMObject value) { StaticFields[index]?.Release(); @@ -238,12 +292,21 @@ public void SetStaticField(int index, VMObject value) StaticFields[index] = value; } + /// + /// Ensures the argument list has at least the specified number of slots, filling with . + /// + /// The required number of argument slots. public void InitArguments(int count) { while (Arguments.Count < count) Arguments.Add(VMNull.Instance); } + /// + /// Sets the argument at the specified index, updating reference counts. + /// + /// The argument index. + /// The value to store. public void SetArguments(int index, VMObject value) { while (Arguments.Count < index) @@ -257,26 +320,39 @@ public void SetArguments(int index, VMObject value) } /// - /// Get local variable at index + /// Gets the local variable at the specified index, or if out of range. /// + /// The local-variable index. + /// The local value, or null when the index is invalid. public VMObject GetLocal(int index) { return (index >= 0 && index < LocalVariables.Count) ? LocalVariables[index] : VMNull.Instance; } + /// + /// Gets the static field at the specified index, or if out of range. + /// + /// The static-field index. + /// The static-field value, or null when the index is invalid. public VMObject GetStaticFields(int index) { return (index >= 0 && index < StaticFields.Count) ? StaticFields[index] : VMNull.Instance; } + /// + /// Gets the argument at the specified index, or if out of range. + /// + /// The argument index. + /// The argument value, or null when the index is invalid. public VMObject GetArguments(int index) { return (index >= 0 && index < Arguments.Count) ? Arguments[index] : VMNull.Instance; } /// - /// Checks if this stack frame contains any circular references + /// Determines whether this stack frame contains any circular references among its stack items. /// + /// if a cycle is detected; otherwise . public bool HasCircularReference() { var visited = new HashSet(ReferenceEqualityComparer.Instance); @@ -284,7 +360,7 @@ public bool HasCircularReference() } /// - /// Clean up this frame (release all references) + /// Releases all references held by this frame and marks it inactive. /// public void Cleanup() { diff --git a/src/Neo.VM/Core/VirtualTable.cs b/src/Neo.VM/Core/VirtualTable.cs index b7005d7..42f5383 100644 --- a/src/Neo.VM/Core/VirtualTable.cs +++ b/src/Neo.VM/Core/VirtualTable.cs @@ -26,20 +26,44 @@ namespace Neo.VM.Core { + /// + /// Represents an opcode handler invoked by the engine for a single instruction. + /// + /// The execution engine. + /// The instruction being executed. public delegate void VTableFunc(VirtualMachineEngine engine, OpCodeInst instruction); + /// + /// Dispatch table that maps values to handler methods. + /// Methods named after opcodes are auto-bound in the constructor via reflection. + /// public partial class VirtualTable { + /// + /// Gets the default virtual table with standard NeoVM opcode handlers. + /// public static readonly VirtualTable Default = new(); + /// + /// Gets the array of handlers indexed by opcode byte value. + /// public VTableFunc[] Functions { get; protected set; } = new VTableFunc[byte.MaxValue]; + /// + /// Gets or sets the handler for the specified opcode. + /// + /// The opcode to look up. + /// The registered handler for . public VTableFunc this[OpCode opCode] { get => Functions[(byte)opCode]; protected set => Functions[(byte)opCode] = value; } + /// + /// Initializes a new virtual table, filling undefined opcodes with + /// and binding public methods whose names match members. + /// public VirtualTable() { Array.Fill(Functions, InvalidOpcode); @@ -52,6 +76,12 @@ public VirtualTable() } + /// + /// Default handler for opcodes that have no registered implementation. + /// + /// The execution engine. + /// The undefined instruction. + /// Always thrown for undefined opcodes. [DoesNotReturn] public static void InvalidOpcode(VirtualMachineEngine engine, OpCodeInst instruction) { diff --git a/src/Neo.VM/Extensions/DictionaryExtensions.cs b/src/Neo.VM/Extensions/DictionaryExtensions.cs index f727678..c79ff76 100644 --- a/src/Neo.VM/Extensions/DictionaryExtensions.cs +++ b/src/Neo.VM/Extensions/DictionaryExtensions.cs @@ -26,8 +26,18 @@ namespace Neo.VM.Extensions { + /// + /// Extension methods for converting dictionaries to VM stack items. + /// public static class DictionaryExtensions { + /// + /// Converts a dictionary into a by mapping each key and value to a . + /// + /// The dictionary key type. + /// The dictionary value type. + /// The dictionary to convert. + /// A containing the converted entries. public static VMMap ToStackItem(this IDictionary source) => new( source.Select(static kv => new KeyValuePair( diff --git a/src/Neo.VM/Extensions/ExecutionContextExtensions.cs b/src/Neo.VM/Extensions/ExecutionContextExtensions.cs index 9ceabe2..3b5eed4 100644 --- a/src/Neo.VM/Extensions/ExecutionContextExtensions.cs +++ b/src/Neo.VM/Extensions/ExecutionContextExtensions.cs @@ -25,11 +25,16 @@ namespace Neo.VM.Extensions { + /// + /// Extension methods for inspecting gas usage across an chain. + /// public static class ExecutionContextExtensions { /// - /// Returns the total GasConsumed across this context and all its parents (call stack). + /// Returns the total across this context and all its parents (call stack). /// + /// The leaf context, or . + /// The summed gas consumed along the parent chain. public static long GetTotalGasConsumed(this ExecutionContext? context) { var total = 0L; @@ -45,9 +50,11 @@ public static long GetTotalGasConsumed(this ExecutionContext? context) } /// - /// Returns a list of GasConsumed values from bottom (root) to top (current). + /// Returns a list of values from bottom (root) to top (current). /// Useful for debugging / logging the full call stack gas usage. /// + /// The leaf context, or . + /// Gas-consumed values ordered from root to leaf. public static List GetGasConsumedChain(this ExecutionContext? context) { var chain = new List(); diff --git a/src/Neo.VM/Extensions/LoggerExtensions.cs b/src/Neo.VM/Extensions/LoggerExtensions.cs index 7b6e861..3a60c07 100644 --- a/src/Neo.VM/Extensions/LoggerExtensions.cs +++ b/src/Neo.VM/Extensions/LoggerExtensions.cs @@ -26,8 +26,14 @@ namespace Neo.VM.Extensions { + /// + /// Source-generated logging helpers for virtual machine execution events. + /// internal static partial class LoggerExtensions { + /// + /// Logs a VM execute message. + /// [LoggerMessage( EventId = VirtualMachineEventId.Execute, EventName = nameof(VirtualMachineEventId.Execute), @@ -39,6 +45,9 @@ public static partial void LogExecuteMessage( string message ); + /// + /// Logs a VM load message. + /// [LoggerMessage( EventId = VirtualMachineEventId.Load, EventName = nameof(VirtualMachineEventId.Load), @@ -50,6 +59,9 @@ public static partial void LogLoadMessage( string message ); + /// + /// Logs a VM fault message. + /// [LoggerMessage( EventId = VirtualMachineEventId.Fault, EventName = nameof(VirtualMachineEventId.Fault), @@ -61,6 +73,9 @@ public static partial void LogFaultMessage( string message ); + /// + /// Logs a VM fault with an exception. + /// [LoggerMessage( EventId = VirtualMachineEventId.Fault, Message = "" @@ -71,6 +86,9 @@ public static partial void LogFaultMessage( Exception exception ); + /// + /// Logs a VM break (debugger breakpoint) message. + /// [LoggerMessage( EventId = VirtualMachineEventId.Break, EventName = nameof(VirtualMachineEventId.Break), diff --git a/src/Neo.VM/Extensions/ObjectExtensions.cs b/src/Neo.VM/Extensions/ObjectExtensions.cs index 6be44ef..3414281 100644 --- a/src/Neo.VM/Extensions/ObjectExtensions.cs +++ b/src/Neo.VM/Extensions/ObjectExtensions.cs @@ -29,8 +29,19 @@ namespace Neo.VM.Extensions { + /// + /// Extension methods for converting CLR values to stack items. + /// public static class ObjectExtensions { + /// + /// Converts a CLR value into the corresponding representation. + /// Supported primitives become integer, boolean, or byte-string items; sequences become arrays; + /// other reference types become wrappers. + /// + /// The source value type. + /// The value to convert; becomes . + /// A stack item suitable for evaluation-stack operations. public static VMObject ToStackItem(this TSource? source) => source switch { diff --git a/src/Neo.VM/Extensions/ServiceCollectionExtensions.cs b/src/Neo.VM/Extensions/ServiceCollectionExtensions.cs index bfdfc72..54a86ba 100644 --- a/src/Neo.VM/Extensions/ServiceCollectionExtensions.cs +++ b/src/Neo.VM/Extensions/ServiceCollectionExtensions.cs @@ -27,8 +27,17 @@ namespace Neo.VM.Extensions { + /// + /// Dependency-injection helpers for registering the Neo VM engine and middleware. + /// public static class ServiceCollectionExtensions { + /// + /// Registers a scoped built with any registered middleware. + /// When a is registered, it is placed first in the pipeline. + /// + /// The service collection. + /// The same service collection for chaining. public static IServiceCollection AddExecutionEngine(this IServiceCollection services) { // Register VirtualMachineEngine using the built pipeline @@ -54,6 +63,12 @@ public static IServiceCollection AddExecutionEngine(this IServiceCollection serv return services; } + /// + /// Registers a middleware type as a singleton . + /// + /// The middleware implementation type. + /// The service collection. + /// The same service collection for chaining. public static IServiceCollection AddEngineMiddleware(this IServiceCollection services) where TMiddleware : class, IEngineMiddleware { @@ -62,6 +77,11 @@ public static IServiceCollection AddEngineMiddleware(this IServiceC return services; } + /// + /// Registers the as a scoped service. + /// + /// The service collection. + /// The same service collection for chaining. public static IServiceCollection AddEngineMiddlewareDebugger(this IServiceCollection services) { services.AddScoped(); @@ -69,6 +89,11 @@ public static IServiceCollection AddEngineMiddlewareDebugger(this IServiceCollec return services; } + /// + /// Registers the as a scoped . + /// + /// The service collection. + /// The same service collection for chaining. public static IServiceCollection AddEngineMiddlewareLogger(this IServiceCollection services) { services.AddScoped(); @@ -76,6 +101,13 @@ public static IServiceCollection AddEngineMiddlewareLogger(this IServiceCollecti return services; } + /// + /// Registers one or more middleware types as singleton implementations. + /// + /// The service collection. + /// Types that implement . + /// The same service collection for chaining. + /// Thrown when a type does not implement . public static IServiceCollection AddEngineMiddleware(this IServiceCollection services, params Type[] middlewareTypes) { diff --git a/src/Neo.VM/Logging/VirtualMachineEventId.cs b/src/Neo.VM/Logging/VirtualMachineEventId.cs index eb45e24..5c09d77 100644 --- a/src/Neo.VM/Logging/VirtualMachineEventId.cs +++ b/src/Neo.VM/Logging/VirtualMachineEventId.cs @@ -22,34 +22,58 @@ namespace Neo.VM.Logging { + /// + /// Well-known event IDs used by LoggerMessage sources in Neo.VM. + /// public static class VirtualMachineEventId { + /// Event ID for unrecoverable VM faults. public const int Fault = 100; + /// Event ID for engine or context creation. public const int Create = 200; + /// Event ID for script or context load operations. public const int Load = 201; + /// Event ID for paired pre/post lifecycle messages. public const int PrePost = 202; + /// Event ID for post-lifecycle messages. public const int Post = 203; + /// Event ID for debugger breakpoint stops. public const int Break = 204; + /// Event ID for general execution progress messages. public const int Execute = 205; + /// Event ID for gas burn operations. public const int Burn = 300; + /// Event ID for interop or contract calls. public const int Call = 301; + /// Event ID for runtime notify events. public const int Notify = 302; + /// Event ID for runtime log events. public const int Log = 303; + /// Event ID for block persist start. public const int Persist = 400; + /// Event ID for block persist completion. public const int PostPersist = 401; + /// Event ID for storage put operations. public const int StoragePut = 500; + /// Event ID for storage get operations. public const int StorageGet = 501; + /// Event ID for storage find/query operations. public const int StorageFind = 502; + /// Event ID for storage delete operations. public const int StorageDelete = 503; + /// Event ID for iterator advance operations. public const int IteratorNext = 600; + /// Event ID for iterator value retrieval. public const int IteratorGet = 601; + /// Event ID for storage read-path diagnostics. public const int ReadStorage = 700; + /// Event ID for storage update-path diagnostics. public const int UpdateStorage = 701; } } diff --git a/src/Neo.VM/Middleware/DebuggerMiddleware.cs b/src/Neo.VM/Middleware/DebuggerMiddleware.cs index d7453cc..7f70bca 100644 --- a/src/Neo.VM/Middleware/DebuggerMiddleware.cs +++ b/src/Neo.VM/Middleware/DebuggerMiddleware.cs @@ -30,10 +30,22 @@ namespace Neo.VM.Middleware { + /// + /// Middleware that supports script-offset breakpoints and single-step execution. + /// When a breakpoint is hit or step mode advances, the engine enters . + /// + /// Service provider used to resolve the . + /// Logger for debug messages. public sealed class DebuggerMiddleware(IServiceProvider sp, ILogger logger) : IEngineMiddleware { + /// + /// Raised when a breakpoint or step stop occurs. + /// public event EventHandler? OnBreakpoint; + /// + /// Gets or sets whether the debugger pauses before each distinct instruction pointer. + /// public bool StepMode { get; set; } private static readonly decimal s_factor = 1_00000000m; @@ -43,12 +55,20 @@ public sealed class DebuggerMiddleware(IServiceProvider sp, ILogger _breakpoints = []; // script offset breakpoints private int _lastStepPosition = -1; + /// + /// Registers a breakpoint at the specified script offset. + /// + /// The instruction pointer value at which to break. public void AddBreakpoint(int scriptOffset) => _breakpoints.Add(scriptOffset); + /// + /// Resumes execution by clearing the state. + /// public void Continue() => Engine.State = VMState.NONE; + /// public void PreExecution(ExecutionDelegate next) { if (logger.IsEnabled(LogLevel.Debug)) @@ -61,6 +81,7 @@ public void PreExecution(ExecutionDelegate next) next(); } + /// public void PostExecution(ExecutionDelegate next) { StepMode = false; @@ -68,6 +89,7 @@ public void PostExecution(ExecutionDelegate next) next(); } + /// public void PreExecute(ExecutionContext? context, ExecuteDelegate next) { if (context is null) @@ -102,14 +124,22 @@ public void PreExecute(ExecutionContext? context, ExecuteDelegate next) next(context); } + /// public void PostExecute(ExecutionContext? context, ExecuteDelegate next) { next(context); } } + /// + /// Event arguments for debugger breakpoint notifications. + /// + /// The execution context at the break site. public class DebuggerEventArgs(ExecutionContext context) : EventArgs { + /// + /// Gets the execution context where the breakpoint was hit. + /// public ExecutionContext Context => context; } } diff --git a/src/Neo.VM/Middleware/ExecuteLoggerMiddleware.cs b/src/Neo.VM/Middleware/ExecuteLoggerMiddleware.cs index 7720941..574443a 100644 --- a/src/Neo.VM/Middleware/ExecuteLoggerMiddleware.cs +++ b/src/Neo.VM/Middleware/ExecuteLoggerMiddleware.cs @@ -29,17 +29,24 @@ namespace Neo.VM.Middleware { + /// + /// Middleware that logs VM startup, per-opcode execution (at trace level), and completion. + /// + /// Service provider used to resolve the . + /// Logger that receives execution messages. public sealed class ExecuteLoggerMiddleware(IServiceProvider sp, ILogger logger) : IEngineMiddleware { private static readonly decimal s_factor = 1_00000000m; private VirtualMachineEngine Engine => sp.GetRequiredService(); + /// public void PostExecute(ExecutionContext? context, ExecuteDelegate next) { next(context); } + /// public void PreExecution(ExecutionDelegate next) { var logLevel = LogLevel.Information; @@ -61,6 +68,7 @@ public void PreExecution(ExecutionDelegate next) next(); } + /// public void PreExecute(ExecutionContext? context, ExecuteDelegate next) { var logLevel = LogLevel.Trace; @@ -81,6 +89,7 @@ public void PreExecute(ExecutionContext? context, ExecuteDelegate next) next(context); } + /// public void PostExecution(ExecutionDelegate next) { var logLevel = LogLevel.Information; diff --git a/src/Neo.VM/Middleware/IEngineMiddleware.cs b/src/Neo.VM/Middleware/IEngineMiddleware.cs index df723a8..9d0c531 100644 --- a/src/Neo.VM/Middleware/IEngineMiddleware.cs +++ b/src/Neo.VM/Middleware/IEngineMiddleware.cs @@ -24,30 +24,47 @@ namespace Neo.VM.Middleware { + /// + /// Represents a middleware handler invoked around individual opcode execution. + /// + /// The current execution context, or when none is active. public delegate void ExecuteDelegate(ExecutionContext? context); + /// + /// Represents a middleware handler invoked around the entire VM run. + /// public delegate void ExecutionDelegate(); + /// + /// Defines middleware hooks that participate in the . + /// Implementations must invoke the provided next delegate to continue the chain. + /// public interface IEngineMiddleware { /// - /// Called when VM starts execution + /// Called when VM starts execution. /// + /// The next handler in the pre-execution chain. void PreExecution(ExecutionDelegate next); /// - /// Called when VM finishes execution (HALT, FAULT, etc.) + /// Called when VM finishes execution (HALT, FAULT, etc.). /// + /// The next handler in the post-execution chain. void PostExecution(ExecutionDelegate next); /// - /// Called before each opcode is executed + /// Called before each opcode is executed. /// + /// The current execution context, if any. + /// The next handler in the pre-execute chain. void PreExecute(ExecutionContext? context, ExecuteDelegate next); /// - /// Called after each opcode is executed + /// Called after each opcode is executed. /// + /// The current execution context, if any. + /// The next handler in the post-execute chain. void PostExecute(ExecutionContext? context, ExecuteDelegate next); } } diff --git a/src/Neo.VM/Types/VMArray.cs b/src/Neo.VM/Types/VMArray.cs index 5c1186e..c53689b 100644 --- a/src/Neo.VM/Types/VMArray.cs +++ b/src/Neo.VM/Types/VMArray.cs @@ -30,24 +30,56 @@ namespace Neo.VM.Types { + /// + /// Represents an ordered collection of items on the evaluation stack. + /// + /// The initial items in the array. + /// Whether the array is immutable after construction. public class VMArray(IEnumerable items, bool isReadonly) : VMObject, IEquatable, IEnumerable, IEnumerable, ICollection, IReadOnlyCollection, IList, IReadOnlyList { + /// public override VMObjectType Type => VMObjectType.Array; + /// + /// Gets the number of items in the array. + /// public int Count => Array.Count; + /// + /// Gets a value indicating whether the array is read-only. + /// public bool IsReadOnly => _isReadOnly; private bool _isReadOnly = isReadonly; + /// + /// The underlying list of items. + /// protected readonly List Array = [.. items]; + /// + /// Initializes an empty mutable array. + /// public VMArray() : this(false) { } + /// + /// Initializes an empty array with the specified mutability. + /// + /// Whether the array is immutable. public VMArray(bool isReadonly) : this([], isReadonly) { } + /// + /// Initializes a mutable array with the specified items. + /// + /// The initial items. public VMArray(IEnumerable items) : this(items, false) { } + /// + /// Gets or sets the item at the specified index. + /// + /// The zero-based index of the item. + /// The item at the specified index. + /// Thrown when setting a value on a read-only array. public VMObject this[int index] { get => Array[index]; @@ -58,6 +90,7 @@ public VMObject this[int index] } } + /// protected override void Dispose(bool disposing) { if (disposing) @@ -67,11 +100,13 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + /// public override int GetHashCode() { return AsSpan().ToHashCode(RefCount ^ 397); } + /// public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -79,6 +114,11 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as VMArray); } + /// + /// Determines whether this array is equal to another array. + /// + /// The other array. + /// if the arrays are equal; otherwise . public bool Equals(VMArray? other) { if (ReferenceEquals(other, this)) return true; @@ -101,9 +141,11 @@ public bool Equals(VMArray? other) #region IEnumerable + /// public IEnumerator GetEnumerator() => Array.GetEnumerator(); + /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -111,23 +153,42 @@ IEnumerator IEnumerable.GetEnumerator() => #region ICollection + /// + /// Adds an item to the end of the array. + /// + /// The item to add. + /// Thrown when the array is read-only. public void Add(VMObject item) { if (_isReadOnly) throw new InvalidOperationException(); Array.Add(item); } + /// + /// Removes all items from the array. + /// + /// Thrown when the array is read-only. public void Clear() { if (_isReadOnly) throw new InvalidOperationException(); Array.Clear(); } + /// + /// Determines whether the array contains the specified item. + /// + /// The item to locate. + /// if the item is found; otherwise . public bool Contains(VMObject item) { return Array.Contains(item); } + /// + /// Copies the items of the array to a destination array starting at the specified index. + /// + /// The destination array. + /// The zero-based index in at which copying begins. public void CopyTo(VMObject[] array, int arrayIndex) { ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex, nameof(arrayIndex)); @@ -136,6 +197,12 @@ public void CopyTo(VMObject[] array, int arrayIndex) Array.CopyTo(array, arrayIndex); } + /// + /// Removes the first occurrence of the specified item from the array. + /// + /// The item to remove. + /// if the item was removed; otherwise . + /// Thrown when the array is read-only. public bool Remove(VMObject item) { if (_isReadOnly) throw new InvalidOperationException(); @@ -146,17 +213,33 @@ public bool Remove(VMObject item) #region IList + /// + /// Returns the zero-based index of the first occurrence of the specified item. + /// + /// The item to locate. + /// The index of the item, or -1 if not found. public int IndexOf(VMObject item) { return Array.IndexOf(item); } + /// + /// Inserts an item into the array at the specified index. + /// + /// The zero-based index at which to insert. + /// The item to insert. + /// Thrown when the array is read-only. public void Insert(int index, VMObject item) { if (_isReadOnly) throw new InvalidOperationException(); Array.Insert(index, item); } + /// + /// Removes the item at the specified index. + /// + /// The zero-based index of the item to remove. + /// Thrown when the array is read-only. public void RemoveAt(int index) { if (_isReadOnly) throw new InvalidOperationException(); @@ -168,6 +251,10 @@ public void RemoveAt(int index) internal override IEnumerable GetChildren() => [.. Array]; + /// + /// Reverses the order of the items in the array. + /// + /// Thrown when the array is read-only. public void Reverse() { if (_isReadOnly) throw new InvalidOperationException(); @@ -178,12 +265,14 @@ public void Reverse() internal void SetAsReadOnly() => _isReadOnly = true; + /// public override VMObject Clone() { var objectMap = new Dictionary(ReferenceEqualityComparer.Instance); return Clone(objectMap); } + /// protected override VMObject CloneCore(Dictionary objectMap) { if (objectMap.TryGetValue(this, out var thisItem)) return thisItem; @@ -204,17 +293,20 @@ protected override VMObject CloneCore(Dictionary objectMap) return clone; } + /// public override bool GetBoolean() { return Array.Count > 0; } + /// [DoesNotReturn] public override BigInteger GetInteger() { throw new InvalidOperationException(); } + /// protected override ReadOnlySpan ComputeSpan(HashSet visited) { var result = new List(); diff --git a/src/Neo.VM/Types/VMBoolean.cs b/src/Neo.VM/Types/VMBoolean.cs index d809cad..5e740ae 100644 --- a/src/Neo.VM/Types/VMBoolean.cs +++ b/src/Neo.VM/Types/VMBoolean.cs @@ -28,12 +28,22 @@ namespace Neo.VM.Types { + /// + /// Represents a boolean stack item. + /// + /// The boolean value. public class VMBoolean(bool value) : VMObject, IEquatable { + /// public override VMObjectType Type => VMObjectType.Boolean; private readonly bool _value = value; + /// + /// Determines whether this boolean is equal to another boolean stack item. + /// + /// The other boolean. + /// if the values are equal; otherwise . public bool Equals(VMBoolean? other) { if (ReferenceEquals(other, this)) return true; @@ -42,6 +52,7 @@ public bool Equals(VMBoolean? other) return _value == other._value; } + /// public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -49,17 +60,20 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as VMBoolean); } + /// public override int GetHashCode() { return _value ? 1 : 0; } + /// [return: MaybeNull] public override string? ToString() { return _value.ToString(); } + /// public override VMObject Clone() { var clone = new VMBoolean(_value); @@ -67,16 +81,19 @@ public override VMObject Clone() return clone; } + /// public override bool GetBoolean() { return _value; } + /// public override BigInteger GetInteger() { return _value ? BigInteger.One : BigInteger.Zero; } + /// protected override ReadOnlySpan ComputeSpan(HashSet visited) { return _value ? [1] : [0]; diff --git a/src/Neo.VM/Types/VMBuffer.cs b/src/Neo.VM/Types/VMBuffer.cs index 115c932..84cd6e0 100644 --- a/src/Neo.VM/Types/VMBuffer.cs +++ b/src/Neo.VM/Types/VMBuffer.cs @@ -32,15 +32,26 @@ namespace Neo.VM.Types { + /// + /// Represents a mutable byte buffer stack item backed by pooled memory. + /// public class VMBuffer : VMObject, IEquatable { + /// public override VMObjectType Type => VMObjectType.Buffer; + /// + /// Gets the length of the buffer in bytes. + /// public int Length => _byteCount; private readonly IMemoryOwner _memoryOwner; private readonly int _byteCount; + /// + /// Initializes a zero-filled buffer of the specified size. + /// + /// The buffer size in bytes. public VMBuffer(int size) { ArgumentOutOfRangeException.ThrowIfNegative(size); @@ -48,6 +59,10 @@ public VMBuffer(int size) _memoryOwner = MemoryPool.Shared.Rent(_byteCount); } + /// + /// Initializes a buffer by copying the specified data. + /// + /// The source bytes. public VMBuffer(ReadOnlySpan data) { _byteCount = data.Length; @@ -55,6 +70,10 @@ public VMBuffer(ReadOnlySpan data) data.TryCopyTo(_memoryOwner.Memory.Span); } + /// + /// Initializes a buffer by copying the specified data. + /// + /// The source bytes. public VMBuffer(Span data) { _byteCount = data.Length; @@ -62,6 +81,10 @@ public VMBuffer(Span data) data.TryCopyTo(_memoryOwner.Memory.Span); } + /// + /// Initializes a buffer by copying the specified data. + /// + /// The source bytes. public VMBuffer(byte[] data) { _byteCount = data.Length; @@ -69,6 +92,10 @@ public VMBuffer(byte[] data) data.AsMemory().TryCopyTo(_memoryOwner.Memory); } + /// + /// Initializes a buffer by copying the contents of a . + /// + /// The source byte array stack item. public VMBuffer(VMByteArray source) { _byteCount = source.Length; @@ -76,6 +103,10 @@ public VMBuffer(VMByteArray source) source.AsSpan().TryCopyTo(_memoryOwner.Memory.Span); } + /// + /// Initializes a buffer by copying the specified memory. + /// + /// The source memory. public VMBuffer(Memory source) { _byteCount = source.Length; @@ -83,6 +114,11 @@ public VMBuffer(Memory source) source.TryCopyTo(_memoryOwner.Memory); } + /// + /// Determines whether this buffer is equal to another buffer. + /// + /// The other buffer. + /// if the contents are equal; otherwise . public bool Equals([NotNullWhen(true)] VMBuffer? other) { if (ReferenceEquals(other, this)) return true; @@ -94,6 +130,7 @@ public bool Equals([NotNullWhen(true)] VMBuffer? other) .SequenceEqual(other._memoryOwner.Memory[..other._byteCount].Span); } + /// public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -101,18 +138,21 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as VMBuffer); } + /// protected override void Dispose(bool disposing) { _memoryOwner.Dispose(); base.Dispose(disposing); } + /// public override int GetHashCode() { return _memoryOwner.Memory[.._byteCount] .ToHashCode(RefCount ^ 397); } + /// public override string ToString() { foreach (var v in _memoryOwner.Memory[.._byteCount].Span) @@ -124,17 +164,28 @@ public override string ToString() return CoreUtilities.StrictUtf8Encoding.GetString(_memoryOwner.Memory[.._byteCount].Span); } + /// + /// Copies a range of bytes from this buffer into another buffer. + /// + /// The destination buffer. + /// The starting index in this buffer. + /// The starting index in the destination buffer. + /// The number of bytes to copy. public void CopyTo(VMBuffer dstBuffer, int startIndex, int dstIndex, int count) { _memoryOwner.Memory.Slice(startIndex, count) .CopyTo(dstBuffer._memoryOwner.Memory[dstIndex..]); } + /// + /// Reverses the order of bytes in this buffer. + /// public void Reverse() { _memoryOwner.Memory[.._byteCount].Span.Reverse(); } + /// public override VMObject Clone() { var clone = new VMBuffer(_memoryOwner.Memory[.._byteCount].ToArray()); @@ -142,11 +193,13 @@ public override VMObject Clone() return clone; } + /// public override bool GetBoolean() { return !_memoryOwner.Memory[.._byteCount].IsEmpty; } + /// public override BigInteger GetInteger() { var span = _memoryOwner.Memory[.._byteCount].Span; @@ -158,17 +211,27 @@ public override BigInteger GetInteger() return new(span); } + /// protected override ReadOnlySpan ComputeSpan(HashSet visited) { return _memoryOwner.Memory[.._byteCount].Span; } + /// + /// Gets or sets the byte at the specified index. + /// + /// The zero-based byte index. + /// The byte at the specified index. public byte this[int index] { get => _memoryOwner.Memory[.._byteCount].Span[index]; set => _memoryOwner.Memory[.._byteCount].Span[index] = value; } + /// + /// Returns the buffer contents as a lowercase hexadecimal string. + /// + /// The hex representation of the buffer. public string ToHexString() { return Convert.ToHexStringLower(_memoryOwner.Memory[.._byteCount].Span); @@ -195,9 +258,15 @@ public string ToHexString() return a.Equals(b); } + /// + /// Inequality comparison. + /// public static bool operator !=(VMBuffer a, VMBuffer b) => !(a == b); + /// + /// Converts a buffer to a using up to bytes. + /// public static implicit operator BigInteger(VMBuffer value) { return new BigInteger(value._memoryOwner.Memory[..value._byteCount] diff --git a/src/Neo.VM/Types/VMByteArray.cs b/src/Neo.VM/Types/VMByteArray.cs index 8e52255..9571d7f 100644 --- a/src/Neo.VM/Types/VMByteArray.cs +++ b/src/Neo.VM/Types/VMByteArray.cs @@ -32,15 +32,26 @@ namespace Neo.VM.Types { + /// + /// Represents an immutable byte-string stack item (Neo ByteString). + /// public class VMByteArray : VMObject, IEquatable { + /// public override VMObjectType Type => VMObjectType.ByteString; + /// + /// Gets the length of the byte string in bytes. + /// public int Length => _byteCount; private readonly IMemoryOwner _memoryOwner; private readonly int _byteCount; + /// + /// Initializes a new byte string by copying the specified data. + /// + /// The source bytes. public VMByteArray(byte[] data) { _byteCount = data.Length; @@ -48,6 +59,10 @@ public VMByteArray(byte[] data) data.AsMemory().TryCopyTo(_memoryOwner.Memory); } + /// + /// Initializes a new byte string by copying the specified memory. + /// + /// The source memory. public VMByteArray(Memory data) { _byteCount = data.Length; @@ -55,6 +70,10 @@ public VMByteArray(Memory data) data.TryCopyTo(_memoryOwner.Memory); } + /// + /// Initializes a new byte string by copying the specified span. + /// + /// The source bytes. public VMByteArray(ReadOnlySpan data) { _byteCount = data.Length; @@ -62,6 +81,10 @@ public VMByteArray(ReadOnlySpan data) data.TryCopyTo(_memoryOwner.Memory.Span); } + /// + /// Initializes a new byte string by copying the specified span. + /// + /// The source bytes. public VMByteArray(Span data) { _byteCount = data.Length; @@ -69,6 +92,7 @@ public VMByteArray(Span data) data.TryCopyTo(_memoryOwner.Memory.Span); } + /// public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -76,18 +100,21 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as VMByteArray); } + /// public override int GetHashCode() { return _memoryOwner.Memory[.._byteCount] .ToHashCode(RefCount ^ 397); } + /// protected override void Dispose(bool disposing) { _memoryOwner.Dispose(); base.Dispose(disposing); } + /// public override string ToString() { foreach (var v in _memoryOwner.Memory[.._byteCount].Span) @@ -99,6 +126,7 @@ public override string ToString() return CoreUtilities.StrictUtf8Encoding.GetString(_memoryOwner.Memory[.._byteCount].Span); } + /// public override VMObject Clone() { var clone = new VMByteArray(_memoryOwner.Memory[.._byteCount].ToArray()); @@ -106,11 +134,13 @@ public override VMObject Clone() return clone; } + /// public override bool GetBoolean() { return !_memoryOwner.Memory[.._byteCount].IsEmpty; } + /// public override BigInteger GetInteger() { var span = _memoryOwner.Memory[.._byteCount].Span; @@ -121,6 +151,7 @@ public override BigInteger GetInteger() return new(span); } + /// protected override ReadOnlySpan ComputeSpan(HashSet visited) { return _memoryOwner.Memory[.._byteCount].Span; @@ -135,11 +166,20 @@ public byte this[int index] set => _memoryOwner.Memory[.._byteCount].Span[index] = value; } + /// + /// Returns the byte string as a lowercase hexadecimal string. + /// + /// The hex representation of the data. public string ToHexString() { return Convert.ToHexStringLower(_memoryOwner.Memory[.._byteCount].Span); } + /// + /// Determines whether this byte string is equal to another. + /// + /// The other byte string. + /// if the contents are equal; otherwise . public bool Equals([NotNullWhen(true)] VMByteArray? other) { if (ReferenceEquals(other, this)) return true; @@ -172,6 +212,9 @@ .. b._memoryOwner.Memory[..b._byteCount].Span return a.Equals(b); } + /// + /// Inequality comparison. + /// public static bool operator !=(VMByteArray a, VMByteArray b) => !(a == b); } diff --git a/src/Neo.VM/Types/VMInteger.cs b/src/Neo.VM/Types/VMInteger.cs index abcffa9..a930d43 100644 --- a/src/Neo.VM/Types/VMInteger.cs +++ b/src/Neo.VM/Types/VMInteger.cs @@ -28,14 +28,26 @@ namespace Neo.VM.Types { + /// + /// Represents an arbitrary-precision integer stack item, limited to bytes. + /// public class VMInteger : VMObject, IEquatable { + /// + /// The maximum allowed size of the integer in bytes. + /// public const int MaxSize = 32; + /// public override VMObjectType Type => VMObjectType.Integer; private readonly BigInteger _value = BigInteger.Zero; + /// + /// Initializes a new integer stack item. + /// + /// The integer value. + /// Thrown when exceeds bytes. public VMInteger(BigInteger value) { if (value.GetByteCount() > MaxSize) @@ -43,11 +55,13 @@ public VMInteger(BigInteger value) _value = value; } + /// protected override void Dispose(bool disposing) { base.Dispose(disposing); } + /// public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -55,11 +69,13 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as VMInteger); } + /// public override int GetHashCode() { return _value.GetHashCode(); } + /// public override VMObject Clone() { var clone = new VMInteger(_value); @@ -67,21 +83,29 @@ public override VMObject Clone() return clone; } + /// public override bool GetBoolean() { return _value != BigInteger.Zero; } + /// public override BigInteger GetInteger() { return _value; } + /// protected override ReadOnlySpan ComputeSpan(HashSet visited) { return _value.ToByteArray(); } + /// + /// Determines whether this integer is equal to another integer stack item. + /// + /// The other integer. + /// if the values are equal; otherwise . public bool Equals([NotNullWhen(true)] VMInteger? other) { if (ReferenceEquals(other, this)) return true; @@ -90,6 +114,9 @@ public bool Equals([NotNullWhen(true)] VMInteger? other) return _value == other._value; } + /// + /// Adds two integer stack items. + /// public static VMInteger operator +(VMInteger a, VMInteger b) { var result = new VMInteger(a._value + b._value); @@ -97,6 +124,9 @@ public bool Equals([NotNullWhen(true)] VMInteger? other) return result; } + /// + /// Subtracts two integer stack items. + /// public static VMInteger operator -(VMInteger a, VMInteger b) { var result = new VMInteger(a._value - b._value); @@ -104,6 +134,9 @@ public bool Equals([NotNullWhen(true)] VMInteger? other) return result; } + /// + /// Multiplies two integer stack items. + /// public static VMInteger operator *(VMInteger a, VMInteger b) { var result = new VMInteger(a._value * b._value); @@ -111,6 +144,10 @@ public bool Equals([NotNullWhen(true)] VMInteger? other) return result; } + /// + /// Divides two integer stack items. + /// + /// Thrown when the divisor is zero. public static VMInteger operator /(VMInteger a, VMInteger b) { if (b._value == BigInteger.Zero) @@ -121,6 +158,9 @@ public bool Equals([NotNullWhen(true)] VMInteger? other) return result; } + /// + /// Computes the remainder of dividing two integer stack items. + /// public static VMInteger operator %(VMInteger a, VMInteger b) { var result = new VMInteger(a._value % b._value); @@ -128,31 +168,55 @@ public bool Equals([NotNullWhen(true)] VMInteger? other) return result; } + /// + /// Negates an integer stack item. + /// public static VMInteger operator -(VMInteger a) => new(-a._value); // Comparison operators + /// + /// Determines whether two integer stack items are equal. + /// public static bool operator ==(VMInteger a, VMInteger b) { if (ReferenceEquals(a, b)) return true; return a.Equals(b); } + /// + /// Determines whether two integer stack items are not equal. + /// public static bool operator !=(VMInteger a, VMInteger b) => !(a == b); + /// + /// Determines whether the first integer is greater than the second. + /// public static bool operator >(VMInteger a, VMInteger b) => a._value > b._value; + /// + /// Determines whether the first integer is less than the second. + /// public static bool operator <(VMInteger a, VMInteger b) => a._value < b._value; + /// + /// Determines whether the first integer is greater than or equal to the second. + /// public static bool operator >=(VMInteger a, VMInteger b) => a._value >= b._value; + /// + /// Determines whether the first integer is less than or equal to the second. + /// public static bool operator <=(VMInteger a, VMInteger b) => a._value <= b._value; + /// + /// Converts a to a . + /// public static implicit operator BigInteger(VMInteger value) { return value.GetInteger(); diff --git a/src/Neo.VM/Types/VMInteropInterface.cs b/src/Neo.VM/Types/VMInteropInterface.cs index 25d00fd..8b0ecf0 100644 --- a/src/Neo.VM/Types/VMInteropInterface.cs +++ b/src/Neo.VM/Types/VMInteropInterface.cs @@ -30,20 +30,46 @@ namespace Neo.VM.Types { + /// + /// Represents an interop interface stack item that wraps a CLR object. + /// + /// The underlying object. + /// A display name for the interface type. public class VMInteropInterface(object obj, string name) : VMObject, IEquatable { + /// public override VMObjectType Type => VMObjectType.Interop; private readonly object _underlyingObject = obj; private readonly string _interfaceName = name; + /// + /// Gets the underlying CLR object. + /// public object UnderlyingObject => _underlyingObject; + + /// + /// Gets the interface name associated with this wrapper. + /// public string InterfaceName => _interfaceName; + /// + /// Initializes a new interop interface wrapping a default . + /// public VMInteropInterface() : this(new()) { } + /// + /// Initializes a new interop interface wrapping the specified object. + /// The interface name defaults to the object's type name. + /// + /// The object to wrap. public VMInteropInterface(object obj) : this(obj, obj.GetType().Name) { } + /// + /// Determines whether this interop item is equal to another. + /// + /// The other interop item. + /// if the underlying objects and names match; otherwise . public bool Equals(VMInteropInterface? other) { if (ReferenceEquals(other, this)) return true; @@ -53,6 +79,7 @@ public bool Equals(VMInteropInterface? other) string.Equals(_interfaceName, other._interfaceName); } + /// public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -60,17 +87,20 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as VMInteropInterface); } + /// public override int GetHashCode() { return HashCode.Combine(RefCount, _underlyingObject, _interfaceName); } + /// protected override void Dispose(bool disposing) { // Do not dispose the underlying object - it's managed externally base.Dispose(disposing); } + /// public override VMObject Clone() { var clone = new VMInteropInterface(_underlyingObject, _interfaceName); @@ -78,6 +108,7 @@ public override VMObject Clone() return clone; } + /// protected override ReadOnlySpan ComputeSpan(HashSet visited) { var ptr = nint.Zero; @@ -107,6 +138,7 @@ protected override ReadOnlySpan ComputeSpan(HashSet visited) } } + /// [DoesNotReturn] public override BigInteger GetInteger() { @@ -114,11 +146,18 @@ public override BigInteger GetInteger() throw new InvalidOperationException($"Cannot convert InteropInterface ({_interfaceName}) to integer"); } + /// public override bool GetBoolean() { return _underlyingObject != null; } + /// + /// Casts the underlying object to the specified type. + /// + /// The target type. + /// The underlying object as . + /// Thrown when the underlying object is not assignable to . public T CastTo() { if (_underlyingObject is T t) @@ -127,6 +166,13 @@ public T CastTo() throw new InvalidCastException($"This {_interfaceName} can't be casted to type {typeof(T)}."); } + /// + /// Invokes a public method on the underlying object by name. + /// + /// The expected return type (must be a reference type). + /// The name of the method to invoke. + /// The method arguments. + /// The method result cast to , or if the method is missing or returns a different type. public TResult? CallMethod(string methodName, params object[] args) where TResult : class? { diff --git a/src/Neo.VM/Types/VMMap.cs b/src/Neo.VM/Types/VMMap.cs index cf7d2ef..d556c93 100644 --- a/src/Neo.VM/Types/VMMap.cs +++ b/src/Neo.VM/Types/VMMap.cs @@ -31,27 +31,54 @@ namespace Neo.VM.Types { + /// + /// Represents a key/value map stack item. Keys are limited to bytes. + /// public class VMMap : VMObject, IEquatable, IDictionary, IReadOnlyDictionary { + /// + /// The maximum allowed size of a map key in bytes. + /// public const int MaxKeySize = 64; + /// public override VMObjectType Type => VMObjectType.Map; + /// + /// Gets the number of key/value pairs in the map. + /// public int Count => _map.Count; + /// + /// Gets a collection containing the keys of the map. + /// public ICollection Keys => _map.Keys; + /// + /// Gets a collection containing the values of the map. + /// public ICollection Values => _map.Values; + /// + /// Gets a value indicating whether the map is read-only. + /// public bool IsReadOnly => _isReadOnly; + /// IEnumerable IReadOnlyDictionary.Keys => Keys; + /// IEnumerable IReadOnlyDictionary.Values => Values; private readonly Dictionary _map = []; private bool _isReadOnly = false; + /// + /// Gets or sets the value associated with the specified key. + /// + /// The key of the value to get or set. + /// The value associated with the specified key. + /// Thrown when the key exceeds . public VMObject this[VMObject key] { get @@ -63,13 +90,25 @@ public VMObject this[VMObject key] set => Add(key, value); } + /// + /// Initializes an empty mutable map. + /// public VMMap() { } + /// + /// Initializes an empty map with the specified mutability. + /// + /// Whether the map is immutable. public VMMap(bool isReadOnly) { _isReadOnly = isReadOnly; } + /// + /// Initializes a map from the specified key/value pairs. + /// + /// The initial entries. + /// Whether the map is immutable after construction. public VMMap(IEnumerable> items, bool isReadOnly = false) { foreach (var kvp in items) @@ -78,6 +117,11 @@ public VMMap(IEnumerable> items, bool isReadOnl _isReadOnly = isReadOnly; } + /// + /// Initializes a map by pairing consecutive items as key/value pairs. + /// + /// A flat list of alternating keys and values. + /// Whether the map is immutable after construction. public VMMap(IList items, bool isReadOnly = false) { for (var i = 0; i < items.Count - 2; i += 2) @@ -86,6 +130,7 @@ public VMMap(IList items, bool isReadOnly = false) _isReadOnly = isReadOnly; } + /// protected override void Dispose(bool disposing) { if (disposing) @@ -95,6 +140,11 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + /// + /// Determines whether this map is equal to another map. + /// + /// The other map. + /// if the maps are equal; otherwise . public bool Equals(VMMap? other) { if (ReferenceEquals(other, this)) return true; @@ -118,6 +168,7 @@ public bool Equals(VMMap? other) return false; } + /// public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -125,11 +176,19 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as VMMap); } + /// public override int GetHashCode() { return AsSpan().ToHashCode(RefCount * 397); } + /// + /// Adds or replaces the value for the specified key. + /// + /// The key to add. + /// The value to associate with the key. + /// Thrown when the key exceeds . + /// Thrown when the map is read-only. public void Add(VMObject key, VMObject value) { if (key.Size > MaxKeySize) @@ -141,6 +200,12 @@ public void Add(VMObject key, VMObject value) _map[key] = value; } + /// + /// Determines whether the map contains the specified key. + /// + /// The key to locate. + /// if the key is present; otherwise . + /// Thrown when the key exceeds . public bool ContainsKey(VMObject key) { if (key.Size > MaxKeySize) @@ -149,6 +214,13 @@ public bool ContainsKey(VMObject key) return key is not VMNull && _map.ContainsKey(key); } + /// + /// Removes the entry with the specified key. + /// + /// The key of the entry to remove. + /// if the entry was removed; otherwise . + /// Thrown when the key exceeds . + /// Thrown when the map is read-only. public bool Remove(VMObject key) { if (key.Size > MaxKeySize) @@ -164,6 +236,13 @@ public bool Remove(VMObject key) return _map.Remove(key); } + /// + /// Attempts to get the value associated with the specified key. + /// + /// The key to locate. + /// When this method returns, the value associated with the key, if found. + /// if the key was found; otherwise . + /// Thrown when the key exceeds . public bool TryGetValue(VMObject key, [MaybeNullWhen(false)] out VMObject value) { if (key.Size > MaxKeySize) @@ -172,11 +251,19 @@ public bool TryGetValue(VMObject key, [MaybeNullWhen(false)] out VMObject value) return _map.TryGetValue(key, out value); } + /// + /// Adds the specified key/value pair to the map. + /// + /// The pair to add. public void Add(KeyValuePair item) { Add(item.Key, item.Value); } + /// + /// Removes all entries from the map. + /// + /// Thrown when the map is read-only. public void Clear() { if (_isReadOnly) @@ -184,6 +271,12 @@ public void Clear() _map.Clear(); } + /// + /// Determines whether the map contains the specified key/value pair. + /// + /// The pair to locate. + /// if the pair is present; otherwise . + /// Thrown when the key exceeds . public bool Contains(KeyValuePair item) { if (item.Key.Size > MaxKeySize) @@ -192,6 +285,11 @@ public bool Contains(KeyValuePair item) return ContainsKey(item.Key) && _map.Values.Any(a => a == item.Value); } + /// + /// Copies the map entries to an array starting at the specified index. + /// + /// The destination array. + /// The zero-based index in at which copying begins. public void CopyTo(KeyValuePair[] array, int arrayIndex) { ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex, nameof(arrayIndex)); @@ -200,6 +298,12 @@ public void CopyTo(KeyValuePair[] array, int arrayIndex) _map.ToArray().CopyTo(array, arrayIndex); } + /// + /// Removes the entry with the specified key from the map. + /// + /// The pair whose key identifies the entry to remove. + /// if the entry was removed; otherwise . + /// Thrown when the key exceeds . public bool Remove(KeyValuePair item) { if (item.Key.Size > MaxKeySize) @@ -217,21 +321,25 @@ internal override IEnumerable GetChildren() } } + /// public IEnumerator> GetEnumerator() => _map.GetEnumerator(); + /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); internal void SetAsReadOnly() => _isReadOnly = true; + /// public override VMObject Clone() { var objectMap = new Dictionary(ReferenceEqualityComparer.Instance); return Clone(objectMap); } + /// protected override VMObject CloneCore(Dictionary objectMap) { if (objectMap.TryGetValue(this, out var thisItem)) return thisItem; @@ -249,6 +357,7 @@ protected override VMObject CloneCore(Dictionary objectMap) return clone; } + /// protected override ReadOnlySpan ComputeSpan(HashSet visited) { var result = new List(); @@ -262,9 +371,11 @@ protected override ReadOnlySpan ComputeSpan(HashSet visited) return result.ToArray(); } + /// public override bool GetBoolean() => _map.Count > 0; + /// [DoesNotReturn] public override BigInteger GetInteger() { diff --git a/src/Neo.VM/Types/VMNull.cs b/src/Neo.VM/Types/VMNull.cs index 4973a5a..13bdc15 100644 --- a/src/Neo.VM/Types/VMNull.cs +++ b/src/Neo.VM/Types/VMNull.cs @@ -27,18 +27,27 @@ namespace Neo.VM.Types { + /// + /// Represents a null stack item. All null values compare equal. + /// public class VMNull : VMObject, IEquatable { + /// public override VMObjectType Type => VMObjectType.Any; + /// + /// Gets a new null stack item instance. + /// public static VMNull Instance => new(); + /// protected override void Dispose(bool disposing) { // Never dispose the singleton instance base.Dispose(disposing); } + /// public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -46,39 +55,55 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as VMNull); } + /// + /// Determines whether this null item is equal to another null item. + /// + /// The other null item. + /// Always for any comparison. public bool Equals([NotNullWhen(true)] VMNull? other) { return true; } + /// public override int GetHashCode() { return 0; } + /// public override VMObject Clone() { return this; } + /// public override bool GetBoolean() { return false; } + /// public override BigInteger GetInteger() { return BigInteger.Zero; } + /// public override string ToString() { return "\0"; } + /// + /// Always returns ; all null stack items are equal. + /// public static bool operator ==(VMNull left, VMNull right) => true; + /// + /// Always returns ; all null stack items are equal. + /// public static bool operator !=(VMNull left, VMNull right) => false; diff --git a/src/Neo.VM/Types/VMObject.cs b/src/Neo.VM/Types/VMObject.cs index c569ff2..550b284 100644 --- a/src/Neo.VM/Types/VMObject.cs +++ b/src/Neo.VM/Types/VMObject.cs @@ -32,12 +32,25 @@ namespace Neo.VM.Types { + /// + /// Base type for all NeoVM stack items. + /// Provides equality, cloning, byte serialization, reference counting, and conversion helpers. + /// public abstract class VMObject : IVMComponent, IEquatable { + /// + /// Gets the NeoVM stack-item type of this object. + /// public virtual VMObjectType Type => VMObjectType.Any; + /// + /// Gets the byte length of this object's serialized representation. + /// public int Size => AsSpan().Length; + /// + /// Gets the current reference count for this stack item. + /// public int RefCount => _refCount; private bool _disposed = false; @@ -45,6 +58,7 @@ public abstract class VMObject : IVMComponent, IEquatable #region IEquatable + /// public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -52,6 +66,11 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as VMObject); } + /// + /// Determines whether this object is equal to another stack item. + /// + /// The other stack item. + /// if the items are equal; otherwise . public bool Equals([NotNullWhen(true)] VMObject? other) { if (other is null) return false; @@ -61,6 +80,7 @@ public bool Equals([NotNullWhen(true)] VMObject? other) return AsSpan().SequenceEqual(other.AsSpan()); } + /// public override int GetHashCode() { return AsSpan().ToHashCode(RefCount ^ 397); @@ -70,6 +90,10 @@ public override int GetHashCode() #region IDisposable + /// + /// Releases managed and unmanaged resources used by this instance. + /// + /// when called from ; otherwise . protected virtual void Dispose(bool disposing) { if (!_disposed) @@ -82,12 +106,18 @@ protected virtual void Dispose(bool disposing) } } + /// + /// Releases resources used by this stack item. + /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } + /// + /// Finalizer that releases unmanaged resources if was not called. + /// ~VMObject() { Dispose(false); @@ -116,6 +146,10 @@ internal void Release() #endregion + /// + /// Determines whether this object graph contains a circular reference. + /// + /// if a cycle is detected; otherwise . public bool HasCircularReference() { var visited = new HashSet(ReferenceEqualityComparer.Instance); @@ -126,8 +160,9 @@ internal virtual IEnumerable GetChildren() => []; /// - /// Returns a deep clone of this object + /// Returns a deep clone of this object. /// + /// A new stack item with the same value. public abstract VMObject Clone(); /// @@ -150,8 +185,9 @@ protected virtual VMObject CloneCore(Dictionary objectMap) } /// - /// Converts this object to a (for serialization/storage) + /// Converts this object to a of bytes for serialization or storage. /// + /// The byte representation of this object. public ReadOnlySpan AsSpan() { var visited = new HashSet(ReferenceEqualityComparer.Instance); @@ -184,16 +220,20 @@ protected virtual ReadOnlySpan ComputeSpan(HashSet visited) } /// - /// Converts this object to a signed integer (if possible) + /// Converts this object to a signed integer, if supported by the concrete type. /// + /// The integer representation of this object. + /// Thrown when the type cannot be converted to an integer. public virtual BigInteger GetInteger() { throw new InvalidOperationException($"Cannot convert {Type} to integer"); } /// - /// Converts this object to a boolean + /// Converts this object to a boolean. + /// Non-null stack items are treated as by default. /// + /// The boolean representation of this object. public virtual bool GetBoolean() { return true; // NeoVM treats most non-null objects as true @@ -221,87 +261,115 @@ private static bool DetectCycle(VMObject current, HashSet visited) return false; } + /// Converts a to a . public static implicit operator VMObject(byte value) => new VMInteger(value); + /// Converts an to a . public static implicit operator VMObject(sbyte value) => new VMInteger(value); + /// Converts a to a . public static implicit operator VMObject(short value) => new VMInteger(value); + /// Converts a to a . public static implicit operator VMObject(ushort value) => new VMInteger(value); + /// Converts an to a . public static implicit operator VMObject(int value) => new VMInteger(value); + /// Converts a to a . public static implicit operator VMObject(uint value) => new VMInteger(value); + /// Converts a to a . public static implicit operator VMObject(long value) => new VMInteger(value); + /// Converts a to a . public static implicit operator VMObject(ulong value) => new VMInteger(value); + /// Converts an to a . public static implicit operator VMObject(Int128 value) => new VMInteger(value); + /// Converts a to a . public static implicit operator VMObject(UInt128 value) => new VMInteger(value); + /// Converts a to a . public static implicit operator VMObject(BigInteger value) => new VMInteger(value); + /// Converts a to a . public static implicit operator VMObject(bool value) => new VMBoolean(value); + /// Converts a byte array to a . public static implicit operator VMObject(byte[] value) => new VMByteArray(value); + /// Converts a UTF-8 string to a . public static implicit operator VMObject(string value) => new VMByteArray(CoreUtilities.StrictUtf8Encoding.GetBytes(value)); + /// Converts a stack item to a via . public static implicit operator byte(VMObject value) => (byte)value.GetInteger(); + /// Converts a stack item to an via . public static implicit operator sbyte(VMObject value) => (sbyte)value.GetInteger(); + /// Converts a stack item to a via integer conversion. public static implicit operator short(VMObject value) => new VMInteger(value); + /// Converts a stack item to a via . public static implicit operator ushort(VMObject value) => (ushort)value.GetInteger(); + /// Converts a stack item to an via . public static implicit operator int(VMObject value) => (int)value.GetInteger(); + /// Converts a stack item to a via . public static implicit operator uint(VMObject value) => (uint)value.GetInteger(); + /// Converts a stack item to a via . public static implicit operator long(VMObject value) => (long)value.GetInteger(); + /// Converts a stack item to a via . public static implicit operator ulong(VMObject value) => (ulong)value.GetInteger(); + /// Converts a stack item to an via . public static implicit operator Int128(VMObject value) => (Int128)value.GetInteger(); + /// Converts a stack item to a via . public static implicit operator UInt128(VMObject value) => (UInt128)value.GetInteger(); + /// Converts a stack item to a via . public static implicit operator BigInteger(VMObject value) => value.GetInteger(); + /// Converts a stack item to a via . public static implicit operator bool(VMObject value) => value.GetBoolean(); + /// Converts a stack item to a byte array via . public static implicit operator byte[](VMObject value) => [.. value.AsSpan()]; + /// Converts a stack item to its string representation. public static implicit operator string(VMObject value) => $"{value}"; } diff --git a/src/Neo.VM/Types/VMPointer.cs b/src/Neo.VM/Types/VMPointer.cs index e94aa10..9283b70 100644 --- a/src/Neo.VM/Types/VMPointer.cs +++ b/src/Neo.VM/Types/VMPointer.cs @@ -31,20 +31,38 @@ namespace Neo.VM.Types { + /// + /// Represents a code pointer stack item: a script image plus an instruction position. + /// public class VMPointer : VMObject, IEquatable { + /// public override VMObjectType Type => VMObjectType.Pointer; + /// + /// Gets the script bytes associated with this pointer. + /// public ReadOnlyMemory Script => _memoryOwner.Memory[.._byteCount]; + /// + /// Gets the length of the script in bytes. + /// public int Length => _byteCount; + /// + /// Gets the instruction position within the script. + /// public int Position => _ip; private readonly IMemoryOwner _memoryOwner; private readonly int _ip = 0; private readonly int _byteCount; + /// + /// Initializes a new pointer from a script array and instruction position. + /// + /// The script bytecode. + /// The instruction pointer offset. public VMPointer(byte[] script, int ip) { _ip = ip; @@ -53,6 +71,11 @@ public VMPointer(byte[] script, int ip) script.AsMemory().TryCopyTo(_memoryOwner.Memory); } + /// + /// Initializes a new pointer from script memory and instruction position. + /// + /// The script bytecode. + /// The instruction pointer offset. public VMPointer(Memory script, int ip) { _ip = ip; @@ -61,6 +84,11 @@ public VMPointer(Memory script, int ip) script.TryCopyTo(_memoryOwner.Memory); } + /// + /// Initializes a new pointer from script memory and instruction position. + /// + /// The script bytecode. + /// The instruction pointer offset. public VMPointer(ReadOnlyMemory script, int ip) { _ip = ip; @@ -69,6 +97,11 @@ public VMPointer(ReadOnlyMemory script, int ip) script.TryCopyTo(_memoryOwner.Memory); } + /// + /// Initializes a new pointer from a script and instruction position. + /// + /// The script as a byte-string stack item. + /// The instruction pointer offset. public VMPointer(VMByteArray script, int ip) { _ip = ip; @@ -77,6 +110,11 @@ public VMPointer(VMByteArray script, int ip) script.AsSpan().TryCopyTo(_memoryOwner.Memory.Span); } + /// + /// Determines whether this pointer is equal to another pointer. + /// + /// The other pointer. + /// if the script and position match; otherwise . public bool Equals(VMPointer? other) { if (ReferenceEquals(other, this)) return true; @@ -89,6 +127,7 @@ public bool Equals(VMPointer? other) .SequenceEqual(other._memoryOwner.Memory[..other._byteCount].Span); } + /// public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(obj, this)) return true; @@ -96,6 +135,7 @@ public override bool Equals([NotNullWhen(true)] object? obj) return Equals(obj as VMPointer); } + /// public override int GetHashCode() { return (31 * _ip) ^ @@ -103,6 +143,7 @@ public override int GetHashCode() .ToHashCode(RefCount ^ 397); } + /// public override VMObject Clone() { var clone = new VMPointer(_memoryOwner.Memory[.._byteCount].ToArray(), _ip); @@ -110,17 +151,20 @@ public override VMObject Clone() return clone; } + /// public override bool GetBoolean() { return true; } + /// [DoesNotReturn] public override BigInteger GetInteger() { throw new InvalidOperationException($"Cannot convert {Type} to integer"); } + /// protected override ReadOnlySpan ComputeSpan(HashSet visited) { return _memoryOwner.Memory[.._byteCount].Span; diff --git a/src/Neo.VM/Types/VMStruct.cs b/src/Neo.VM/Types/VMStruct.cs index c791d6a..0743395 100644 --- a/src/Neo.VM/Types/VMStruct.cs +++ b/src/Neo.VM/Types/VMStruct.cs @@ -25,12 +25,22 @@ namespace Neo.VM.Types { + /// + /// Represents a NeoVM structure: a value-type compound similar to + /// that deep-clones its fields when copied. + /// + /// The initial field values. public class VMStruct(IEnumerable items) : VMArray(items) { + /// public override VMObjectType Type => VMObjectType.Struct; + /// + /// Initializes an empty structure. + /// public VMStruct() : this([]) { } + /// protected override VMObject CloneCore(Dictionary objectMap) { if (objectMap.TryGetValue(this, out var thisItem)) return thisItem; diff --git a/src/Neo.VM/VirtualMachineEngine.cs b/src/Neo.VM/VirtualMachineEngine.cs index d5a5b7a..c61ba4d 100644 --- a/src/Neo.VM/VirtualMachineEngine.cs +++ b/src/Neo.VM/VirtualMachineEngine.cs @@ -38,12 +38,24 @@ namespace Neo.VM /// public partial class VirtualMachineEngine : IDisposable { + /// + /// Gets the current execution status of the VM. + /// public VMState State { get; internal set; } = VMState.NONE; + /// + /// Gets the total amount of gas consumed so far during execution. + /// public long GasConsumed => _maxGasConsumed; + /// + /// Gets the remaining gas available before the limit is reached. + /// public long GasLeft => _maxGasLimit - _maxGasConsumed; + /// + /// Gets the maximum gas allowed for this execution. + /// public long GasLimit => _maxGasLimit; /// @@ -71,8 +83,14 @@ public partial class VirtualMachineEngine : IDisposable /// public Stack ResultStack { get; } = []; + /// + /// Gets the exception that caused a , if any. + /// public Exception? FaultException { get; private set; } + /// + /// Gets the hard fork active for the current persisting block. + /// public HardFork ActiveFork => _currentFork; @@ -94,6 +112,15 @@ public partial class VirtualMachineEngine : IDisposable private long _maxGasConsumed; + /// + /// Initializes a new instance of the class. + /// + /// The block being persisted; when , an empty block is used. + /// The verifiable container (typically a transaction) that supplies the gas limit. + /// Protocol settings used to resolve the active hard fork; defaults to . + /// The opcode dispatch table; defaults to . + /// Execution limits; defaults to . + /// Middleware pipeline; defaults to . public VirtualMachineEngine( Block? persistingBlock = default, IVerifiable? container = default, @@ -112,11 +139,20 @@ public VirtualMachineEngine( _pipeline = pipeline ?? VirtualMachinePipeline.Empty; } + /// + /// Releases resources used by the engine. + /// public void Dispose() { GC.SuppressFinalize(this); } + /// + /// Loads a script as a new root and pushes it onto the invocation stack. + /// + /// The bytecode to execute. + /// Reserved for future use; the context starts at offset zero. + /// The loaded root execution context. public ExecutionContext LoadScript(byte[] script, int initialPosition = 0) { var activeFork = _protocolSettings.GetActiveHardFork(_persistingBlock.Index); @@ -170,6 +206,10 @@ internal virtual void UnloadedContext(ExecutionContext context) context.Cleanup(); } + /// + /// Runs the loaded script until completion, a fault, or a breakpoint. + /// + /// The final after execution. public VMState Execute() { _maxGasConsumed = 0; diff --git a/src/Neo.Wallet/ChainWallet.cs b/src/Neo.Wallet/ChainWallet.cs index 134616b..4d11b1f 100644 --- a/src/Neo.Wallet/ChainWallet.cs +++ b/src/Neo.Wallet/ChainWallet.cs @@ -33,8 +33,14 @@ namespace Neo.Wallet { + /// + /// Provides cryptographic helpers and file open operations for Neo wallets. + /// public static class ChainWallet { + /// + /// The maximum supported private key size in bytes. + /// public const int MaxPrivateKeySizeInBytes = 32; /// @@ -56,6 +62,14 @@ public static byte[] GetKeyFromWifString(string wifString) return decodedWifBytes[1..^1]; } + /// + /// Encrypts a private key into a NEP-2 string using the specified password and SCrypt parameters. + /// + /// The private key to encrypt. + /// The password used for key derivation. + /// The SCrypt parameters; uses when . + /// The address version used by the protocol (currently unused by the encryption payload). + /// The NEP-2 encoded private key string. public static string ToNep2String(byte[] privateKeyBytes, string password, ScryptParameters? scryptParameters = default, byte addressVersion = 53) { const int SCryptKeyLengthInBytes = 64; @@ -81,6 +95,15 @@ public static string ToNep2String(byte[] privateKeyBytes, string password, Scryp return Base58.EncodeCheck(nep2StringBytes); } + /// + /// Decrypts a NEP-2 string into a private key using the specified password and SCrypt parameters. + /// + /// The NEP-2 encoded private key string. + /// The password used for key derivation. + /// The SCrypt parameters used when the key was encrypted. + /// The address version used by the protocol (currently unused by the decryption payload). + /// The decrypted private key bytes. + /// Thrown when the NEP-2 payload is invalid. public static byte[] GetKeyFromNep2String(string nep2String, string password, ScryptParameters scryptParameters, byte addressVersion = 53) { ArgumentNullException.ThrowIfNullOrWhiteSpace(nep2String, nameof(nep2String)); @@ -112,6 +135,12 @@ public static byte[] GetKeyFromNep2String(string nep2String, string password, Sc return privateKeyBytes; } + /// + /// Decrypts ciphertext using AES in ECB mode with no padding. + /// + /// The encrypted data. + /// The AES key. + /// The decrypted plaintext bytes. public static byte[] AesDecryptECB(byte[] ciphertextBytes, byte[] keyBytes) { using var aes = Aes.Create(); @@ -123,6 +152,12 @@ public static byte[] AesDecryptECB(byte[] ciphertextBytes, byte[] keyBytes) return decryptor.TransformFinalBlock(ciphertextBytes, 0, ciphertextBytes.Length); } + /// + /// Encrypts plaintext using AES in ECB mode with no padding. + /// + /// The data to encrypt. + /// The AES key. + /// The encrypted ciphertext bytes. public static byte[] AesEncryptECB(byte[] plaintextBytes, byte[] keyBytes) { using var aes = Aes.Create(); @@ -134,9 +169,19 @@ public static byte[] AesEncryptECB(byte[] plaintextBytes, byte[] keyBytes) return encryptor.TransformFinalBlock(plaintextBytes, 0, plaintextBytes.Length); } + /// + /// Opens a development wallet from a JSON file. + /// + /// The wallet file to open. + /// The loaded . public static DevWallet OpenDevWalletFile(FileInfo file) => JsonModel.FromJson(file)!.ToObject(); + /// + /// Opens a NEP-6 wallet from a JSON file. + /// + /// The wallet file to open. + /// The loaded , or if unavailable. public static Nep6Wallet? OpenNep6WalletFie(FileInfo file) => JsonModel.FromJson(file)!.ToObject(); } diff --git a/src/Neo.Wallet/Cryptography/ScryptParameters.cs b/src/Neo.Wallet/Cryptography/ScryptParameters.cs index 646ceb0..b877c53 100644 --- a/src/Neo.Wallet/Cryptography/ScryptParameters.cs +++ b/src/Neo.Wallet/Cryptography/ScryptParameters.cs @@ -56,6 +56,10 @@ public class ScryptParameters(int n, int r, int p) : IMap /// public int P => p; + /// + /// Converts these parameters to their JSON model representation. + /// + /// A with the same N, R, and P values. public SCryptModel ToObject() => new() { diff --git a/src/Neo.Wallet/DevWallet.cs b/src/Neo.Wallet/DevWallet.cs index 4d1025c..85dcd48 100644 --- a/src/Neo.Wallet/DevWallet.cs +++ b/src/Neo.Wallet/DevWallet.cs @@ -34,26 +34,42 @@ namespace Neo.Wallet { + /// + /// An in-memory development wallet that stores private keys without NEP-2 encryption. + /// public class DevWallet : IWallet, IMap { private static readonly Version s_walletVersion = new(1, 0); + /// public Version Version => s_walletVersion; + /// public ScryptParameters SCrypt => _scryptParameters; + /// + /// Gets or sets the optional display name of the wallet. + /// public string? Name { get; set; } + /// public object? Extra => null; private readonly Dictionary _walletAccounts = []; private readonly ScryptParameters _scryptParameters; + /// + /// Initializes a new empty development wallet with default SCrypt parameters. + /// public DevWallet() { _scryptParameters = ScryptParameters.Default; } + /// + /// Initializes a development wallet from a JSON model. + /// + /// The model containing wallet data and accounts. public DevWallet(DevWalletModel devWalletModel) : this() { _scryptParameters = devWalletModel.SCrypt?.ToObject() ?? ScryptParameters.Default; @@ -66,15 +82,19 @@ public DevWallet(DevWalletModel devWalletModel) : this() } } + /// public override string ToString() => $"{ToObject()}"; + /// public bool Contains(UInt160 scriptHash) => _walletAccounts.ContainsKey(scriptHash); + /// public IWalletAccount CreateAccount(ProtocolSettings protocolSettings) => CreateAccount(RandomNumberGenerator.GetBytes(ChainWallet.MaxPrivateKeySizeInBytes), protocolSettings); + /// public IWalletAccount CreateAccount(byte[] privateKeyBytes, ProtocolSettings protocolSettings) { var newWalletAccount = new DevWalletAccount(privateKeyBytes, protocolSettings); @@ -85,9 +105,11 @@ public IWalletAccount CreateAccount(byte[] privateKeyBytes, Pr return _walletAccounts[newWalletAccount.ScriptHash] = newWalletAccount; } + /// public IWalletAccount CreateAccount(string wifString, ProtocolSettings protocolSettings) => CreateAccount(ChainWallet.GetKeyFromWifString(wifString), protocolSettings); + /// public IWalletAccount CreateAccount(UInt160 contractHash, ProtocolSettings protocolSettings) { var newWalletAccount = new DevWalletAccount(contractHash, protocolSettings); @@ -98,6 +120,12 @@ public IWalletAccount CreateAccount(UInt160 contractHash, Prot return _walletAccounts[newWalletAccount.ScriptHash] = newWalletAccount; } + /// + /// Creates an account from a development wallet account model and adds it to the wallet. + /// + /// The account model to import. + /// The created wallet account. + /// Thrown when the model address is . public IWalletAccount CreateAccount(DevWalletAccountModel walletAccountModel) { if (walletAccountModel.Address is null) @@ -106,9 +134,11 @@ public IWalletAccount CreateAccount(DevWalletAccountModel wall return _walletAccounts[walletAccountModel.Address] = walletAccountModel.ToObject(); } + /// public IWalletAccount CreateMultiSigAccount(ProtocolSettings protocolSettings, params ECPoint[] publicKeys) => CreateMultiSigAccount(protocolSettings, (int)Math.Ceiling((2 * publicKeys.Length + 1) / 3m), publicKeys); + /// public IWalletAccount CreateMultiSigAccount(ProtocolSettings protocolSettings, int m, params ECPoint[] publicKeys) { var newWalletAccount = new DevWalletAccount(m, publicKeys, protocolSettings); @@ -119,36 +149,45 @@ public IWalletAccount CreateMultiSigAccount(ProtocolSettings p return _walletAccounts[newWalletAccount.ScriptHash] = newWalletAccount; } + /// public bool RemoveAccount(UInt160 scriptHash) => _walletAccounts.Remove(scriptHash); + /// public IWalletAccount GetAccount(UInt160 scriptHash) => _walletAccounts[scriptHash]; + /// public IWalletAccount GetAccount(ECPoint publicKey) => _walletAccounts[publicKey.Encode(true).ToScriptHash()]; + /// public IEnumerable> GetAccounts() => _walletAccounts.Values; + /// public IEnumerable> GetNetworkAccounts(uint network) => _walletAccounts.Values.Where(s => s.ProtocolConfiguration.Network == network); + /// public IEnumerable> GetContractAccounts() => _walletAccounts.Values.Where( static s => s.Contract.Script.Length == 0 ); + /// public IWalletAccount GetDefaultAccount() => _walletAccounts.Values.Single(static s => s.IsDefault); + /// public IEnumerable> GetMultiSigAccounts() => _walletAccounts.Values.Where( static w => WitnessContract.IsMultiSigContract(w.Contract.Script) ); + /// public void SetDefaultAccount(UInt160 scriptHash) { if (Contains(scriptHash)) @@ -162,6 +201,10 @@ public void SetDefaultAccount(UInt160 scriptHash) _walletAccounts[scriptHash].IsDefault = true; } + /// + /// Converts this wallet to its JSON model representation. + /// + /// A that mirrors the current wallet state. public DevWalletModel ToObject() => new() { diff --git a/src/Neo.Wallet/DevWalletAccount.cs b/src/Neo.Wallet/DevWalletAccount.cs index 6c8f156..8406172 100644 --- a/src/Neo.Wallet/DevWalletAccount.cs +++ b/src/Neo.Wallet/DevWalletAccount.cs @@ -35,24 +35,44 @@ namespace Neo.Wallet { + /// + /// A development wallet account that keeps the private key in memory without password encryption. + /// public class DevWalletAccount : IWalletAccount, IMap { + /// public ProtocolSettings ProtocolConfiguration => _protocolSettings; + /// public UInt160 ScriptHash => Contract.ScriptHash; + /// public string Address => ScriptHash.ToAddress(_protocolSettings.AddressVersion); + /// + /// Gets or sets an optional human-readable label for the account. + /// public string? Label { get; set; } + /// + /// Gets or sets a value indicating whether this is the default account in the wallet. + /// public bool IsDefault { get; set; } + /// + /// Gets or sets a value indicating whether the account is locked. + /// public bool IsLocked { get; set; } + /// public bool HasKey => _privateKeyBytes.Length > 0; + /// + /// Gets the protocol settings stored as account extras. + /// public ProtocolSettings Extra => ProtocolConfiguration; + /// public WitnessContract Contract => _witnessContract; private readonly ProtocolSettings _protocolSettings; @@ -60,8 +80,19 @@ public class DevWalletAccount : IWalletAccount, IMap + /// Initializes a multi-signature account using a default m threshold of roughly two-thirds of the public keys. + /// + /// The public keys that participate in the multi-signature contract. + /// The protocol settings used for the account. public DevWalletAccount(ECPoint[] publicKeys, ProtocolSettings protocolSettings) : this((int)Math.Ceiling((2 * publicKeys.Length + 1) / 3m), publicKeys, protocolSettings) { } + /// + /// Initializes a multi-signature account that requires of the given public keys. + /// + /// The minimum number of signatures required. + /// The public keys that participate in the multi-signature contract. + /// The protocol settings used for the account. public DevWalletAccount(int m, ECPoint[] publicKeys, ProtocolSettings protocolSettings) { ArgumentOutOfRangeException.ThrowIfEqual(publicKeys.Length, 0, nameof(publicKeys)); @@ -73,6 +104,13 @@ public DevWalletAccount(int m, ECPoint[] publicKeys, ProtocolSettings protocolSe _witnessContract = WitnessContract.CreateMultiSigContract(m, publicKeys); } + /// + /// Initializes a contract or watch-only account for the specified script hash. + /// + /// The contract script hash. + /// The protocol settings used for the account. + /// Optional witness parameter types. + /// Optional private key associated with the account. public DevWalletAccount(UInt160 contractHash, ProtocolSettings protocolSettings, MethodParameterType[]? contractParameters = default, byte[]? privateKeyBytes = default) { _protocolSettings = protocolSettings; @@ -82,6 +120,11 @@ public DevWalletAccount(UInt160 contractHash, ProtocolSettings protocolSettings, _privateKeyBytes = privateKeyBytes; } + /// + /// Initializes a signature account from the specified private key. + /// + /// The private key bytes. + /// The protocol settings used for the account. public DevWalletAccount(byte[] privateKeyBytes, ProtocolSettings protocolSettings) { _privateKeyBytes = privateKeyBytes; @@ -93,30 +136,56 @@ public DevWalletAccount(byte[] privateKeyBytes, ProtocolSettings protocolSetting _witnessContract = WitnessContract.CreateSignatureContract(publicKeyPoint); } + /// public override string ToString() => $"{ToObject()}"; + /// + /// Not supported for development accounts. + /// + /// The current password. + /// The new password. + /// This method never returns. + /// Always thrown. [DoesNotReturn] public bool ChangePassword(ProtectedString oldPassword, ProtectedString newPassword) { throw new NotSupportedException(); } + /// + /// Not supported for development accounts. + /// + /// The password to verify. + /// This method never returns. + /// Always thrown. [DoesNotReturn] public bool VerifyPassword(ProtectedString password) { throw new NotSupportedException(); } + /// + /// Not supported for development accounts. + /// + /// Always thrown. [DoesNotReturn] public void SetLock() { throw new NotSupportedException(); } + /// + /// Gets a copy of the account private key. + /// + /// The private key bytes, or an empty array when no key is stored. public byte[] GetPrivateKey() => _privateKeyBytes[..]; + /// + /// Converts this account to its JSON model representation. + /// + /// A that mirrors the current account state. public DevWalletAccountModel ToObject() => new() { diff --git a/src/Neo.Wallet/IWallet.cs b/src/Neo.Wallet/IWallet.cs index 1cf068c..05d62a1 100644 --- a/src/Neo.Wallet/IWallet.cs +++ b/src/Neo.Wallet/IWallet.cs @@ -28,48 +28,146 @@ namespace Neo.Wallet { + /// + /// Defines a Neo wallet that manages accounts and optional wallet-level extras. + /// + /// The type of wallet-level extra data. + /// The type of account-level extra data. public interface IWallet where TExtras : class?, new() where TAccountExtras : class?, new() { + /// + /// Gets the wallet format version. + /// Version Version { get; } + /// + /// Gets the optional display name of the wallet. + /// string? Name { get; } + /// + /// Gets the SCrypt parameters used for key derivation. + /// ScryptParameters SCrypt { get; } + /// + /// Gets optional wallet-level extra data. + /// TExtras? Extra { get; } + /// + /// Determines whether the wallet contains an account with the specified script hash. + /// + /// The account script hash to look up. + /// if the account exists; otherwise, . bool Contains(UInt160 scriptHash); + /// + /// Creates a new account with a randomly generated private key. + /// + /// The protocol settings used for the account. + /// The created wallet account. IWalletAccount CreateAccount(ProtocolSettings protocolSettings); + /// + /// Creates a new account from the specified private key. + /// + /// The private key bytes. + /// The protocol settings used for the account. + /// The created wallet account. IWalletAccount CreateAccount(byte[] privateKeyBytes, ProtocolSettings protocolSettings); + /// + /// Creates a new account from a WIF-encoded private key. + /// + /// The WIF string of the private key. + /// The protocol settings used for the account. + /// The created wallet account. IWalletAccount CreateAccount(string wifString, ProtocolSettings protocolSettings); + /// + /// Creates a new watch-only or contract account for the specified script hash. + /// + /// The contract script hash. + /// The protocol settings used for the account. + /// The created wallet account. IWalletAccount CreateAccount(UInt160 contractHash, ProtocolSettings protocolSettings); + /// + /// Removes the account with the specified script hash. + /// + /// The script hash of the account to remove. + /// if the account was removed; otherwise, . bool RemoveAccount(UInt160 scriptHash); + /// + /// Sets the account with the specified script hash as the default account. + /// + /// The script hash of the account to mark as default. void SetDefaultAccount(UInt160 scriptHash); + /// + /// Gets the default account in the wallet. + /// + /// The default wallet account. IWalletAccount GetDefaultAccount(); + /// + /// Gets the account with the specified script hash. + /// + /// The account script hash. + /// The matching wallet account. IWalletAccount GetAccount(UInt160 scriptHash); + /// + /// Gets the account associated with the specified public key. + /// + /// The public key of the account. + /// The matching wallet account. IWalletAccount GetAccount(ECPoint publicKey); + /// + /// Gets all accounts in the wallet. + /// + /// An enumerable of all wallet accounts. IEnumerable> GetAccounts(); + /// + /// Gets accounts that belong to the specified network. + /// + /// The network magic number. + /// An enumerable of accounts for the network. IEnumerable> GetNetworkAccounts(uint network); + /// + /// Gets accounts that represent deployed contracts (empty witness scripts). + /// + /// An enumerable of contract accounts. IEnumerable> GetContractAccounts(); + /// + /// Gets accounts that use multi-signature contracts. + /// + /// An enumerable of multi-signature accounts. IEnumerable> GetMultiSigAccounts(); + /// + /// Creates a multi-signature account using a default m threshold of roughly two-thirds of the public keys. + /// + /// The protocol settings used for the account. + /// The public keys that participate in the multi-signature contract. + /// The created multi-signature account. IWalletAccount CreateMultiSigAccount(ProtocolSettings protocolSettings, params ECPoint[] publicKeys); + /// + /// Creates a multi-signature account that requires of the given public keys. + /// + /// The protocol settings used for the account. + /// The minimum number of signatures required. + /// The public keys that participate in the multi-signature contract. + /// The created multi-signature account. IWalletAccount CreateMultiSigAccount(ProtocolSettings protocolSettings, int m, params ECPoint[] publicKeys); } } diff --git a/src/Neo.Wallet/IWalletAccount.cs b/src/Neo.Wallet/IWalletAccount.cs index 3755309..86efae0 100644 --- a/src/Neo.Wallet/IWalletAccount.cs +++ b/src/Neo.Wallet/IWalletAccount.cs @@ -26,33 +26,82 @@ namespace Neo.Wallet { + /// + /// Defines a single account within a Neo wallet. + /// + /// The type of account-level extra data. public interface IWalletAccount where TExtras : class?, new() { + /// + /// Gets the protocol settings associated with this account. + /// ProtocolSettings ProtocolConfiguration { get; } + /// + /// Gets the script hash of the account. + /// UInt160 ScriptHash { get; } + /// + /// Gets the base58-check address of the account. + /// string Address { get; } + /// + /// Gets an optional human-readable label for the account. + /// string? Label { get; } + /// + /// Gets a value indicating whether this is the default account in the wallet. + /// bool IsDefault { get; } + /// + /// Gets a value indicating whether the account is locked. + /// bool IsLocked { get; } + /// + /// Gets a value indicating whether a private key is available for this account. + /// bool HasKey { get; } + /// + /// Gets optional account-level extra data. + /// TExtras Extra { get; } + /// + /// Gets the witness contract used to authorize the account. + /// WitnessContract Contract { get; } + /// + /// Changes the password used to protect the account private key. + /// + /// The current password. + /// The new password. + /// if the password was changed; otherwise, . bool ChangePassword(ProtectedString oldPassword, ProtectedString newPassword); + /// + /// Verifies the password and unlocks the account when successful. + /// + /// The password to verify. + /// if the password is valid; otherwise, . bool VerifyPassword(ProtectedString password); + /// + /// Gets a copy of the account private key. + /// + /// The private key bytes. byte[] GetPrivateKey(); + /// + /// Locks the account so that the private key cannot be retrieved until unlocked. + /// void SetLock(); } } diff --git a/src/Neo.Wallet/Json/ContractModel.cs b/src/Neo.Wallet/Json/ContractModel.cs index be6a21a..2d2c58b 100644 --- a/src/Neo.Wallet/Json/ContractModel.cs +++ b/src/Neo.Wallet/Json/ContractModel.cs @@ -29,15 +29,31 @@ namespace Neo.Wallet.Json { + /// + /// JSON model for a wallet account witness contract. + /// public class ContractModel : JsonModel, IMap { + /// + /// Gets or sets the contract script bytes, serialized as a hex string. + /// [JsonConverter(typeof(JsonStringHexFormatConverter))] public byte[]? Script { get; set; } + /// + /// Gets or sets the contract parameter definitions. + /// public ContractParameterModel[]? Parameters { get; set; } + /// + /// Gets or sets a value indicating whether the contract is considered deployed. + /// public bool Deployed { get; set; } + /// + /// Converts this model to a . + /// + /// A witness contract built from and . public WitnessContract ToObject() => WitnessContract.Create( Script ?? [], diff --git a/src/Neo.Wallet/Json/ContractParameterModel.cs b/src/Neo.Wallet/Json/ContractParameterModel.cs index a3c1201..b3a6a9c 100644 --- a/src/Neo.Wallet/Json/ContractParameterModel.cs +++ b/src/Neo.Wallet/Json/ContractParameterModel.cs @@ -26,12 +26,25 @@ namespace Neo.Wallet.Json { + /// + /// JSON model for a named contract parameter type. + /// public class ContractParameterModel : JsonModel, IMap { + /// + /// Gets or sets the parameter name. + /// public string? Name { get; set; } + /// + /// Gets or sets the parameter type. + /// public MethodParameterType Type { get; set; } + /// + /// Converts this model to a . + /// + /// The parameter . public MethodParameterType ToObject() => Type; } diff --git a/src/Neo.Wallet/Json/DevWalletAccountModel.cs b/src/Neo.Wallet/Json/DevWalletAccountModel.cs index 786b2b8..21a42bb 100644 --- a/src/Neo.Wallet/Json/DevWalletAccountModel.cs +++ b/src/Neo.Wallet/Json/DevWalletAccountModel.cs @@ -30,12 +30,25 @@ namespace Neo.Wallet.Json { + /// + /// JSON model for a , storing the private key as hex. + /// public class DevWalletAccountModel : WalletAccountModel, IMap { + /// + /// Gets or sets the private key bytes, serialized as a hex string. + /// [JsonConverter(typeof(JsonStringHexFormatConverter))] public override byte[]? Key { get => base.Key; set => base.Key = value; } // TODO: Add support for MultiSigAddresses + /// + /// Converts this model to a . + /// + /// + /// A signature account when is present; otherwise a contract or watch-only account. + /// + /// Thrown when creating a keyless account without an address. public DevWalletAccount ToObject() => (Key is not null && Key.Length > 0) ? new( diff --git a/src/Neo.Wallet/Json/DevWalletModel.cs b/src/Neo.Wallet/Json/DevWalletModel.cs index 437508d..65526f1 100644 --- a/src/Neo.Wallet/Json/DevWalletModel.cs +++ b/src/Neo.Wallet/Json/DevWalletModel.cs @@ -24,8 +24,15 @@ namespace Neo.Wallet.Json { + /// + /// JSON model for a . + /// public class DevWalletModel : WalletModel, IMap { + /// + /// Converts this model to a . + /// + /// A development wallet initialized from this model. public DevWallet ToObject() => new(this); } diff --git a/src/Neo.Wallet/Json/Nep6WalletAccountModel.cs b/src/Neo.Wallet/Json/Nep6WalletAccountModel.cs index 3486a18..8c071cb 100644 --- a/src/Neo.Wallet/Json/Nep6WalletAccountModel.cs +++ b/src/Neo.Wallet/Json/Nep6WalletAccountModel.cs @@ -27,8 +27,14 @@ namespace Neo.Wallet.Json { + /// + /// JSON model for a , storing the NEP-2 key material. + /// public class Nep6WalletAccountModel : WalletAccountModel, IMap { + /// + /// Gets or sets the NEP-2 key material as a UTF-8 byte array. + /// [JsonConverter(typeof(JsonStringByteArrayConverter))] public override byte[]? Key { get => base.Key; set => base.Key = value; } diff --git a/src/Neo.Wallet/Json/Nep6WalletModel.cs b/src/Neo.Wallet/Json/Nep6WalletModel.cs index 9dcd6d4..ce17b2a 100644 --- a/src/Neo.Wallet/Json/Nep6WalletModel.cs +++ b/src/Neo.Wallet/Json/Nep6WalletModel.cs @@ -24,8 +24,15 @@ namespace Neo.Wallet.Json { + /// + /// JSON model for a . + /// public class Nep6WalletModel : WalletModel, IMap { + /// + /// Converts this model to a . + /// + /// A NEP-6 wallet initialized from this model. public Nep6Wallet ToObject() => new(this); } diff --git a/src/Neo.Wallet/Json/SCryptModel.cs b/src/Neo.Wallet/Json/SCryptModel.cs index 9715bee..4b5b281 100644 --- a/src/Neo.Wallet/Json/SCryptModel.cs +++ b/src/Neo.Wallet/Json/SCryptModel.cs @@ -26,8 +26,14 @@ namespace Neo.Wallet.Json { + /// + /// JSON model for SCrypt key-derivation parameters. + /// public class SCryptModel : JsonModel, IMap { + /// + /// The default SCrypt parameters (N=16384, R=8, P=8). + /// public static readonly SCryptModel Default = new() { N = 16384, @@ -50,6 +56,10 @@ public class SCryptModel : JsonModel, IMap /// public int P { get; set; } + /// + /// Converts this model to a instance. + /// + /// A with the same N, R, and P values. public ScryptParameters ToObject() => new(N, R, P); } diff --git a/src/Neo.Wallet/Json/WalletAccountModel.cs b/src/Neo.Wallet/Json/WalletAccountModel.cs index 9ef4e03..292f5a7 100644 --- a/src/Neo.Wallet/Json/WalletAccountModel.cs +++ b/src/Neo.Wallet/Json/WalletAccountModel.cs @@ -27,22 +27,47 @@ namespace Neo.Wallet.Json { + /// + /// Base JSON model for a wallet account. + /// + /// The type of account-level extra data. public class WalletAccountModel : JsonModel where TExtra : class?, new() { + /// + /// Gets or sets the account address as a script hash. + /// [JsonConverter(typeof(JsonStringAddressConverter))] public UInt160? Address { get; set; } + /// + /// Gets or sets an optional human-readable label for the account. + /// public string? Label { get; set; } + /// + /// Gets or sets a value indicating whether this is the default account. + /// public bool IsDefault { get; set; } + /// + /// Gets or sets a value indicating whether the account is locked. + /// public bool Lock { get; set; } + /// + /// Gets or sets the account key material (format depends on the concrete account model). + /// public virtual byte[]? Key { get; set; } + /// + /// Gets or sets the witness contract associated with the account. + /// public ContractModel? Contract { get; set; } + /// + /// Gets or sets optional account-level extra data. + /// public TExtra? Extra { get; set; } } } diff --git a/src/Neo.Wallet/Json/WalletModel.cs b/src/Neo.Wallet/Json/WalletModel.cs index 15fc7b6..826781d 100644 --- a/src/Neo.Wallet/Json/WalletModel.cs +++ b/src/Neo.Wallet/Json/WalletModel.cs @@ -26,19 +26,39 @@ namespace Neo.Wallet.Json { + /// + /// Base JSON model for a wallet file. + /// + /// The type of wallet-level extra data. + /// The type of account model entries. public class WalletModel : JsonModel where TExtras : class?, new() where TAccountModel : class?, new() { + /// + /// Gets or sets the optional display name of the wallet. + /// public string? Name { get; set; } + /// + /// Gets or sets the wallet format version. + /// public Version? Version { get; set; } + /// + /// Gets or sets the SCrypt parameters used for key derivation. + /// [JsonPropertyName("scrypt")] public SCryptModel? SCrypt { get; set; } + /// + /// Gets or sets the accounts contained in the wallet. + /// public TAccountModel[]? Accounts { get; set; } + /// + /// Gets or sets optional wallet-level extra data. + /// public TExtras? Extra { get; set; } } } diff --git a/src/Neo.Wallet/Nep6Wallet.cs b/src/Neo.Wallet/Nep6Wallet.cs index 22d483f..c8daac8 100644 --- a/src/Neo.Wallet/Nep6Wallet.cs +++ b/src/Neo.Wallet/Nep6Wallet.cs @@ -34,26 +34,45 @@ namespace Neo.Wallet { + /// + /// A NEP-6 compliant wallet that stores accounts with password-protected NEP-2 keys. + /// public class Nep6Wallet : IWallet, IMap { private static readonly Version s_walletVersion = new(1, 0); + /// public Version Version => s_walletVersion; + /// + /// Gets or sets the optional display name of the wallet. + /// public string? Name { get; set; } + /// public ScryptParameters SCrypt => _scryptParameters; + /// + /// Gets or sets optional wallet-level extra data. + /// public object? Extra { get; set; } private readonly Dictionary _walletAccounts = []; private readonly ScryptParameters _scryptParameters; + /// + /// Initializes a new empty NEP-6 wallet. + /// + /// The SCrypt parameters to use; defaults to . public Nep6Wallet(ScryptParameters? scryptParameters = default) { _scryptParameters = scryptParameters ?? ScryptParameters.Default; } + /// + /// Initializes a NEP-6 wallet from a JSON model. + /// + /// The model containing wallet data and accounts. public Nep6Wallet(Nep6WalletModel nep6WalletModel) { _scryptParameters = nep6WalletModel.SCrypt?.ToObject() ?? ScryptParameters.Default; @@ -72,9 +91,16 @@ public Nep6Wallet(Nep6WalletModel nep6WalletModel) } } + /// public bool Contains(UInt160 scriptHash) => _walletAccounts.ContainsKey(scriptHash); + /// + /// Creates an account from a NEP-6 account model and adds it to the wallet. + /// + /// The account model to import. + /// The created wallet account. + /// Thrown when the model address is . public IWalletAccount CreateAccount(Nep6WalletAccountModel walletAccountModel) { if (walletAccountModel.Address is null) @@ -85,9 +111,11 @@ public IWalletAccount CreateAccount(Nep6WalletAccountModel wal return _walletAccounts[walletAccountModel.Address] = account; } + /// public IWalletAccount CreateAccount(ProtocolSettings protocolSettings) => CreateAccount(RandomNumberGenerator.GetBytes(ChainWallet.MaxPrivateKeySizeInBytes), protocolSettings); + /// public IWalletAccount CreateAccount(byte[] privateKeyBytes, ProtocolSettings protocolSettings) { var newWalletAccount = new Nep6WalletAccount(privateKeyBytes, protocolSettings, scryptParameters: _scryptParameters); @@ -98,9 +126,11 @@ public IWalletAccount CreateAccount(byte[] privateKeyBytes, Pr return _walletAccounts[newWalletAccount.ScriptHash] = newWalletAccount; } + /// public IWalletAccount CreateAccount(string wifString, ProtocolSettings protocolSettings) => CreateAccount(ChainWallet.GetKeyFromWifString(wifString), protocolSettings); + /// public IWalletAccount CreateAccount(UInt160 contractHash, ProtocolSettings protocolSettings) { var newWalletAccount = new Nep6WalletAccount(contractHash, protocolSettings, scryptParameters: _scryptParameters); @@ -111,9 +141,11 @@ public IWalletAccount CreateAccount(UInt160 contractHash, Prot return _walletAccounts[newWalletAccount.ScriptHash] = newWalletAccount; } + /// public IWalletAccount CreateMultiSigAccount(ProtocolSettings protocolSettings, params ECPoint[] publicKeys) => CreateMultiSigAccount(protocolSettings, (int)Math.Ceiling((2 * publicKeys.Length + 1) / 3m), publicKeys); + /// public IWalletAccount CreateMultiSigAccount(ProtocolSettings protocolSettings, int m, params ECPoint[] publicKeys) { var newWalletAccount = new Nep6WalletAccount(m, publicKeys, protocolSettings, _scryptParameters); @@ -124,36 +156,45 @@ public IWalletAccount CreateMultiSigAccount(ProtocolSettings p return _walletAccounts[newWalletAccount.ScriptHash] = newWalletAccount; } + /// public IWalletAccount GetAccount(UInt160 scriptHash) => _walletAccounts[scriptHash]; + /// public IWalletAccount GetAccount(ECPoint publicKey) => _walletAccounts[publicKey.Encode(true).ToScriptHash()]; + /// public IEnumerable> GetAccounts() => _walletAccounts.Values; + /// public IEnumerable> GetContractAccounts() => _walletAccounts.Values.Where( static s => s.Contract.Script.Length == 0 ); + /// public IWalletAccount GetDefaultAccount() => _walletAccounts.Values.Single(static s => s.IsDefault); + /// public IEnumerable> GetMultiSigAccounts() => _walletAccounts.Values.Where( static w => WitnessContract.IsMultiSigContract(w.Contract.Script) ); + /// public IEnumerable> GetNetworkAccounts(uint network) => _walletAccounts.Values.Where(s => s.ProtocolConfiguration.Network == network); + /// public bool RemoveAccount(UInt160 scriptHash) => _walletAccounts.Remove(scriptHash); + /// public void SetDefaultAccount(UInt160 scriptHash) { if (Contains(scriptHash)) @@ -167,6 +208,10 @@ public void SetDefaultAccount(UInt160 scriptHash) _walletAccounts[scriptHash].IsDefault = true; } + /// + /// Converts this wallet to its JSON model representation. + /// + /// A that mirrors the current wallet state. public Nep6WalletModel ToObject() => new() { diff --git a/src/Neo.Wallet/Nep6WalletAccount.cs b/src/Neo.Wallet/Nep6WalletAccount.cs index 207c79d..2ffe04a 100644 --- a/src/Neo.Wallet/Nep6WalletAccount.cs +++ b/src/Neo.Wallet/Nep6WalletAccount.cs @@ -36,24 +36,42 @@ namespace Neo.Wallet { + /// + /// A NEP-6 wallet account that can protect its private key with a password using NEP-2 encryption. + /// public class Nep6WalletAccount : IWalletAccount, IMap { + /// public ProtocolSettings ProtocolConfiguration => _protocolSettings; + /// public UInt160 ScriptHash => Contract.ScriptHash; + /// public string Address => ScriptHash.ToAddress(_protocolSettings.AddressVersion); + /// + /// Gets or sets an optional human-readable label for the account. + /// public string? Label { get; set; } + /// + /// Gets or sets a value indicating whether this is the default account in the wallet. + /// public bool IsDefault { get; set; } + /// public bool IsLocked => _isLocked; + /// public bool HasKey => _privateKeyBytes.Length > 0; + /// + /// Gets the protocol settings stored as account extras. + /// public ProtocolSettings Extra => ProtocolConfiguration; + /// public WitnessContract Contract => _witnessContract; private readonly ProtocolSettings _protocolSettings; @@ -67,8 +85,21 @@ public class Nep6WalletAccount : IWalletAccount, IMap + /// Initializes a multi-signature account using a default m threshold of roughly two-thirds of the public keys. + /// + /// The public keys that participate in the multi-signature contract. + /// The protocol settings used for the account. + /// The SCrypt parameters used for NEP-2 encryption; defaults to . public Nep6WalletAccount(ECPoint[] publicKeys, ProtocolSettings protocolSettings, ScryptParameters? scryptParameters = default) : this((int)Math.Ceiling((2 * publicKeys.Length + 1) / 3m), publicKeys, protocolSettings, scryptParameters) { } + /// + /// Initializes a multi-signature account that requires of the given public keys. + /// + /// The minimum number of signatures required. + /// The public keys that participate in the multi-signature contract. + /// The protocol settings used for the account. + /// The SCrypt parameters used for NEP-2 encryption; defaults to . public Nep6WalletAccount(int m, ECPoint[] publicKeys, ProtocolSettings protocolSettings, ScryptParameters? scryptParameters = default) { ArgumentOutOfRangeException.ThrowIfEqual(publicKeys.Length, 0, nameof(publicKeys)); @@ -81,6 +112,14 @@ public Nep6WalletAccount(int m, ECPoint[] publicKeys, ProtocolSettings protocolS _witnessContract = WitnessContract.CreateMultiSigContract(m, publicKeys); } + /// + /// Initializes a contract or watch-only account for the specified script hash. + /// + /// The contract script hash. + /// The protocol settings used for the account. + /// Optional witness parameter types. + /// Optional private key associated with the account. + /// The SCrypt parameters used for NEP-2 encryption; defaults to . public Nep6WalletAccount(UInt160 contractHash, ProtocolSettings protocolSettings, MethodParameterType[]? contractParameters = default, byte[]? privateKeyBytes = default, ScryptParameters? scryptParameters = default) { _scryptParameters = scryptParameters ?? ScryptParameters.Default; @@ -91,6 +130,12 @@ public Nep6WalletAccount(UInt160 contractHash, ProtocolSettings protocolSettings _privateKeyBytes = privateKeyBytes; } + /// + /// Initializes a signature account from the specified private key. + /// + /// The private key bytes. + /// The protocol settings used for the account. + /// The SCrypt parameters used for NEP-2 encryption; defaults to . public Nep6WalletAccount(byte[] privateKeyBytes, ProtocolSettings protocolSettings, ScryptParameters? scryptParameters = default) { _privateKeyBytes = privateKeyBytes; @@ -103,6 +148,13 @@ public Nep6WalletAccount(byte[] privateKeyBytes, ProtocolSettings protocolSettin _witnessContract = WitnessContract.CreateSignatureContract(publicKeyPoint); } + /// + /// Initializes a signature account by decrypting a NEP-2 key with the specified password. + /// + /// The NEP-2 encoded private key. + /// The password used to decrypt the key. + /// The protocol settings used for the account. + /// The SCrypt parameters used for NEP-2 decryption; defaults to . public Nep6WalletAccount(string nep2String, string password, ProtocolSettings protocolSettings, ScryptParameters? scryptParameters = default) { _nep2String = nep2String; @@ -118,6 +170,11 @@ public Nep6WalletAccount(string nep2String, string password, ProtocolSettings pr _witnessContract = WitnessContract.CreateSignatureContract(publicKeyPoint); } + /// + /// Initializes a locked account from a NEP-6 account model. + /// + /// The account model to import. + /// The SCrypt parameters used for NEP-2 operations; defaults to . public Nep6WalletAccount(Nep6WalletAccountModel accountModel, ScryptParameters? scryptParameters = default) { _nep2String = Encoding.UTF8.GetString(accountModel.Key ?? []); @@ -127,9 +184,16 @@ public Nep6WalletAccount(Nep6WalletAccountModel accountModel, ScryptParameters? _isLocked = true; } + /// public override string ToString() => $"{ToObject()}"; + /// + /// Changes the password used to protect the account private key and re-encrypts it as NEP-2. + /// + /// The current password. + /// The new password. + /// if the password was changed; otherwise, . public bool ChangePassword(ProtectedString oldPassword, ProtectedString newPassword) { if (_isLocked) return false; @@ -153,6 +217,12 @@ public bool ChangePassword(ProtectedString oldPassword, ProtectedString newPassw return true; } + /// + /// Verifies the password against the stored NEP-2 key and unlocks the account when successful. + /// + /// The password to verify. + /// if the password is valid and the account was unlocked; otherwise, . + /// Thrown when no NEP-2 key has been set. public bool VerifyPassword(ProtectedString password) { if (string.IsNullOrEmpty(password)) return false; @@ -176,9 +246,17 @@ public bool VerifyPassword(ProtectedString password) return true; } + /// + /// Locks the account so that the private key cannot be retrieved until unlocked. + /// public void SetLock() => _isLocked = true; + /// + /// Gets a copy of the account private key. + /// + /// The private key bytes. + /// Thrown when the account is locked or has no private key. public byte[] GetPrivateKey() { if (_isLocked == true || _privateKeyBytes.Length == 0) @@ -187,6 +265,10 @@ public byte[] GetPrivateKey() return _privateKeyBytes[..]; } + /// + /// Converts this account to its JSON model representation. + /// + /// A that mirrors the current account state. public Nep6WalletAccountModel ToObject() => new() {