diff --git a/.editorconfig b/.editorconfig
index 4f76e265c0..6be4df71a5 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -66,6 +66,8 @@ dotnet_diagnostic.IDE0251.severity = warning
dotnet_diagnostic.IDE0044.severity = warning
dotnet_diagnostic.CS1591.severity = silent
+// Use primary constructor
+csharp_style_prefer_primary_constructors = false
# Sort using and Import directives with System.* appearing first
dotnet_sort_system_directives_first = false
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index e52af69fde..83340e2abb 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -21,6 +21,13 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
+ - name: Cache NuGet packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.nuget/packages
+ key : ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
+ restore-keys: ${{ runner.os }}-nuget-
+
- name: Check Format (*.cs)
run: dotnet format --verify-no-changes --verbosity diagnostic
@@ -37,6 +44,13 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
+ - name: Cache NuGet packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.nuget/packages
+ key : ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
+ restore-keys: ${{ runner.os }}-nuget-
+
- name: Build (Everything)
run: dotnet build --disable-parallel -p:GITHUB_ACTIONS=true
@@ -65,6 +79,13 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
+ - name: Cache NuGet packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.nuget/packages
+ key : ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
+ restore-keys: ${{ runner.os }}-nuget-
+
- name: Test (MacOS)
if: matrix.os == 'macos-latest'
run: |
@@ -109,6 +130,7 @@ jobs:
${{ github.workspace }}/TestResults/Neo.Json.UnitTests/coverage.info
${{ github.workspace }}/TestResults/Neo.Cryptography.MPTTrie.Tests/coverage.info
${{ github.workspace }}/TestResults/Neo.Network.RPC.Tests/coverage.info
+ ${{ github.workspace }}/TestResults/Neo.Plugins.DBFTPlugin.Tests/coverage.info
${{ github.workspace }}/TestResults/Neo.Plugins.OracleService.Tests/coverage.info
${{ github.workspace }}/TestResults/Neo.Plugins.RpcServer.Tests/coverage.info
${{ github.workspace }}/TestResults/Neo.Plugins.Storage.Tests/coverage.info
diff --git a/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj b/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj
index debd94430d..a04351b777 100644
--- a/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj
+++ b/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj
@@ -9,10 +9,10 @@
-
+
-
+
diff --git a/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Benchmarks.Cache.cs b/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Benchmarks.Cache.cs
new file mode 100644
index 0000000000..9d41c1e21c
--- /dev/null
+++ b/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Benchmarks.Cache.cs
@@ -0,0 +1,50 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// Benchmarks.Cache.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using BenchmarkDotNet.Attributes;
+
+namespace Neo.Cryptography.MPTTrie.Benchmarks
+{
+ public class Benchmarks_Cache
+ {
+ private readonly byte _prefix = 0x01;
+ private readonly UInt256 _hash;
+
+ public Benchmarks_Cache()
+ {
+ var randomBytes = new byte[UInt256.Length];
+ new Random(42).NextBytes(randomBytes);
+ _hash = new UInt256(randomBytes);
+ }
+
+ [Benchmark]
+ public byte[] Key_Original()
+ {
+ var buffer = new byte[UInt256.Length + 1];
+ using (var ms = new MemoryStream(buffer, true))
+ using (var writer = new BinaryWriter(ms))
+ {
+ writer.Write(_prefix);
+ _hash.Serialize(writer);
+ }
+ return buffer;
+ }
+
+ [Benchmark]
+ public byte[] Key_Optimized()
+ {
+ var buffer = new byte[UInt256.Length + 1];
+ buffer[0] = _prefix;
+ _hash.GetSpan().CopyTo(buffer.AsSpan(1));
+ return buffer;
+ }
+ }
+}
diff --git a/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Neo.Cryptography.MPTTrie.Benchmarks.csproj b/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Neo.Cryptography.MPTTrie.Benchmarks.csproj
new file mode 100644
index 0000000000..07b47fa911
--- /dev/null
+++ b/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Neo.Cryptography.MPTTrie.Benchmarks.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Program.cs b/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Program.cs
new file mode 100644
index 0000000000..16255a17ee
--- /dev/null
+++ b/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Program.cs
@@ -0,0 +1,23 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// Program.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using BenchmarkDotNet.Running;
+
+// List all benchmarks:
+// dotnet run -c Release --framework [for example: net9.0] -- --list flat(or tree)
+// Run a specific benchmark:
+// dotnet run -c Release --framework [for example: net9.0] -- -f [benchmark name]
+// Run all benchmarks:
+// dotnet run -c Release --framework [for example: net9.0] -- -f *
+// Run all benchmarks of a class:
+// dotnet run -c Release --framework [for example: net9.0] -- -f '*Class*'
+// More options: https://benchmarkdotnet.org/articles/guides/console-args.html
+BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
diff --git a/benchmarks/Neo.Extensions.Benchmarks/Neo.Extensions.Benchmarks.csproj b/benchmarks/Neo.Extensions.Benchmarks/Neo.Extensions.Benchmarks.csproj
index 1d9290d19b..56eccd8683 100644
--- a/benchmarks/Neo.Extensions.Benchmarks/Neo.Extensions.Benchmarks.csproj
+++ b/benchmarks/Neo.Extensions.Benchmarks/Neo.Extensions.Benchmarks.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/benchmarks/Neo.Json.Benchmarks/Data/RpcTestCases.json b/benchmarks/Neo.Json.Benchmarks/Data/RpcTestCases.json
index 623b59f0d9..bcc82c91d9 100644
--- a/benchmarks/Neo.Json.Benchmarks/Data/RpcTestCases.json
+++ b/benchmarks/Neo.Json.Benchmarks/Data/RpcTestCases.json
@@ -3628,7 +3628,7 @@
"netfee": "1272390",
"validuntilblock": 2105487,
"attributes": [],
- "cosigners": [
+ "signers": [
{
"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569",
"scopes": "CalledByEntry"
@@ -3679,7 +3679,7 @@
"netfee": "2483780",
"validuntilblock": 2105494,
"attributes": [],
- "cosigners": [
+ "signers": [
{
"account": "0x36d6200fb4c9737c7b552d2b5530ab43605c5869",
"scopes": "CalledByEntry"
@@ -3724,7 +3724,7 @@
"netfee": "2381780",
"validuntilblock": 2105500,
"attributes": [],
- "cosigners": [
+ "signers": [
{
"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569",
"scopes": "CalledByEntry"
diff --git a/benchmarks/Neo.Json.Benchmarks/Neo.Json.Benchmarks.csproj b/benchmarks/Neo.Json.Benchmarks/Neo.Json.Benchmarks.csproj
index 1d75612a58..6720cfc7c1 100644
--- a/benchmarks/Neo.Json.Benchmarks/Neo.Json.Benchmarks.csproj
+++ b/benchmarks/Neo.Json.Benchmarks/Neo.Json.Benchmarks.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/benchmarks/Neo.VM.Benchmarks/Neo.VM.Benchmarks.csproj b/benchmarks/Neo.VM.Benchmarks/Neo.VM.Benchmarks.csproj
index 109fab93e3..e0de03af9a 100644
--- a/benchmarks/Neo.VM.Benchmarks/Neo.VM.Benchmarks.csproj
+++ b/benchmarks/Neo.VM.Benchmarks/Neo.VM.Benchmarks.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/src/Neo.Network.RpcClient/API_REFERENCE.md b/docs/RpcClient/API_REFERENCE.md
similarity index 100%
rename from src/Neo.Network.RpcClient/API_REFERENCE.md
rename to docs/RpcClient/API_REFERENCE.md
diff --git a/docs/plugin-secure-sign-guide.md b/docs/plugin-secure-sign-guide.md
new file mode 100644
index 0000000000..0d9f38105f
--- /dev/null
+++ b/docs/plugin-secure-sign-guide.md
@@ -0,0 +1,189 @@
+# Secure Sign Plugin
+
+## Purpose
+
+The Secure Sign Plugin (SignClient) is a Neo blockchain plugin that provides secure `ExtensiblePayload` and `Block` signing capabilities through a gRPC-based sign service. This plugin enables:
+
+- **Secure Key Management**: Private keys are stored and managed by a separate sign service, not within the Neo node itself. The private keys should be protected by some mechanisms(like Intel SGX or AWS Nitro Enclave)
+- **Multi Transport Layers Support**: Supports both TCP and Vsock connections for different deployment scenarios
+
+## How to enable plugin `SignClient`
+
+Users can enable plugin `SignClient` by installing it or compiling it manually.
+
+### Install by `neo-cli`
+
+1. **Start the Signing Service**: Ensure, your sign service is running and accessible. You can select a sign service implementation or implement a sign service on your own.
+2. **Download the Plugin**: The SignClient plugin should be installed. You can run `neo-cli` then execute `help install` to get help abort how to install plugin.
+3. **Configure the Plugin**: Create or modify the `SignClient.json` configuration file in the `neo-cli` binary directory (`Plugins/SignClient`).
+4. **Start `neo-cli`**: Start/Restart `neo-cli` if needed.
+
+### Compile Manually
+
+The .Net SDK needs to be installed before compiling it.
+
+1. **Clone the Repository**:
+ ```bash
+ git clone https://github.com/neo-project/neo
+ cd neo
+ donet build
+ ```
+
+2. **Copy to `neo-cli` folder**: Copy the built plugin to the `neo-cli` binary directory.
+- Step 0. Find the `.dll` files. For example:
+ - The `neo-cli` compile products should exist in `./bin/Neo.CLI/net{dotnet-version}/`(i.e. `neo-cli` binary directory, `./bin/Neo.CLI/net9.0/`).
+ - The plugin `SignClient` should exist in `./bin/Neo.Plugins.SignClient/{dotnet-version}/`(i.e. `SignClient` binary directory, `./bin/Neo.Network.RpcClient/9.0/`).
+- Step 1. Copy files `Google.Protobuf.dll Grpc.Core.Api.dll Grpc.Net.Client.dll Grpc.Net.Common.dll `(These files should exist in folder `Neo.Plugins.SignClient`) to the `neo-cli` binary directory.
+- Step 2. `mkdir -p Plugins/SignClient` in the `neo-cli` binary directory. Then copy file `SignClient.dll` from the plugin `SignClient` binary directory to `Plugins/SignClient`.
+- Step 3. Create a `SignClient.json` file `Plugins/SignClient` directory according to the next section.
+- Step 4. Start the `neo-cli`.
+
+
+## Configuration
+
+### Basic Configuration
+
+Create a `SignClient.json` file in `Plugins/SignClient` directory:
+
+```json
+{
+ "PluginConfiguration": {
+ "Name": "SignClient",
+ "Endpoint": "http://127.0.0.1:9991"
+ }
+}
+```
+
+### Configuration Parameters
+
+- **Name**: The name of the sign client (default: "SignClient")
+- **Endpoint**: The endpoint of the sign service
+ - TCP: `http://host:port` or `https://host:port`
+ - VSock: `vsock://contextId:port`
+
+### Connection Types
+
+#### TCP Connection
+```json
+{
+ "PluginConfiguration": {
+ "Name": "SignClient",
+ "Endpoint": "http://127.0.0.1:9991"
+ }
+}
+```
+
+#### VSock Connection
+```json
+{
+ "PluginConfiguration": {
+ "Name": "SignClient",
+ "Endpoint": "vsock://2345:9991"
+ }
+}
+```
+
+## Sign Service Implementation Guide
+
+The SignClient plugin communicates with a sign service using gRPC.
+The service must implement the following interface defined in `proto/servicepb.proto`:
+
+### Service Interface
+
+```protobuf
+service SecureSign {
+ rpc SignExtensiblePayload(SignExtensiblePayloadRequest) returns (SignExtensiblePayloadResponse) {}
+ rpc SignBlock(SignBlockRequest) returns (SignBlockResponse) {}
+ rpc GetAccountStatus(GetAccountStatusRequest) returns (GetAccountStatusResponse) {}
+}
+```
+
+### Methods
+
+#### SignExtensiblePayload
+
+Signs extensible payloads for the specified script hashes.
+
+**Request**:
+- `payload`: The extensible payload to sign
+- `script_hashes`: List of script hashes (UInt160) that need signatures
+- `network`: Network ID
+
+**Response**:
+- `signs`: List of account signs corresponding to each script hash
+
+**Implementation Notes**:
+- The service should check if it has private keys for the requested script hashes.
+- For multi-signature accounts, return all available signatures.
+- Return appropriate account status for each script hash.
+- If a feature not support(for example, multi-signature account), it should return gRPC error code `Unimplemented`.
+- If the `payload` or `script_hashes` is not provided or invalid, it should return gRPC error code `InvalidArgument`.
+
+#### SignBlock
+
+Signs a block with the specified public key.
+
+**Request**:
+- `block`: The block header and transaction hashes
+- `public_key`: The public key to sign with (compressed or uncompressed)
+- `network`: Network ID
+
+**Response**:
+- `signature`: The signature bytes
+
+**Implementation Notes**:
+- The service should verify it has the private key corresponding to the public key.
+- Sign the block header data according to Neo's block signing specification.
+- If the `block` or `public_key` is not provided or invalid, it should return gRPC error code `InvalidArgument`.
+
+#### GetAccountStatus
+
+Retrieves the status of an account for the specified public key.
+
+**Request**:
+- `public_key`: The public key to check (compressed or uncompressed)
+
+**Response**:
+- `status`: Account status enum value
+
+**Implementation Notes**:
+- If the `public_key` is not provided or invalid, it should return gRPC error code `InvalidArgument`.
+
+**Account Status Values**:
+- `NoSuchAccount`: Account doesn't exist
+- `NoPrivateKey`: Account exists but no private key available
+- `Single`: Single-signature account with private key available
+- `Multiple`: Multi-signature account with private key available
+- `Locked`: Account is locked and cannot sign
+
+## Usage Examples
+
+### Console Commands
+
+The plugin provides a console command to check account status:
+
+```bash
+get account status
+```
+
+Example:
+```bash
+get account status 026ff03b949241ce1dadd43519e6960e0a85b41a69a05c328103aa2bce1594ca16
+```
+
+## Troubleshooting
+
+### Common Issues
+
+1. **"No signer service is connected"**
+ - Check if the sign service is running
+ - Verify the endpoint configuration
+ - Check network connectivity
+
+2. **"Invalid vsock endpoint"**
+ - Ensure VSock is only used on Linux
+ - Verify the VSock address format: `vsock://contextId:port`
+
+3. **"Failed to get account status"**
+ - Check if the public key format is correct
+ - Verify the sign service has the requested account
diff --git a/neo.sln b/neo.sln
index e1de480f6e..4cb2595b51 100644
--- a/neo.sln
+++ b/neo.sln
@@ -81,18 +81,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Extensions.Benchmarks",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Json.Benchmarks", "benchmarks\Neo.Json.Benchmarks\Neo.Json.Benchmarks.csproj", "{5F984D2B-793F-4683-B53A-80050E6E0286}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Network.RpcClient", "src\Neo.Network.RpcClient\Neo.Network.RpcClient.csproj", "{9ADB4E11-8655-42C2-8A75-E4436F56F17A}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Cryptography.MPTTrie", "src\Neo.Cryptography.MPTTrie\Neo.Cryptography.MPTTrie.csproj", "{E384C5EF-493E-4ED6-813C-6364F968CEE8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Cryptography.MPTTrie.Tests", "tests\Neo.Cryptography.MPTTrie.Tests\Neo.Cryptography.MPTTrie.Tests.csproj", "{40A23D45-1E81-41A4-B587-16AF26630103}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Network.RPC.Tests", "tests\Neo.Network.RPC.Tests\Neo.Network.RPC.Tests.csproj", "{19B1CF1A-17F4-4E04-AB9C-55CE74952E11}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignClient", "src\Plugins\SignClient\SignClient.csproj", "{CAD55942-48A3-4526-979D-7519FADF19FE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.SignClient.Tests", "tests\Neo.Plugins.SignClient.Tests\Neo.Plugins.SignClient.Tests.csproj", "{E2CFEAA1-45F2-4075-94ED-866862C6863F}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Cryptography.MPTTrie.Benchmarks", "benchmarks\Neo.Cryptography.MPTTrie.Benchmarks\Neo.Cryptography.MPTTrie.Benchmarks.csproj", "{69B0D53B-D97A-4315-B205-CCEBB7289EA9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Network.RPC.Tests", "tests\Neo.Network.RPC.RpcClient.Tests\Neo.Network.RPC.Tests.csproj", "{A3D3F4B9-34C9-CE3E-FB14-14F450C462BE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RpcClient", "src\RpcClient\RpcClient.csproj", "{977B7BD7-93AE-14AD-CA79-91537F8964E5}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -235,10 +237,6 @@ Global
{5F984D2B-793F-4683-B53A-80050E6E0286}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F984D2B-793F-4683-B53A-80050E6E0286}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F984D2B-793F-4683-B53A-80050E6E0286}.Release|Any CPU.Build.0 = Release|Any CPU
- {9ADB4E11-8655-42C2-8A75-E4436F56F17A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9ADB4E11-8655-42C2-8A75-E4436F56F17A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9ADB4E11-8655-42C2-8A75-E4436F56F17A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9ADB4E11-8655-42C2-8A75-E4436F56F17A}.Release|Any CPU.Build.0 = Release|Any CPU
{E384C5EF-493E-4ED6-813C-6364F968CEE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E384C5EF-493E-4ED6-813C-6364F968CEE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E384C5EF-493E-4ED6-813C-6364F968CEE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -247,10 +245,6 @@ Global
{40A23D45-1E81-41A4-B587-16AF26630103}.Debug|Any CPU.Build.0 = Debug|Any CPU
{40A23D45-1E81-41A4-B587-16AF26630103}.Release|Any CPU.ActiveCfg = Release|Any CPU
{40A23D45-1E81-41A4-B587-16AF26630103}.Release|Any CPU.Build.0 = Release|Any CPU
- {19B1CF1A-17F4-4E04-AB9C-55CE74952E11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {19B1CF1A-17F4-4E04-AB9C-55CE74952E11}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {19B1CF1A-17F4-4E04-AB9C-55CE74952E11}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {19B1CF1A-17F4-4E04-AB9C-55CE74952E11}.Release|Any CPU.Build.0 = Release|Any CPU
{CAD55942-48A3-4526-979D-7519FADF19FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CAD55942-48A3-4526-979D-7519FADF19FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CAD55942-48A3-4526-979D-7519FADF19FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -259,6 +253,18 @@ Global
{E2CFEAA1-45F2-4075-94ED-866862C6863F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E2CFEAA1-45F2-4075-94ED-866862C6863F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E2CFEAA1-45F2-4075-94ED-866862C6863F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {69B0D53B-D97A-4315-B205-CCEBB7289EA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {69B0D53B-D97A-4315-B205-CCEBB7289EA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {69B0D53B-D97A-4315-B205-CCEBB7289EA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {69B0D53B-D97A-4315-B205-CCEBB7289EA9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A3D3F4B9-34C9-CE3E-FB14-14F450C462BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A3D3F4B9-34C9-CE3E-FB14-14F450C462BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A3D3F4B9-34C9-CE3E-FB14-14F450C462BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A3D3F4B9-34C9-CE3E-FB14-14F450C462BE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {977B7BD7-93AE-14AD-CA79-91537F8964E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {977B7BD7-93AE-14AD-CA79-91537F8964E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {977B7BD7-93AE-14AD-CA79-91537F8964E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {977B7BD7-93AE-14AD-CA79-91537F8964E5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -299,12 +305,13 @@ Global
{77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1}
{B6CB2559-10F9-41AC-8D58-364BFEF9688B} = {C25EB0B0-0CAC-4CC1-8F36-F9229EFB99EC}
{5F984D2B-793F-4683-B53A-80050E6E0286} = {C25EB0B0-0CAC-4CC1-8F36-F9229EFB99EC}
- {9ADB4E11-8655-42C2-8A75-E4436F56F17A} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
{E384C5EF-493E-4ED6-813C-6364F968CEE8} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
{40A23D45-1E81-41A4-B587-16AF26630103} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1}
- {19B1CF1A-17F4-4E04-AB9C-55CE74952E11} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1}
{CAD55942-48A3-4526-979D-7519FADF19FE} = {C2DC830A-327A-42A7-807D-295216D30DBB}
{E2CFEAA1-45F2-4075-94ED-866862C6863F} = {7F257712-D033-47FF-B439-9D4320D06599}
+ {69B0D53B-D97A-4315-B205-CCEBB7289EA9} = {C25EB0B0-0CAC-4CC1-8F36-F9229EFB99EC}
+ {A3D3F4B9-34C9-CE3E-FB14-14F450C462BE} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1}
+ {977B7BD7-93AE-14AD-CA79-91537F8964E5} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BCBA19D9-F868-4C6D-8061-A2B91E06E3EC}
diff --git a/src/Neo.CLI/CLI/Helper.cs b/src/Neo.CLI/CLI/Helper.cs
index c7b1437a70..d0893a14e7 100644
--- a/src/Neo.CLI/CLI/Helper.cs
+++ b/src/Neo.CLI/CLI/Helper.cs
@@ -35,7 +35,7 @@ public static void IsScriptValid(this ReadOnlyMemory script, ContractAbi a
}
catch (Exception e)
{
- throw new FormatException($"Bad Script or Manifest Format: {e.Message}");
+ throw new FormatException($"Contract script validation failed. The provided script or manifest format is invalid and cannot be processed. Please verify the script bytecode and manifest are correctly formatted and compatible. Original error: {e.Message}", e);
}
}
}
diff --git a/src/Neo.CLI/CLI/MainService.Block.cs b/src/Neo.CLI/CLI/MainService.Block.cs
index 61531f9d63..b778992468 100644
--- a/src/Neo.CLI/CLI/MainService.Block.cs
+++ b/src/Neo.CLI/CLI/MainService.Block.cs
@@ -83,7 +83,7 @@ private IEnumerable GetBlocks(Stream stream, bool read_start = false)
{
var size = r.ReadInt32();
if (size > Message.PayloadMaxSize)
- throw new ArgumentException($"Block {height} exceeds the maximum allowed size");
+ throw new ArgumentException($"Block at height {height} has a size of {size} bytes, which exceeds the maximum allowed payload size of {Message.PayloadMaxSize} bytes. This block cannot be processed due to size constraints.");
byte[] array = r.ReadBytes(size);
if (height > currentHeight)
diff --git a/src/Neo.CLI/CLI/MainService.Plugins.cs b/src/Neo.CLI/CLI/MainService.Plugins.cs
index e82fd21a99..921c3f4deb 100644
--- a/src/Neo.CLI/CLI/MainService.Plugins.cs
+++ b/src/Neo.CLI/CLI/MainService.Plugins.cs
@@ -80,29 +80,28 @@ private void OnReinstallCommand(string pluginName)
/// Downloaded content
private static async Task DownloadPluginAsync(string pluginName, Version pluginVersion, string? customDownloadUrl = null, bool prerelease = false)
{
- ConsoleHelper.Info($"Downloading {pluginName} {pluginVersion}...");
using var httpClient = new HttpClient();
var asmName = Assembly.GetExecutingAssembly().GetName();
httpClient.DefaultRequestHeaders.UserAgent.Add(new(asmName.Name!, asmName.Version!.ToString(3)));
var url = customDownloadUrl == null ? Settings.Default.Plugins.DownloadUrl : new Uri(customDownloadUrl);
- var json = await httpClient.GetFromJsonAsync(url) ?? throw new HttpRequestException($"Failed: {url}");
+ var json = await httpClient.GetFromJsonAsync(url) ?? throw new HttpRequestException($"Failed to retrieve plugin catalog from URL: {url}. Please check your network connection and verify the plugin repository is accessible.");
var jsonRelease = json.AsArray()
.SingleOrDefault(s =>
s != null &&
s["tag_name"]!.GetValue() == $"v{pluginVersion.ToString(3)}" &&
- s["prerelease"]!.GetValue() == prerelease) ?? throw new Exception($"Could not find Release {pluginVersion}");
+ s["prerelease"]!.GetValue() == prerelease) ?? throw new Exception($"Plugin release version {pluginVersion} (prerelease: {prerelease}) was not found in the plugin repository. Please verify the version number or check if the release is available.");
var jsonAssets = jsonRelease
.AsObject()
- .SingleOrDefault(s => s.Key == "assets").Value ?? throw new Exception("Could not find any Plugins");
+ .SingleOrDefault(s => s.Key == "assets").Value ?? throw new Exception($"No plugin assets found for release version {pluginVersion}. The plugin release may be incomplete or corrupted in the repository.");
var jsonPlugin = jsonAssets
.AsArray()
.SingleOrDefault(s =>
Path.GetFileNameWithoutExtension(
s!["name"]!.GetValue()).Equals(pluginName, StringComparison.InvariantCultureIgnoreCase))
- ?? throw new Exception($"Could not find {pluginName}");
+ ?? throw new Exception($"Plugin '{pluginName}' was not found in the available assets for version {pluginVersion}. Please verify the plugin name is correct and the plugin is available for this version.");
var downloadUrl = jsonPlugin["browser_download_url"]!.GetValue();
return await httpClient.GetStreamAsync(downloadUrl);
@@ -270,7 +269,7 @@ private async Task> GetPluginListAsync()
httpClient.DefaultRequestHeaders.UserAgent.Add(new(asmName.Name!, asmName.Version!.ToString(3)));
var json = await httpClient.GetFromJsonAsync(Settings.Default.Plugins.DownloadUrl)
- ?? throw new HttpRequestException($"Failed: {Settings.Default.Plugins.DownloadUrl}");
+ ?? throw new HttpRequestException($"Failed to retrieve plugin catalog from URL: {Settings.Default.Plugins.DownloadUrl}. Please check your network connection and verify the plugin repository is accessible.");
return json.AsArray()
.Where(w =>
w != null &&
diff --git a/src/Neo.CLI/CLI/MainService.Vote.cs b/src/Neo.CLI/CLI/MainService.Vote.cs
index 0edbb6fd81..23e18b411b 100644
--- a/src/Neo.CLI/CLI/MainService.Vote.cs
+++ b/src/Neo.CLI/CLI/MainService.Vote.cs
@@ -24,6 +24,17 @@
namespace Neo.CLI
{
+ public static class VoteMethods
+ {
+ public const string Register = "registerCandidate";
+ public const string Unregister = "unregisterCandidate";
+ public const string Vote = "vote";
+ public const string GetAccountState = "getAccountState";
+ public const string GetCandidates = "getCandidates";
+ public const string GetCommittee = "getCommittee";
+ public const string GetNextBlockValidators = "getNextBlockValidators";
+ }
+
partial class MainService
{
///
@@ -35,30 +46,12 @@ private void OnRegisterCandidateCommand(UInt160 account)
{
var testGas = NativeContract.NEO.GetRegisterPrice(NeoSystem.StoreView) + (BigInteger)Math.Pow(10, NativeContract.GAS.Decimals) * 10;
if (NoWallet()) return;
- WalletAccount currentAccount = CurrentWallet!.GetAccount(account);
-
- if (currentAccount == null)
- {
- ConsoleHelper.Warning("This address isn't in your wallet!");
- return;
- }
- else
- {
- if (currentAccount.Lock || currentAccount.WatchOnly)
- {
- ConsoleHelper.Warning("Locked or WatchOnly address.");
- return;
- }
- }
- ECPoint? publicKey = currentAccount.GetKey()?.PublicKey;
- byte[] script;
- using (ScriptBuilder scriptBuilder = new())
- {
- scriptBuilder.EmitDynamicCall(NativeContract.NEO.Hash, "registerCandidate", publicKey);
- script = scriptBuilder.ToArray();
- }
+ var currentAccount = GetValidAccountOrWarn(account);
+ if (currentAccount == null) return;
+ var publicKey = currentAccount.GetKey()?.PublicKey;
+ var script = BuildNeoScript(VoteMethods.Register, publicKey);
SendTransaction(script, account, (long)testGas);
}
@@ -70,30 +63,12 @@ private void OnRegisterCandidateCommand(UInt160 account)
private void OnUnregisterCandidateCommand(UInt160 account)
{
if (NoWallet()) return;
- WalletAccount currentAccount = CurrentWallet!.GetAccount(account);
- if (currentAccount == null)
- {
- ConsoleHelper.Warning("This address isn't in your wallet!");
- return;
- }
- else
- {
- if (currentAccount.Lock || currentAccount.WatchOnly)
- {
- ConsoleHelper.Warning("Locked or WatchOnly address.");
- return;
- }
- }
-
- ECPoint? publicKey = currentAccount?.GetKey()?.PublicKey;
- byte[] script;
- using (ScriptBuilder scriptBuilder = new())
- {
- scriptBuilder.EmitDynamicCall(NativeContract.NEO.Hash, "unregisterCandidate", publicKey);
- script = scriptBuilder.ToArray();
- }
+ var currentAccount = GetValidAccountOrWarn(account);
+ if (currentAccount == null) return;
+ var publicKey = currentAccount?.GetKey()?.PublicKey;
+ var script = BuildNeoScript(VoteMethods.Unregister, publicKey);
SendTransaction(script, account);
}
@@ -106,13 +81,8 @@ private void OnUnregisterCandidateCommand(UInt160 account)
private void OnVoteCommand(UInt160 senderAccount, ECPoint publicKey)
{
if (NoWallet()) return;
- byte[] script;
- using (ScriptBuilder scriptBuilder = new())
- {
- scriptBuilder.EmitDynamicCall(NativeContract.NEO.Hash, "vote", senderAccount, publicKey);
- script = scriptBuilder.ToArray();
- }
+ var script = BuildNeoScript(VoteMethods.Vote, senderAccount, publicKey);
SendTransaction(script, senderAccount);
}
@@ -124,13 +94,8 @@ private void OnVoteCommand(UInt160 senderAccount, ECPoint publicKey)
private void OnUnvoteCommand(UInt160 senderAccount)
{
if (NoWallet()) return;
- byte[] script;
- using (ScriptBuilder scriptBuilder = new())
- {
- scriptBuilder.EmitDynamicCall(NativeContract.NEO.Hash, "vote", senderAccount, null);
- script = scriptBuilder.ToArray();
- }
+ var script = BuildNeoScript(VoteMethods.Vote, senderAccount, null);
SendTransaction(script, senderAccount);
}
@@ -140,7 +105,7 @@ private void OnUnvoteCommand(UInt160 senderAccount)
[ConsoleCommand("get candidates", Category = "Vote Commands")]
private void OnGetCandidatesCommand()
{
- if (!OnInvokeWithResult(NativeContract.NEO.Hash, "getCandidates", out StackItem result, null, null, false)) return;
+ if (!OnInvokeWithResult(NativeContract.NEO.Hash, VoteMethods.GetCandidates, out var result, null, null, false)) return;
var resJArray = (Array)result;
@@ -166,7 +131,7 @@ private void OnGetCandidatesCommand()
[ConsoleCommand("get committee", Category = "Vote Commands")]
private void OnGetCommitteeCommand()
{
- if (!OnInvokeWithResult(NativeContract.NEO.Hash, "getCommittee", out StackItem result, null, null, false)) return;
+ if (!OnInvokeWithResult(NativeContract.NEO.Hash, VoteMethods.GetCommittee, out StackItem result, null, null, false)) return;
var resJArray = (Array)result;
@@ -188,7 +153,7 @@ private void OnGetCommitteeCommand()
[ConsoleCommand("get next validators", Category = "Vote Commands")]
private void OnGetNextBlockValidatorsCommand()
{
- if (!OnInvokeWithResult(NativeContract.NEO.Hash, "getNextBlockValidators", out StackItem result, null, null, false)) return;
+ if (!OnInvokeWithResult(NativeContract.NEO.Hash, VoteMethods.GetNextBlockValidators, out var result, null, null, false)) return;
var resJArray = (Array)result;
@@ -210,24 +175,24 @@ private void OnGetNextBlockValidatorsCommand()
[ConsoleCommand("get accountstate", Category = "Vote Commands")]
private void OnGetAccountState(UInt160 address)
{
- const string notice = "No vote record!";
+ const string Notice = "No vote record!";
var arg = new JObject
{
["type"] = "Hash160",
["value"] = address.ToString()
};
- if (!OnInvokeWithResult(NativeContract.NEO.Hash, "getAccountState", out var result, null, new JArray(arg))) return;
+ if (!OnInvokeWithResult(NativeContract.NEO.Hash, VoteMethods.GetAccountState, out var result, null, new JArray(arg))) return;
Console.WriteLine();
if (result.IsNull)
{
- ConsoleHelper.Warning(notice);
+ ConsoleHelper.Warning(Notice);
return;
}
var resJArray = (Array)result;
if (resJArray is null)
{
- ConsoleHelper.Warning(notice);
+ ConsoleHelper.Warning(Notice);
return;
}
@@ -235,7 +200,7 @@ private void OnGetAccountState(UInt160 address)
{
if (value.IsNull)
{
- ConsoleHelper.Warning(notice);
+ ConsoleHelper.Warning(Notice);
return;
}
}
@@ -258,5 +223,28 @@ private void OnGetAccountState(UInt160 address)
ConsoleHelper.Error("Error parsing the result");
}
}
+ ///
+ /// Get account or log a warm
+ ///
+ ///
+ /// account or null
+ private WalletAccount? GetValidAccountOrWarn(UInt160 account)
+ {
+ var acct = CurrentWallet?.GetAccount(account);
+ if (acct == null)
+ {
+ ConsoleHelper.Warning("This address isn't in your wallet!");
+ return null;
+ }
+ if (acct.Lock || acct.WatchOnly)
+ {
+ ConsoleHelper.Warning("Locked or WatchOnly address.");
+ return null;
+ }
+ return acct;
+ }
+
+ private byte[] BuildNeoScript(string method, params object?[] args)
+ => NativeContract.NEO.Hash.MakeScript(method, args);
}
}
diff --git a/src/Neo.CLI/CLI/MainService.cs b/src/Neo.CLI/CLI/MainService.cs
index f4abd36fff..03d4ce255f 100644
--- a/src/Neo.CLI/CLI/MainService.cs
+++ b/src/Neo.CLI/CLI/MainService.cs
@@ -177,17 +177,18 @@ private bool NoWallet()
// Read manifest
var info = new FileInfo(manifestFilePath);
if (!info.Exists)
- throw new ArgumentException("Manifest file not found", nameof(manifestFilePath));
+ throw new ArgumentException($"Contract manifest file not found at path: {manifestFilePath}. Please ensure the manifest file exists and the path is correct.", nameof(manifestFilePath));
if (info.Length >= Transaction.MaxTransactionSize)
- throw new ArgumentException("Manifest file is too large", nameof(manifestFilePath));
+ throw new ArgumentException($"Contract manifest file size ({info.Length} bytes) exceeds the maximum allowed transaction size ({Transaction.MaxTransactionSize} bytes). Please check the file size and ensure it's within limits.", nameof(manifestFilePath));
manifest = ContractManifest.Parse(File.ReadAllBytes(manifestFilePath));
// Read nef
info = new FileInfo(nefFilePath);
- if (!info.Exists) throw new ArgumentException("Nef file not found", nameof(nefFilePath));
+ if (!info.Exists)
+ throw new ArgumentException($"Contract NEF file not found at path: {nefFilePath}. Please ensure the NEF file exists and the path is correct.", nameof(nefFilePath));
if (info.Length >= Transaction.MaxTransactionSize)
- throw new ArgumentException("Nef file is too large", nameof(nefFilePath));
+ throw new ArgumentException($"Contract NEF file size ({info.Length} bytes) exceeds the maximum allowed transaction size ({Transaction.MaxTransactionSize} bytes). Please check the file size and ensure it's within limits.", nameof(nefFilePath));
nef = File.ReadAllBytes(nefFilePath).AsSerializable();
@@ -202,7 +203,7 @@ private bool NoWallet()
}
catch (Exception ex)
{
- throw new FormatException("invalid data", ex);
+ throw new FormatException($"Invalid contract deployment data format. The provided JSON data could not be parsed as valid contract parameters. Original error: {ex.Message}", ex);
}
}
@@ -259,12 +260,12 @@ public void OpenWallet(string path, string password)
{
if (!File.Exists(path))
{
- throw new FileNotFoundException($"Wallet file \"{path}\" not found.");
+ throw new FileNotFoundException($"Wallet file not found at path: {path}. Please verify the file path is correct and the wallet file exists.", path);
}
if (CurrentWallet is not null) SignerManager.UnregisterSigner(CurrentWallet.Name);
- CurrentWallet = Wallet.Open(path, password, NeoSystem.Settings) ?? throw new NotSupportedException();
+ CurrentWallet = Wallet.Open(path, password, NeoSystem.Settings) ?? throw new NotSupportedException($"Failed to open wallet at path: {path}. The wallet format may not be supported or the password may be incorrect. Please verify the wallet file integrity and password.");
SignerManager.RegisterSigner(CurrentWallet.Name, CurrentWallet);
}
@@ -537,7 +538,7 @@ static string GetExceptionMessage(Exception exception)
public UInt160? ResolveNeoNameServiceAddress(string domain)
{
if (Settings.Default.Contracts.NeoNameService == UInt160.Zero)
- throw new Exception("Neo Name Service (NNS): is disabled on this network.");
+ throw new Exception($"Neo Name Service (NNS) is not available on the current network. The NNS contract is not configured for network: {NeoSystem.Settings.Network}. Please ensure you are connected to a network that supports NNS functionality.");
using var sb = new ScriptBuilder();
sb.EmitDynamicCall(Settings.Default.Contracts.NeoNameService, "resolve", CallFlags.ReadOnly, domain, 16);
@@ -560,18 +561,18 @@ static string GetExceptionMessage(Exception exception)
}
else if (data is Null)
{
- throw new Exception($"Neo Name Service (NNS): \"{domain}\" domain not found.");
+ throw new Exception($"Neo Name Service (NNS): Domain '{domain}' was not found in the NNS registry. Please verify the domain name is correct and has been registered in the NNS system.");
}
- throw new Exception("Neo Name Service (NNS): Record invalid address format.");
+ throw new Exception($"Neo Name Service (NNS): The resolved record for domain '{domain}' contains an invalid address format. The NNS record exists but the address data is not in the expected format.");
}
else
{
if (appEng.FaultException is not null)
{
- throw new Exception($"Neo Name Service (NNS): \"{appEng.FaultException.Message}\".");
+ throw new Exception($"Neo Name Service (NNS): Failed to resolve domain '{domain}' due to contract execution error: {appEng.FaultException.Message}. Please verify the domain exists and try again.");
}
}
- throw new Exception($"Neo Name Service (NNS): \"{domain}\" domain not found.");
+ throw new Exception($"Neo Name Service (NNS): Domain '{domain}' was not found in the NNS registry. The resolution operation completed but no valid record was returned. Please verify the domain name is correct and has been registered.");
}
}
}
diff --git a/src/Neo.ConsoleService/CommandToken.cs b/src/Neo.ConsoleService/CommandToken.cs
index 5f1e8b8035..9f07e8ac98 100644
--- a/src/Neo.ConsoleService/CommandToken.cs
+++ b/src/Neo.ConsoleService/CommandToken.cs
@@ -14,6 +14,7 @@ namespace Neo.ConsoleService
public readonly struct CommandToken(int offset, string value, char quoteChar)
{
public const char NoQuoteChar = '\0';
+ public const char NoEscapedChar = '`';
///
/// The start offset of the token in the command line
@@ -25,6 +26,14 @@ public readonly struct CommandToken(int offset, string value, char quoteChar)
///
public readonly string Value { get; } = value;
+ ///
+ /// Whether the token is an indicator. Like --key key.
+ ///
+ public readonly bool IsIndicator => _quoteChar == NoQuoteChar && Value.StartsWith("--");
+
+ ///
+ /// The quote character of the token. It can be ', " or `.
+ ///
private readonly char _quoteChar = quoteChar;
///
diff --git a/src/Neo.ConsoleService/CommandTokenizer.cs b/src/Neo.ConsoleService/CommandTokenizer.cs
index 3b51da0a63..8cce7c9e68 100644
--- a/src/Neo.ConsoleService/CommandTokenizer.cs
+++ b/src/Neo.ConsoleService/CommandTokenizer.cs
@@ -59,10 +59,14 @@ public static List Tokenize(this string commandLine)
for (var index = 0; index < commandLine.Length; index++)
{
var ch = commandLine[index];
- if (ch == '\\')
+ if (ch == '\\' && quoteChar != CommandToken.NoEscapedChar)
{
index++;
- if (index >= commandLine.Length) throw new ArgumentException("Unexpected end of command line");
+ if (index >= commandLine.Length)
+ {
+ throw new ArgumentException("Unexpected end of command line while processing escape sequence." +
+ " The command line ends with a backslash character.");
+ }
token.Append(EscapedChar(commandLine[index]));
}
else if (quoteChar != CommandToken.NoQuoteChar)
@@ -77,7 +81,7 @@ public static List Tokenize(this string commandLine)
token.Append(ch);
}
}
- else if (ch == '"' || ch == '\'')
+ else if (ch == '"' || ch == '\'' || ch == CommandToken.NoEscapedChar)
{
if (token.Length == 0) // If ch is the first char. To keep consistency with legacy behavior
{
diff --git a/src/Neo.ConsoleService/ConsoleServiceBase.cs b/src/Neo.ConsoleService/ConsoleServiceBase.cs
index 6bcd72c283..720cbeba08 100644
--- a/src/Neo.ConsoleService/ConsoleServiceBase.cs
+++ b/src/Neo.ConsoleService/ConsoleServiceBase.cs
@@ -44,6 +44,84 @@ public abstract class ConsoleServiceBase
private readonly List _commandHistory = new();
+ ///
+ /// Parse sequential arguments.
+ /// For example, if a method defined as `void Method(string arg1, int arg2, bool arg3)`,
+ /// the arguments will be parsed as `"arg1" 2 true`.
+ ///
+ /// the MethodInfo of the called method
+ /// the raw arguments
+ /// the parsed arguments
+ /// Missing argument
+ internal object?[] ParseSequentialArguments(MethodInfo method, IList args)
+ {
+ var parameters = method.GetParameters();
+ var arguments = new List