diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..8f3532c12
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,263 @@
+###############################
+# Core EditorConfig Options #
+###############################
+
+# dotnet-format requires version 3.1.37601
+# dotnet tool update -g dotnet-format
+# remember to have: git config --global core.autocrlf false #(which is usually default)
+
+# top-most EditorConfig file
+root = true
+
+# Don't use tabs for indentation.
+[*]
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+end_of_line = lf
+
+# (Please don't specify an indent_size here; that has too many unintended consequences.)
+spelling_exclusion_path = SpellingExclusions.dic
+
+# Code files
+[*.{cs,csx,vb,vbx}]
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+
+# XML project files
+[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
+indent_size = 2
+
+# XML config files
+[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
+indent_size = 2
+
+# JSON files
+[*.json]
+indent_size = 2
+
+# Powershell files
+[*.ps1]
+indent_size = 2
+
+# Shell script files
+[*.sh]
+end_of_line = lf
+indent_size = 2
+
+# YAML files
+[*.yml]
+end_of_line = lf
+indent_size = 2
+
+# Dotnet code style settings:
+[*.{cs,vb}]
+# Use file-scoped namespace
+csharp_style_namespace_declarations = file_scoped:warning
+
+# Member can be made 'readonly'
+csharp_style_prefer_readonly_struct_member = true
+dotnet_diagnostic.IDE0251.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
+dotnet_separate_import_directive_groups = false
+
+# Avoid "this." and "Me." if not necessary
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+
+# Use language keywords instead of framework type names for type references
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# Suggest more modern language features when available
+dotnet_style_object_initializer = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_prefer_collection_expression = never
+
+# Whitespace options
+dotnet_style_allow_multiple_blank_lines_experimental = false
+
+# Non-private static fields are PascalCase
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
+
+dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
+dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
+
+dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
+
+# Non-private readonly fields are PascalCase
+dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields
+dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style
+
+dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
+dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly
+
+dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case
+
+# Local functions are PascalCase
+dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
+dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style
+
+dotnet_naming_symbols.local_functions.applicable_kinds = local_function
+
+dotnet_naming_style.local_function_style.capitalization = pascal_case
+
+file_header_template = Copyright (C) 2015-2026 The Neo Project.\n\n{fileName} file belongs to the neo project and is free\nsoftware distributed under the MIT software license, see the\naccompanying file LICENSE in the main directory of the\nrepository or http://www.opensource.org/licenses/mit-license.php\nfor more details.\n\nRedistribution and use in source and binary forms with or without\nmodifications are permitted.
+
+# Require file header
+dotnet_diagnostic.IDE0073.severity = warning
+
+# RS0016: Only enable if API files are present
+dotnet_public_api_analyzer.require_api_files = true
+
+# IDE0055: Fix formatting
+# Workaround for https://github.com/dotnet/roslyn/issues/70570
+dotnet_diagnostic.IDE0055.severity = warning
+
+# CSharp code style settings:
+[*.cs]
+# Newline settings
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = flush_left
+
+# Whitespace options
+csharp_style_allow_embedded_statements_on_same_line_experimental = false
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false
+
+# Prefer method-like constructs to have a block body
+csharp_style_expression_bodied_methods = false:none
+csharp_style_expression_bodied_constructors = false:none
+csharp_style_expression_bodied_operators = false:none
+
+# Prefer property-like constructs to have an expression-body
+csharp_style_expression_bodied_properties = true:none
+csharp_style_expression_bodied_indexers = true:none
+csharp_style_expression_bodied_accessors = true:none
+
+# IDE0230: Use UTF-8 string literal
+csharp_style_prefer_utf8_string_literals = true:silent
+
+# Suggest more modern language features when available
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+csharp_style_prefer_extended_property_pattern = true:suggestion
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = do_not_ignore
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Blocks are allowed
+csharp_prefer_braces = true:silent
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+# IDE0060: Remove unused parameter
+dotnet_diagnostic.IDE0060.severity = none
+
+[src/{Analyzers,CodeStyle,Features,Workspaces,EditorFeatures,VisualStudio}/**/*.{cs,vb}]
+
+# Avoid "this." and "Me." if not necessary
+dotnet_diagnostic.IDE0003.severity = warning
+dotnet_diagnostic.IDE0009.severity = warning
+
+# IDE0011: Add braces
+csharp_prefer_braces = when_multiline:warning
+# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201
+dotnet_diagnostic.IDE0011.severity = warning
+
+# IDE0040: Add accessibility modifiers
+dotnet_diagnostic.IDE0040.severity = warning
+
+# IDE0052: Remove unread private member
+dotnet_diagnostic.IDE0052.severity = warning
+
+# IDE0059: Unnecessary assignment to a value
+dotnet_diagnostic.IDE0059.severity = warning
+
+# Use collection expression for array
+dotnet_diagnostic.IDE0300.severity = warning
+
+# CA1012: Abstract types should not have public constructors
+dotnet_diagnostic.CA1012.severity = warning
+
+# CA1822: Make member static
+dotnet_diagnostic.CA1822.severity = warning
+
+# csharp_style_allow_embedded_statements_on_same_line_experimental
+dotnet_diagnostic.IDE2001.severity = warning
+
+# csharp_style_allow_blank_lines_between_consecutive_braces_experimental
+dotnet_diagnostic.IDE2002.severity = warning
+
+# csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental
+dotnet_diagnostic.IDE2004.severity = warning
+
+# csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental
+dotnet_diagnostic.IDE2005.severity = warning
+
+# csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental
+dotnet_diagnostic.IDE2006.severity = warning
+
+[src/{VisualStudio}/**/*.{cs,vb}]
+# CA1822: Make member static
+# There is a risk of accidentally breaking an internal API that partners rely on though IVT.
+dotnet_code_quality.CA1822.api_surface = private
diff --git a/.github/workflows/auto-labels.yml b/.github/workflows/auto-labels.yml
new file mode 100644
index 000000000..80f329540
--- /dev/null
+++ b/.github/workflows/auto-labels.yml
@@ -0,0 +1,28 @@
+name: Auto-label PRs
+
+on:
+ pull_request_target:
+ types: [opened, synchronize, reopened]
+
+permissions:
+ pull-requests: write
+
+jobs:
+ add-label:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Add N4 label to PRs targeting master
+ if: github.event.pull_request.base.ref == 'master'
+ uses: actions-ecosystem/action-add-labels@v1.1.3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ labels: |
+ N4
+
+ - name: Add N3 label to PRs targeting master-n3
+ if: github.event.pull_request.base.ref == 'master-n3'
+ uses: actions-ecosystem/action-add-labels@v1.1.3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ labels: |
+ N3
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 000000000..e71855c18
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,46 @@
+name: .NET Core Test
+
+on: pull_request
+
+env:
+ DOTNET_VERSION: 10.0.x
+
+jobs:
+ Test:
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.DOTNET_VERSION }}
+ - name: Check format
+ if: runner.os == 'Linux'
+ run: dotnet format --no-restore --verify-no-changes --verbosity diagnostic
+ - name: Build CLI
+ if: runner.os == 'Linux'
+ run: |
+ dotnet publish -o ./out -c Release src/Neo.CLI
+ find ./out -name 'config.json' | xargs perl -pi -e 's|LevelDBStore|MemoryStore|g'
+ - name: Install dependencies
+ if: runner.os == 'Linux'
+ run: sudo apt-get install libleveldb-dev expect
+ - name: Run tests with expect
+ if: runner.os == 'Linux'
+ run: expect ./.github/workflows/test-neo-cli.expect
+ - name: Run Unit Tests
+ if: runner.os == 'Windows'
+ run: |
+ forfiles /p tests /m *.csproj /s /c "cmd /c dotnet add @PATH package coverlet.msbuild"
+ dotnet test /p:CollectCoverage=true /p:CoverletOutput='${{ github.workspace }}/TestResults/coverage/' /p:MergeWith='${{ github.workspace }}/TestResults/coverage/coverage.json' /p:CoverletOutputFormat=lcov%2cjson -m:1
+ - name: Coveralls
+ if: runner.os == 'Windows'
+ uses: coverallsapp/github-action@v2
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ format: lcov
+ file: ${{ github.workspace }}/TestResults/coverage/coverage.info
diff --git a/ci/test-neo-cli.expect b/.github/workflows/test-neo-cli.expect
similarity index 58%
rename from ci/test-neo-cli.expect
rename to .github/workflows/test-neo-cli.expect
index 61e5d82df..dbf41526b 100755
--- a/ci/test-neo-cli.expect
+++ b/.github/workflows/test-neo-cli.expect
@@ -5,7 +5,7 @@
set timeout 10
# Start neo-cli
-spawn dotnet neo-cli.dll --rpc
+spawn dotnet out/neo-cli.dll
# Expect the main input prompt
expect {
@@ -17,7 +17,31 @@ expect {
#
# Test 'create wallet'
#
-send "create wallet test-wallet.json\n"
+send "create wallet test-wallet1.json\n"
+
+expect {
+ "password:" { send "asd\n" }
+ "error" { exit 2 }
+ timeout { exit 1 }
+}
+
+expect {
+ "password:" { send "asd\n" }
+ "error" { exit 2 }
+ timeout { exit 1 }
+}
+
+expect {
+ " Address:" { }
+ "error" { exit 2 }
+ timeout { exit 1 }
+}
+
+#
+# Test 'create wallet'
+#
+send "create wallet test-wallet2.json L2ArHTuiDL4FHu4nfyhamrG8XVYB4QyRbmhj7vD6hFMB5iAMSTf6\n"
+
expect {
"password:" { send "asd\n" }
"error" { exit 2 }
@@ -31,7 +55,7 @@ expect {
}
expect {
- "address:" { }
+ "Address: NUj249PQg9EMJfAuxKizdJwMG7GSBzYX2Y" { }
"error" { exit 2 }
timeout { exit 1 }
}
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 2f93a0507..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-sudo: required
-
-services:
- - docker
-
-script:
- - docker build -f ci/Dockerfile -t neo-cli-ci .
- - docker run neo-cli-ci /opt/ci/run-tests-in-docker.sh
diff --git a/CHANGELOG b/CHANGELOG
deleted file mode 100644
index c51422595..000000000
--- a/CHANGELOG
+++ /dev/null
@@ -1,143 +0,0 @@
-# Changelog
-All notable changes to this project will be documented in this file.
-
-## [Unreleased]
-### Changed
-- Optimize RPC command `getbalance` for NEP-5.
-- Optimize config.json
-
-### Fixed
-- Prevents blocking when the second instance is started.
-
-## [2.7.4] - 2018-03-29
-### Added
-- New smart contract feature: Maps.
-
-### Changed
-- Optimize protocol.json
-
-### Fixed
-- Fix the issue of `Neo.Storage.Find`.(smart contract)
-- Record application logs when importing blocks.
-
-## [2.7.3] - 2018-03-14
-### Added
-- New CLI command: `broadcast`.
-- GzipCompression over RPC.
-- New smart contract APIs: `Neo.Iterator.*`, `Neo.Storage.Find`.
-- New smart contract APIs: `Neo.Runtime.Serialize`, `Neo.Runtime.Deserialize`.
-- New smart contract API: `Neo.TransactionInvocation.GetScript`.
-
-### Changed
-- Improve the performance of importing blocks.
-- Improve the performance of p2p network.
-- Optimize CLI commands: `show node`, `show pool`.
-
-### Fixed
-- Fix crash on exiting.
-
-## [2.7.1] - 2018-01-31
-### Added
-- Allow user to create db3 wallet.
-
-## [2.7.0] - 2018-01-26
-### Added
-- New RPC command: `listaddress`.
-- New RPC command: `getapplicationlog`.
-- New opcode `REMOVE`.(smart contract)
-
-### Removed
-- Remove option `--record-notifications`.
-
-## [2.6.0] - 2018-01-15
-### Added
-- New RPC command: `sendfrom`.
-
-### Changed
-- Improve the performance of rebuilding wallet index.
-- Prevent the creation of wallet files with blank password.
-- Add `time` to the outputs of `Blockchain_Notify`.
-
-### Fixed
-- Save wallet file when creating address by calling RPC command `getnewaddress`.
-- Fix the issue of RPC commands `invoke*`.
-
-### Removed
-- Remove `Neo.Account.SetVotes` and `Neo.Validator.Register`.(smart contract)
-
-## [2.5.2] - 2017-12-14
-### Added
-- New smart contract API: `Neo.Runtime.GetTime`.
-- New opcodes `APPEND`, `REVERSE`.(smart contract)
-
-### Changed
-- Add fields `tx` and `script` to RPC commands `invoke*`.
-- Improve the performance of p2p network.
-- Optimize protocol.json
-
-### Fixed
-- Fix the network issue when restart the client.
-
-## [2.5.0] - 2017-12-12
-### Added
-- Support for NEP-6 wallets.
-- Add startup parameter: `--nopeers`.
-
-## [2.4.1] - 2017-11-24
-### Added
-- New smart contract feature: Dynamic Invocation.(NEP-4)
-- New smart contract APIs: `Neo.Transaction.GetUnspentCoins`, `Neo.Header.GetIndex`.
-
-### Changed
-- Optimize CLI command: `show state`.
-- Optimize config.json
-- Improve the performance of p2p network.
-
-## [2.3.5] - 2017-10-27
-### Changed
-- Optimize RPC commands `sendtoaddress` and `sendmany` for NEP-5 transfer.
-- Optimize CLI command `send` for NEP-5 transfer.
-
-## [2.3.4] - 2017-10-12
-### Added
-- Add startup parameter: `--record-notifications`.
-- New RPC commands: `invoke`, `invokefunction`, `invokescript`.
-- New RPC command: `getversion`.
-- Console colors.
-
-### Fixed
-- Improve stability.
-
-## [2.3.2] - 2017-09-06
-### Added
-- New CLI command: `send all`.
-- New opcodes `THROW`, `THROWIFNOT`.(smart contract)
-
-### Changed
-- Optimize opcode `CHECKMULTISIG`.
-
-### Fixed
-- Fix the issue of `Neo.Runtime.CheckWitness`.(smart contract)
-
-## [2.1.0] - 2017-08-15
-### Added
-- New RPC command: `sendmany`.
-- New CLI command: `show utxo`.
-- New smart contract feature: Triggers.
-
-## [2.0.2] - 2017-08-14
-### Changed
-- Improve the performance of p2p network.
-
-## [2.0.1] - 2017-07-20
-### Added
-- New RPC commands: `getpeers`, `getblocksysfee`.
-- New RPC commands: `getaccountstate`, `getassetstate`, `getcontractstate`, `getstorage`.
-- Add default config files for MAINNET and TESTNET.
-
-### Changed
-- Improve the performance of p2p network.
-
-## [2.0.0] - 2017-07-13
-### Changed
-- Rebrand from AntShares to NEO.
diff --git a/LICENSE b/LICENSE
index 8864d4a39..4422d9c73 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2017
+Copyright (c) 2016-2019
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/NuGet.Config b/NuGet.Config
new file mode 100644
index 000000000..640fd0fe3
--- /dev/null
+++ b/NuGet.Config
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 6aedebe90..001338854 100644
--- a/README.md
+++ b/README.md
@@ -1,45 +1,103 @@
-[](https://travis-ci.org/neo-project/neo-cli)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Currently, neo-cli and neo-gui are integrated into one repository. You can enter the corresponding folder and follow the instructions to run each node.
## Prerequisites
-You will need Window or Linux. Use a virtual machine if you have a Mac. Ubuntu 14 and 16 are supported. Ubuntu 17 is not supported.
+You will need Window, Linux or macOS. Ubuntu LTS 14, 16 and 18 are supported.
Install [.NET Core](https://www.microsoft.com/net/download/core).
-On Linux, install the LevelDB and SQLite3 dev packages. E.g. on Ubuntu:
+On Linux, install the LevelDB and SQLite3 dev packages. E.g. on Ubuntu or Fedora:
```sh
-sudo apt-get install libleveldb-dev sqlite3 libsqlite3-dev libunwind8-dev
+sudo apt-get install libleveldb-dev sqlite3 libsqlite3-dev libunwind8-dev # Ubuntu
+sudo dnf install leveldb-devel sqlite sqlite-devel libunwind-devel # Fedora
+```
+
+On macOS, install the LevelDB package. E.g. install via Homebrew:
+```
+brew install --ignore-dependencies --build-from-source leveldb
```
On Windows, use the [Neo version of LevelDB](https://github.com/neo-project/leveldb).
## Download pre-compiled binaries
-See also [official docs](http://docs.neo.org/en-us/node/introduction.html). Download and unzip [latest release](https://github.com/neo-project/neo-cli/releases).
+See also [official docs](https://docs.neo.org/docs/en-us/node/introduction.html). Download and unzip the [latest release](https://github.com/neo-project/neo-node/releases).
+
+On Linux, you can type the command:
```sh
-dotnet neo-cli.dll
+./neo-cli
```
+On Windows, you can just double click the exe to run the node.
+
## Compile from source
-Clone the neo-cli repository.
+Clone the neo-node repository.
+
+For neo-cli, you can type the following commands:
```sh
-cd neo-cli
+cd neo-node/neo-cli
dotnet restore
dotnet publish -c Release
```
-In order to run, you need version 1.1.2 of .Net Core. Download the SDK [binary](https://www.microsoft.com/net/download/linux).
+Next, you should enter the working directory (i.e. /bin/Debug, /bin/Release) and paste the `libleveldb.dll` here. In addition, you need to create `Plugins` folder and put the `LevelDBStore` or `RocksDBStore` or other supported storage engine, as well as the configuration file, in the Plugins folder.
+
+Assuming you are in the working directory:
+
+```sh
+dotnet neo-cli.dll
+```
+
+For neo-gui, you just need to enter the `neo-node/neo-gui` folder and follow the above steps to run the node.
+
+## Build into Docker
+
+Clone the neo-node repository.
+
+```sh
+cd neo-node
+docker build -t neo-cli .
+docker run -p 10332:10332 -p 10333:10333 --name=neo-cli-mainnet neo-cli
+```
-Assuming you extracted .Net in the parent folder:
+After start the container successfully, use the following scripts to open neo-cli interactive window:
```sh
-../dotnet bin/Release/netcoreapp1.0/neo-cli.dll .
+docker exec -it neo-cli-mainnet /bin/bash
+screen -r node
```
+## Logging
+
+To enable logs in neo-cli, you need to add the ApplicationLogs plugin. Please check [here](https://github.com/neo-project/neo-modules.git) for more information.
+
+
+## Bootstrapping the network.
+In order to synchronize the network faster, please check [here](https://docs.neo.org/docs/en-us/node/syncblocks.html).
+
+
## Usage
-See [documentation](http://docs.neo.org/en-us/node/cli.html). E.g. try `show state` or `create wallet wallet.db3`.
+For more information about these two nodes, you can refer to [documentation](https://docs.neo.org/docs/en-us/node/introduction.html) to try out more features.
+
diff --git a/ci/Dockerfile b/ci/Dockerfile
deleted file mode 100644
index 30d9d9d19..000000000
--- a/ci/Dockerfile
+++ /dev/null
@@ -1,30 +0,0 @@
-FROM microsoft/dotnet:2.0-sdk
-
-# Install dependencies:
-RUN apt-get update && apt-get install -y \
- libleveldb-dev \
- sqlite3 \
- libsqlite3-dev \
- libunwind8-dev \
- wget \
- expect \
- screen \
- zip
-
-# APT cleanup to reduce image size
-RUN rm -rf /var/lib/apt/lists/*
-
-WORKDIR /opt
-
-# Get code to test
-ADD neo-cli /opt/neo-cli-github
-ADD ci /opt/ci
-
-WORKDIR /opt/neo-cli-github
-
-# Build the project
-RUN dotnet restore
-RUN dotnet publish -c Release
-RUN mv bin/Release/netcoreapp2.0/publish /opt/neo-cli
-
-WORKDIR /opt
diff --git a/ci/README.md b/ci/README.md
deleted file mode 100644
index a8d48230f..000000000
--- a/ci/README.md
+++ /dev/null
@@ -1,20 +0,0 @@
-### Test & Continuous Integration Setup
-
-The test suite can be run manually, and is automatically run by [Travis CI](https://travis-ci.org/neo-project/neo-cli) on each code push.
-
-To run the tests manually, you need to install [Docker CE](https://www.docker.com/community-edition#/download), and run the test script from a bash compatible shell (eg. Git bash on Windows) like this:
-
- ./ci/build-and-test.sh
-
-The test suite performs the following tasks:
-
-* Build the latest code
-* Verify the basic neo-cli functionality using [expect](https://linux.die.net/man/1/expect)
-* Verify JSON-RPC functionality with curl
-
-Files:
-
-* `Dockerfile`: the system to build neo-cli and to run the tests
-* `build-and-test.sh`: this builds the Docker image, starts it and runs the tests inside. This is useful for testing the CI run on a local dev machine.
-* `run-tests-in-docker.sh`: is run inside the Docker container and executes the tests
-* `test-neo-cli.expect`: [expect](https://linux.die.net/man/1/expect) script which verifies neo-cli functionality
diff --git a/ci/build-and-test.sh b/ci/build-and-test.sh
deleted file mode 100755
index ddfc3f453..000000000
--- a/ci/build-and-test.sh
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/bin/bash
-#
-# This script builds neo-cli with dotnet 2.0, and runs the tests.
-#
-CONTAINER_NAME="neo-cli-ci"
-
-# Get absolute path of code and ci folder. This allows to run this script from
-# anywhere, whether from inside this directory or outside.
-DIR_CI="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-DIR_BASE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
-# echo "CI directory: $DIR_CI"
-# echo "Base directory: $DIR_BASE"
-
-# Build the Docker image (includes building the current neo-cli code)
-# docker build --no-cache -f $DIR_CI/Dockerfile -t $CONTAINER_NAME $DIR_BASE
-docker build -f $DIR_CI/Dockerfile -t $CONTAINER_NAME $DIR_BASE
-
-# Stop already running containers
-CONTAINER=$(docker ps -aqf name=$CONTAINER_NAME)
-if [ -n "$CONTAINER" ]; then
- echo "Stopping container named $CONTAINER_NAME"
- docker stop $CONTAINER_NAME 1>/dev/null
- echo "Removing container named $CONTAINER_NAME"
- docker rm -f $CONTAINER_NAME 1>/dev/null
-fi
-
-# Start a new test container
-docker run --name $CONTAINER_NAME $CONTAINER_NAME /opt/ci/run-tests-in-docker.sh
diff --git a/ci/run-tests-in-docker.sh b/ci/run-tests-in-docker.sh
deleted file mode 100755
index 5db61969e..000000000
--- a/ci/run-tests-in-docker.sh
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/bin/bash
-#
-# This script is run inside the Docker container and tests neo-cli
-#
-set -e
-
-cd /opt/neo-cli
-
-# Run tests with expect
-expect /opt/ci/test-neo-cli.expect
-
-# Start neo-cli in background for additional JSON-RPC tests
-screen -dmS node1 bash -c "dotnet neo-cli.dll --rpc"
-
-# Wait a little bit
-sleep 3
-
-# Test a RPX smart contract query
-JSONRPC_RES=$( curl --silent \
- --request POST \
- --url localhost:10332/ \
- --header 'accept: application/json' \
- --header 'content-type: application/json' \
- --data '{
- "jsonrpc": "2.0",
- "method": "invokefunction",
- "params": [
- "ecc6b20d3ccac1ee9ef109af5a7cdb85706b1df9",
- "totalSupply"
- ],
- "id": 3
- }' )
-
-echo "JSON-RPC response: $JSONRPC_RES"
-
-# Make sure we get a valid response
-echo ${JSONRPC_RES} | grep --quiet "00c10b746f74616c537570706c7967f91d6b7085db7c5aaf09f19eeec1ca3c0db2c6ec"
-
-# Make sure the response doesn't include "error"
-if echo ${JSONRPC_RES} | grep --quiet "\"error\""; then
- echo "Error: \"error\" found in json-rpc response"
- exit 1
-fi
diff --git a/neo-cli.sln b/neo-cli.sln
deleted file mode 100644
index eec6be893..000000000
--- a/neo-cli.sln
+++ /dev/null
@@ -1,22 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.26430.15
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "neo-cli", "neo-cli\neo-cli.csproj", "{900CA179-AEF0-43F3-9833-5DB060272D8E}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {900CA179-AEF0-43F3-9833-5DB060272D8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {900CA179-AEF0-43F3-9833-5DB060272D8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {900CA179-AEF0-43F3-9833-5DB060272D8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {900CA179-AEF0-43F3-9833-5DB060272D8E}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
-EndGlobal
diff --git a/neo-cli/Consensus/ConsensusWithPolicy.cs b/neo-cli/Consensus/ConsensusWithPolicy.cs
deleted file mode 100644
index b5ced7f2f..000000000
--- a/neo-cli/Consensus/ConsensusWithPolicy.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using Neo.Core;
-using Neo.Network;
-using Neo.Wallets;
-using System;
-using System.IO;
-using System.Linq;
-
-namespace Neo.Consensus
-{
- internal class ConsensusWithPolicy : ConsensusService
- {
- private string log_dictionary;
-
- public ConsensusWithPolicy(LocalNode localNode, Wallet wallet, string log_dictionary)
- : base(localNode, wallet)
- {
- this.log_dictionary = log_dictionary;
- }
-
- protected override bool CheckPolicy(Transaction tx)
- {
- switch (Policy.Default.PolicyLevel)
- {
- case PolicyLevel.AllowAll:
- return true;
- case PolicyLevel.AllowList:
- return tx.Scripts.All(p => Policy.Default.List.Contains(p.VerificationScript.ToScriptHash())) || tx.Outputs.All(p => Policy.Default.List.Contains(p.ScriptHash));
- case PolicyLevel.DenyList:
- return tx.Scripts.All(p => !Policy.Default.List.Contains(p.VerificationScript.ToScriptHash())) && tx.Outputs.All(p => !Policy.Default.List.Contains(p.ScriptHash));
- default:
- return base.CheckPolicy(tx);
- }
- }
-
- protected override void Log(string message)
- {
- DateTime now = DateTime.Now;
- string line = $"[{now.TimeOfDay:hh\\:mm\\:ss}] {message}";
- Console.WriteLine(line);
- if (string.IsNullOrEmpty(log_dictionary)) return;
- lock (log_dictionary)
- {
- Directory.CreateDirectory(log_dictionary);
- string path = Path.Combine(log_dictionary, $"{now:yyyy-MM-dd}.log");
- File.AppendAllLines(path, new[] { line });
- }
- }
-
- public void RefreshPolicy()
- {
- Policy.Default.Refresh();
- }
- }
-}
diff --git a/neo-cli/Consensus/Policy.cs b/neo-cli/Consensus/Policy.cs
deleted file mode 100644
index c7cc384f8..000000000
--- a/neo-cli/Consensus/Policy.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using Neo.Wallets;
-using Microsoft.Extensions.Configuration;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-
-namespace Neo.Consensus
-{
- internal class Policy
- {
- public PolicyLevel PolicyLevel { get; private set; }
- public HashSet List { get; private set; }
-
- public static Policy Default { get; private set; }
-
- static Policy()
- {
- Default = new Policy();
- Default.Refresh();
- }
-
- public void Refresh()
- {
- if (File.Exists("policy.json"))
- {
- IConfigurationSection section = new ConfigurationBuilder().AddJsonFile("policy.json").Build().GetSection("PolicyConfiguration");
- PolicyLevel = (PolicyLevel)Enum.Parse(typeof(PolicyLevel), section.GetSection("PolicyLevel").Value, true);
- List = new HashSet(section.GetSection("List").GetChildren().Select(p => Wallet.ToScriptHash(p.Value)));
- }
- else
- {
- PolicyLevel = PolicyLevel.AllowAll;
- List = new HashSet();
- }
- }
- }
-}
diff --git a/neo-cli/Consensus/PolicyLevel.cs b/neo-cli/Consensus/PolicyLevel.cs
deleted file mode 100644
index 55ea03489..000000000
--- a/neo-cli/Consensus/PolicyLevel.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Neo.Consensus
-{
- internal enum PolicyLevel : byte
- {
- AllowAll,
- DenyAll,
- AllowList,
- DenyList
- }
-}
diff --git a/neo-cli/Helper.cs b/neo-cli/Helper.cs
deleted file mode 100644
index f35ebb3ab..000000000
--- a/neo-cli/Helper.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-using System.Security;
-
-namespace Neo
-{
- internal static class Helper
- {
- public static bool CompareTo(this SecureString s1, SecureString s2)
- {
- if (s1.Length != s2.Length)
- return false;
- IntPtr p1 = IntPtr.Zero;
- IntPtr p2 = IntPtr.Zero;
- try
- {
- p1 = SecureStringMarshal.SecureStringToGlobalAllocAnsi(s1);
- p2 = SecureStringMarshal.SecureStringToGlobalAllocAnsi(s2);
- int i = 0;
- while (true)
- {
- byte b1 = Marshal.ReadByte(p1, i);
- byte b2 = Marshal.ReadByte(p2, i++);
- if (b1 == 0 && b2 == 0)
- return true;
- if (b1 != b2)
- return false;
- if (b1 == 0 || b2 == 0)
- return false;
- }
- }
- finally
- {
- Marshal.ZeroFreeGlobalAllocAnsi(p1);
- Marshal.ZeroFreeGlobalAllocAnsi(p2);
- }
- }
- }
-}
diff --git a/neo-cli/Network/RPC/RpcServerWithWallet.cs b/neo-cli/Network/RPC/RpcServerWithWallet.cs
deleted file mode 100644
index bf19b559f..000000000
--- a/neo-cli/Network/RPC/RpcServerWithWallet.cs
+++ /dev/null
@@ -1,240 +0,0 @@
-using Neo.Core;
-using Neo.Implementations.Wallets.NEP6;
-using Neo.IO;
-using Neo.IO.Json;
-using Neo.SmartContract;
-using Neo.Wallets;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-
-namespace Neo.Network.RPC
-{
- internal class RpcServerWithWallet : RpcServer
- {
- public RpcServerWithWallet(LocalNode localNode)
- : base(localNode)
- {
- }
-
- protected override JObject Process(string method, JArray _params)
- {
- switch (method)
- {
- case "getapplicationlog":
- {
- UInt256 hash = UInt256.Parse(_params[0].AsString());
- string path = Path.Combine(Settings.Default.Paths.ApplicationLogs, $"{hash}.json");
- return File.Exists(path)
- ? JObject.Parse(File.ReadAllText(path))
- : throw new RpcException(-100, "Unknown transaction");
- }
- case "getbalance":
- if (Program.Wallet == null)
- throw new RpcException(-400, "Access denied.");
- else
- {
- JObject json = new JObject();
- switch (UIntBase.Parse(_params[0].AsString()))
- {
- case UInt160 asset_id_160: //NEP-5 balance
- json["balance"] = Program.Wallet.GetAvailable(asset_id_160).ToString();
- break;
- case UInt256 asset_id_256: //Global Assets balance
- IEnumerable coins = Program.Wallet.GetCoins().Where(p => !p.State.HasFlag(CoinState.Spent) && p.Output.AssetId.Equals(asset_id_256));
- json["balance"] = coins.Sum(p => p.Output.Value).ToString();
- json["confirmed"] = coins.Where(p => p.State.HasFlag(CoinState.Confirmed)).Sum(p => p.Output.Value).ToString();
- break;
- }
- return json;
- }
- case "listaddress":
- if (Program.Wallet == null)
- throw new RpcException(-400, "Access denied.");
- else
- return Program.Wallet.GetAccounts().Select(p =>
- {
- JObject account = new JObject();
- account["address"] = p.Address;
- account["haskey"] = p.HasKey;
- account["label"] = p.Label;
- account["watchonly"] = p.WatchOnly;
- return account;
- }).ToArray();
- case "sendfrom":
- if (Program.Wallet == null)
- throw new RpcException(-400, "Access denied");
- else
- {
- UIntBase assetId = UIntBase.Parse(_params[0].AsString());
- AssetDescriptor descriptor = new AssetDescriptor(assetId);
- UInt160 from = Wallet.ToScriptHash(_params[1].AsString());
- UInt160 to = Wallet.ToScriptHash(_params[2].AsString());
- BigDecimal value = BigDecimal.Parse(_params[3].AsString(), descriptor.Decimals);
- if (value.Sign <= 0)
- throw new RpcException(-32602, "Invalid params");
- Fixed8 fee = _params.Count >= 5 ? Fixed8.Parse(_params[4].AsString()) : Fixed8.Zero;
- if (fee < Fixed8.Zero)
- throw new RpcException(-32602, "Invalid params");
- UInt160 change_address = _params.Count >= 6 ? Wallet.ToScriptHash(_params[5].AsString()) : null;
- Transaction tx = Program.Wallet.MakeTransaction(null, new[]
- {
- new TransferOutput
- {
- AssetId = assetId,
- Value = value,
- ScriptHash = to
- }
- }, from: from, change_address: change_address, fee: fee);
- if (tx == null)
- throw new RpcException(-300, "Insufficient funds");
- ContractParametersContext context = new ContractParametersContext(tx);
- Program.Wallet.Sign(context);
- if (context.Completed)
- {
- tx.Scripts = context.GetScripts();
- Program.Wallet.ApplyTransaction(tx);
- LocalNode.Relay(tx);
- return tx.ToJson();
- }
- else
- {
- return context.ToJson();
- }
- }
- case "sendtoaddress":
- if (Program.Wallet == null)
- throw new RpcException(-400, "Access denied");
- else
- {
- UIntBase assetId = UIntBase.Parse(_params[0].AsString());
- AssetDescriptor descriptor = new AssetDescriptor(assetId);
- UInt160 scriptHash = Wallet.ToScriptHash(_params[1].AsString());
- BigDecimal value = BigDecimal.Parse(_params[2].AsString(), descriptor.Decimals);
- if (value.Sign <= 0)
- throw new RpcException(-32602, "Invalid params");
- Fixed8 fee = _params.Count >= 4 ? Fixed8.Parse(_params[3].AsString()) : Fixed8.Zero;
- if (fee < Fixed8.Zero)
- throw new RpcException(-32602, "Invalid params");
- UInt160 change_address = _params.Count >= 5 ? Wallet.ToScriptHash(_params[4].AsString()) : null;
- Transaction tx = Program.Wallet.MakeTransaction(null, new[]
- {
- new TransferOutput
- {
- AssetId = assetId,
- Value = value,
- ScriptHash = scriptHash
- }
- }, change_address: change_address, fee: fee);
- if (tx == null)
- throw new RpcException(-300, "Insufficient funds");
- ContractParametersContext context = new ContractParametersContext(tx);
- Program.Wallet.Sign(context);
- if (context.Completed)
- {
- tx.Scripts = context.GetScripts();
- Program.Wallet.ApplyTransaction(tx);
- LocalNode.Relay(tx);
- return tx.ToJson();
- }
- else
- {
- return context.ToJson();
- }
- }
- case "sendmany":
- if (Program.Wallet == null)
- throw new RpcException(-400, "Access denied");
- else
- {
- JArray to = (JArray)_params[0];
- if (to.Count == 0)
- throw new RpcException(-32602, "Invalid params");
- TransferOutput[] outputs = new TransferOutput[to.Count];
- for (int i = 0; i < to.Count; i++)
- {
- UIntBase asset_id = UIntBase.Parse(to[i]["asset"].AsString());
- AssetDescriptor descriptor = new AssetDescriptor(asset_id);
- outputs[i] = new TransferOutput
- {
- AssetId = asset_id,
- Value = BigDecimal.Parse(to[i]["value"].AsString(), descriptor.Decimals),
- ScriptHash = Wallet.ToScriptHash(to[i]["address"].AsString())
- };
- if (outputs[i].Value.Sign <= 0)
- throw new RpcException(-32602, "Invalid params");
- }
- Fixed8 fee = _params.Count >= 2 ? Fixed8.Parse(_params[1].AsString()) : Fixed8.Zero;
- if (fee < Fixed8.Zero)
- throw new RpcException(-32602, "Invalid params");
- UInt160 change_address = _params.Count >= 3 ? Wallet.ToScriptHash(_params[2].AsString()) : null;
- Transaction tx = Program.Wallet.MakeTransaction(null, outputs, change_address: change_address, fee: fee);
- if (tx == null)
- throw new RpcException(-300, "Insufficient funds");
- ContractParametersContext context = new ContractParametersContext(tx);
- Program.Wallet.Sign(context);
- if (context.Completed)
- {
- tx.Scripts = context.GetScripts();
- Program.Wallet.ApplyTransaction(tx);
- LocalNode.Relay(tx);
- return tx.ToJson();
- }
- else
- {
- return context.ToJson();
- }
- }
- case "getnewaddress":
- if (Program.Wallet == null)
- throw new RpcException(-400, "Access denied");
- else
- {
- WalletAccount account = Program.Wallet.CreateAccount();
- if (Program.Wallet is NEP6Wallet wallet)
- wallet.Save();
- return account.Address;
- }
- case "dumpprivkey":
- if (Program.Wallet == null)
- throw new RpcException(-400, "Access denied");
- else
- {
- UInt160 scriptHash = Wallet.ToScriptHash(_params[0].AsString());
- WalletAccount account = Program.Wallet.GetAccount(scriptHash);
- return account.GetKey().Export();
- }
- case "invoke":
- case "invokefunction":
- case "invokescript":
- JObject result = base.Process(method, _params);
- if (Program.Wallet != null)
- {
- InvocationTransaction tx = new InvocationTransaction
- {
- Version = 1,
- Script = result["script"].AsString().HexToBytes(),
- Gas = Fixed8.Parse(result["gas_consumed"].AsString())
- };
- tx.Gas -= Fixed8.FromDecimal(10);
- if (tx.Gas < Fixed8.Zero) tx.Gas = Fixed8.Zero;
- tx.Gas = tx.Gas.Ceiling();
- tx = Program.Wallet.MakeTransaction(tx);
- if (tx != null)
- {
- ContractParametersContext context = new ContractParametersContext(tx);
- Program.Wallet.Sign(context);
- if (context.Completed)
- tx.Scripts = context.GetScripts();
- else
- tx = null;
- }
- result["tx"] = tx?.ToArray().ToHexString();
- }
- return result;
- default:
- return base.Process(method, _params);
- }
- }
- }
-}
diff --git a/neo-cli/Program.cs b/neo-cli/Program.cs
deleted file mode 100644
index 01c8a38a4..000000000
--- a/neo-cli/Program.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using Neo.Shell;
-using Neo.Wallets;
-using System;
-using System.IO;
-
-namespace Neo
-{
- static class Program
- {
- internal static Wallet Wallet;
-
- private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
- {
- using (FileStream fs = new FileStream("error.log", FileMode.Create, FileAccess.Write, FileShare.None))
- using (StreamWriter w = new StreamWriter(fs))
- {
- PrintErrorLogs(w, (Exception)e.ExceptionObject);
- }
- }
-
- static void Main(string[] args)
- {
- AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
- new MainService().Run(args);
- }
-
- private static void PrintErrorLogs(StreamWriter writer, Exception ex)
- {
- writer.WriteLine(ex.GetType());
- writer.WriteLine(ex.Message);
- writer.WriteLine(ex.StackTrace);
- if (ex is AggregateException ex2)
- {
- foreach (Exception inner in ex2.InnerExceptions)
- {
- writer.WriteLine();
- PrintErrorLogs(writer, inner);
- }
- }
- else if (ex.InnerException != null)
- {
- writer.WriteLine();
- PrintErrorLogs(writer, ex.InnerException);
- }
- }
- }
-}
diff --git a/neo-cli/Services/ConsoleServiceBase.cs b/neo-cli/Services/ConsoleServiceBase.cs
deleted file mode 100644
index fb2a9e51f..000000000
--- a/neo-cli/Services/ConsoleServiceBase.cs
+++ /dev/null
@@ -1,155 +0,0 @@
-using System;
-using System.Reflection;
-using System.Security;
-using System.Text;
-
-namespace Neo.Services
-{
- public abstract class ConsoleServiceBase
- {
- protected virtual string Prompt => "service";
-
- public abstract string ServiceName { get; }
-
- protected bool ShowPrompt { get; set; } = true;
-
- protected virtual bool OnCommand(string[] args)
- {
- switch (args[0].ToLower())
- {
- case "clear":
- Console.Clear();
- return true;
- case "exit":
- return false;
- case "version":
- Console.WriteLine(Assembly.GetEntryAssembly().GetName().Version);
- return true;
- default:
- Console.WriteLine("error");
- return true;
- }
- }
-
- protected internal abstract void OnStart(string[] args);
-
- protected internal abstract void OnStop();
-
- public static string ReadPassword(string prompt)
- {
- const string t = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
- StringBuilder sb = new StringBuilder();
- ConsoleKeyInfo key;
- Console.Write(prompt);
- Console.Write(": ");
-
- Console.ForegroundColor = ConsoleColor.Yellow;
-
- do
- {
- key = Console.ReadKey(true);
- if (t.IndexOf(key.KeyChar) != -1)
- {
- sb.Append(key.KeyChar);
- Console.Write('*');
- }
- else if (key.Key == ConsoleKey.Backspace && sb.Length > 0)
- {
- sb.Length--;
- Console.Write(key.KeyChar);
- Console.Write(' ');
- Console.Write(key.KeyChar);
- }
- } while (key.Key != ConsoleKey.Enter);
-
- Console.ForegroundColor = ConsoleColor.White;
- Console.WriteLine();
- return sb.ToString();
- }
-
- public static SecureString ReadSecureString(string prompt)
- {
- const string t = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
- SecureString securePwd = new SecureString();
- ConsoleKeyInfo key;
- Console.Write(prompt);
- Console.Write(": ");
-
- Console.ForegroundColor = ConsoleColor.Yellow;
-
- do
- {
- key = Console.ReadKey(true);
- if (t.IndexOf(key.KeyChar) != -1)
- {
- securePwd.AppendChar(key.KeyChar);
- Console.Write('*');
- }
- else if (key.Key == ConsoleKey.Backspace && securePwd.Length > 0)
- {
- securePwd.RemoveAt(securePwd.Length - 1);
- Console.Write(key.KeyChar);
- Console.Write(' ');
- Console.Write(key.KeyChar);
- }
- } while (key.Key != ConsoleKey.Enter);
-
- Console.ForegroundColor = ConsoleColor.White;
- Console.WriteLine();
- securePwd.MakeReadOnly();
- return securePwd;
- }
-
- public void Run(string[] args)
- {
- OnStart(args);
- RunConsole();
- OnStop();
- }
-
- private void RunConsole()
- {
- bool running = true;
-#if NET461
- Console.Title = ServiceName;
-#endif
- Console.OutputEncoding = Encoding.Unicode;
-
- Console.ForegroundColor = ConsoleColor.DarkGreen;
- Version ver = Assembly.GetEntryAssembly().GetName().Version;
- Console.WriteLine($"{ServiceName} Version: {ver}");
- Console.WriteLine();
-
- while (running)
- {
- if (ShowPrompt)
- {
- Console.ForegroundColor = ConsoleColor.Green;
- Console.Write($"{Prompt}> ");
- }
-
- Console.ForegroundColor = ConsoleColor.Yellow;
- string line = Console.ReadLine().Trim();
- Console.ForegroundColor = ConsoleColor.White;
-
- string[] args = line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
- if (args.Length == 0)
- continue;
- try
- {
- running = OnCommand(args);
- }
- catch (Exception ex)
- {
-#if DEBUG
- Console.WriteLine($"error: {ex.Message}");
-#else
- Console.WriteLine("error");
-#endif
- }
- }
-
- Console.ResetColor();
- }
- }
-}
diff --git a/neo-cli/Settings.cs b/neo-cli/Settings.cs
deleted file mode 100644
index 80eac8a3c..000000000
--- a/neo-cli/Settings.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-using Microsoft.Extensions.Configuration;
-using Neo.Network;
-
-namespace Neo
-{
- internal class Settings
- {
- public PathsSettings Paths { get; }
- public P2PSettings P2P { get; }
- public RPCSettings RPC { get; }
-
- public static Settings Default { get; }
-
- static Settings()
- {
- IConfigurationSection section = new ConfigurationBuilder().AddJsonFile("config.json").Build().GetSection("ApplicationConfiguration");
- Default = new Settings(section);
- }
-
- public Settings(IConfigurationSection section)
- {
- this.Paths = new PathsSettings(section.GetSection("Paths"));
- this.P2P = new P2PSettings(section.GetSection("P2P"));
- this.RPC = new RPCSettings(section.GetSection("RPC"));
- }
- }
-
- internal class PathsSettings
- {
- public string Chain { get; }
- public string ApplicationLogs { get; }
-
- public PathsSettings(IConfigurationSection section)
- {
- this.Chain = string.Format(section.GetSection("Chain").Value, Message.Magic.ToString("X8"));
- this.ApplicationLogs = string.Format(section.GetSection("ApplicationLogs").Value, Message.Magic.ToString("X8"));
- }
- }
-
- internal class P2PSettings
- {
- public ushort Port { get; }
- public ushort WsPort { get; }
-
- public P2PSettings(IConfigurationSection section)
- {
- this.Port = ushort.Parse(section.GetSection("Port").Value);
- this.WsPort = ushort.Parse(section.GetSection("WsPort").Value);
- }
- }
-
- internal class RPCSettings
- {
- public ushort Port { get; }
- public string SslCert { get; }
- public string SslCertPassword { get; }
-
- public RPCSettings(IConfigurationSection section)
- {
- this.Port = ushort.Parse(section.GetSection("Port").Value);
- this.SslCert = section.GetSection("SslCert").Value;
- this.SslCertPassword = section.GetSection("SslCertPassword").Value;
- }
- }
-}
diff --git a/neo-cli/Shell/Coins.cs b/neo-cli/Shell/Coins.cs
deleted file mode 100644
index a777243ec..000000000
--- a/neo-cli/Shell/Coins.cs
+++ /dev/null
@@ -1,125 +0,0 @@
-using Neo.Core;
-using Neo.Network;
-using Neo.SmartContract;
-using Neo.Wallets;
-using System;
-using System.Linq;
-
-namespace Neo.Shell
-{
-
- public class Coins
- {
- private Wallet current_wallet;
- private LocalNode local_node;
-
- public Coins(Wallet wallet, LocalNode node)
- {
- current_wallet = wallet;
- local_node = node;
- }
-
- public Fixed8 UnavailableBonus()
- {
- uint height = Blockchain.Default.Height + 1;
- Fixed8 unavailable;
-
- try
- {
- unavailable = Blockchain.CalculateBonus(current_wallet.FindUnspentCoins().Where(p => p.Output.AssetId.Equals(Blockchain.GoverningToken.Hash)).Select(p => p.Reference), height);
- }
- catch (Exception)
- {
- unavailable = Fixed8.Zero;
- }
-
- return unavailable;
- }
-
-
- public Fixed8 AvailableBonus()
- {
- return Blockchain.CalculateBonus(current_wallet.GetUnclaimedCoins().Select(p => p.Reference));
- }
-
-
- public ClaimTransaction Claim()
- {
-
- if (this.AvailableBonus() == Fixed8.Zero)
- {
- Console.WriteLine($"no gas to claim");
- return null;
- }
-
- CoinReference[] claims = current_wallet.GetUnclaimedCoins().Select(p => p.Reference).ToArray();
- if (claims.Length == 0) return null;
-
- ClaimTransaction tx = new ClaimTransaction
- {
- Claims = claims,
- Attributes = new TransactionAttribute[0],
- Inputs = new CoinReference[0],
- Outputs = new[]
- {
- new TransactionOutput
- {
- AssetId = Blockchain.UtilityToken.Hash,
- Value = Blockchain.CalculateBonus(claims),
- ScriptHash = current_wallet.GetChangeAddress()
- }
- }
-
- };
-
- return (ClaimTransaction)SignTransaction(tx);
- }
-
-
- private Transaction SignTransaction(Transaction tx)
- {
- if (tx == null)
- {
- Console.WriteLine($"no transaction specified");
- return null;
- }
- ContractParametersContext context;
-
- try
- {
- context = new ContractParametersContext(tx);
- }
- catch (InvalidOperationException)
- {
- Console.WriteLine($"unsynchronized block");
-
- return null;
- }
-
- current_wallet.Sign(context);
-
- if (context.Completed)
- {
- context.Verifiable.Scripts = context.GetScripts();
- current_wallet.ApplyTransaction(tx);
-
- bool relay_result = local_node.Relay(tx);
-
- if (relay_result)
- {
- return tx;
- }
- else
- {
- Console.WriteLine($"Local Node could not relay transaction: {tx.Hash.ToString()}");
- }
- }
- else
- {
- Console.WriteLine($"Incomplete Signature: {context.ToString()}");
- }
-
- return null;
- }
- }
-}
diff --git a/neo-cli/Shell/MainService.cs b/neo-cli/Shell/MainService.cs
deleted file mode 100644
index 647cdd863..000000000
--- a/neo-cli/Shell/MainService.cs
+++ /dev/null
@@ -1,955 +0,0 @@
-using Neo.Consensus;
-using Neo.Core;
-using Neo.Implementations.Blockchains.LevelDB;
-using Neo.Implementations.Wallets.EntityFramework;
-using Neo.Implementations.Wallets.NEP6;
-using Neo.IO;
-using Neo.IO.Json;
-using Neo.Network;
-using Neo.Network.Payloads;
-using Neo.Network.RPC;
-using Neo.Services;
-using Neo.SmartContract;
-using Neo.VM;
-using Neo.Wallets;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.IO.Compression;
-using System.Linq;
-using System.Net;
-using System.Security.Cryptography;
-using System.Threading.Tasks;
-
-namespace Neo.Shell
-{
- internal class MainService : ConsoleServiceBase
- {
- private const string PeerStatePath = "peers.dat";
-
- private RpcServerWithWallet rpc;
- private ConsensusWithPolicy consensus;
-
- protected LocalNode LocalNode { get; private set; }
- protected override string Prompt => "neo";
- public override string ServiceName => "NEO-CLI";
-
- private void ImportBlocks(Stream stream)
- {
- LevelDBBlockchain blockchain = (LevelDBBlockchain)Blockchain.Default;
- using (BinaryReader r = new BinaryReader(stream))
- {
- uint count = r.ReadUInt32();
- for (int height = 0; height < count; height++)
- {
- byte[] array = r.ReadBytes(r.ReadInt32());
- if (height > blockchain.Height)
- {
- Block block = array.AsSerializable();
- blockchain.AddBlockDirectly(block);
- }
- }
- }
- }
-
- protected override bool OnCommand(string[] args)
- {
- switch (args[0].ToLower())
- {
- case "broadcast":
- return OnBroadcastCommand(args);
- case "create":
- return OnCreateCommand(args);
- case "export":
- return OnExportCommand(args);
- case "help":
- return OnHelpCommand(args);
- case "import":
- return OnImportCommand(args);
- case "list":
- return OnListCommand(args);
- case "claim":
- return OnClaimCommand(args);
- case "open":
- return OnOpenCommand(args);
- case "rebuild":
- return OnRebuildCommand(args);
- case "refresh":
- return OnRefreshCommand(args);
- case "send":
- return OnSendCommand(args);
- case "show":
- return OnShowCommand(args);
- case "start":
- return OnStartCommand(args);
- case "upgrade":
- return OnUpgradeCommand(args);
- default:
- return base.OnCommand(args);
- }
- }
-
- private bool OnBroadcastCommand(string[] args)
- {
- string command = args[1].ToLower();
- ISerializable payload = null;
- switch (command)
- {
- case "addr":
- payload = AddrPayload.Create(NetworkAddressWithTime.Create(new IPEndPoint(IPAddress.Parse(args[2]), ushort.Parse(args[3])), NetworkAddressWithTime.NODE_NETWORK, DateTime.UtcNow.ToTimestamp()));
- break;
- case "block":
- if (args[2].Length == 64 || args[2].Length == 66)
- payload = Blockchain.Default.GetBlock(UInt256.Parse(args[2]));
- else
- payload = Blockchain.Default.GetBlock(uint.Parse(args[2]));
- break;
- case "getblocks":
- case "getheaders":
- payload = GetBlocksPayload.Create(UInt256.Parse(args[2]));
- break;
- case "getdata":
- case "inv":
- payload = InvPayload.Create(Enum.Parse(args[2], true), args.Skip(3).Select(p => UInt256.Parse(p)).ToArray());
- break;
- case "tx":
- payload = LocalNode.GetTransaction(UInt256.Parse(args[2]));
- if (payload == null)
- payload = Blockchain.Default.GetTransaction(UInt256.Parse(args[2]));
- break;
- case "alert":
- case "consensus":
- case "filteradd":
- case "filterload":
- case "headers":
- case "merkleblock":
- case "ping":
- case "pong":
- case "reject":
- case "verack":
- case "version":
- Console.WriteLine($"Command \"{command}\" is not supported.");
- return true;
- }
- foreach (RemoteNode node in LocalNode.GetRemoteNodes())
- node.EnqueueMessage(command, payload);
- return true;
- }
-
- private bool OnCreateCommand(string[] args)
- {
- switch (args[1].ToLower())
- {
- case "address":
- return OnCreateAddressCommand(args);
- case "wallet":
- return OnCreateWalletCommand(args);
- default:
- return base.OnCommand(args);
- }
- }
-
- private bool OnCreateAddressCommand(string[] args)
- {
- if (Program.Wallet == null)
- {
- Console.WriteLine("You have to open the wallet first.");
- return true;
- }
- if (args.Length > 3)
- {
- Console.WriteLine("error");
- return true;
- }
- ushort count = 1;
- if (args.Length >= 3)
- count = ushort.Parse(args[2]);
- List addresses = new List();
- for (int i = 1; i <= count; i++)
- {
- WalletAccount account = Program.Wallet.CreateAccount();
- addresses.Add(account.Address);
- Console.SetCursorPosition(0, Console.CursorTop);
- Console.Write($"[{i}/{count}]");
- }
- if (Program.Wallet is NEP6Wallet wallet)
- wallet.Save();
- Console.WriteLine();
- string path = "address.txt";
- Console.WriteLine($"export addresses to {path}");
- File.WriteAllLines(path, addresses);
- return true;
- }
-
- private bool OnCreateWalletCommand(string[] args)
- {
- if (args.Length < 3)
- {
- Console.WriteLine("error");
- return true;
- }
- string path = args[2];
- string password = ReadPassword("password");
- if (password.Length == 0)
- {
- Console.WriteLine("cancelled");
- return true;
- }
- string password2 = ReadPassword("password");
- if (password != password2)
- {
- Console.WriteLine("error");
- return true;
- }
- switch (Path.GetExtension(path))
- {
- case ".db3":
- {
- Program.Wallet = UserWallet.Create(path, password);
- WalletAccount account = Program.Wallet.CreateAccount();
- Console.WriteLine($"address: {account.Address}");
- Console.WriteLine($" pubkey: {account.GetKey().PublicKey.EncodePoint(true).ToHexString()}");
- }
- break;
- case ".json":
- {
- NEP6Wallet wallet = new NEP6Wallet(path);
- wallet.Unlock(password);
- WalletAccount account = wallet.CreateAccount();
- wallet.Save();
- Program.Wallet = wallet;
- Console.WriteLine($"address: {account.Address}");
- Console.WriteLine($" pubkey: {account.GetKey().PublicKey.EncodePoint(true).ToHexString()}");
- }
- break;
- default:
- Console.WriteLine("Wallet files in that format are not supported, please use a .json or .db3 file extension.");
- break;
- }
- return true;
- }
-
- private bool OnExportCommand(string[] args)
- {
- switch (args[1].ToLower())
- {
- case "blocks":
- return OnExportBlocksCommand(args);
- case "key":
- return OnExportKeyCommand(args);
- default:
- return base.OnCommand(args);
- }
- }
-
- private bool OnExportBlocksCommand(string[] args)
- {
- if (args.Length > 3)
- {
- Console.WriteLine("error");
- return true;
- }
- string path = args.Length >= 3 ? args[2] : "chain.acc";
- using (FileStream fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
- {
- uint count = Blockchain.Default.Height + 1;
- uint start = 0;
- if (fs.Length > 0)
- {
- byte[] buffer = new byte[sizeof(uint)];
- fs.Read(buffer, 0, buffer.Length);
- start = BitConverter.ToUInt32(buffer, 0);
- fs.Seek(0, SeekOrigin.Begin);
- }
- if (start < count)
- fs.Write(BitConverter.GetBytes(count), 0, sizeof(uint));
- fs.Seek(0, SeekOrigin.End);
- for (uint i = start; i < count; i++)
- {
- Block block = Blockchain.Default.GetBlock(i);
- byte[] array = block.ToArray();
- fs.Write(BitConverter.GetBytes(array.Length), 0, sizeof(int));
- fs.Write(array, 0, array.Length);
- Console.SetCursorPosition(0, Console.CursorTop);
- Console.Write($"[{i + 1}/{count}]");
- }
- }
- Console.WriteLine();
- return true;
- }
-
- private bool OnExportKeyCommand(string[] args)
- {
- if (Program.Wallet == null)
- {
- Console.WriteLine("You have to open the wallet first.");
- return true;
- }
- if (args.Length < 2 || args.Length > 4)
- {
- Console.WriteLine("error");
- return true;
- }
- UInt160 scriptHash = null;
- string path = null;
- if (args.Length == 3)
- {
- try
- {
- scriptHash = Wallet.ToScriptHash(args[2]);
- }
- catch (FormatException)
- {
- path = args[2];
- }
- }
- else if (args.Length == 4)
- {
- scriptHash = Wallet.ToScriptHash(args[2]);
- path = args[3];
- }
- string password = ReadPassword("password");
- if (password.Length == 0)
- {
- Console.WriteLine("cancelled");
- return true;
- }
- if (!Program.Wallet.VerifyPassword(password))
- {
- Console.WriteLine("Incorrect password");
- return true;
- }
- IEnumerable keys;
- if (scriptHash == null)
- keys = Program.Wallet.GetAccounts().Where(p => p.HasKey).Select(p => p.GetKey());
- else
- keys = new[] { Program.Wallet.GetAccount(scriptHash).GetKey() };
- if (path == null)
- foreach (KeyPair key in keys)
- Console.WriteLine(key.Export());
- else
- File.WriteAllLines(path, keys.Select(p => p.Export()));
- return true;
- }
-
- private bool OnHelpCommand(string[] args)
- {
- Console.Write(
- "Normal Commands:\n" +
- "\tversion\n" +
- "\thelp\n" +
- "\tclear\n" +
- "\texit\n" +
- "Wallet Commands:\n" +
- "\tcreate wallet \n" +
- "\topen wallet \n" +
- "\tupgrade wallet \n" +
- "\trebuild index\n" +
- "\tlist address\n" +
- "\tlist asset\n" +
- "\tlist key\n" +
- "\tshow utxo [id|alias]\n" +
- "\tshow gas\n" +
- "\tclaim gas\n" +
- "\tcreate address [n=1]\n" +
- "\timport key \n" +
- "\texport key [address] [path]\n" +
- "\tsend |all [fee=0]\n" +
- "Node Commands:\n" +
- "\tshow state\n" +
- "\tshow node\n" +
- "\tshow pool [verbose]\n" +
- "\texport blocks [path=chain.acc]\n" +
- "Advanced Commands:\n" +
- "\tstart consensus\n" +
- "\trefresh policy\n");
- return true;
- }
-
- private bool OnImportCommand(string[] args)
- {
- switch (args[1].ToLower())
- {
- case "key":
- return OnImportKeyCommand(args);
- default:
- return base.OnCommand(args);
- }
- }
-
- private bool OnImportKeyCommand(string[] args)
- {
- if (args.Length > 3)
- {
- Console.WriteLine("error");
- return true;
- }
- byte[] prikey = null;
- try
- {
- prikey = Wallet.GetPrivateKeyFromWIF(args[2]);
- }
- catch (FormatException) { }
- if (prikey == null)
- {
- string[] lines = File.ReadAllLines(args[2]);
- for (int i = 0; i < lines.Length; i++)
- {
- if (lines[i].Length == 64)
- prikey = lines[i].HexToBytes();
- else
- prikey = Wallet.GetPrivateKeyFromWIF(lines[i]);
- Program.Wallet.CreateAccount(prikey);
- Array.Clear(prikey, 0, prikey.Length);
- Console.SetCursorPosition(0, Console.CursorTop);
- Console.Write($"[{i + 1}/{lines.Length}]");
- }
- Console.WriteLine();
- }
- else
- {
- WalletAccount account = Program.Wallet.CreateAccount(prikey);
- Array.Clear(prikey, 0, prikey.Length);
- Console.WriteLine($"address: {account.Address}");
- Console.WriteLine($" pubkey: {account.GetKey().PublicKey.EncodePoint(true).ToHexString()}");
- }
- if (Program.Wallet is NEP6Wallet wallet)
- wallet.Save();
- return true;
- }
-
- private bool OnListCommand(string[] args)
- {
- switch (args[1].ToLower())
- {
- case "address":
- return OnListAddressCommand(args);
- case "asset":
- return OnListAssetCommand(args);
- case "key":
- return OnListKeyCommand(args);
- default:
- return base.OnCommand(args);
- }
- }
-
- private bool OnClaimCommand(string[] args)
- {
- if (Program.Wallet == null)
- {
- Console.WriteLine($"Please open a wallet");
- return true;
- }
-
- Coins coins = new Coins(Program.Wallet, LocalNode);
-
- switch (args[1].ToLower())
- {
- case "gas":
- ClaimTransaction tx = coins.Claim();
- if (tx is ClaimTransaction)
- {
- Console.WriteLine($"Tranaction Suceeded: {tx.Hash.ToString()}");
- }
- return true;
- default:
- return base.OnCommand(args);
- }
- }
-
- private bool OnShowGasCommand(string[] args)
- {
- if (Program.Wallet == null)
- {
- Console.WriteLine($"Please open a wallet");
- return true;
- }
-
- Coins coins = new Coins(Program.Wallet, LocalNode);
- Console.WriteLine($"unavailable: {coins.UnavailableBonus().ToString()}");
- Console.WriteLine($" available: {coins.AvailableBonus().ToString()}");
- return true;
- }
-
- private bool OnListKeyCommand(string[] args)
- {
- if (Program.Wallet == null) return true;
- foreach (KeyPair key in Program.Wallet.GetAccounts().Where(p => p.HasKey).Select(p => p.GetKey()))
- {
- Console.WriteLine(key.PublicKey);
- }
- return true;
- }
-
- private bool OnListAddressCommand(string[] args)
- {
- if (Program.Wallet == null) return true;
- foreach (Contract contract in Program.Wallet.GetAccounts().Where(p => !p.WatchOnly).Select(p => p.Contract))
- {
- Console.WriteLine($"{contract.Address}\t{(contract.IsStandard ? "Standard" : "Nonstandard")}");
- }
- return true;
- }
-
- private bool OnListAssetCommand(string[] args)
- {
- if (Program.Wallet == null) return true;
- foreach (var item in Program.Wallet.GetCoins().Where(p => !p.State.HasFlag(CoinState.Spent)).GroupBy(p => p.Output.AssetId, (k, g) => new
- {
- Asset = Blockchain.Default.GetAssetState(k),
- Balance = g.Sum(p => p.Output.Value),
- Confirmed = g.Where(p => p.State.HasFlag(CoinState.Confirmed)).Sum(p => p.Output.Value)
- }))
- {
- Console.WriteLine($" id:{item.Asset.AssetId}");
- Console.WriteLine($" name:{item.Asset.GetName()}");
- Console.WriteLine($" balance:{item.Balance}");
- Console.WriteLine($"confirmed:{item.Confirmed}");
- Console.WriteLine();
- }
- return true;
- }
-
- private bool OnOpenCommand(string[] args)
- {
- switch (args[1].ToLower())
- {
- case "wallet":
- return OnOpenWalletCommand(args);
- default:
- return base.OnCommand(args);
- }
- }
-
- //TODO: 目前没有想到其它安全的方法来保存密码
- //所以只能暂时手动输入,但如此一来就不能以服务的方式启动了
- //未来再想想其它办法,比如采用智能卡之类的
- private bool OnOpenWalletCommand(string[] args)
- {
- if (args.Length < 3)
- {
- Console.WriteLine("error");
- return true;
- }
- string path = args[2];
- if (!File.Exists(path))
- {
- Console.WriteLine($"File does not exist");
- return true;
- }
- string password = ReadPassword("password");
- if (password.Length == 0)
- {
- Console.WriteLine("cancelled");
- return true;
- }
- if (Path.GetExtension(path) == ".db3")
- {
- try
- {
- Program.Wallet = UserWallet.Open(path, password);
- }
- catch (CryptographicException)
- {
- Console.WriteLine($"failed to open file \"{path}\"");
- return true;
- }
- }
- else
- {
- NEP6Wallet nep6wallet = new NEP6Wallet(path);
- try
- {
- nep6wallet.Unlock(password);
- }
- catch (CryptographicException)
- {
- Console.WriteLine($"failed to open file \"{path}\"");
- return true;
- }
- Program.Wallet = nep6wallet;
- }
- return true;
- }
-
- private bool OnRebuildCommand(string[] args)
- {
- switch (args[1].ToLower())
- {
- case "index":
- return OnRebuildIndexCommand(args);
- default:
- return base.OnCommand(args);
- }
- }
-
- private bool OnRebuildIndexCommand(string[] args)
- {
- WalletIndexer.RebuildIndex();
- return true;
- }
-
- private bool OnRefreshCommand(string[] args)
- {
- switch (args[1].ToLower())
- {
- case "policy":
- return OnRefreshPolicyCommand(args);
- default:
- return base.OnCommand(args);
- }
- }
-
- private bool OnRefreshPolicyCommand(string[] args)
- {
- if (consensus == null) return true;
- consensus.RefreshPolicy();
- return true;
- }
-
- private bool OnSendCommand(string[] args)
- {
- if (args.Length < 4 || args.Length > 5)
- {
- Console.WriteLine("error");
- return true;
- }
- if (Program.Wallet == null)
- {
- Console.WriteLine("You have to open the wallet first.");
- return true;
- }
- string password = ReadPassword("password");
- if (password.Length == 0)
- {
- Console.WriteLine("cancelled");
- return true;
- }
- if (!Program.Wallet.VerifyPassword(password))
- {
- Console.WriteLine("Incorrect password");
- return true;
- }
- UIntBase assetId;
- switch (args[1].ToLower())
- {
- case "neo":
- case "ans":
- assetId = Blockchain.GoverningToken.Hash;
- break;
- case "gas":
- case "anc":
- assetId = Blockchain.UtilityToken.Hash;
- break;
- default:
- assetId = UIntBase.Parse(args[1]);
- break;
- }
- UInt160 scriptHash = Wallet.ToScriptHash(args[2]);
- bool isSendAll = string.Equals(args[3], "all", StringComparison.OrdinalIgnoreCase);
- Transaction tx;
- if (isSendAll)
- {
- Coin[] coins = Program.Wallet.FindUnspentCoins().Where(p => p.Output.AssetId.Equals(assetId)).ToArray();
- tx = new ContractTransaction
- {
- Attributes = new TransactionAttribute[0],
- Inputs = coins.Select(p => p.Reference).ToArray(),
- Outputs = new[]
- {
- new TransactionOutput
- {
- AssetId = (UInt256)assetId,
- Value = coins.Sum(p => p.Output.Value),
- ScriptHash = scriptHash
- }
- }
- };
- }
- else
- {
- AssetDescriptor descriptor = new AssetDescriptor(assetId);
- if (!BigDecimal.TryParse(args[3], descriptor.Decimals, out BigDecimal amount))
- {
- Console.WriteLine("Incorrect Amount Format");
- return true;
- }
- Fixed8 fee = args.Length >= 5 ? Fixed8.Parse(args[4]) : Fixed8.Zero;
- tx = Program.Wallet.MakeTransaction(null, new[]
- {
- new TransferOutput
- {
- AssetId = assetId,
- Value = amount,
- ScriptHash = scriptHash
- }
- }, fee: fee);
- if (tx == null)
- {
- Console.WriteLine("Insufficient funds");
- return true;
- }
- }
- ContractParametersContext context = new ContractParametersContext(tx);
- Program.Wallet.Sign(context);
- if (context.Completed)
- {
- tx.Scripts = context.GetScripts();
- Program.Wallet.ApplyTransaction(tx);
- LocalNode.Relay(tx);
- Console.WriteLine($"TXID: {tx.Hash}");
- }
- else
- {
- Console.WriteLine("SignatureContext:");
- Console.WriteLine(context.ToString());
- }
- return true;
- }
-
- private bool OnShowCommand(string[] args)
- {
- switch (args[1].ToLower())
- {
- case "gas":
- return OnShowGasCommand(args);
- case "node":
- return OnShowNodeCommand(args);
- case "pool":
- return OnShowPoolCommand(args);
- case "state":
- return OnShowStateCommand(args);
- case "utxo":
- return OnShowUtxoCommand(args);
- default:
- return base.OnCommand(args);
- }
- }
-
- private bool OnShowNodeCommand(string[] args)
- {
- RemoteNode[] nodes = LocalNode.GetRemoteNodes();
- for (int i = 0; i < nodes.Length; i++)
- {
- Console.WriteLine($"{nodes[i].RemoteEndpoint.Address} port:{nodes[i].RemoteEndpoint.Port} listen:{nodes[i].ListenerEndpoint?.Port ?? 0} height:{nodes[i].Version?.StartHeight ?? 0} [{i + 1}/{nodes.Length}]");
- }
- return true;
- }
-
- private bool OnShowPoolCommand(string[] args)
- {
- bool verbose = args.Length >= 3 && args[2] == "verbose";
- Transaction[] transactions = LocalNode.GetMemoryPool().ToArray();
- if (verbose)
- foreach (Transaction tx in transactions)
- Console.WriteLine($"{tx.Hash} {tx.GetType().Name}");
- Console.WriteLine($"total: {transactions.Length}");
- return true;
- }
-
- private bool OnShowStateCommand(string[] args)
- {
- uint wh = 0;
- if (Program.Wallet != null)
- {
- wh = (Program.Wallet.WalletHeight > 0) ? Program.Wallet.WalletHeight - 1 : 0;
- }
- Console.WriteLine($"Height: {wh}/{Blockchain.Default.Height}/{Blockchain.Default.HeaderHeight}, Nodes: {LocalNode.RemoteNodeCount}");
- return true;
- }
-
- private bool OnShowUtxoCommand(string[] args)
- {
- if (Program.Wallet == null)
- {
- Console.WriteLine("You have to open the wallet first.");
- return true;
- }
- IEnumerable coins = Program.Wallet.FindUnspentCoins();
- if (args.Length >= 3)
- {
- UInt256 assetId;
- switch (args[2].ToLower())
- {
- case "neo":
- case "ans":
- assetId = Blockchain.GoverningToken.Hash;
- break;
- case "gas":
- case "anc":
- assetId = Blockchain.UtilityToken.Hash;
- break;
- default:
- assetId = UInt256.Parse(args[2]);
- break;
- }
- coins = coins.Where(p => p.Output.AssetId.Equals(assetId));
- }
- Coin[] coins_array = coins.ToArray();
- const int MAX_SHOW = 100;
- for (int i = 0; i < coins_array.Length && i < MAX_SHOW; i++)
- Console.WriteLine($"{coins_array[i].Reference.PrevHash}:{coins_array[i].Reference.PrevIndex}");
- if (coins_array.Length > MAX_SHOW)
- Console.WriteLine($"({coins_array.Length - MAX_SHOW} more)");
- Console.WriteLine($"total: {coins_array.Length} UTXOs");
- return true;
- }
-
- protected internal override void OnStart(string[] args)
- {
- bool useRPC = false, nopeers = false, useLog = false;
- for (int i = 0; i < args.Length; i++)
- switch (args[i])
- {
- case "/rpc":
- case "--rpc":
- case "-r":
- useRPC = true;
- break;
- case "--nopeers":
- nopeers = true;
- break;
- case "-l":
- case "--log":
- useLog = true;
- break;
- }
- Blockchain.RegisterBlockchain(new LevelDBBlockchain(Path.GetFullPath(Settings.Default.Paths.Chain)));
- if (!nopeers && File.Exists(PeerStatePath))
- using (FileStream fs = new FileStream(PeerStatePath, FileMode.Open, FileAccess.Read, FileShare.Read))
- {
- LocalNode.LoadState(fs);
- }
- LocalNode = new LocalNode();
- if (useLog)
- LevelDBBlockchain.ApplicationExecuted += LevelDBBlockchain_ApplicationExecuted;
- Task.Run(() =>
- {
- const string acc_path = "chain.acc";
- const string acc_zip_path = acc_path + ".zip";
- if (File.Exists(acc_path))
- {
- using (FileStream fs = new FileStream(acc_path, FileMode.Open, FileAccess.Read, FileShare.None))
- {
- ImportBlocks(fs);
- }
- File.Delete(acc_path);
- }
- else if (File.Exists(acc_zip_path))
- {
- using (FileStream fs = new FileStream(acc_zip_path, FileMode.Open, FileAccess.Read, FileShare.None))
- using (ZipArchive zip = new ZipArchive(fs, ZipArchiveMode.Read))
- using (Stream zs = zip.GetEntry(acc_path).Open())
- {
- ImportBlocks(zs);
- }
- File.Delete(acc_zip_path);
- }
- LocalNode.Start(Settings.Default.P2P.Port, Settings.Default.P2P.WsPort);
- if (useRPC)
- {
- rpc = new RpcServerWithWallet(LocalNode);
- rpc.Start(Settings.Default.RPC.Port, Settings.Default.RPC.SslCert, Settings.Default.RPC.SslCertPassword);
- }
- });
- }
-
- private bool OnStartCommand(string[] args)
- {
- switch (args[1].ToLower())
- {
- case "consensus":
- return OnStartConsensusCommand(args);
- default:
- return base.OnCommand(args);
- }
- }
-
- private bool OnStartConsensusCommand(string[] args)
- {
- if (consensus != null) return true;
- if (Program.Wallet == null)
- {
- Console.WriteLine("You have to open the wallet first.");
- return true;
- }
- string log_dictionary = Path.Combine(AppContext.BaseDirectory, "Logs");
- consensus = new ConsensusWithPolicy(LocalNode, Program.Wallet, log_dictionary);
- ShowPrompt = false;
- consensus.Start();
- return true;
- }
-
- protected internal override void OnStop()
- {
- if (consensus != null) consensus.Dispose();
- if (rpc != null) rpc.Dispose();
- LocalNode.Dispose();
- using (FileStream fs = new FileStream(PeerStatePath, FileMode.Create, FileAccess.Write, FileShare.None))
- {
- LocalNode.SaveState(fs);
- }
- Blockchain.Default.Dispose();
- }
-
- private bool OnUpgradeCommand(string[] args)
- {
- switch (args[1].ToLower())
- {
- case "wallet":
- return OnUpgradeWalletCommand(args);
- default:
- return base.OnCommand(args);
- }
- }
-
- private bool OnUpgradeWalletCommand(string[] args)
- {
- if (args.Length < 3)
- {
- Console.WriteLine("error");
- return true;
- }
- string path = args[2];
- if (Path.GetExtension(path) != ".db3")
- {
- Console.WriteLine("Can't upgrade the wallet file.");
- return true;
- }
- if (!File.Exists(path))
- {
- Console.WriteLine("File does not exist.");
- return true;
- }
- string password = ReadPassword("password");
- if (password.Length == 0)
- {
- Console.WriteLine("cancelled");
- return true;
- }
- string path_new = Path.ChangeExtension(path, ".json");
- NEP6Wallet.Migrate(path_new, path, password).Save();
- Console.WriteLine($"Wallet file upgrade complete. New wallet file has been auto-saved at: {path_new}");
- return true;
- }
-
- private void LevelDBBlockchain_ApplicationExecuted(object sender, ApplicationExecutedEventArgs e)
- {
- JObject json = new JObject();
- json["txid"] = e.Transaction.Hash.ToString();
- json["vmstate"] = e.VMState;
- json["gas_consumed"] = e.GasConsumed.ToString();
- json["stack"] = e.Stack.Select(p => p.ToParameter().ToJson()).ToArray();
- json["notifications"] = e.Notifications.Select(p =>
- {
- JObject notification = new JObject();
- notification["contract"] = p.ScriptHash.ToString();
- notification["state"] = p.State.ToParameter().ToJson();
- return notification;
- }).ToArray();
- Directory.CreateDirectory(Settings.Default.Paths.ApplicationLogs);
- string path = Path.Combine(Settings.Default.Paths.ApplicationLogs, $"{e.Transaction.Hash}.json");
- File.WriteAllText(path, json.ToString());
- }
- }
-}
diff --git a/neo-cli/config.json b/neo-cli/config.json
deleted file mode 100644
index 235a4263d..000000000
--- a/neo-cli/config.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "ApplicationConfiguration": {
- "Paths": {
- "Chain": "Chain_{0}",
- "ApplicationLogs": "ApplicationLogs_{0}"
- },
- "P2P": {
- "Port": 10333,
- "WsPort": 10334
- },
- "RPC": {
- "Port": 10332,
- "SslCert": "",
- "SslCertPassword": ""
- }
- }
-}
diff --git a/neo-cli/config.mainnet.json b/neo-cli/config.mainnet.json
deleted file mode 100644
index 235a4263d..000000000
--- a/neo-cli/config.mainnet.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "ApplicationConfiguration": {
- "Paths": {
- "Chain": "Chain_{0}",
- "ApplicationLogs": "ApplicationLogs_{0}"
- },
- "P2P": {
- "Port": 10333,
- "WsPort": 10334
- },
- "RPC": {
- "Port": 10332,
- "SslCert": "",
- "SslCertPassword": ""
- }
- }
-}
diff --git a/neo-cli/config.testnet.json b/neo-cli/config.testnet.json
deleted file mode 100644
index 5ca87762a..000000000
--- a/neo-cli/config.testnet.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "ApplicationConfiguration": {
- "Paths": {
- "Chain": "Chain_{0}",
- "ApplicationLogs": "ApplicationLogs_{0}"
- },
- "P2P": {
- "Port": 20333,
- "WsPort": 20334
- },
- "RPC": {
- "Port": 20332,
- "SslCert": "",
- "SslCertPassword": ""
- }
- }
-}
diff --git a/neo-cli/neo-cli.csproj b/neo-cli/neo-cli.csproj
deleted file mode 100644
index 511f30821..000000000
--- a/neo-cli/neo-cli.csproj
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
- 2016-2017 The Neo Project
- Neo.CLI
- 2.7.4
- The Neo Project
- netcoreapp2.0
- neo-cli
- Exe
- Neo.CLI
- Neo
- The Neo Project
- Neo.CLI
- Neo.CLI
-
-
-
- none
- False
-
-
-
-
- PreserveNewest
- PreserveNewest
-
-
-
-
-
-
-
-
diff --git a/neo-cli/protocol.json b/neo-cli/protocol.json
deleted file mode 100644
index d32abd432..000000000
--- a/neo-cli/protocol.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "ProtocolConfiguration": {
- "Magic": 7630401,
- "AddressVersion": 23,
- "SecondsPerBlock": 15,
- "StandbyValidators": [
- "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c",
- "02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093",
- "03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a",
- "02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554",
- "024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d",
- "02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e",
- "02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70"
- ],
- "SeedList": [
- "seed1.neo.org:10333",
- "seed2.neo.org:10333",
- "seed3.neo.org:10333",
- "seed4.neo.org:10333",
- "seed5.neo.org:10333"
- ],
- "SystemFee": {
- "EnrollmentTransaction": 1000,
- "IssueTransaction": 500,
- "PublishTransaction": 500,
- "RegisterTransaction": 10000
- }
- }
-}
diff --git a/neo-cli/protocol.mainnet.json b/neo-cli/protocol.mainnet.json
deleted file mode 100644
index fedbbbe39..000000000
--- a/neo-cli/protocol.mainnet.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "ProtocolConfiguration": {
- "Magic": 7630401,
- "AddressVersion": 23,
- "SecondsPerBlock": 15,
- "StandbyValidators": [
- "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c",
- "02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093",
- "03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a",
- "02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554",
- "024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d",
- "02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e",
- "02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70"
- ],
- "SeedList": [
- "seed1.neo.org:10333",
- "seed2.neo.org:10333",
- "seed3.neo.org:10333",
- "seed4.neo.org:10333",
- "seed5.neo.org:10333"
- ],
- "SystemFee": {
- "EnrollmentTransaction": 1000,
- "IssueTransaction": 500,
- "PublishTransaction": 500,
- "RegisterTransaction": 10000
- }
- }
-}
diff --git a/neo-cli/protocol.testnet.json b/neo-cli/protocol.testnet.json
deleted file mode 100644
index 998d04c5c..000000000
--- a/neo-cli/protocol.testnet.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "ProtocolConfiguration": {
- "Magic": 1953787457,
- "AddressVersion": 23,
- "SecondsPerBlock": 15,
- "StandbyValidators": [
- "0327da12b5c40200e9f65569476bbff2218da4f32548ff43b6387ec1416a231ee8",
- "026ce35b29147ad09e4afe4ec4a7319095f08198fa8babbe3c56e970b143528d22",
- "0209e7fd41dfb5c2f8dc72eb30358ac100ea8c72da18847befe06eade68cebfcb9",
- "039dafd8571a641058ccc832c5e2111ea39b09c0bde36050914384f7a48bce9bf9",
- "038dddc06ce687677a53d54f096d2591ba2302068cf123c1f2d75c2dddc5425579",
- "02d02b1873a0863cd042cc717da31cea0d7cf9db32b74d4c72c01b0011503e2e22",
- "034ff5ceeac41acf22cd5ed2da17a6df4dd8358fcb2bfb1a43208ad0feaab2746b"
- ],
- "SeedList": [
- "seed1.neo.org:20333",
- "seed2.neo.org:20333",
- "seed3.neo.org:20333",
- "seed4.neo.org:20333",
- "seed5.neo.org:20333"
- ],
- "SystemFee": {
- "EnrollmentTransaction": 10,
- "IssueTransaction": 5,
- "PublishTransaction": 5,
- "RegisterTransaction": 100
- }
- }
-}
diff --git a/neo-node.sln b/neo-node.sln
new file mode 100644
index 000000000..8af6a2e61
--- /dev/null
+++ b/neo-node.sln
@@ -0,0 +1,237 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 18
+VisualStudioVersion = 18.0.11201.2
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.CLI", "src\Neo.CLI\Neo.CLI.csproj", "{900CA179-AEF0-43F3-9833-5DB060272D8E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.GUI", "src\Neo.GUI\Neo.GUI.csproj", "{1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.ConsoleService.Tests", "tests\Neo.ConsoleService.Tests\Neo.ConsoleService.Tests.csproj", "{CC845558-D7C2-412D-8014-15699DFBA530}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.ConsoleService", "src\Neo.ConsoleService\Neo.ConsoleService.csproj", "{8D2BC669-11AC-42DB-BE75-FD53FA2475C6}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{705EBADA-05F7-45D1-9D63-D399E87525DB}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "plugins", "plugins", "{876880F3-B389-4388-B3A4-00E6F2581D52}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.CLI.Tests", "tests\Neo.CLI.Tests\Neo.CLI.Tests.csproj", "{B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApplicationLogs", "plugins\ApplicationLogs\ApplicationLogs.csproj", "{CD0F1C4D-977C-8B51-A623-083D15776E5E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DBFTPlugin", "plugins\DBFTPlugin\DBFTPlugin.csproj", "{538500ED-2C8B-3174-BA9C-98A35024F32B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LevelDBStore", "plugins\LevelDBStore\LevelDBStore.csproj", "{7129EDEA-C8B3-E574-C1FB-C58226873860}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MPTTrie", "plugins\MPTTrie\MPTTrie.csproj", "{D554C319-B2F5-8F95-C29C-2EACC34F7FC7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OracleService", "plugins\OracleService\OracleService.csproj", "{13243D40-CC80-7017-7C4B-898B07AA67EA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestServer", "plugins\RestServer\RestServer.csproj", "{FF12A836-AE0D-A788-C6A8-15F020597130}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RocksDBStore", "plugins\RocksDBStore\RocksDBStore.csproj", "{E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RpcClient", "plugins\RpcClient\RpcClient.csproj", "{1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RpcServer", "plugins\RpcServer\RpcServer.csproj", "{295FE1B7-BFBF-3390-3080-997DAF8EFE80}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignClient", "plugins\SignClient\SignClient.csproj", "{A2CF46E9-C584-9E94-7BF9-38C33F83795E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLiteWallet", "plugins\SQLiteWallet\SQLiteWallet.csproj", "{24DA3784-5D2E-648F-771E-193CF0B3D6FF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StateService", "plugins\StateService\StateService.csproj", "{C0327365-D644-C32A-1AEF-B7004899339B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StorageDumper", "plugins\StorageDumper\StorageDumper.csproj", "{6D1FE94A-0769-61C6-D870-B32919AE3881}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TokensTracker", "plugins\TokensTracker\TokensTracker.csproj", "{AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Cryptography.MPTTrie.Tests", "tests\Neo.Cryptography.MPTTrie.Tests\Neo.Cryptography.MPTTrie.Tests.csproj", "{1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Network.RPC.Tests", "tests\Neo.Network.RPC.Tests\Neo.Network.RPC.Tests.csproj", "{60E16A49-06EB-6F80-7228-21A7793B8250}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.ApplicationLogs.Tests", "tests\Neo.Plugins.ApplicationLogs.Tests\Neo.Plugins.ApplicationLogs.Tests.csproj", "{8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.DBFTPlugin.Tests", "tests\Neo.Plugins.DBFTPlugin.Tests\Neo.Plugins.DBFTPlugin.Tests.csproj", "{5B0634E5-484B-E9F7-02D5-F97D36BE88EF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.OracleService.Tests", "tests\Neo.Plugins.OracleService.Tests\Neo.Plugins.OracleService.Tests.csproj", "{6B77BE88-212C-D04B-3C5A-169E38443777}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.RestServer.Tests", "tests\Neo.Plugins.RestServer.Tests\Neo.Plugins.RestServer.Tests.csproj", "{A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.RpcServer.Tests", "tests\Neo.Plugins.RpcServer.Tests\Neo.Plugins.RpcServer.Tests.csproj", "{D65E9D81-A7B0-696C-11F1-E923CAD972B3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.SignClient.Tests", "tests\Neo.Plugins.SignClient.Tests\Neo.Plugins.SignClient.Tests.csproj", "{F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.SQLiteWallet.Tests", "tests\Neo.Plugins.SQLiteWallet.Tests\Neo.Plugins.SQLiteWallet.Tests.csproj", "{099504A6-0275-8DA3-D52C-06726AD8E77D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.StateService.Tests", "tests\Neo.Plugins.StateService.Tests\Neo.Plugins.StateService.Tests.csproj", "{834D4327-7DDF-4D6A-1624-B074700964F0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.Storage.Tests", "tests\Neo.Plugins.Storage.Tests\Neo.Plugins.Storage.Tests.csproj", "{52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {900CA179-AEF0-43F3-9833-5DB060272D8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {900CA179-AEF0-43F3-9833-5DB060272D8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {900CA179-AEF0-43F3-9833-5DB060272D8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {900CA179-AEF0-43F3-9833-5DB060272D8E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CC845558-D7C2-412D-8014-15699DFBA530}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CC845558-D7C2-412D-8014-15699DFBA530}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CC845558-D7C2-412D-8014-15699DFBA530}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CC845558-D7C2-412D-8014-15699DFBA530}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {538500ED-2C8B-3174-BA9C-98A35024F32B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {538500ED-2C8B-3174-BA9C-98A35024F32B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {538500ED-2C8B-3174-BA9C-98A35024F32B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {538500ED-2C8B-3174-BA9C-98A35024F32B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7129EDEA-C8B3-E574-C1FB-C58226873860}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7129EDEA-C8B3-E574-C1FB-C58226873860}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7129EDEA-C8B3-E574-C1FB-C58226873860}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7129EDEA-C8B3-E574-C1FB-C58226873860}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {13243D40-CC80-7017-7C4B-898B07AA67EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {13243D40-CC80-7017-7C4B-898B07AA67EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {13243D40-CC80-7017-7C4B-898B07AA67EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {13243D40-CC80-7017-7C4B-898B07AA67EA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FF12A836-AE0D-A788-C6A8-15F020597130}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FF12A836-AE0D-A788-C6A8-15F020597130}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FF12A836-AE0D-A788-C6A8-15F020597130}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FF12A836-AE0D-A788-C6A8-15F020597130}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C0327365-D644-C32A-1AEF-B7004899339B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C0327365-D644-C32A-1AEF-B7004899339B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C0327365-D644-C32A-1AEF-B7004899339B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C0327365-D644-C32A-1AEF-B7004899339B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6D1FE94A-0769-61C6-D870-B32919AE3881}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6D1FE94A-0769-61C6-D870-B32919AE3881}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6D1FE94A-0769-61C6-D870-B32919AE3881}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6D1FE94A-0769-61C6-D870-B32919AE3881}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {60E16A49-06EB-6F80-7228-21A7793B8250}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {60E16A49-06EB-6F80-7228-21A7793B8250}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {60E16A49-06EB-6F80-7228-21A7793B8250}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {60E16A49-06EB-6F80-7228-21A7793B8250}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6B77BE88-212C-D04B-3C5A-169E38443777}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6B77BE88-212C-D04B-3C5A-169E38443777}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6B77BE88-212C-D04B-3C5A-169E38443777}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6B77BE88-212C-D04B-3C5A-169E38443777}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Release|Any CPU.Build.0 = Release|Any CPU
+ {099504A6-0275-8DA3-D52C-06726AD8E77D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {099504A6-0275-8DA3-D52C-06726AD8E77D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {099504A6-0275-8DA3-D52C-06726AD8E77D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {099504A6-0275-8DA3-D52C-06726AD8E77D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {834D4327-7DDF-4D6A-1624-B074700964F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {834D4327-7DDF-4D6A-1624-B074700964F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {834D4327-7DDF-4D6A-1624-B074700964F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {834D4327-7DDF-4D6A-1624-B074700964F0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {900CA179-AEF0-43F3-9833-5DB060272D8E} = {705EBADA-05F7-45D1-9D63-D399E87525DB}
+ {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7} = {705EBADA-05F7-45D1-9D63-D399E87525DB}
+ {CC845558-D7C2-412D-8014-15699DFBA530} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9}
+ {8D2BC669-11AC-42DB-BE75-FD53FA2475C6} = {705EBADA-05F7-45D1-9D63-D399E87525DB}
+ {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9}
+ {CD0F1C4D-977C-8B51-A623-083D15776E5E} = {876880F3-B389-4388-B3A4-00E6F2581D52}
+ {538500ED-2C8B-3174-BA9C-98A35024F32B} = {876880F3-B389-4388-B3A4-00E6F2581D52}
+ {7129EDEA-C8B3-E574-C1FB-C58226873860} = {876880F3-B389-4388-B3A4-00E6F2581D52}
+ {D554C319-B2F5-8F95-C29C-2EACC34F7FC7} = {876880F3-B389-4388-B3A4-00E6F2581D52}
+ {13243D40-CC80-7017-7C4B-898B07AA67EA} = {876880F3-B389-4388-B3A4-00E6F2581D52}
+ {FF12A836-AE0D-A788-C6A8-15F020597130} = {876880F3-B389-4388-B3A4-00E6F2581D52}
+ {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34} = {876880F3-B389-4388-B3A4-00E6F2581D52}
+ {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E} = {876880F3-B389-4388-B3A4-00E6F2581D52}
+ {295FE1B7-BFBF-3390-3080-997DAF8EFE80} = {876880F3-B389-4388-B3A4-00E6F2581D52}
+ {A2CF46E9-C584-9E94-7BF9-38C33F83795E} = {876880F3-B389-4388-B3A4-00E6F2581D52}
+ {24DA3784-5D2E-648F-771E-193CF0B3D6FF} = {876880F3-B389-4388-B3A4-00E6F2581D52}
+ {C0327365-D644-C32A-1AEF-B7004899339B} = {876880F3-B389-4388-B3A4-00E6F2581D52}
+ {6D1FE94A-0769-61C6-D870-B32919AE3881} = {876880F3-B389-4388-B3A4-00E6F2581D52}
+ {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34} = {876880F3-B389-4388-B3A4-00E6F2581D52}
+ {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9}
+ {60E16A49-06EB-6F80-7228-21A7793B8250} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9}
+ {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9}
+ {5B0634E5-484B-E9F7-02D5-F97D36BE88EF} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9}
+ {6B77BE88-212C-D04B-3C5A-169E38443777} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9}
+ {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9}
+ {D65E9D81-A7B0-696C-11F1-E923CAD972B3} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9}
+ {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9}
+ {099504A6-0275-8DA3-D52C-06726AD8E77D} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9}
+ {834D4327-7DDF-4D6A-1624-B074700964F0} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9}
+ {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {6C1293A1-8EC4-44E8-9EE9-67892696FE26}
+ EndGlobalSection
+EndGlobal
diff --git a/plugins/ApplicationLogs/ApplicationLogs.csproj b/plugins/ApplicationLogs/ApplicationLogs.csproj
new file mode 100644
index 000000000..aa3a759af
--- /dev/null
+++ b/plugins/ApplicationLogs/ApplicationLogs.csproj
@@ -0,0 +1,21 @@
+
+
+
+
+
+ false
+ runtime
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
diff --git a/plugins/ApplicationLogs/ApplicationLogs.json b/plugins/ApplicationLogs/ApplicationLogs.json
new file mode 100644
index 000000000..2664665dd
--- /dev/null
+++ b/plugins/ApplicationLogs/ApplicationLogs.json
@@ -0,0 +1,12 @@
+{
+ "PluginConfiguration": {
+ "Path": "ApplicationLogs_{0}",
+ "Network": 860833102,
+ "MaxStackSize": 65535,
+ "Debug": false,
+ "UnhandledExceptionPolicy": "StopPlugin"
+ },
+ "Dependency": [
+ "RpcServer"
+ ]
+}
diff --git a/plugins/ApplicationLogs/LogReader.cs b/plugins/ApplicationLogs/LogReader.cs
new file mode 100644
index 000000000..39d5d9f14
--- /dev/null
+++ b/plugins/ApplicationLogs/LogReader.cs
@@ -0,0 +1,509 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// LogReader.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 Neo.ConsoleService;
+using Neo.Extensions.VM;
+using Neo.IEventHandlers;
+using Neo.Json;
+using Neo.Ledger;
+using Neo.Network.P2P.Payloads;
+using Neo.Persistence;
+using Neo.Plugins.ApplicationLogs.Store;
+using Neo.Plugins.ApplicationLogs.Store.Models;
+using Neo.Plugins.RpcServer;
+using Neo.SmartContract;
+using Neo.SmartContract.Native;
+using System.Numerics;
+using static System.IO.Path;
+
+namespace Neo.Plugins.ApplicationLogs;
+
+public class LogReader : Plugin, ICommittingHandler, ICommittedHandler, ILogHandler
+{
+ #region Globals
+
+ internal NeoStore _neostore;
+ private NeoSystem _neosystem;
+ private readonly List _logEvents;
+
+ #endregion
+
+ public override string Name => "ApplicationLogs";
+ public override string Description => "Synchronizes smart contract VM executions and notifications (NotifyLog) on blockchain.";
+ protected override UnhandledExceptionPolicy ExceptionPolicy => ApplicationLogsSettings.Default.ExceptionPolicy;
+
+ #region Ctor
+
+ public LogReader()
+ {
+ _neostore = default!;
+ _neosystem = default!;
+ _logEvents = new();
+ Blockchain.Committing += ((ICommittingHandler)this).Blockchain_Committing_Handler;
+ Blockchain.Committed += ((ICommittedHandler)this).Blockchain_Committed_Handler;
+ }
+
+ #endregion
+
+ #region Override Methods
+
+ public override string ConfigFile => Combine(RootPath, "ApplicationLogs.json");
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ Blockchain.Committing -= ((ICommittingHandler)this).Blockchain_Committing_Handler;
+ Blockchain.Committed -= ((ICommittedHandler)this).Blockchain_Committed_Handler;
+ if (ApplicationLogsSettings.Default.Debug)
+ ApplicationEngine.InstanceCreated -= ConfigureAppEngine;
+ }
+ base.Dispose(disposing);
+ }
+
+ private void ConfigureAppEngine(ApplicationEngine engine)
+ {
+ engine.Log += ((ILogHandler)this).ApplicationEngine_Log_Handler;
+ }
+
+ protected override void Configure()
+ {
+ ApplicationLogsSettings.Load(GetConfiguration());
+ }
+
+ protected override void OnSystemLoaded(NeoSystem system)
+ {
+ if (system.Settings.Network != ApplicationLogsSettings.Default.Network)
+ return;
+ string path = string.Format(ApplicationLogsSettings.Default.Path, ApplicationLogsSettings.Default.Network.ToString("X8"));
+ var store = system.LoadStore(GetFullPath(path));
+ _neostore = new NeoStore(store);
+ _neosystem = system;
+ RpcServerPlugin.RegisterMethods(this, ApplicationLogsSettings.Default.Network);
+
+ if (ApplicationLogsSettings.Default.Debug)
+ ApplicationEngine.InstanceCreated += ConfigureAppEngine;
+ }
+
+ #endregion
+
+ #region JSON RPC Methods
+
+ ///
+ /// Gets the block or the transaction execution log. The execution logs are stored if the ApplicationLogs plugin is enabled.
+ ///
+ /// The block hash or the transaction hash(UInt256)
+ ///
+ /// The trigger type(string), optional, default is "" and means no filter trigger type.
+ /// It can be "OnPersist", "PostPersist", "Verification", "Application", "System" or "All"(see TriggerType).
+ /// If want to filter by trigger type, need to set the trigger type.
+ ///
+ /// The block or the transaction execution log.
+ /// Thrown when the hash is invalid or the trigger type is invalid.
+ [RpcMethod]
+ public JToken GetApplicationLog(UInt256 hash, string triggerType = "")
+ {
+ var raw = BlockToJObject(hash);
+ if (raw == null)
+ {
+ raw = TransactionToJObject(hash);
+ if (raw == null) throw new RpcException(RpcError.InvalidParams.WithData("Unknown transaction/blockhash"));
+ }
+
+ if (!string.IsNullOrEmpty(triggerType) && Enum.TryParse(triggerType, true, out TriggerType _))
+ {
+ if (raw["executions"] is JArray executions)
+ {
+ for (var i = 0; i < executions.Count;)
+ {
+ if (executions[i]!["trigger"]?.AsString().Equals(triggerType, StringComparison.OrdinalIgnoreCase) == false)
+ executions.RemoveAt(i);
+ else
+ i++;
+ }
+ }
+ }
+
+ return raw;
+
+ }
+
+ #endregion
+
+ #region Console Commands
+
+ [ConsoleCommand("log block", Category = "ApplicationLog Commands")]
+ internal void OnGetBlockCommand(string blockHashOrIndex, string? eventName = null)
+ {
+ UInt256? blockhash;
+ if (uint.TryParse(blockHashOrIndex, out var blockIndex))
+ {
+ blockhash = NativeContract.Ledger.GetBlockHash(_neosystem.StoreView, blockIndex);
+ }
+ else
+ {
+ _ = UInt256.TryParse(blockHashOrIndex, out blockhash);
+ }
+
+ if (blockhash is null)
+ {
+ ConsoleHelper.Error("Invalid block hash or index.");
+ return;
+ }
+
+ var blockOnPersist = string.IsNullOrEmpty(eventName) ?
+ _neostore.GetBlockLog(blockhash, TriggerType.OnPersist) :
+ _neostore.GetBlockLog(blockhash, TriggerType.OnPersist, eventName);
+ var blockPostPersist = string.IsNullOrEmpty(eventName) ?
+ _neostore.GetBlockLog(blockhash, TriggerType.PostPersist) :
+ _neostore.GetBlockLog(blockhash, TriggerType.PostPersist, eventName);
+
+ if (blockOnPersist == null && blockPostPersist == null)
+ ConsoleHelper.Error($"No logs.");
+ else
+ {
+ if (blockOnPersist != null)
+ {
+ PrintExecutionToConsole(blockOnPersist);
+ if (blockPostPersist != null)
+ {
+ ConsoleHelper.Info("--------------------------------");
+ }
+ }
+ if (blockPostPersist != null)
+ {
+ PrintExecutionToConsole(blockPostPersist);
+ }
+ }
+ }
+
+ [ConsoleCommand("log tx", Category = "ApplicationLog Commands")]
+ internal void OnGetTransactionCommand(UInt256 txhash, string? eventName = null)
+ {
+ var txApplication = string.IsNullOrEmpty(eventName) ?
+ _neostore.GetTransactionLog(txhash) :
+ _neostore.GetTransactionLog(txhash, eventName);
+
+ if (txApplication == null)
+ ConsoleHelper.Error($"No logs.");
+ else
+ PrintExecutionToConsole(txApplication);
+ }
+
+ [ConsoleCommand("log contract", Category = "ApplicationLog Commands")]
+ internal void OnGetContractCommand(UInt160 scripthash, uint page = 1, uint pageSize = 1, string? eventName = null)
+ {
+ if (page == 0)
+ {
+ ConsoleHelper.Error("Page is invalid. Pick a number 1 and above.");
+ return;
+ }
+
+ if (pageSize == 0)
+ {
+ ConsoleHelper.Error("PageSize is invalid. Pick a number between 1 and 10.");
+ return;
+ }
+
+ var txContract = string.IsNullOrEmpty(eventName) ?
+ _neostore.GetContractLog(scripthash, TriggerType.Application, page, pageSize) :
+ _neostore.GetContractLog(scripthash, TriggerType.Application, eventName, page, pageSize);
+
+ if (txContract.Count == 0)
+ ConsoleHelper.Error($"No logs.");
+ else
+ PrintEventModelToConsole(txContract);
+ }
+
+
+ #endregion
+
+ #region Blockchain Events
+
+ void ICommittingHandler.Blockchain_Committing_Handler(NeoSystem system, Block block, DataCache snapshot,
+ IReadOnlyList applicationExecutedList)
+ {
+ if (system.Settings.Network != ApplicationLogsSettings.Default.Network)
+ return;
+
+ if (_neostore is null)
+ return;
+ _neostore.StartBlockLogBatch();
+ _neostore.PutBlockLog(block, applicationExecutedList);
+ if (ApplicationLogsSettings.Default.Debug)
+ {
+ foreach (var appEng in applicationExecutedList.Where(w => w.Transaction != null))
+ {
+ var logs = _logEvents.Where(w => w.ScriptContainer?.Hash == appEng.Transaction!.Hash).ToList();
+ if (logs.Count != 0)
+ _neostore.PutTransactionEngineLogState(appEng.Transaction!.Hash, logs);
+ }
+ _logEvents.Clear();
+ }
+ }
+
+ void ICommittedHandler.Blockchain_Committed_Handler(NeoSystem system, Block block)
+ {
+ if (system.Settings.Network != ApplicationLogsSettings.Default.Network)
+ return;
+ if (_neostore is null)
+ return;
+ _neostore.CommitBlockLog();
+ }
+
+ void ILogHandler.ApplicationEngine_Log_Handler(ApplicationEngine sender, LogEventArgs e)
+ {
+ if (ApplicationLogsSettings.Default.Debug == false)
+ return;
+
+ if (_neosystem.Settings.Network != ApplicationLogsSettings.Default.Network)
+ return;
+
+ if (e.ScriptContainer == null)
+ return;
+
+ _logEvents.Add(e);
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private void PrintExecutionToConsole(BlockchainExecutionModel model)
+ {
+ ConsoleHelper.Info("Trigger: ", $"{model.Trigger}");
+ ConsoleHelper.Info("VM State: ", $"{model.VmState}");
+ if (string.IsNullOrEmpty(model.Exception) == false)
+ ConsoleHelper.Error($"Exception: {model.Exception}");
+ else
+ ConsoleHelper.Info("Exception: ", "null");
+ ConsoleHelper.Info("Gas Consumed: ", $"{new BigDecimal((BigInteger)model.GasConsumed, NativeContract.GAS.Decimals)}");
+ if (model.Stack.Length == 0)
+ ConsoleHelper.Info("Stack: ", "[]");
+ else
+ {
+ ConsoleHelper.Info("Stack: ");
+ for (int i = 0; i < model.Stack.Length; i++)
+ ConsoleHelper.Info($" {i}: ", $"{model.Stack[i].ToJson()}");
+ }
+ if (model.Notifications.Length == 0)
+ ConsoleHelper.Info("Notifications: ", "[]");
+ else
+ {
+ ConsoleHelper.Info("Notifications:");
+ foreach (var notifyItem in model.Notifications)
+ {
+ ConsoleHelper.Info();
+ ConsoleHelper.Info(" ScriptHash: ", $"{notifyItem.ScriptHash}");
+ ConsoleHelper.Info(" Event Name: ", $"{notifyItem.EventName}");
+ ConsoleHelper.Info(" State Parameters:");
+ var ncount = (uint)notifyItem.State.Length;
+ for (var i = 0; i < ncount; i++)
+ ConsoleHelper.Info($" {GetMethodParameterName(notifyItem.ScriptHash, notifyItem.EventName, ncount, i)}: ", $"{notifyItem.State[i].ToJson()}");
+ }
+ }
+ if (ApplicationLogsSettings.Default.Debug)
+ {
+ if (model.Logs.Length == 0)
+ ConsoleHelper.Info("Logs: ", "[]");
+ else
+ {
+ ConsoleHelper.Info("Logs:");
+ foreach (var logItem in model.Logs)
+ {
+ ConsoleHelper.Info();
+ ConsoleHelper.Info(" ScriptHash: ", $"{logItem.ScriptHash}");
+ ConsoleHelper.Info(" Message: ", $"{logItem.Message}");
+ }
+ }
+ }
+ }
+
+ private void PrintEventModelToConsole(IReadOnlyCollection<(BlockchainEventModel NotifyLog, UInt256 TxHash)> models)
+ {
+ foreach (var (notifyItem, txhash) in models)
+ {
+ ConsoleHelper.Info("Transaction Hash: ", $"{txhash}");
+ ConsoleHelper.Info();
+ ConsoleHelper.Info(" Event Name: ", $"{notifyItem.EventName}");
+ ConsoleHelper.Info(" State Parameters:");
+ var ncount = (uint)notifyItem.State.Length;
+ for (var i = 0; i < ncount; i++)
+ ConsoleHelper.Info($" {GetMethodParameterName(notifyItem.ScriptHash, notifyItem.EventName, ncount, i)}: ", $"{notifyItem.State[i].ToJson()}");
+ ConsoleHelper.Info("--------------------------------");
+ }
+ }
+
+ private string GetMethodParameterName(UInt160 scriptHash, string methodName, uint ncount, int parameterIndex)
+ {
+ var contract = NativeContract.ContractManagement.GetContract(_neosystem.StoreView, scriptHash);
+ if (contract == null)
+ return $"{parameterIndex}";
+ var contractEvent = contract.Manifest.Abi.Events.SingleOrDefault(s => s.Name == methodName && (uint)s.Parameters.Length == ncount);
+ if (contractEvent is null)
+ return $"{parameterIndex}";
+ return contractEvent.Parameters[parameterIndex].Name;
+ }
+
+ private static JObject EventModelToJObject(BlockchainEventModel model)
+ {
+ return new JObject()
+ {
+ ["contract"] = model.ScriptHash.ToString(),
+ ["eventname"] = model.EventName,
+ ["state"] = model.State.Select(s => s.ToJson()).ToArray()
+ };
+ }
+
+ private JObject? TransactionToJObject(UInt256 txHash)
+ {
+ var appLog = _neostore.GetTransactionLog(txHash);
+ if (appLog == null)
+ return null;
+
+ var raw = new JObject() { ["txid"] = txHash.ToString() };
+ var trigger = new JObject()
+ {
+ ["trigger"] = appLog.Trigger,
+ ["vmstate"] = appLog.VmState,
+ ["exception"] = string.IsNullOrEmpty(appLog.Exception) ? null : appLog.Exception,
+ ["gasconsumed"] = appLog.GasConsumed.ToString()
+ };
+
+ try
+ {
+ trigger["stack"] = appLog.Stack.Select(s => s.ToJson(ApplicationLogsSettings.Default.MaxStackSize)).ToArray();
+ }
+ catch (Exception ex)
+ {
+ trigger["exception"] = ex.Message;
+ }
+
+ trigger["notifications"] = appLog.Notifications.Select(s =>
+ {
+ var notification = new JObject()
+ {
+ ["contract"] = s.ScriptHash.ToString(),
+ ["eventname"] = s.EventName
+ };
+
+ try
+ {
+ var state = new JObject()
+ {
+ ["type"] = "Array",
+ ["value"] = s.State.Select(ss => ss.ToJson()).ToArray()
+ };
+
+ notification["state"] = state;
+ }
+ catch (InvalidOperationException)
+ {
+ notification["state"] = "error: recursive reference";
+ }
+
+ return notification;
+ }).ToArray();
+
+ if (ApplicationLogsSettings.Default.Debug)
+ {
+ trigger["logs"] = appLog.Logs.Select(s =>
+ {
+ return new JObject()
+ {
+ ["contract"] = s.ScriptHash.ToString(),
+ ["message"] = s.Message
+ };
+ }).ToArray();
+ }
+
+ raw["executions"] = new[] { trigger };
+ return raw;
+ }
+
+ private JObject? BlockToJObject(UInt256 blockHash)
+ {
+ var blockOnPersist = _neostore.GetBlockLog(blockHash, TriggerType.OnPersist);
+ var blockPostPersist = _neostore.GetBlockLog(blockHash, TriggerType.PostPersist);
+
+ if (blockOnPersist == null && blockPostPersist == null)
+ return null;
+
+ var blockJson = new JObject() { ["blockhash"] = blockHash.ToString() };
+ var triggerList = new List();
+
+ if (blockOnPersist != null)
+ triggerList.Add(BlockItemToJObject(blockOnPersist));
+ if (blockPostPersist != null)
+ triggerList.Add(BlockItemToJObject(blockPostPersist));
+
+ blockJson["executions"] = triggerList.ToArray();
+ return blockJson;
+ }
+
+ private static JObject BlockItemToJObject(BlockchainExecutionModel blockExecutionModel)
+ {
+ var trigger = new JObject()
+ {
+ ["trigger"] = blockExecutionModel.Trigger,
+ ["vmstate"] = blockExecutionModel.VmState,
+ ["gasconsumed"] = blockExecutionModel.GasConsumed.ToString()
+ };
+ try
+ {
+ trigger["stack"] = blockExecutionModel.Stack.Select(q => q.ToJson(ApplicationLogsSettings.Default.MaxStackSize)).ToArray();
+ }
+ catch (Exception ex)
+ {
+ trigger["exception"] = ex.Message;
+ }
+
+ trigger["notifications"] = blockExecutionModel.Notifications.Select(s =>
+ {
+ var notification = new JObject()
+ {
+ ["contract"] = s.ScriptHash.ToString(),
+ ["eventname"] = s.EventName
+ };
+ try
+ {
+ var state = new JObject()
+ {
+ ["type"] = "Array",
+ ["value"] = s.State.Select(ss => ss.ToJson()).ToArray()
+ };
+
+ notification["state"] = state;
+ }
+ catch (InvalidOperationException)
+ {
+ notification["state"] = "error: recursive reference";
+ }
+ return notification;
+ }).ToArray();
+
+ if (ApplicationLogsSettings.Default.Debug)
+ {
+ trigger["logs"] = blockExecutionModel.Logs.Select(s =>
+ {
+ return new JObject()
+ {
+ ["contract"] = s.ScriptHash.ToString(),
+ ["message"] = s.Message
+ };
+ }).ToArray();
+ }
+
+ return trigger;
+ }
+
+ #endregion
+}
diff --git a/plugins/ApplicationLogs/Settings.cs b/plugins/ApplicationLogs/Settings.cs
new file mode 100644
index 000000000..7b9044bc1
--- /dev/null
+++ b/plugins/ApplicationLogs/Settings.cs
@@ -0,0 +1,41 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Settings.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 Microsoft.Extensions.Configuration;
+
+namespace Neo.Plugins.ApplicationLogs;
+
+internal class ApplicationLogsSettings : IPluginSettings
+{
+ public string Path { get; }
+ public uint Network { get; }
+ public int MaxStackSize { get; }
+
+ public bool Debug { get; }
+
+ public static ApplicationLogsSettings Default { get; private set; } = default!;
+
+ public UnhandledExceptionPolicy ExceptionPolicy { get; }
+
+ private ApplicationLogsSettings(IConfigurationSection section)
+ {
+ Path = section.GetValue("Path", "ApplicationLogs_{0}");
+ Network = section.GetValue("Network", 5195086u);
+ MaxStackSize = section.GetValue("MaxStackSize", (int)ushort.MaxValue);
+ Debug = section.GetValue("Debug", false);
+ ExceptionPolicy = section.GetValue("UnhandledExceptionPolicy", UnhandledExceptionPolicy.Ignore);
+ }
+
+ public static void Load(IConfigurationSection section)
+ {
+ Default = new ApplicationLogsSettings(section);
+ }
+}
diff --git a/plugins/ApplicationLogs/Store/LogStorageStore.cs b/plugins/ApplicationLogs/Store/LogStorageStore.cs
new file mode 100644
index 000000000..12d54b204
--- /dev/null
+++ b/plugins/ApplicationLogs/Store/LogStorageStore.cs
@@ -0,0 +1,385 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// LogStorageStore.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 Neo.Extensions;
+using Neo.Extensions.IO;
+using Neo.Persistence;
+using Neo.Plugins.ApplicationLogs.Store.States;
+using Neo.SmartContract;
+using Neo.VM;
+using Neo.VM.Types;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Neo.Plugins.ApplicationLogs.Store;
+
+public sealed class LogStorageStore : IDisposable
+{
+ #region Prefixes
+
+ private static readonly int Prefix_Size = sizeof(int) + sizeof(byte);
+ private static readonly int Prefix_Block_Trigger_Size = Prefix_Size + UInt256.Length;
+ private static readonly int Prefix_Execution_Block_Trigger_Size = Prefix_Size + UInt256.Length;
+
+ private static readonly int Prefix_Id = 0x414c4f47; // Magic Code: (ALOG);
+ private static readonly byte Prefix_Engine = 0x18; // Engine_GUID -> ScriptHash, Message
+ private static readonly byte Prefix_Engine_Transaction = 0x19; // TxHash -> Engine_GUID_List
+ private static readonly byte Prefix_Block = 0x20; // BlockHash, Trigger -> NotifyLog_GUID_List
+ private static readonly byte Prefix_Notify = 0x21; // NotifyLog_GUID -> ScriptHash, EventName, StackItem_GUID_List
+ private static readonly byte Prefix_Contract = 0x22; // ScriptHash, TimeStamp, EventIterIndex -> txHash, Trigger, NotifyLog_GUID
+ private static readonly byte Prefix_Execution = 0x23; // Execution_GUID -> Data, StackItem_GUID_List
+ private static readonly byte Prefix_Execution_Block = 0x24; // BlockHash, Trigger -> Execution_GUID
+ private static readonly byte Prefix_Execution_Transaction = 0x25; // TxHash -> Execution_GUID
+ private static readonly byte Prefix_Transaction = 0x26; // TxHash -> NotifyLog_GUID_List
+ private static readonly byte Prefix_StackItem = 0xed; // StackItem_GUID -> Data
+
+ #endregion
+
+ #region Global Variables
+
+ private readonly IStoreSnapshot _snapshot;
+
+ #endregion
+
+ #region Ctor
+
+ public LogStorageStore(IStoreSnapshot snapshot)
+ {
+ ArgumentNullException.ThrowIfNull(snapshot, nameof(snapshot));
+ _snapshot = snapshot;
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ }
+
+ #endregion
+
+ #region Put
+
+ public Guid PutEngineState(EngineLogState state)
+ {
+ var id = Guid.NewGuid();
+ var key = new KeyBuilder(Prefix_Id, Prefix_Engine)
+ .Add(id.ToByteArray())
+ .ToArray();
+ _snapshot.Put(key, state.ToArray());
+ return id;
+ }
+
+ public void PutTransactionEngineState(UInt256 hash, TransactionEngineLogState state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Engine_Transaction)
+ .Add(hash)
+ .ToArray();
+ _snapshot.Put(key, state.ToArray());
+ }
+
+ public void PutBlockState(UInt256 hash, TriggerType trigger, BlockLogState state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Block)
+ .Add(hash)
+ .Add((byte)trigger)
+ .ToArray();
+ _snapshot.Put(key, state.ToArray());
+ }
+
+ public Guid PutNotifyState(NotifyLogState state)
+ {
+ var id = Guid.NewGuid();
+ var key = new KeyBuilder(Prefix_Id, Prefix_Notify)
+ .Add(id.ToByteArray())
+ .ToArray();
+ _snapshot.Put(key, state.ToArray());
+ return id;
+ }
+
+ public void PutContractState(UInt160 scriptHash, ulong timestamp, uint iterIndex, ContractLogState state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Contract)
+ .Add(scriptHash)
+ .Add(timestamp)
+ .Add(iterIndex)
+ .ToArray();
+ _snapshot.Put(key, state.ToArray());
+ }
+
+ public Guid PutExecutionState(ExecutionLogState state)
+ {
+ var id = Guid.NewGuid();
+ var key = new KeyBuilder(Prefix_Id, Prefix_Execution)
+ .Add(id.ToByteArray())
+ .ToArray();
+ _snapshot.Put(key, state.ToArray());
+ return id;
+ }
+
+ public void PutExecutionBlockState(UInt256 blockHash, TriggerType trigger, Guid executionStateId)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Execution_Block)
+ .Add(blockHash)
+ .Add((byte)trigger)
+ .ToArray();
+ _snapshot.Put(key, executionStateId.ToByteArray());
+ }
+
+ public void PutExecutionTransactionState(UInt256 txHash, Guid executionStateId)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Execution_Transaction)
+ .Add(txHash)
+ .ToArray();
+ _snapshot.Put(key, executionStateId.ToByteArray());
+ }
+
+ public void PutTransactionState(UInt256 hash, TransactionLogState state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Transaction)
+ .Add(hash)
+ .ToArray();
+ _snapshot.Put(key, state.ToArray());
+ }
+
+ public Guid PutStackItemState(StackItem stackItem)
+ {
+ var id = Guid.NewGuid();
+ var key = new KeyBuilder(Prefix_Id, Prefix_StackItem)
+ .Add(id.ToByteArray())
+ .ToArray();
+ try
+ {
+ _snapshot.Put(key, BinarySerializer.Serialize(stackItem, ExecutionEngineLimits.Default with
+ {
+ MaxItemSize = (uint)ApplicationLogsSettings.Default.MaxStackSize
+ }));
+ }
+ catch
+ {
+ _snapshot.Put(key, BinarySerializer.Serialize(StackItem.Null, ExecutionEngineLimits.Default with
+ {
+ MaxItemSize = (uint)ApplicationLogsSettings.Default.MaxStackSize
+ }));
+ }
+ return id;
+ }
+
+ #endregion
+
+ #region Find
+
+ public IEnumerable<(BlockLogState State, TriggerType Trigger)> FindBlockState(UInt256 hash)
+ {
+ var prefixKey = new KeyBuilder(Prefix_Id, Prefix_Block)
+ .Add(hash)
+ .ToArray();
+ foreach (var (key, value) in _snapshot.Find(prefixKey, SeekDirection.Forward))
+ {
+ if (key.AsSpan().StartsWith(prefixKey))
+ yield return (value.AsSerializable(), (TriggerType)key.AsSpan(Prefix_Block_Trigger_Size)[0]);
+ else
+ yield break;
+ }
+ }
+
+ public IEnumerable FindContractState(UInt160 scriptHash, uint page, uint pageSize)
+ {
+ var prefix = new KeyBuilder(Prefix_Id, Prefix_Contract)
+ .Add(scriptHash)
+ .ToArray();
+ var prefixKey = new KeyBuilder(Prefix_Id, Prefix_Contract)
+ .Add(scriptHash)
+ .Add(ulong.MaxValue) // Get newest to oldest (timestamp)
+ .ToArray();
+ uint index = 1;
+ foreach (var (key, value) in _snapshot.Find(prefixKey, SeekDirection.Backward)) // Get newest to oldest
+ {
+ if (key.AsSpan().StartsWith(prefix))
+ {
+ if (index >= page && index < (pageSize + page))
+ yield return value.AsSerializable();
+ index++;
+ }
+ else
+ yield break;
+ }
+ }
+
+ public IEnumerable FindContractState(UInt160 scriptHash, TriggerType trigger, uint page, uint pageSize)
+ {
+ var prefix = new KeyBuilder(Prefix_Id, Prefix_Contract)
+ .Add(scriptHash)
+ .ToArray();
+ var prefixKey = new KeyBuilder(Prefix_Id, Prefix_Contract)
+ .Add(scriptHash)
+ .Add(ulong.MaxValue) // Get newest to oldest (timestamp)
+ .ToArray();
+ uint index = 1;
+ foreach (var (key, value) in _snapshot.Find(prefixKey, SeekDirection.Backward)) // Get newest to oldest
+ {
+ if (key.AsSpan().StartsWith(prefix))
+ {
+ var state = value.AsSerializable();
+ if (state.Trigger == trigger)
+ {
+ if (index >= page && index < (pageSize + page))
+ yield return state;
+ index++;
+ }
+ }
+ else
+ yield break;
+ }
+ }
+
+ public IEnumerable FindContractState(UInt160 scriptHash, TriggerType trigger, string eventName, uint page, uint pageSize)
+ {
+ var prefix = new KeyBuilder(Prefix_Id, Prefix_Contract)
+ .Add(scriptHash)
+ .ToArray();
+ var prefixKey = new KeyBuilder(Prefix_Id, Prefix_Contract)
+ .Add(scriptHash)
+ .Add(ulong.MaxValue) // Get newest to oldest (timestamp)
+ .ToArray();
+ uint index = 1;
+ foreach (var (key, value) in _snapshot.Find(prefixKey, SeekDirection.Backward)) // Get newest to oldest
+ {
+ if (key.AsSpan().StartsWith(prefix))
+ {
+ var state = value.AsSerializable();
+ if (state.Trigger == trigger && state.EventName.Equals(eventName, StringComparison.OrdinalIgnoreCase))
+ {
+ if (index >= page && index < (pageSize + page))
+ yield return state;
+ index++;
+ }
+ }
+ else
+ yield break;
+ }
+ }
+
+ public IEnumerable<(Guid ExecutionStateId, TriggerType Trigger)> FindExecutionBlockState(UInt256 hash)
+ {
+ var prefixKey = new KeyBuilder(Prefix_Id, Prefix_Execution_Block)
+ .Add(hash)
+ .ToArray();
+ foreach (var (key, value) in _snapshot.Find(prefixKey, SeekDirection.Forward))
+ {
+ if (key.AsSpan().StartsWith(prefixKey))
+ yield return (new Guid(value), (TriggerType)key.AsSpan(Prefix_Execution_Block_Trigger_Size)[0]);
+ else
+ yield break;
+ }
+ }
+
+ #endregion
+
+ #region TryGet
+
+ public bool TryGetEngineState(Guid engineStateId, [NotNullWhen(true)] out EngineLogState? state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Engine)
+ .Add(engineStateId.ToByteArray())
+ .ToArray();
+ state = _snapshot.TryGet(key, out var data) ? data.AsSerializable()! : null;
+ return data != null && data.Length > 0;
+ }
+
+ public bool TryGetTransactionEngineState(UInt256 hash, [NotNullWhen(true)] out TransactionEngineLogState? state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Engine_Transaction)
+ .Add(hash)
+ .ToArray();
+ state = _snapshot.TryGet(key, out var data) ? data.AsSerializable()! : null;
+ return data != null && data.Length > 0;
+ }
+
+ public bool TryGetBlockState(UInt256 hash, TriggerType trigger, [NotNullWhen(true)] out BlockLogState? state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Block)
+ .Add(hash)
+ .Add((byte)trigger)
+ .ToArray();
+ state = _snapshot.TryGet(key, out var data) ? data.AsSerializable()! : null;
+ return data != null && data.Length > 0;
+ }
+
+ public bool TryGetNotifyState(Guid notifyStateId, [NotNullWhen(true)] out NotifyLogState? state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Notify)
+ .Add(notifyStateId.ToByteArray())
+ .ToArray();
+ state = _snapshot.TryGet(key, out var data) ? data.AsSerializable()! : null;
+ return data != null && data.Length > 0;
+ }
+
+ public bool TryGetContractState(UInt160 scriptHash, ulong timestamp, uint iterIndex, [NotNullWhen(true)] out ContractLogState? state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Contract)
+ .Add(scriptHash)
+ .Add(timestamp)
+ .Add(iterIndex)
+ .ToArray();
+ state = _snapshot.TryGet(key, out var data) ? data.AsSerializable()! : null;
+ return data != null && data.Length > 0;
+ }
+
+ public bool TryGetExecutionState(Guid executionStateId, [NotNullWhen(true)] out ExecutionLogState? state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Execution)
+ .Add(executionStateId.ToByteArray())
+ .ToArray();
+ state = _snapshot.TryGet(key, out var data) ? data.AsSerializable()! : null;
+ return data != null && data.Length > 0;
+ }
+
+ public bool TryGetExecutionBlockState(UInt256 blockHash, TriggerType trigger, out Guid executionStateId)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Execution_Block)
+ .Add(blockHash)
+ .Add((byte)trigger)
+ .ToArray();
+ executionStateId = _snapshot.TryGet(key, out var data) ? new Guid(data) : Guid.Empty;
+ return data != null;
+ }
+
+ public bool TryGetExecutionTransactionState(UInt256 txHash, out Guid executionStateId)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Execution_Transaction)
+ .Add(txHash)
+ .ToArray();
+ executionStateId = _snapshot.TryGet(key, out var data) ? new Guid(data) : Guid.Empty;
+ return data != null;
+ }
+
+ public bool TryGetTransactionState(UInt256 hash, [NotNullWhen(true)] out TransactionLogState? state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Transaction)
+ .Add(hash)
+ .ToArray();
+ state = _snapshot.TryGet(key, out var data) ? data.AsSerializable()! : null;
+ return data != null && data.Length > 0;
+ }
+
+ public bool TryGetStackItemState(Guid stackItemId, [NotNullWhen(true)] out StackItem stackItem)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_StackItem)
+ .Add(stackItemId.ToByteArray())
+ .ToArray();
+ stackItem = _snapshot.TryGet(key, out var data) ? BinarySerializer.Deserialize(data, ExecutionEngineLimits.Default) : StackItem.Null;
+ return data != null;
+ }
+
+ #endregion
+}
diff --git a/plugins/ApplicationLogs/Store/Models/ApplicationEngineLogModel.cs b/plugins/ApplicationLogs/Store/Models/ApplicationEngineLogModel.cs
new file mode 100644
index 000000000..c50ee6b00
--- /dev/null
+++ b/plugins/ApplicationLogs/Store/Models/ApplicationEngineLogModel.cs
@@ -0,0 +1,27 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ApplicationEngineLogModel.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 Neo.Plugins.ApplicationLogs.Store.States;
+
+namespace Neo.Plugins.ApplicationLogs.Store.Models;
+
+public class ApplicationEngineLogModel
+{
+ public required UInt160 ScriptHash { get; init; }
+ public required string Message { get; init; }
+
+ public static ApplicationEngineLogModel Create(EngineLogState logEventState) =>
+ new()
+ {
+ ScriptHash = logEventState.ScriptHash,
+ Message = logEventState.Message,
+ };
+}
diff --git a/plugins/ApplicationLogs/Store/Models/BlockchainEventModel.cs b/plugins/ApplicationLogs/Store/Models/BlockchainEventModel.cs
new file mode 100644
index 000000000..9c4709fe9
--- /dev/null
+++ b/plugins/ApplicationLogs/Store/Models/BlockchainEventModel.cs
@@ -0,0 +1,46 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// BlockchainEventModel.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 Neo.Plugins.ApplicationLogs.Store.States;
+using Neo.VM.Types;
+
+namespace Neo.Plugins.ApplicationLogs.Store.Models;
+
+public class BlockchainEventModel
+{
+ public required UInt160 ScriptHash { get; init; }
+ public required string EventName { get; init; }
+ public required StackItem[] State { get; init; }
+
+ public static BlockchainEventModel Create(UInt160 scriptHash, string eventName, params StackItem[] state) =>
+ new()
+ {
+ ScriptHash = scriptHash,
+ EventName = eventName ?? string.Empty,
+ State = state,
+ };
+
+ public static BlockchainEventModel Create(NotifyLogState notifyLogState, params StackItem[] state) =>
+ new()
+ {
+ ScriptHash = notifyLogState.ScriptHash,
+ EventName = notifyLogState.EventName,
+ State = state,
+ };
+
+ public static BlockchainEventModel Create(ContractLogState contractLogState, params StackItem[] state) =>
+ new()
+ {
+ ScriptHash = contractLogState.ScriptHash,
+ EventName = contractLogState.EventName,
+ State = state,
+ };
+}
diff --git a/plugins/ApplicationLogs/Store/Models/BlockchainExecutionModel.cs b/plugins/ApplicationLogs/Store/Models/BlockchainExecutionModel.cs
new file mode 100644
index 000000000..a24cf1e12
--- /dev/null
+++ b/plugins/ApplicationLogs/Store/Models/BlockchainExecutionModel.cs
@@ -0,0 +1,40 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// BlockchainExecutionModel.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 Neo.Plugins.ApplicationLogs.Store.States;
+using Neo.SmartContract;
+using Neo.VM;
+using Neo.VM.Types;
+
+namespace Neo.Plugins.ApplicationLogs.Store.Models;
+
+public class BlockchainExecutionModel
+{
+ public required TriggerType Trigger { get; init; }
+ public required VMState VmState { get; init; }
+ public required string Exception { get; init; }
+ public required long GasConsumed { get; init; }
+ public required StackItem[] Stack { get; init; }
+ public required BlockchainEventModel[] Notifications { get; set; }
+ public required ApplicationEngineLogModel[] Logs { get; set; }
+
+ public static BlockchainExecutionModel Create(TriggerType trigger, ExecutionLogState executionLogState, params StackItem[] stack) =>
+ new()
+ {
+ Trigger = trigger,
+ VmState = executionLogState.VmState,
+ Exception = executionLogState.Exception ?? string.Empty,
+ GasConsumed = executionLogState.GasConsumed,
+ Stack = stack,
+ Notifications = [],
+ Logs = []
+ };
+}
diff --git a/plugins/ApplicationLogs/Store/NeoStore.cs b/plugins/ApplicationLogs/Store/NeoStore.cs
new file mode 100644
index 000000000..042160a7a
--- /dev/null
+++ b/plugins/ApplicationLogs/Store/NeoStore.cs
@@ -0,0 +1,309 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// NeoStore.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 Neo.Ledger;
+using Neo.Network.P2P.Payloads;
+using Neo.Persistence;
+using Neo.Plugins.ApplicationLogs.Store.Models;
+using Neo.Plugins.ApplicationLogs.Store.States;
+using Neo.SmartContract;
+using Neo.VM.Types;
+
+namespace Neo.Plugins.ApplicationLogs.Store;
+
+public sealed class NeoStore : IDisposable
+{
+ #region Globals
+
+ private readonly IStore _store;
+ private IStoreSnapshot? _blocklogsnapshot;
+
+ #endregion
+
+ #region ctor
+
+ public NeoStore(
+ IStore store)
+ {
+ _store = store;
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ public void Dispose()
+ {
+ _blocklogsnapshot?.Dispose();
+ _store?.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ #endregion
+
+ #region Batching
+
+ public void StartBlockLogBatch()
+ {
+ _blocklogsnapshot?.Dispose();
+ _blocklogsnapshot = _store.GetSnapshot();
+ }
+
+ public void CommitBlockLog() =>
+ _blocklogsnapshot?.Commit();
+
+ #endregion
+
+ #region Store
+
+ public IStore GetStore() => _store;
+
+ #endregion
+
+ #region Contract
+
+ public IReadOnlyCollection<(BlockchainEventModel NotifyLog, UInt256 TxHash)> GetContractLog(UInt160 scriptHash, uint page = 1, uint pageSize = 10)
+ {
+ using var lss = new LogStorageStore(_store.GetSnapshot());
+ var lstModels = new List<(BlockchainEventModel NotifyLog, UInt256 TxHash)>();
+ foreach (var contractState in lss.FindContractState(scriptHash, page, pageSize))
+ lstModels.Add((BlockchainEventModel.Create(contractState, CreateStackItemArray(lss, contractState.StackItemIds)), contractState.TransactionHash));
+ return lstModels;
+ }
+
+ public IReadOnlyCollection<(BlockchainEventModel NotifyLog, UInt256 TxHash)> GetContractLog(UInt160 scriptHash, TriggerType triggerType, uint page = 1, uint pageSize = 10)
+ {
+ using var lss = new LogStorageStore(_store.GetSnapshot());
+ var lstModels = new List<(BlockchainEventModel NotifyLog, UInt256 TxHash)>();
+ foreach (var contractState in lss.FindContractState(scriptHash, triggerType, page, pageSize))
+ lstModels.Add((BlockchainEventModel.Create(contractState, CreateStackItemArray(lss, contractState.StackItemIds)), contractState.TransactionHash));
+ return lstModels;
+ }
+
+ public IReadOnlyCollection<(BlockchainEventModel NotifyLog, UInt256 TxHash)> GetContractLog(UInt160 scriptHash, TriggerType triggerType, string eventName, uint page = 1, uint pageSize = 10)
+ {
+ using var lss = new LogStorageStore(_store.GetSnapshot());
+ var lstModels = new List<(BlockchainEventModel NotifyLog, UInt256 TxHash)>();
+ foreach (var contractState in lss.FindContractState(scriptHash, triggerType, eventName, page, pageSize))
+ lstModels.Add((BlockchainEventModel.Create(contractState, CreateStackItemArray(lss, contractState.StackItemIds)), contractState.TransactionHash));
+ return lstModels;
+ }
+
+ #endregion
+
+ #region Engine
+
+ public void PutTransactionEngineLogState(UInt256 hash, IReadOnlyList logs)
+ {
+ ArgumentNullException.ThrowIfNull(_blocklogsnapshot, nameof(_blocklogsnapshot));
+
+ using var lss = new LogStorageStore(_blocklogsnapshot);
+ var ids = new List();
+ foreach (var log in logs)
+ ids.Add(lss.PutEngineState(EngineLogState.Create(log.ScriptHash, log.Message)));
+ lss.PutTransactionEngineState(hash, TransactionEngineLogState.Create(ids.ToArray()));
+ }
+
+ #endregion
+
+ #region Block
+
+ public BlockchainExecutionModel? GetBlockLog(UInt256 hash, TriggerType trigger)
+ {
+ using var lss = new LogStorageStore(_store.GetSnapshot());
+ if (lss.TryGetExecutionBlockState(hash, trigger, out var executionBlockStateId) &&
+ lss.TryGetExecutionState(executionBlockStateId, out var executionLogState))
+ {
+ var model = BlockchainExecutionModel.Create(trigger, executionLogState, CreateStackItemArray(lss, executionLogState.StackItemIds));
+ if (lss.TryGetBlockState(hash, trigger, out var blockLogState))
+ {
+ var lstOfEventModel = new List();
+ foreach (var notifyLogItem in blockLogState.NotifyLogIds)
+ {
+ if (lss.TryGetNotifyState(notifyLogItem, out var notifyLogState))
+ lstOfEventModel.Add(BlockchainEventModel.Create(notifyLogState, CreateStackItemArray(lss, notifyLogState.StackItemIds)));
+ }
+ model.Notifications = lstOfEventModel.ToArray();
+ }
+ return model;
+ }
+ return null;
+ }
+
+ public BlockchainExecutionModel? GetBlockLog(UInt256 hash, TriggerType trigger, string eventName)
+ {
+ using var lss = new LogStorageStore(_store.GetSnapshot());
+ if (lss.TryGetExecutionBlockState(hash, trigger, out var executionBlockStateId) &&
+ lss.TryGetExecutionState(executionBlockStateId, out var executionLogState))
+ {
+ var model = BlockchainExecutionModel.Create(trigger, executionLogState, CreateStackItemArray(lss, executionLogState.StackItemIds));
+ if (lss.TryGetBlockState(hash, trigger, out var blockLogState))
+ {
+ var lstOfEventModel = new List();
+ foreach (var notifyLogItem in blockLogState.NotifyLogIds)
+ {
+ if (lss.TryGetNotifyState(notifyLogItem, out var notifyLogState))
+ {
+ if (notifyLogState.EventName.Equals(eventName, StringComparison.OrdinalIgnoreCase))
+ lstOfEventModel.Add(BlockchainEventModel.Create(notifyLogState, CreateStackItemArray(lss, notifyLogState.StackItemIds)));
+ }
+ }
+ model.Notifications = lstOfEventModel.ToArray();
+ }
+ return model;
+ }
+ return null;
+ }
+
+ public void PutBlockLog(Block block, IReadOnlyList applicationExecutedList)
+ {
+ ArgumentNullException.ThrowIfNull(_blocklogsnapshot, nameof(_blocklogsnapshot));
+
+ foreach (var appExecution in applicationExecutedList)
+ {
+ using var lss = new LogStorageStore(_blocklogsnapshot);
+ var exeStateId = PutExecutionLogBlock(lss, block, appExecution);
+ PutBlockAndTransactionLog(lss, block, appExecution, exeStateId);
+ }
+ }
+
+ private static Guid PutExecutionLogBlock(LogStorageStore logStore, Block block, Blockchain.ApplicationExecuted appExecution)
+ {
+ var exeStateId = logStore.PutExecutionState(ExecutionLogState.Create(appExecution, CreateStackItemIdList(logStore, appExecution)));
+ logStore.PutExecutionBlockState(block.Hash, appExecution.Trigger, exeStateId);
+ return exeStateId;
+ }
+
+ #endregion
+
+ #region Transaction
+
+ public BlockchainExecutionModel? GetTransactionLog(UInt256 hash)
+ {
+ using var lss = new LogStorageStore(_store.GetSnapshot());
+ if (lss.TryGetExecutionTransactionState(hash, out var executionTransactionStateId) &&
+ lss.TryGetExecutionState(executionTransactionStateId, out var executionLogState))
+ {
+ var model = BlockchainExecutionModel.Create(TriggerType.Application, executionLogState, CreateStackItemArray(lss, executionLogState.StackItemIds));
+ if (lss.TryGetTransactionState(hash, out var transactionLogState))
+ {
+ var lstOfEventModel = new List();
+ foreach (var notifyLogItem in transactionLogState.NotifyLogIds)
+ {
+ if (lss.TryGetNotifyState(notifyLogItem, out var notifyLogState))
+ lstOfEventModel.Add(BlockchainEventModel.Create(notifyLogState, CreateStackItemArray(lss, notifyLogState.StackItemIds)));
+ }
+ model.Notifications = lstOfEventModel.ToArray();
+
+ if (lss.TryGetTransactionEngineState(hash, out var transactionEngineLogState))
+ {
+ var lstOfLogs = new List();
+ foreach (var logItem in transactionEngineLogState.LogIds)
+ {
+ if (lss.TryGetEngineState(logItem, out var engineLogState))
+ lstOfLogs.Add(ApplicationEngineLogModel.Create(engineLogState));
+ }
+ model.Logs = lstOfLogs.ToArray();
+ }
+ }
+ return model;
+ }
+ return null;
+ }
+
+ public BlockchainExecutionModel? GetTransactionLog(UInt256 hash, string eventName)
+ {
+ using var lss = new LogStorageStore(_store.GetSnapshot());
+ if (lss.TryGetExecutionTransactionState(hash, out var executionTransactionStateId) &&
+ lss.TryGetExecutionState(executionTransactionStateId, out var executionLogState))
+ {
+ var model = BlockchainExecutionModel.Create(TriggerType.Application, executionLogState, CreateStackItemArray(lss, executionLogState.StackItemIds));
+ if (lss.TryGetTransactionState(hash, out var transactionLogState))
+ {
+ var lstOfEventModel = new List();
+ foreach (var notifyLogItem in transactionLogState.NotifyLogIds)
+ {
+ if (lss.TryGetNotifyState(notifyLogItem, out var notifyLogState))
+ {
+ if (notifyLogState.EventName.Equals(eventName, StringComparison.OrdinalIgnoreCase))
+ lstOfEventModel.Add(BlockchainEventModel.Create(notifyLogState, CreateStackItemArray(lss, notifyLogState.StackItemIds)));
+ }
+ }
+ model.Notifications = lstOfEventModel.ToArray();
+
+ if (lss.TryGetTransactionEngineState(hash, out var transactionEngineLogState))
+ {
+ var lstOfLogs = new List();
+ foreach (var logItem in transactionEngineLogState.LogIds)
+ {
+ if (lss.TryGetEngineState(logItem, out var engineLogState))
+ lstOfLogs.Add(ApplicationEngineLogModel.Create(engineLogState));
+ }
+ model.Logs = lstOfLogs.ToArray();
+ }
+ }
+ return model;
+ }
+ return null;
+ }
+
+ private static void PutBlockAndTransactionLog(LogStorageStore logStore, Block block, Blockchain.ApplicationExecuted appExecution, Guid executionStateId)
+ {
+ if (appExecution.Transaction != null)
+ logStore.PutExecutionTransactionState(appExecution.Transaction.Hash, executionStateId); // For looking up execution log by transaction hash
+
+ var lstNotifyLogIds = new List();
+ for (uint i = 0; i < appExecution.Notifications.Length; i++)
+ {
+ var notifyItem = appExecution.Notifications[i];
+ var stackItemStateIds = CreateStackItemIdList(logStore, notifyItem); // Save notify stack items
+ logStore.PutContractState(notifyItem.ScriptHash, block.Timestamp, i, // save notifylog for the contracts
+ ContractLogState.Create(appExecution, notifyItem, stackItemStateIds));
+ lstNotifyLogIds.Add(logStore.PutNotifyState(NotifyLogState.Create(notifyItem, stackItemStateIds)));
+ }
+
+ if (appExecution.Transaction != null)
+ logStore.PutTransactionState(appExecution.Transaction.Hash, TransactionLogState.Create(lstNotifyLogIds.ToArray()));
+
+ logStore.PutBlockState(block.Hash, appExecution.Trigger, BlockLogState.Create(lstNotifyLogIds.ToArray()));
+ }
+
+ #endregion
+
+ #region StackItem
+
+ private static StackItem[] CreateStackItemArray(LogStorageStore logStore, Guid[] stackItemIds)
+ {
+ var lstStackItems = new List();
+ foreach (var stackItemId in stackItemIds)
+ if (logStore.TryGetStackItemState(stackItemId, out var stackItem))
+ lstStackItems.Add(stackItem);
+ return lstStackItems.ToArray();
+ }
+
+ private static Guid[] CreateStackItemIdList(LogStorageStore logStore, Blockchain.ApplicationExecuted appExecution)
+ {
+ var lstStackItemIds = new List();
+ foreach (var stackItem in appExecution.Stack)
+ lstStackItemIds.Add(logStore.PutStackItemState(stackItem));
+ return lstStackItemIds.ToArray();
+ }
+
+ private static Guid[] CreateStackItemIdList(LogStorageStore logStore, NotifyEventArgs notifyEventArgs)
+ {
+ var lstStackItemIds = new List();
+ foreach (var stackItem in notifyEventArgs.State)
+ lstStackItemIds.Add(logStore.PutStackItemState(stackItem));
+ return lstStackItemIds.ToArray();
+ }
+
+ #endregion
+}
diff --git a/plugins/ApplicationLogs/Store/States/BlockLogState.cs b/plugins/ApplicationLogs/Store/States/BlockLogState.cs
new file mode 100644
index 000000000..10c8cfd73
--- /dev/null
+++ b/plugins/ApplicationLogs/Store/States/BlockLogState.cs
@@ -0,0 +1,73 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// BlockLogState.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 Neo.Extensions;
+using Neo.Extensions.Collections;
+using Neo.Extensions.IO;
+using Neo.IO;
+
+namespace Neo.Plugins.ApplicationLogs.Store.States;
+
+public class BlockLogState : ISerializable, IEquatable
+{
+ public Guid[] NotifyLogIds { get; private set; } = [];
+
+ public static BlockLogState Create(Guid[] notifyLogIds) =>
+ new()
+ {
+ NotifyLogIds = notifyLogIds,
+ };
+
+ #region ISerializable
+
+ public virtual int Size =>
+ sizeof(uint) +
+ NotifyLogIds.Sum(s => s.ToByteArray().GetVarSize());
+
+ public virtual void Deserialize(ref MemoryReader reader)
+ {
+ // It should be safe because it filled from a block's notifications.
+ uint aLen = reader.ReadUInt32();
+ NotifyLogIds = new Guid[aLen];
+ for (int i = 0; i < aLen; i++)
+ NotifyLogIds[i] = new Guid(reader.ReadVarMemory().Span);
+ }
+
+ public virtual void Serialize(BinaryWriter writer)
+ {
+ writer.Write((uint)NotifyLogIds.Length);
+ for (int i = 0; i < NotifyLogIds.Length; i++)
+ writer.WriteVarBytes(NotifyLogIds[i].ToByteArray());
+ }
+
+ #endregion
+
+ #region IEquatable
+
+ public bool Equals(BlockLogState? other) =>
+ other != null && NotifyLogIds.SequenceEqual(other.NotifyLogIds);
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(this, obj)) return true;
+ return Equals(obj as BlockLogState);
+ }
+
+ public override int GetHashCode()
+ {
+ var h = new HashCode();
+ foreach (var id in NotifyLogIds)
+ h.Add(id);
+ return h.ToHashCode();
+ }
+
+ #endregion
+}
diff --git a/plugins/ApplicationLogs/Store/States/ContractLogState.cs b/plugins/ApplicationLogs/Store/States/ContractLogState.cs
new file mode 100644
index 000000000..b23866c24
--- /dev/null
+++ b/plugins/ApplicationLogs/Store/States/ContractLogState.cs
@@ -0,0 +1,74 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractLogState.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 Neo.Extensions.IO;
+using Neo.IO;
+using Neo.Ledger;
+using Neo.SmartContract;
+
+namespace Neo.Plugins.ApplicationLogs.Store.States;
+
+public class ContractLogState : NotifyLogState, IEquatable
+{
+ public UInt256 TransactionHash { get; private set; } = new();
+ public TriggerType Trigger { get; private set; } = TriggerType.All;
+
+ public static ContractLogState Create(Blockchain.ApplicationExecuted applicationExecuted, NotifyEventArgs notifyEventArgs, Guid[] stackItemIds) =>
+ new()
+ {
+ TransactionHash = applicationExecuted.Transaction?.Hash ?? new(),
+ ScriptHash = notifyEventArgs.ScriptHash,
+ Trigger = applicationExecuted.Trigger,
+ EventName = notifyEventArgs.EventName,
+ StackItemIds = stackItemIds,
+ };
+
+ #region ISerializable
+
+ public override int Size =>
+ TransactionHash.Size +
+ sizeof(byte) +
+ base.Size;
+
+ public override void Deserialize(ref MemoryReader reader)
+ {
+ TransactionHash = reader.ReadSerializable();
+ Trigger = (TriggerType)reader.ReadByte();
+ base.Deserialize(ref reader);
+ }
+
+ public override void Serialize(BinaryWriter writer)
+ {
+ TransactionHash.Serialize(writer);
+ writer.Write((byte)Trigger);
+ base.Serialize(writer);
+ }
+
+ #endregion
+
+ #region IEquatable
+
+ public bool Equals(ContractLogState? other) =>
+ other != null &&
+ Trigger == other.Trigger && EventName == other.EventName &&
+ TransactionHash == other.TransactionHash && StackItemIds.SequenceEqual(other.StackItemIds);
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(this, obj)) return true;
+ return Equals(obj as ContractLogState);
+ }
+
+ public override int GetHashCode() =>
+ HashCode.Combine(TransactionHash, Trigger, base.GetHashCode());
+
+ #endregion
+}
diff --git a/plugins/ApplicationLogs/Store/States/EngineLogState.cs b/plugins/ApplicationLogs/Store/States/EngineLogState.cs
new file mode 100644
index 000000000..20e3f921b
--- /dev/null
+++ b/plugins/ApplicationLogs/Store/States/EngineLogState.cs
@@ -0,0 +1,67 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// EngineLogState.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 Neo.Extensions.IO;
+using Neo.IO;
+
+namespace Neo.Plugins.ApplicationLogs.Store.States;
+
+public class EngineLogState : ISerializable, IEquatable
+{
+ public UInt160 ScriptHash { get; private set; } = new();
+ public string Message { get; private set; } = string.Empty;
+
+ public static EngineLogState Create(UInt160 scriptHash, string message) =>
+ new()
+ {
+ ScriptHash = scriptHash,
+ Message = message,
+ };
+
+ #region ISerializable
+
+ public virtual int Size =>
+ ScriptHash.Size +
+ Message.GetVarSize();
+
+ public virtual void Deserialize(ref MemoryReader reader)
+ {
+ ScriptHash = reader.ReadSerializable();
+ // It should be safe because it filled from a transaction's logs.
+ Message = reader.ReadVarString();
+ }
+
+ public virtual void Serialize(BinaryWriter writer)
+ {
+ ScriptHash.Serialize(writer);
+ writer.WriteVarString(Message ?? string.Empty);
+ }
+
+ #endregion
+
+ #region IEquatable
+
+ public bool Equals(EngineLogState? other) =>
+ other != null &&
+ ScriptHash == other.ScriptHash &&
+ Message == other.Message;
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(this, obj)) return true;
+ return Equals(obj as EngineLogState);
+ }
+
+ public override int GetHashCode() =>
+ HashCode.Combine(ScriptHash, Message);
+
+ #endregion
+}
diff --git a/plugins/ApplicationLogs/Store/States/ExecutionLogState.cs b/plugins/ApplicationLogs/Store/States/ExecutionLogState.cs
new file mode 100644
index 000000000..62b7895a4
--- /dev/null
+++ b/plugins/ApplicationLogs/Store/States/ExecutionLogState.cs
@@ -0,0 +1,97 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ExecutionLogState.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 Neo.Extensions;
+using Neo.Extensions.Collections;
+using Neo.Extensions.IO;
+using Neo.IO;
+using Neo.Ledger;
+using Neo.VM;
+
+namespace Neo.Plugins.ApplicationLogs.Store.States;
+
+public class ExecutionLogState : ISerializable, IEquatable
+{
+ public VMState VmState { get; private set; } = VMState.NONE;
+ public string Exception { get; private set; } = string.Empty;
+ public long GasConsumed { get; private set; } = 0L;
+ public Guid[] StackItemIds { get; private set; } = [];
+
+ public static ExecutionLogState Create(Blockchain.ApplicationExecuted appExecution, Guid[] stackItemIds) =>
+ new()
+ {
+ VmState = appExecution.VMState,
+ Exception = appExecution.Exception?.InnerException?.Message ?? appExecution.Exception?.Message!,
+ GasConsumed = appExecution.GasConsumed,
+ StackItemIds = stackItemIds,
+ };
+
+ #region ISerializable
+
+ public int Size =>
+ sizeof(byte) +
+ Exception.GetVarSize() +
+ sizeof(long) +
+ sizeof(uint) +
+ StackItemIds.Sum(s => s.ToByteArray().GetVarSize());
+
+ public void Deserialize(ref MemoryReader reader)
+ {
+ VmState = (VMState)reader.ReadByte();
+ Exception = reader.ReadVarString();
+ GasConsumed = reader.ReadInt64();
+
+ // It should be safe because it filled from a transaction's stack.
+ uint aLen = reader.ReadUInt32();
+ StackItemIds = new Guid[aLen];
+ for (int i = 0; i < aLen; i++)
+ StackItemIds[i] = new Guid(reader.ReadVarMemory().Span);
+ }
+
+ public void Serialize(BinaryWriter writer)
+ {
+ writer.Write((byte)VmState);
+ writer.WriteVarString(Exception ?? string.Empty);
+ writer.Write(GasConsumed);
+
+ writer.Write((uint)StackItemIds.Length);
+ for (int i = 0; i < StackItemIds.Length; i++)
+ writer.WriteVarBytes(StackItemIds[i].ToByteArray());
+ }
+
+ #endregion
+
+ #region IEquatable
+
+ public bool Equals(ExecutionLogState? other) =>
+ other != null &&
+ VmState == other.VmState && Exception == other.Exception &&
+ GasConsumed == other.GasConsumed && StackItemIds.SequenceEqual(other.StackItemIds);
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(this, obj)) return true;
+ return Equals(obj as ExecutionLogState);
+ }
+
+ public override int GetHashCode()
+ {
+ var h = new HashCode();
+ h.Add(VmState);
+ h.Add(Exception);
+ h.Add(GasConsumed);
+ foreach (var id in StackItemIds)
+ h.Add(id);
+ return h.ToHashCode();
+ }
+
+ #endregion
+}
diff --git a/plugins/ApplicationLogs/Store/States/NotifyLogState.cs b/plugins/ApplicationLogs/Store/States/NotifyLogState.cs
new file mode 100644
index 000000000..ccb011e2e
--- /dev/null
+++ b/plugins/ApplicationLogs/Store/States/NotifyLogState.cs
@@ -0,0 +1,89 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// NotifyLogState.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 Neo.Extensions;
+using Neo.Extensions.Collections;
+using Neo.Extensions.IO;
+using Neo.IO;
+using Neo.SmartContract;
+
+namespace Neo.Plugins.ApplicationLogs.Store.States;
+
+public class NotifyLogState : ISerializable, IEquatable
+{
+ public UInt160 ScriptHash { get; protected set; } = new();
+ public string EventName { get; protected set; } = string.Empty;
+ public Guid[] StackItemIds { get; protected set; } = [];
+
+ public static NotifyLogState Create(NotifyEventArgs notifyItem, Guid[] stackItemsIds) =>
+ new()
+ {
+ ScriptHash = notifyItem.ScriptHash,
+ EventName = notifyItem.EventName,
+ StackItemIds = stackItemsIds,
+ };
+
+ #region ISerializable
+
+ public virtual int Size =>
+ ScriptHash.Size +
+ EventName.GetVarSize() +
+ StackItemIds.Sum(s => s.ToByteArray().GetVarSize());
+
+ public virtual void Deserialize(ref MemoryReader reader)
+ {
+ ScriptHash = reader.ReadSerializable();
+ EventName = reader.ReadVarString();
+
+ // It should be safe because it filled from a transaction's notifications.
+ uint aLen = reader.ReadUInt32();
+ StackItemIds = new Guid[aLen];
+ for (var i = 0; i < aLen; i++)
+ StackItemIds[i] = new Guid(reader.ReadVarMemory().Span);
+ }
+
+ public virtual void Serialize(BinaryWriter writer)
+ {
+ ScriptHash.Serialize(writer);
+ writer.WriteVarString(EventName ?? string.Empty);
+
+ writer.Write((uint)StackItemIds.Length);
+ for (var i = 0; i < StackItemIds.Length; i++)
+ writer.WriteVarBytes(StackItemIds[i].ToByteArray());
+ }
+
+ #endregion
+
+ #region IEquatable
+
+ public bool Equals(NotifyLogState? other) =>
+ other != null &&
+ EventName == other.EventName && ScriptHash == other.ScriptHash &&
+ StackItemIds.SequenceEqual(other.StackItemIds);
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(this, obj)) return true;
+ return Equals(obj as NotifyLogState);
+ }
+
+ public override int GetHashCode()
+ {
+ var h = new HashCode();
+ h.Add(ScriptHash);
+ h.Add(EventName);
+ foreach (var id in StackItemIds)
+ h.Add(id);
+ return h.ToHashCode();
+ }
+
+ #endregion
+}
diff --git a/plugins/ApplicationLogs/Store/States/TransactionEngineLogState.cs b/plugins/ApplicationLogs/Store/States/TransactionEngineLogState.cs
new file mode 100644
index 000000000..68ef9ce7c
--- /dev/null
+++ b/plugins/ApplicationLogs/Store/States/TransactionEngineLogState.cs
@@ -0,0 +1,74 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// TransactionEngineLogState.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 Neo.Extensions;
+using Neo.Extensions.Collections;
+using Neo.Extensions.IO;
+using Neo.IO;
+
+namespace Neo.Plugins.ApplicationLogs.Store.States;
+
+public class TransactionEngineLogState : ISerializable, IEquatable
+{
+ public Guid[] LogIds { get; private set; } = Array.Empty();
+
+ public static TransactionEngineLogState Create(Guid[] logIds) =>
+ new()
+ {
+ LogIds = logIds,
+ };
+
+ #region ISerializable
+
+ public virtual int Size =>
+ sizeof(uint) +
+ LogIds.Sum(s => s.ToByteArray().GetVarSize());
+
+ public virtual void Deserialize(ref MemoryReader reader)
+ {
+ // It should be safe because it filled from a transaction's logs.
+ uint aLen = reader.ReadUInt32();
+ LogIds = new Guid[aLen];
+ for (int i = 0; i < aLen; i++)
+ LogIds[i] = new Guid(reader.ReadVarMemory().Span);
+ }
+
+ public virtual void Serialize(BinaryWriter writer)
+ {
+ writer.Write((uint)LogIds.Length);
+ for (int i = 0; i < LogIds.Length; i++)
+ writer.WriteVarBytes(LogIds[i].ToByteArray());
+ }
+
+ #endregion
+
+ #region IEquatable
+
+ public bool Equals(TransactionEngineLogState? other) =>
+ other != null &&
+ LogIds.SequenceEqual(other.LogIds);
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(this, obj)) return true;
+ return Equals(obj as TransactionEngineLogState);
+ }
+
+ public override int GetHashCode()
+ {
+ var h = new HashCode();
+ foreach (var id in LogIds)
+ h.Add(id);
+ return h.ToHashCode();
+ }
+
+ #endregion
+}
diff --git a/plugins/ApplicationLogs/Store/States/TransactionLogState.cs b/plugins/ApplicationLogs/Store/States/TransactionLogState.cs
new file mode 100644
index 000000000..9d0238526
--- /dev/null
+++ b/plugins/ApplicationLogs/Store/States/TransactionLogState.cs
@@ -0,0 +1,74 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// TransactionLogState.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 Neo.Extensions;
+using Neo.Extensions.Collections;
+using Neo.Extensions.IO;
+using Neo.IO;
+
+namespace Neo.Plugins.ApplicationLogs.Store.States;
+
+public class TransactionLogState : ISerializable, IEquatable
+{
+ public Guid[] NotifyLogIds { get; private set; } = Array.Empty();
+
+ public static TransactionLogState Create(Guid[] notifyLogIds) =>
+ new()
+ {
+ NotifyLogIds = notifyLogIds,
+ };
+
+ #region ISerializable
+
+ public virtual int Size =>
+ sizeof(uint) +
+ NotifyLogIds.Sum(s => s.ToByteArray().GetVarSize());
+
+ public virtual void Deserialize(ref MemoryReader reader)
+ {
+ // It should be safe because it filled from a transaction's notifications.
+ uint aLen = reader.ReadUInt32();
+ NotifyLogIds = new Guid[aLen];
+ for (int i = 0; i < aLen; i++)
+ NotifyLogIds[i] = new Guid(reader.ReadVarMemory().Span);
+ }
+
+ public virtual void Serialize(BinaryWriter writer)
+ {
+ writer.Write((uint)NotifyLogIds.Length);
+ for (int i = 0; i < NotifyLogIds.Length; i++)
+ writer.WriteVarBytes(NotifyLogIds[i].ToByteArray());
+ }
+
+ #endregion
+
+ #region IEquatable
+
+ public bool Equals(TransactionLogState? other) =>
+ other != null &&
+ NotifyLogIds.SequenceEqual(other.NotifyLogIds);
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(this, obj)) return true;
+ return Equals(obj as TransactionLogState);
+ }
+
+ public override int GetHashCode()
+ {
+ var h = new HashCode();
+ foreach (var id in NotifyLogIds)
+ h.Add(id);
+ return h.ToHashCode();
+ }
+
+ #endregion
+}
diff --git a/plugins/DBFTPlugin/Consensus/ConsensusContext.Get.cs b/plugins/DBFTPlugin/Consensus/ConsensusContext.Get.cs
new file mode 100644
index 000000000..2c3062765
--- /dev/null
+++ b/plugins/DBFTPlugin/Consensus/ConsensusContext.Get.cs
@@ -0,0 +1,117 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ConsensusContext.Get.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 Neo.Network.P2P.Payloads;
+using Neo.Plugins.DBFTPlugin.Messages;
+using Neo.SmartContract;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace Neo.Plugins.DBFTPlugin.Consensus;
+
+partial class ConsensusContext
+{
+ [return: NotNullIfNotNull(nameof(payload))]
+ public ConsensusMessage? GetMessage(ExtensiblePayload? payload)
+ {
+ if (payload is null) return null;
+ if (!cachedMessages.TryGetValue(payload.Hash, out ConsensusMessage? message))
+ cachedMessages.Add(payload.Hash, message = ConsensusMessage.DeserializeFrom(payload.Data));
+ return message;
+ }
+
+ [return: NotNullIfNotNull(nameof(payload))]
+ public T? GetMessage(ExtensiblePayload? payload) where T : ConsensusMessage
+ {
+ return (T?)GetMessage(payload);
+ }
+
+ private RecoveryMessage.ChangeViewPayloadCompact GetChangeViewPayloadCompact(ExtensiblePayload payload)
+ {
+ ChangeView message = GetMessage(payload);
+ return new RecoveryMessage.ChangeViewPayloadCompact
+ {
+ ValidatorIndex = message.ValidatorIndex,
+ OriginalViewNumber = message.ViewNumber,
+ Timestamp = message.Timestamp,
+ InvocationScript = payload.Witness.InvocationScript
+ };
+ }
+
+ private RecoveryMessage.CommitPayloadCompact GetCommitPayloadCompact(ExtensiblePayload payload)
+ {
+ Commit message = GetMessage(payload);
+ return new RecoveryMessage.CommitPayloadCompact
+ {
+ ViewNumber = message.ViewNumber,
+ ValidatorIndex = message.ValidatorIndex,
+ Signature = message.Signature,
+ InvocationScript = payload.Witness.InvocationScript
+ };
+ }
+
+ private RecoveryMessage.PreparationPayloadCompact GetPreparationPayloadCompact(ExtensiblePayload payload)
+ {
+ return new RecoveryMessage.PreparationPayloadCompact
+ {
+ ValidatorIndex = GetMessage(payload).ValidatorIndex,
+ InvocationScript = payload.Witness.InvocationScript
+ };
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public byte GetPrimaryIndex(byte viewNumber)
+ {
+ int p = ((int)Block.Index - viewNumber) % Validators.Length;
+ return p >= 0 ? (byte)p : (byte)(p + Validators.Length);
+ }
+
+ public UInt160 GetSender(int index)
+ {
+ return Contract.CreateSignatureRedeemScript(Validators[index]).ToScriptHash();
+ }
+
+ ///
+ /// Return the expected block size
+ ///
+ public int GetExpectedBlockSize()
+ {
+ return GetExpectedBlockSizeWithoutTransactions(Transactions!.Count) + // Base size
+ Transactions.Values.Sum(u => u.Size); // Sum Txs
+ }
+
+ ///
+ /// Return the expected block system fee
+ ///
+ public long GetExpectedBlockSystemFee()
+ {
+ return Transactions!.Values.Sum(u => u.SystemFee); // Sum Txs
+ }
+
+ ///
+ /// Return the expected block size without txs
+ ///
+ /// Expected transactions
+ internal int GetExpectedBlockSizeWithoutTransactions(int expectedTransactions)
+ {
+ return
+ sizeof(uint) + // Version
+ UInt256.Length + // PrevHash
+ UInt256.Length + // MerkleRoot
+ sizeof(ulong) + // Timestamp
+ sizeof(ulong) + // Nonce
+ sizeof(uint) + // Index
+ sizeof(byte) + // PrimaryIndex
+ UInt160.Length + // NextConsensus
+ 1 + _witnessSize + // Witness
+ expectedTransactions.GetVarSize();
+ }
+}
diff --git a/plugins/DBFTPlugin/Consensus/ConsensusContext.MakePayload.cs b/plugins/DBFTPlugin/Consensus/ConsensusContext.MakePayload.cs
new file mode 100644
index 000000000..33bfe6f93
--- /dev/null
+++ b/plugins/DBFTPlugin/Consensus/ConsensusContext.MakePayload.cs
@@ -0,0 +1,191 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ConsensusContext.MakePayload.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 Neo.Ledger;
+using Neo.Network.P2P.Payloads;
+using Neo.Plugins.DBFTPlugin.Messages;
+using Neo.Plugins.DBFTPlugin.Types;
+using System.Buffers.Binary;
+using System.Security.Cryptography;
+
+namespace Neo.Plugins.DBFTPlugin.Consensus;
+
+partial class ConsensusContext
+{
+ public ExtensiblePayload MakeChangeView(ChangeViewReason reason)
+ {
+ return ChangeViewPayloads[MyIndex] = MakeSignedPayload(new ChangeView
+ {
+ Reason = reason,
+ Timestamp = TimeProvider.Current.UtcNow.ToTimestampMS()
+ });
+ }
+
+ public ExtensiblePayload MakeCommit()
+ {
+ if (CommitPayloads[MyIndex] is ExtensiblePayload payload)
+ return payload;
+
+ var block = EnsureHeader()!;
+ CommitPayloads[MyIndex] = MakeSignedPayload(new Commit
+ {
+ Signature = _signer.SignBlock(block, _myPublicKey!, dbftSettings.Network)
+ });
+ return CommitPayloads[MyIndex]!;
+ }
+
+ private ExtensiblePayload MakeSignedPayload(ConsensusMessage message)
+ {
+ message.BlockIndex = Block.Index;
+ message.ValidatorIndex = (byte)MyIndex;
+ message.ViewNumber = ViewNumber;
+ ExtensiblePayload payload = CreatePayload(message, null);
+ SignPayload(payload);
+ return payload;
+ }
+
+ private void SignPayload(ExtensiblePayload payload)
+ {
+ try
+ {
+ payload.Witness = _signer.SignExtensiblePayload(payload, Snapshot, dbftSettings.Network);
+ }
+ catch (InvalidOperationException ex)
+ {
+ Utility.Log(nameof(ConsensusContext), LogLevel.Debug, ex.ToString());
+ return;
+ }
+ }
+
+ ///
+ /// Prevent that block exceed the max size
+ ///
+ /// Ordered transactions
+ internal void EnsureMaxBlockLimitation(Transaction[] txs)
+ {
+ var hashes = new List();
+ Transactions = new Dictionary();
+ VerificationContext = new TransactionVerificationContext();
+
+ // Expected block size
+ var blockSize = GetExpectedBlockSizeWithoutTransactions(txs.Length);
+ var blockSystemFee = 0L;
+
+ // Iterate transaction until reach the size or maximum system fee
+ foreach (Transaction tx in txs)
+ {
+ // Check if maximum block size has been already exceeded with the current selected set
+ blockSize += tx.Size;
+ if (blockSize > dbftSettings.MaxBlockSize) break;
+
+ // Check if maximum block system fee has been already exceeded with the current selected set
+ blockSystemFee += tx.SystemFee;
+ if (blockSystemFee > dbftSettings.MaxBlockSystemFee) break;
+
+ hashes.Add(tx.Hash);
+ Transactions.Add(tx.Hash, tx);
+ VerificationContext.AddTransaction(tx);
+ }
+
+ TransactionHashes = hashes.ToArray();
+ }
+
+ public ExtensiblePayload MakePrepareRequest()
+ {
+ var maxTransactionsPerBlock = neoSystem.Settings.MaxTransactionsPerBlock;
+ // Limit Speaker proposal to the limit `MaxTransactionsPerBlock` or all available transactions of the mempool
+ EnsureMaxBlockLimitation(neoSystem.MemPool.GetSortedVerifiedTransactions((int)maxTransactionsPerBlock));
+ Block.Header.Timestamp = Math.Max(TimeProvider.Current.UtcNow.ToTimestampMS(), PrevHeader.Timestamp + 1);
+ Block.Header.Nonce = GetNonce();
+ return PreparationPayloads[MyIndex] = MakeSignedPayload(new PrepareRequest
+ {
+ Version = Block.Version,
+ PrevHash = Block.PrevHash,
+ Timestamp = Block.Timestamp,
+ Nonce = Block.Nonce,
+ TransactionHashes = TransactionHashes!
+ });
+ }
+
+ public ExtensiblePayload MakeRecoveryRequest()
+ {
+ return MakeSignedPayload(new RecoveryRequest
+ {
+ Timestamp = TimeProvider.Current.UtcNow.ToTimestampMS()
+ });
+ }
+
+ public ExtensiblePayload MakeRecoveryMessage()
+ {
+ PrepareRequest? prepareRequestMessage = null;
+ if (TransactionHashes != null)
+ {
+ prepareRequestMessage = new PrepareRequest
+ {
+ Version = Block.Version,
+ PrevHash = Block.PrevHash,
+ ViewNumber = ViewNumber,
+ Timestamp = Block.Timestamp,
+ Nonce = Block.Nonce,
+ BlockIndex = Block.Index,
+ ValidatorIndex = Block.PrimaryIndex,
+ TransactionHashes = TransactionHashes
+ };
+ }
+ return MakeSignedPayload(new RecoveryMessage
+ {
+ ChangeViewMessages = LastChangeViewPayloads.Where(p => p != null)
+ .Select(p => GetChangeViewPayloadCompact(p!))
+ .Take(M)
+ .ToDictionary(p => p.ValidatorIndex),
+ PrepareRequestMessage = prepareRequestMessage,
+ // We only need a PreparationHash set if we don't have the PrepareRequest information.
+ PreparationHash = TransactionHashes == null
+ ? PreparationPayloads.Where(p => p != null)
+ .GroupBy(p => GetMessage(p!).PreparationHash, (k, g) => new { Hash = k, Count = g.Count() })
+ .OrderByDescending(p => p.Count)
+ .Select(p => p.Hash)
+ .FirstOrDefault()
+ : null,
+ PreparationMessages = PreparationPayloads.Where(p => p != null)
+ .Select(p => GetPreparationPayloadCompact(p!))
+ .ToDictionary(p => p.ValidatorIndex),
+ CommitMessages = CommitSent
+ ? CommitPayloads.Where(p => p != null).Select(p => GetCommitPayloadCompact(p!)).ToDictionary(p => p.ValidatorIndex)
+ : new Dictionary()
+ });
+ }
+
+ public ExtensiblePayload MakePrepareResponse()
+ {
+ return PreparationPayloads[MyIndex] = MakeSignedPayload(new PrepareResponse
+ {
+ PreparationHash = PreparationPayloads[Block.PrimaryIndex]!.Hash
+ });
+ }
+
+ // Related to issue https://github.com/neo-project/neo/issues/3431
+ // Ref. https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.randomnumbergenerator?view=net-8.0
+ //
+ //The System.Random class relies on a seed value that can be predictable,
+ //especially if the seed is based on the system clock or other low-entropy sources.
+ //RandomNumberGenerator, however, uses sources of entropy provided by the operating
+ //system, which are designed to be unpredictable.
+ private static ulong GetNonce()
+ {
+ Span buffer = stackalloc byte[8];
+ using (var rng = RandomNumberGenerator.Create())
+ {
+ rng.GetBytes(buffer);
+ }
+ return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
+ }
+}
diff --git a/plugins/DBFTPlugin/Consensus/ConsensusContext.cs b/plugins/DBFTPlugin/Consensus/ConsensusContext.cs
new file mode 100644
index 000000000..d807e19fa
--- /dev/null
+++ b/plugins/DBFTPlugin/Consensus/ConsensusContext.cs
@@ -0,0 +1,332 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ConsensusContext.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 Neo.Cryptography;
+using Neo.Cryptography.ECC;
+using Neo.Extensions.IO;
+using Neo.IO;
+using Neo.Ledger;
+using Neo.Network.P2P.Payloads;
+using Neo.Persistence;
+using Neo.Plugins.DBFTPlugin.Messages;
+using Neo.Sign;
+using Neo.SmartContract;
+using Neo.SmartContract.Native;
+using Neo.VM;
+
+namespace Neo.Plugins.DBFTPlugin.Consensus;
+
+public sealed partial class ConsensusContext : IDisposable, ISerializable
+{
+ ///
+ /// Key for saving consensus state.
+ ///
+ private static readonly byte[] ConsensusStateKey = { 0xf4 };
+
+ public Block Block = null!;
+ public byte ViewNumber;
+ public TimeSpan TimePerBlock;
+ public ECPoint[] Validators = null!;
+ public int MyIndex;
+ public UInt256[]? TransactionHashes;
+ public Dictionary? Transactions;
+ public ExtensiblePayload?[] PreparationPayloads = null!;
+ public ExtensiblePayload?[] CommitPayloads = null!;
+ public ExtensiblePayload?[] ChangeViewPayloads = null!;
+ public ExtensiblePayload?[] LastChangeViewPayloads = null!;
+ // LastSeenMessage array stores the height of the last seen message, for each validator.
+ // if this node never heard from validator i, LastSeenMessage[i] will be -1.
+ public Dictionary? LastSeenMessage { get; private set; }
+
+ ///
+ /// Store all verified unsorted transactions' senders' fee currently in the consensus context.
+ ///
+ public TransactionVerificationContext VerificationContext = new();
+
+ public StoreCache Snapshot { get; private set; } = null!;
+ private ECPoint? _myPublicKey;
+ private int _witnessSize;
+ private readonly NeoSystem neoSystem;
+ private readonly DbftSettings dbftSettings;
+ private readonly ISigner _signer;
+ private readonly IStore? store;
+ private Dictionary cachedMessages = null!;
+
+ public int F => (Validators.Length - 1) / 3;
+ public int M => Validators.Length - F;
+ public bool IsPrimary => MyIndex == Block.PrimaryIndex;
+ public bool IsBackup => MyIndex >= 0 && MyIndex != Block.PrimaryIndex;
+ public bool WatchOnly => MyIndex < 0;
+ public Header PrevHeader => NativeContract.Ledger.GetHeader(Snapshot, Block.PrevHash)!;
+ public int CountCommitted => CommitPayloads.Count(p => p != null);
+ public int CountFailed
+ {
+ get
+ {
+ if (LastSeenMessage == null) return 0;
+ return Validators.Count(p => !LastSeenMessage.TryGetValue(p, out var value) || value < (Block.Index - 1));
+ }
+ }
+ public bool ValidatorsChanged
+ {
+ get
+ {
+ if (NativeContract.Ledger.CurrentIndex(Snapshot) == 0) return false;
+ UInt256 hash = NativeContract.Ledger.CurrentHash(Snapshot);
+ TrimmedBlock currentBlock = NativeContract.Ledger.GetTrimmedBlock(Snapshot, hash)!;
+ TrimmedBlock previousBlock = NativeContract.Ledger.GetTrimmedBlock(Snapshot, currentBlock.Header.PrevHash)!;
+ return currentBlock.Header.NextConsensus != previousBlock.Header.NextConsensus;
+ }
+ }
+
+ #region Consensus States
+ public bool RequestSentOrReceived => PreparationPayloads[Block.PrimaryIndex] != null;
+ public bool ResponseSent => !WatchOnly && PreparationPayloads[MyIndex] != null;
+ public bool CommitSent => !WatchOnly && CommitPayloads[MyIndex] != null;
+ public bool BlockSent => Block.Transactions != null;
+ public bool ViewChanging => !WatchOnly && GetMessage(ChangeViewPayloads[MyIndex])?.NewViewNumber > ViewNumber;
+ // NotAcceptingPayloadsDueToViewChanging imposes nodes to not accept some payloads if View is Changing,
+ // i.e: OnTransaction function will not process any transaction; OnPrepareRequestReceived will also return;
+ // as well as OnPrepareResponseReceived and also similar logic for recovering.
+ // On the other hand, if more than MoreThanFNodesCommittedOrLost is true, we keep accepting those payloads.
+ // This helps the node to still commit, even while almost changing view.
+ public bool NotAcceptingPayloadsDueToViewChanging => ViewChanging && !MoreThanFNodesCommittedOrLost;
+ // A possible attack can happen if the last node to commit is malicious and either sends change view after his
+ // commit to stall nodes in a higher view, or if he refuses to send recovery messages. In addition, if a node
+ // asking change views loses network or crashes and comes back when nodes are committed in more than one higher
+ // numbered view, it is possible for the node accepting recovery to commit in any of the higher views, thus
+ // potentially splitting nodes among views and stalling the network.
+ public bool MoreThanFNodesCommittedOrLost => (CountCommitted + CountFailed) > F;
+ #endregion
+
+ public int Size => throw new NotImplementedException();
+
+ public ConsensusContext(NeoSystem neoSystem, DbftSettings settings, ISigner signer)
+ {
+ _signer = signer;
+ this.neoSystem = neoSystem;
+ dbftSettings = settings;
+
+ if (dbftSettings.IgnoreRecoveryLogs == false)
+ store = neoSystem.LoadStore(settings.RecoveryLogs);
+ }
+
+ public Block CreateBlock()
+ {
+ EnsureHeader();
+ var contract = Contract.CreateMultiSigContract(M, Validators);
+ var sc = new ContractParametersContext(neoSystem.StoreView, Block.Header, dbftSettings.Network);
+ for (int i = 0, j = 0; i < Validators.Length && j < M; i++)
+ {
+ if (GetMessage(CommitPayloads[i])?.ViewNumber != ViewNumber) continue;
+ sc.AddSignature(contract, Validators[i], GetMessage(CommitPayloads[i]!).Signature.ToArray());
+ j++;
+ }
+ Block.Header.Witness = sc.GetWitnesses()[0];
+ Block.Transactions = TransactionHashes!.Select(p => Transactions![p]).ToArray();
+ return Block;
+ }
+
+ public ExtensiblePayload CreatePayload(ConsensusMessage message, ReadOnlyMemory invocationScript = default)
+ {
+ var payload = new ExtensiblePayload
+ {
+ Category = "dBFT",
+ ValidBlockStart = 0,
+ ValidBlockEnd = message.BlockIndex,
+ Sender = GetSender(message.ValidatorIndex),
+ Data = message.ToArray(),
+ Witness = invocationScript.IsEmpty ? null! : new Witness
+ {
+ InvocationScript = invocationScript,
+ VerificationScript = Contract.CreateSignatureRedeemScript(Validators[message.ValidatorIndex])
+ }
+ };
+ cachedMessages.TryAdd(payload.Hash, message);
+ return payload;
+ }
+
+ public void Dispose()
+ {
+ Snapshot?.Dispose();
+ }
+
+ public Block? EnsureHeader()
+ {
+ if (TransactionHashes == null) return null;
+ Block.Header.MerkleRoot ??= MerkleTree.ComputeRoot(TransactionHashes);
+ return Block;
+ }
+
+ public bool Load()
+ {
+ if (store is null || !store.TryGet(ConsensusStateKey, out var data) || data.Length == 0)
+ return false;
+
+ MemoryReader reader = new(data);
+ try
+ {
+ Deserialize(ref reader);
+ }
+ catch (InvalidOperationException)
+ {
+ return false;
+ }
+ catch (Exception exception)
+ {
+ Utility.Log(nameof(ConsensusContext), LogLevel.Debug, exception.ToString());
+ return false;
+ }
+ return true;
+ }
+
+ public void Reset(byte viewNumber)
+ {
+ if (viewNumber == 0)
+ {
+ Snapshot?.Dispose();
+ Snapshot = neoSystem.GetSnapshotCache();
+ uint height = NativeContract.Ledger.CurrentIndex(Snapshot);
+ Block = new Block
+ {
+ Header = new Header
+ {
+ PrevHash = NativeContract.Ledger.CurrentHash(Snapshot),
+ MerkleRoot = null!,
+ Index = height + 1,
+ NextConsensus = Contract.GetBFTAddress(
+ NeoToken.ShouldRefreshCommittee(height + 1, neoSystem.Settings.CommitteeMembersCount) ?
+ NativeContract.NEO.ComputeNextBlockValidators(Snapshot, neoSystem.Settings) :
+ NativeContract.NEO.GetNextBlockValidators(Snapshot, neoSystem.Settings.ValidatorsCount)),
+ Witness = null!
+ },
+ Transactions = null!
+ };
+ TimePerBlock = neoSystem.Settings.TimePerBlock;
+ var pv = Validators;
+ Validators = NativeContract.NEO.GetNextBlockValidators(Snapshot, neoSystem.Settings.ValidatorsCount);
+ if (_witnessSize == 0 || (pv != null && pv.Length != Validators.Length))
+ {
+ // Compute the expected size of the witness
+ using ScriptBuilder sb = new(65 * M + 34 * Validators.Length + 64); // 64 is extra space
+ var buf = new byte[64];
+ for (int x = 0; x < M; x++)
+ {
+ sb.EmitPush(buf);
+ }
+ _witnessSize = new Witness
+ {
+ InvocationScript = sb.ToArray(),
+ VerificationScript = Contract.CreateMultiSigRedeemScript(M, Validators)
+ }.Size;
+ }
+ MyIndex = -1;
+ ChangeViewPayloads = new ExtensiblePayload[Validators.Length];
+ LastChangeViewPayloads = new ExtensiblePayload[Validators.Length];
+ CommitPayloads = new ExtensiblePayload[Validators.Length];
+ if (ValidatorsChanged || LastSeenMessage is null)
+ {
+ var previous_last_seen_message = LastSeenMessage;
+ LastSeenMessage = new Dictionary();
+ foreach (var validator in Validators)
+ {
+ if (previous_last_seen_message != null && previous_last_seen_message.TryGetValue(validator, out var value))
+ LastSeenMessage[validator] = value;
+ else
+ LastSeenMessage[validator] = height;
+ }
+ }
+
+ _myPublicKey = null;
+ for (int i = 0; i < Validators.Length; i++)
+ {
+ // ContainsKeyPair may be called multiple times
+ if (!_signer.ContainsSignable(Validators[i])) continue;
+ MyIndex = i;
+ _myPublicKey = Validators[MyIndex];
+ break;
+ }
+ cachedMessages = new Dictionary();
+ }
+ else
+ {
+ for (int i = 0; i < LastChangeViewPayloads.Length; i++)
+ if (GetMessage(ChangeViewPayloads[i])?.NewViewNumber >= viewNumber)
+ LastChangeViewPayloads[i] = ChangeViewPayloads[i];
+ else
+ LastChangeViewPayloads[i] = null;
+ }
+ ViewNumber = viewNumber;
+ Block.Header.PrimaryIndex = GetPrimaryIndex(viewNumber);
+ Block.Header.MerkleRoot = null!;
+ Block.Header.Timestamp = 0;
+ Block.Header.Nonce = 0;
+ Block.Transactions = null!;
+ TransactionHashes = null;
+ PreparationPayloads = new ExtensiblePayload[Validators.Length];
+ if (MyIndex >= 0) LastSeenMessage![Validators[MyIndex]] = Block.Index;
+ }
+
+ public void Save()
+ {
+ store?.PutSync(ConsensusStateKey, this.ToArray());
+ }
+
+ public void Deserialize(ref MemoryReader reader)
+ {
+ Reset(0);
+
+ var blockVersion = reader.ReadUInt32();
+ if (blockVersion != Block.Version)
+ throw new FormatException($"Invalid block version: {blockVersion}/{Block.Version}");
+
+ if (reader.ReadUInt32() != Block.Index) throw new InvalidOperationException();
+ Block.Header.Timestamp = reader.ReadUInt64();
+ Block.Header.Nonce = reader.ReadUInt64();
+ Block.Header.PrimaryIndex = reader.ReadByte();
+ Block.Header.NextConsensus = reader.ReadSerializable();
+ if (Block.NextConsensus.Equals(UInt160.Zero))
+ Block.Header.NextConsensus = null!;
+ ViewNumber = reader.ReadByte();
+ TransactionHashes = reader.ReadSerializableArray(ushort.MaxValue);
+ Transaction[] transactions = reader.ReadSerializableArray(ushort.MaxValue);
+ PreparationPayloads = reader.ReadNullableArray(neoSystem.Settings.ValidatorsCount);
+ CommitPayloads = reader.ReadNullableArray(neoSystem.Settings.ValidatorsCount);
+ ChangeViewPayloads = reader.ReadNullableArray(neoSystem.Settings.ValidatorsCount);
+ LastChangeViewPayloads = reader.ReadNullableArray(neoSystem.Settings.ValidatorsCount);
+ if (TransactionHashes.Length == 0 && !RequestSentOrReceived)
+ TransactionHashes = null;
+ Transactions = transactions.Length == 0 && !RequestSentOrReceived ? null : transactions.ToDictionary(p => p.Hash);
+ VerificationContext = new TransactionVerificationContext();
+ if (Transactions != null)
+ {
+ foreach (Transaction tx in Transactions.Values)
+ VerificationContext.AddTransaction(tx);
+ }
+ }
+
+ public void Serialize(BinaryWriter writer)
+ {
+ writer.Write(Block.Version);
+ writer.Write(Block.Index);
+ writer.Write(Block.Timestamp);
+ writer.Write(Block.Nonce);
+ writer.Write(Block.PrimaryIndex);
+ writer.Write(Block.NextConsensus ?? UInt160.Zero);
+ writer.Write(ViewNumber);
+ writer.Write(TransactionHashes ?? Array.Empty());
+ writer.Write(Transactions?.Values.ToArray() ?? Array.Empty());
+ writer.WriteNullableArray(PreparationPayloads);
+ writer.WriteNullableArray(CommitPayloads);
+ writer.WriteNullableArray(ChangeViewPayloads);
+ writer.WriteNullableArray(LastChangeViewPayloads);
+ }
+}
diff --git a/plugins/DBFTPlugin/Consensus/ConsensusService.Check.cs b/plugins/DBFTPlugin/Consensus/ConsensusService.Check.cs
new file mode 100644
index 000000000..ee6e60f07
--- /dev/null
+++ b/plugins/DBFTPlugin/Consensus/ConsensusService.Check.cs
@@ -0,0 +1,99 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ConsensusService.Check.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 Akka.Actor;
+using Neo.Network.P2P;
+using Neo.Network.P2P.Payloads;
+using Neo.Plugins.DBFTPlugin.Messages;
+using Neo.Plugins.DBFTPlugin.Types;
+
+namespace Neo.Plugins.DBFTPlugin.Consensus;
+
+partial class ConsensusService
+{
+ private bool CheckPrepareResponse()
+ {
+ if (context.TransactionHashes!.Length == context.Transactions!.Count)
+ {
+ // if we are the primary for this view, but acting as a backup because we recovered our own
+ // previously sent prepare request, then we don't want to send a prepare response.
+ if (context.IsPrimary || context.WatchOnly) return true;
+
+ // Check maximum block size via Native Contract policy
+ if (context.GetExpectedBlockSize() > dbftSettings.MaxBlockSize)
+ {
+ Log($"Rejected block: {context.Block.Index} The size exceed the policy", LogLevel.Warning);
+ RequestChangeView(ChangeViewReason.BlockRejectedByPolicy);
+ return false;
+ }
+ // Check maximum block system fee via Native Contract policy
+ if (context.GetExpectedBlockSystemFee() > dbftSettings.MaxBlockSystemFee)
+ {
+ Log($"Rejected block: {context.Block.Index} The system fee exceed the policy", LogLevel.Warning);
+ RequestChangeView(ChangeViewReason.BlockRejectedByPolicy);
+ return false;
+ }
+
+ // Timeout extension due to prepare response sent
+ // around 2*15/M=30.0/5 ~ 40% block time (for M=5)
+ ExtendTimerByFactor(2);
+
+ Log($"Sending {nameof(PrepareResponse)}");
+ localNode.Tell(new LocalNode.SendDirectly(context.MakePrepareResponse()));
+ CheckPreparations();
+ }
+ return true;
+ }
+
+ private void CheckCommits()
+ {
+ if (context.CommitPayloads.Count(p => context.GetMessage(p)?.ViewNumber == context.ViewNumber) >= context.M && context.TransactionHashes!.All(p => context.Transactions!.ContainsKey(p)))
+ {
+ block_received_index = context.Block.Index;
+ Block block = context.CreateBlock();
+ Log($"Sending {nameof(Block)}: height={block.Index} hash={block.Hash} tx={block.Transactions.Length}");
+ blockchain.Tell(block);
+ }
+ }
+
+ private void CheckExpectedView(byte viewNumber)
+ {
+ if (context.ViewNumber >= viewNumber) return;
+ var messages = context.ChangeViewPayloads.Select(p => context.GetMessage(p)).ToArray();
+ // if there are `M` change view payloads with NewViewNumber greater than viewNumber, then, it is safe to move
+ if (messages.Count(p => p != null && p.NewViewNumber >= viewNumber) >= context.M)
+ {
+ if (!context.WatchOnly)
+ {
+ ChangeView? message = messages[context.MyIndex];
+ // Communicate the network about my agreement to move to `viewNumber`
+ // if my last change view payload, `message`, has NewViewNumber lower than current view to change
+ if (message is null || message.NewViewNumber < viewNumber)
+ localNode.Tell(new LocalNode.SendDirectly(context.MakeChangeView(ChangeViewReason.ChangeAgreement)));
+ }
+ InitializeConsensus(viewNumber);
+ }
+ }
+
+ private void CheckPreparations()
+ {
+ if (context.PreparationPayloads.Count(p => p != null) >= context.M && context.TransactionHashes!.All(p => context.Transactions!.ContainsKey(p)))
+ {
+ ExtensiblePayload payload = context.MakeCommit();
+ Log($"Sending {nameof(Commit)}");
+ context.Save();
+ localNode.Tell(new LocalNode.SendDirectly(payload));
+ // Set timer, so we will resend the commit in case of a networking issue
+ ChangeTimer(context.TimePerBlock);
+ CheckCommits();
+ }
+ }
+}
diff --git a/plugins/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs b/plugins/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs
new file mode 100644
index 000000000..c7663d2dd
--- /dev/null
+++ b/plugins/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs
@@ -0,0 +1,313 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ConsensusService.OnMessage.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 Akka.Actor;
+using Neo.Cryptography;
+using Neo.Ledger;
+using Neo.Network.P2P;
+using Neo.Network.P2P.Payloads;
+using Neo.Plugins.DBFTPlugin.Messages;
+using Neo.SmartContract;
+using Neo.SmartContract.Native;
+
+namespace Neo.Plugins.DBFTPlugin.Consensus;
+
+partial class ConsensusService
+{
+ private void OnConsensusPayload(ExtensiblePayload payload)
+ {
+ if (context.BlockSent) return;
+ ConsensusMessage message;
+ try
+ {
+ message = context.GetMessage(payload);
+ }
+ catch (Exception ex)
+ {
+ Utility.Log(nameof(ConsensusService), LogLevel.Debug, ex.ToString());
+ return;
+ }
+
+ if (!message.Verify(neoSystem.Settings)) return;
+ if (message.BlockIndex != context.Block.Index)
+ {
+ if (context.Block.Index < message.BlockIndex)
+ {
+ Log($"Chain is behind: expected={message.BlockIndex} current={context.Block.Index - 1}", LogLevel.Warning);
+ }
+ return;
+ }
+ if (message.ValidatorIndex >= context.Validators.Length) return;
+ if (payload.Sender != Contract.CreateSignatureRedeemScript(context.Validators[message.ValidatorIndex]).ToScriptHash()) return;
+ context.LastSeenMessage?[context.Validators[message.ValidatorIndex]] = message.BlockIndex;
+ switch (message)
+ {
+ case PrepareRequest request:
+ OnPrepareRequestReceived(payload, request);
+ break;
+ case PrepareResponse response:
+ OnPrepareResponseReceived(payload, response);
+ break;
+ case ChangeView view:
+ OnChangeViewReceived(payload, view);
+ break;
+ case Commit commit:
+ OnCommitReceived(payload, commit);
+ break;
+ case RecoveryRequest request:
+ OnRecoveryRequestReceived(payload, request);
+ break;
+ case RecoveryMessage recovery:
+ OnRecoveryMessageReceived(recovery);
+ break;
+ }
+ }
+
+ private void OnPrepareRequestReceived(ExtensiblePayload payload, PrepareRequest message)
+ {
+ if (context.RequestSentOrReceived || context.NotAcceptingPayloadsDueToViewChanging) return;
+ if (message.ValidatorIndex != context.Block.PrimaryIndex || message.ViewNumber != context.ViewNumber) return;
+ if (message.Version != context.Block.Version || message.PrevHash != context.Block.PrevHash) return;
+ if (message.TransactionHashes.Length > neoSystem.Settings.MaxTransactionsPerBlock) return;
+ Log($"{nameof(OnPrepareRequestReceived)}: height={message.BlockIndex} view={message.ViewNumber} index={message.ValidatorIndex} tx={message.TransactionHashes.Length}");
+ if (message.Timestamp <= context.PrevHeader.Timestamp || message.Timestamp > TimeProvider.Current.UtcNow.AddMilliseconds(8 * context.TimePerBlock.TotalMilliseconds).ToTimestampMS())
+ {
+ Log($"Timestamp incorrect: {message.Timestamp}", LogLevel.Warning);
+ return;
+ }
+
+ if (message.TransactionHashes.Any(p => NativeContract.Ledger.ContainsTransaction(context.Snapshot, p)))
+ {
+ Log($"Invalid request: transaction already exists", LogLevel.Warning);
+ return;
+ }
+
+ // Timeout extension: prepare request has been received with success
+ // around 2*15/M=30.0/5 ~ 40% block time (for M=5)
+ ExtendTimerByFactor(2);
+
+ prepareRequestReceivedTime = TimeProvider.Current.UtcNow;
+ prepareRequestReceivedBlockIndex = message.BlockIndex;
+
+ context.Block.Header.Timestamp = message.Timestamp;
+ context.Block.Header.Nonce = message.Nonce;
+ context.TransactionHashes = message.TransactionHashes;
+
+ context.Transactions = new Dictionary();
+ context.VerificationContext = new TransactionVerificationContext();
+ for (int i = 0; i < context.PreparationPayloads.Length; i++)
+ if (context.PreparationPayloads[i] != null)
+ if (!context.GetMessage(context.PreparationPayloads[i]!).PreparationHash.Equals(payload.Hash))
+ context.PreparationPayloads[i] = null;
+ context.PreparationPayloads[message.ValidatorIndex] = payload;
+ byte[] hashData = context.EnsureHeader()!.GetSignData(neoSystem.Settings.Network);
+ for (int i = 0; i < context.CommitPayloads.Length; i++)
+ if (context.GetMessage(context.CommitPayloads[i])?.ViewNumber == context.ViewNumber)
+ if (!Crypto.VerifySignature(hashData, context.GetMessage(context.CommitPayloads[i]!).Signature.Span, context.Validators[i]))
+ context.CommitPayloads[i] = null;
+
+ if (context.TransactionHashes.Length == 0)
+ {
+ // There are no tx so we should act like if all the transactions were filled
+ CheckPrepareResponse();
+ return;
+ }
+
+ Dictionary mempoolVerified = neoSystem.MemPool.GetVerifiedTransactions().ToDictionary(p => p.Hash);
+ var unverified = new List();
+ var mtb = neoSystem.Settings.MaxTraceableBlocks;
+ foreach (UInt256 hash in context.TransactionHashes)
+ {
+ if (mempoolVerified.TryGetValue(hash, out Transaction? tx))
+ {
+ if (NativeContract.Ledger.ContainsConflictHash(context.Snapshot, hash, tx.Signers.Select(s => s.Account), mtb))
+ {
+ Log($"Invalid request: transaction has on-chain conflict", LogLevel.Warning);
+ return;
+ }
+
+ if (!AddTransaction(tx, false))
+ return;
+ }
+ else
+ {
+ if (neoSystem.MemPool.TryGetValue(hash, out tx))
+ {
+ if (NativeContract.Ledger.ContainsConflictHash(context.Snapshot, hash, tx.Signers.Select(s => s.Account), mtb))
+ {
+ Log($"Invalid request: transaction has on-chain conflict", LogLevel.Warning);
+ return;
+ }
+ unverified.Add(tx);
+ }
+ }
+ }
+ foreach (Transaction tx in unverified)
+ if (!AddTransaction(tx, true))
+ return;
+ if (context.Transactions.Count < context.TransactionHashes.Length)
+ {
+ UInt256[] hashes = context.TransactionHashes.Where(i => !context.Transactions.ContainsKey(i)).ToArray();
+ taskManager.Tell(new TaskManager.RestartTasks(InvPayload.Create(InventoryType.TX, hashes)));
+ }
+ }
+
+ private void OnPrepareResponseReceived(ExtensiblePayload payload, PrepareResponse message)
+ {
+ if (message.ViewNumber != context.ViewNumber) return;
+ if (context.PreparationPayloads[message.ValidatorIndex] != null || context.NotAcceptingPayloadsDueToViewChanging) return;
+ if (context.PreparationPayloads[context.Block.PrimaryIndex] != null && !message.PreparationHash.Equals(context.PreparationPayloads[context.Block.PrimaryIndex]!.Hash))
+ return;
+
+ // Timeout extension: prepare response has been received with success
+ // around 2*15/M=30.0/5 ~ 40% block time (for M=5)
+ ExtendTimerByFactor(2);
+
+ Log($"{nameof(OnPrepareResponseReceived)}: height={message.BlockIndex} view={message.ViewNumber} index={message.ValidatorIndex}");
+ context.PreparationPayloads[message.ValidatorIndex] = payload;
+ if (context.WatchOnly || context.CommitSent) return;
+ if (context.RequestSentOrReceived)
+ CheckPreparations();
+ }
+
+ private void OnChangeViewReceived(ExtensiblePayload payload, ChangeView message)
+ {
+ if (message.NewViewNumber <= context.ViewNumber)
+ OnRecoveryRequestReceived(payload, message);
+
+ if (context.CommitSent) return;
+
+ var expectedView = context.GetMessage(context.ChangeViewPayloads[message.ValidatorIndex])?.NewViewNumber ?? 0;
+ if (message.NewViewNumber <= expectedView)
+ return;
+
+ Log($"{nameof(OnChangeViewReceived)}: height={message.BlockIndex} view={message.ViewNumber} index={message.ValidatorIndex} nv={message.NewViewNumber} reason={message.Reason}");
+ context.ChangeViewPayloads[message.ValidatorIndex] = payload;
+ CheckExpectedView(message.NewViewNumber);
+ }
+
+ private void OnCommitReceived(ExtensiblePayload payload, Commit commit)
+ {
+ ref ExtensiblePayload? existingCommitPayload = ref context.CommitPayloads[commit.ValidatorIndex];
+ if (existingCommitPayload != null)
+ {
+ if (existingCommitPayload.Hash != payload.Hash)
+ Log($"Rejected {nameof(Commit)}: height={commit.BlockIndex} index={commit.ValidatorIndex} view={commit.ViewNumber} existingView={context.GetMessage(existingCommitPayload).ViewNumber}", LogLevel.Warning);
+ return;
+ }
+
+ if (commit.ViewNumber == context.ViewNumber)
+ {
+ // Timeout extension: commit has been received with success
+ // around 4*15s/M=60.0s/5=12.0s ~ 80% block time (for M=5)
+ ExtendTimerByFactor(4);
+
+ Log($"{nameof(OnCommitReceived)}: height={commit.BlockIndex} view={commit.ViewNumber} index={commit.ValidatorIndex} nc={context.CountCommitted} nf={context.CountFailed}");
+
+ byte[]? hashData = context.EnsureHeader()?.GetSignData(neoSystem.Settings.Network);
+ if (hashData == null)
+ {
+ existingCommitPayload = payload;
+ }
+ else if (Crypto.VerifySignature(hashData, commit.Signature.Span, context.Validators[commit.ValidatorIndex]))
+ {
+ existingCommitPayload = payload;
+ CheckCommits();
+ }
+ return;
+ }
+ else
+ {
+ // Receiving commit from another view
+ existingCommitPayload = payload;
+ }
+ }
+
+ private void OnRecoveryMessageReceived(RecoveryMessage message)
+ {
+ // isRecovering is always set to false again after OnRecoveryMessageReceived
+ isRecovering = true;
+ int validChangeViews = 0, totalChangeViews = 0, validPrepReq = 0, totalPrepReq = 0;
+ int validPrepResponses = 0, totalPrepResponses = 0, validCommits = 0, totalCommits = 0;
+
+ Log($"{nameof(OnRecoveryMessageReceived)}: height={message.BlockIndex} view={message.ViewNumber} index={message.ValidatorIndex}");
+ try
+ {
+ if (message.ViewNumber > context.ViewNumber)
+ {
+ if (context.CommitSent) return;
+ ExtensiblePayload[] changeViewPayloads = message.GetChangeViewPayloads(context);
+ totalChangeViews = changeViewPayloads.Length;
+ foreach (ExtensiblePayload changeViewPayload in changeViewPayloads)
+ if (ReverifyAndProcessPayload(changeViewPayload)) validChangeViews++;
+ }
+ if (message.ViewNumber == context.ViewNumber && !context.NotAcceptingPayloadsDueToViewChanging && !context.CommitSent)
+ {
+ if (!context.RequestSentOrReceived)
+ {
+ ExtensiblePayload? prepareRequestPayload = message.GetPrepareRequestPayload(context);
+ if (prepareRequestPayload != null)
+ {
+ totalPrepReq = 1;
+ if (ReverifyAndProcessPayload(prepareRequestPayload)) validPrepReq++;
+ }
+ }
+ ExtensiblePayload[] prepareResponsePayloads = message.GetPrepareResponsePayloads(context);
+ totalPrepResponses = prepareResponsePayloads.Length;
+ foreach (ExtensiblePayload prepareResponsePayload in prepareResponsePayloads)
+ if (ReverifyAndProcessPayload(prepareResponsePayload)) validPrepResponses++;
+ }
+ if (message.ViewNumber <= context.ViewNumber)
+ {
+ // Ensure we know about all commits from lower view numbers.
+ ExtensiblePayload[] commitPayloads = message.GetCommitPayloadsFromRecoveryMessage(context);
+ totalCommits = commitPayloads.Length;
+ foreach (ExtensiblePayload commitPayload in commitPayloads)
+ if (ReverifyAndProcessPayload(commitPayload)) validCommits++;
+ }
+ }
+ finally
+ {
+ Log($"Recovery finished: (valid/total) ChgView: {validChangeViews}/{totalChangeViews} PrepReq: {validPrepReq}/{totalPrepReq} PrepResp: {validPrepResponses}/{totalPrepResponses} Commits: {validCommits}/{totalCommits}");
+ isRecovering = false;
+ }
+ }
+
+ private void OnRecoveryRequestReceived(ExtensiblePayload payload, ConsensusMessage message)
+ {
+ // We keep track of the payload hashes received in this block, and don't respond with recovery
+ // in response to the same payload that we already responded to previously.
+ // ChangeView messages include a Timestamp when the change view is sent, thus if a node restarts
+ // and issues a change view for the same view, it will have a different hash and will correctly respond
+ // again; however replay attacks of the ChangeView message from arbitrary nodes will not trigger an
+ // additional recovery message response.
+ if (!knownHashes.Add(payload.Hash)) return;
+
+ Log($"{nameof(OnRecoveryRequestReceived)}: height={message.BlockIndex} index={message.ValidatorIndex} view={message.ViewNumber}");
+ if (context.WatchOnly) return;
+ if (!context.CommitSent)
+ {
+ bool shouldSendRecovery = false;
+ int allowedRecoveryNodeCount = context.F + 1;
+ // Limit recoveries to be sent from an upper limit of `f + 1` nodes
+ for (int i = 1; i <= allowedRecoveryNodeCount; i++)
+ {
+ var chosenIndex = (message.ValidatorIndex + i) % context.Validators.Length;
+ if (chosenIndex != context.MyIndex) continue;
+ shouldSendRecovery = true;
+ break;
+ }
+
+ if (!shouldSendRecovery) return;
+ }
+ localNode.Tell(new LocalNode.SendDirectly(context.MakeRecoveryMessage()));
+ }
+}
diff --git a/plugins/DBFTPlugin/Consensus/ConsensusService.cs b/plugins/DBFTPlugin/Consensus/ConsensusService.cs
new file mode 100644
index 000000000..ef81caac3
--- /dev/null
+++ b/plugins/DBFTPlugin/Consensus/ConsensusService.cs
@@ -0,0 +1,342 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ConsensusService.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 Akka.Actor;
+using Neo.Extensions.IO;
+using Neo.Ledger;
+using Neo.Network.P2P;
+using Neo.Network.P2P.Payloads;
+using Neo.Plugins.DBFTPlugin.Messages;
+using Neo.Plugins.DBFTPlugin.Types;
+using Neo.Sign;
+using static Neo.Ledger.Blockchain;
+
+namespace Neo.Plugins.DBFTPlugin.Consensus;
+
+internal partial class ConsensusService : UntypedActor
+{
+ public class Start { }
+ private class Timer { public uint Height; public byte ViewNumber; }
+
+ private readonly ConsensusContext context;
+ private readonly IActorRef localNode;
+ private readonly IActorRef taskManager;
+ private readonly IActorRef blockchain;
+ private ICancelable? timer_token;
+ private DateTime prepareRequestReceivedTime;
+ private uint prepareRequestReceivedBlockIndex;
+ private uint block_received_index;
+ private bool started = false;
+
+ ///
+ /// This will record the information from last scheduled timer
+ ///
+ private DateTime clock_started = TimeProvider.Current.UtcNow;
+ private TimeSpan expected_delay = TimeSpan.Zero;
+
+ ///
+ /// This will be cleared every block (so it will not grow out of control, but is used to prevent repeatedly
+ /// responding to the same message.
+ ///
+ private readonly HashSet knownHashes = new();
+ ///
+ /// This variable is only true during OnRecoveryMessageReceived
+ ///
+ private bool isRecovering = false;
+ private readonly DbftSettings dbftSettings;
+ private readonly NeoSystem neoSystem;
+
+ public ConsensusService(NeoSystem neoSystem, DbftSettings settings, ISigner signer)
+ : this(neoSystem, settings, new ConsensusContext(neoSystem, settings, signer)) { }
+
+ internal ConsensusService(NeoSystem neoSystem, DbftSettings settings, ConsensusContext context)
+ {
+ this.neoSystem = neoSystem;
+ localNode = neoSystem.LocalNode;
+ taskManager = neoSystem.TaskManager;
+ blockchain = neoSystem.Blockchain;
+ dbftSettings = settings;
+ this.context = context;
+ Context.System.EventStream.Subscribe(Self, typeof(PersistCompleted));
+ Context.System.EventStream.Subscribe(Self, typeof(RelayResult));
+ }
+
+ private void OnPersistCompleted(Block block)
+ {
+ Log($"Persisted {nameof(Block)}: height={block.Index} hash={block.Hash} tx={block.Transactions.Length} nonce={block.Nonce}");
+ knownHashes.Clear();
+ InitializeConsensus(0);
+ }
+
+ private void InitializeConsensus(byte viewNumber)
+ {
+ context.Reset(viewNumber);
+ if (viewNumber > 0)
+ Log($"View changed: view={viewNumber} primary={context.Validators[context.GetPrimaryIndex((byte)(viewNumber - 1u))]}", LogLevel.Warning);
+ Log($"Initialize: height={context.Block.Index} view={viewNumber} index={context.MyIndex} role={(context.IsPrimary ? "Primary" : context.WatchOnly ? "WatchOnly" : "Backup")}");
+ if (context.WatchOnly) return;
+ if (context.IsPrimary)
+ {
+ if (isRecovering)
+ {
+ ChangeTimer(TimeSpan.FromMilliseconds((int)context.TimePerBlock.TotalMilliseconds << (viewNumber + 1)));
+ }
+ else
+ {
+ TimeSpan span = context.TimePerBlock;
+ if (block_received_index + 1 == context.Block.Index && prepareRequestReceivedBlockIndex + 1 == context.Block.Index)
+ {
+ // Include consensus time into the block acceptance interval.
+ var diff = TimeProvider.Current.UtcNow - prepareRequestReceivedTime;
+ if (diff >= span)
+ span = TimeSpan.Zero;
+ else
+ span -= diff;
+ }
+ ChangeTimer(span);
+ }
+ }
+ else
+ {
+ ChangeTimer(TimeSpan.FromMilliseconds((int)context.TimePerBlock.TotalMilliseconds << (viewNumber + 1)));
+ }
+ }
+
+ protected override void OnReceive(object message)
+ {
+ if (message is Start)
+ {
+ if (started) return;
+ OnStart();
+ }
+ else
+ {
+ if (!started) return;
+ switch (message)
+ {
+ case Timer timer:
+ OnTimer(timer);
+ break;
+ case Transaction transaction:
+ OnTransaction(transaction);
+ break;
+ case PersistCompleted completed:
+ OnPersistCompleted(completed.Block);
+ break;
+ case RelayResult rr:
+ if (rr.Result == VerifyResult.Succeed && rr.Inventory is ExtensiblePayload payload && payload.Category == "dBFT")
+ OnConsensusPayload(payload);
+ break;
+ }
+ }
+ }
+
+ private void OnStart()
+ {
+ Log("OnStart");
+ started = true;
+ if (!dbftSettings.IgnoreRecoveryLogs && context.Load())
+ {
+ if (context.Transactions != null)
+ {
+ blockchain.Ask(new FillMemoryPool(context.Transactions.Values)).Wait();
+ }
+ if (context.CommitSent)
+ {
+ CheckPreparations();
+ return;
+ }
+ }
+ InitializeConsensus(context.ViewNumber);
+ // Issue a recovery request on start-up in order to possibly catch up with other nodes
+ if (!context.WatchOnly)
+ RequestRecovery();
+ }
+
+ private void OnTimer(Timer timer)
+ {
+ if (context.WatchOnly || context.BlockSent) return;
+ if (timer.Height != context.Block.Index || timer.ViewNumber != context.ViewNumber) return;
+ if (context.IsPrimary && !context.RequestSentOrReceived)
+ {
+ SendPrepareRequest();
+ }
+ else if ((context.IsPrimary && context.RequestSentOrReceived) || context.IsBackup)
+ {
+ if (context.CommitSent)
+ {
+ // Re-send commit periodically by sending recover message in case of a network issue.
+ Log($"Sending {nameof(RecoveryMessage)} to resend {nameof(Commit)}");
+ localNode.Tell(new LocalNode.SendDirectly(context.MakeRecoveryMessage()));
+ ChangeTimer(TimeSpan.FromMilliseconds((int)context.TimePerBlock.TotalMilliseconds << 1));
+ }
+ else
+ {
+ var reason = ChangeViewReason.Timeout;
+
+ if (context.Block != null && context.TransactionHashes?.Length > context.Transactions?.Count)
+ {
+ reason = ChangeViewReason.TxNotFound;
+ }
+
+ RequestChangeView(reason);
+ }
+ }
+ }
+
+ private void SendPrepareRequest()
+ {
+ Log($"Sending {nameof(PrepareRequest)}: height={context.Block.Index} view={context.ViewNumber}");
+ localNode.Tell(new LocalNode.SendDirectly(context.MakePrepareRequest()));
+
+ if (context.Validators.Length == 1)
+ CheckPreparations();
+
+ if (context.TransactionHashes!.Length > 0)
+ {
+ foreach (InvPayload payload in InvPayload.CreateGroup(InventoryType.TX, context.TransactionHashes))
+ localNode.Tell(Message.Create(MessageCommand.Inv, payload));
+ }
+ ChangeTimer(TimeSpan.FromMilliseconds(((int)context.TimePerBlock.TotalMilliseconds << (context.ViewNumber + 1)) - (context.ViewNumber == 0 ? context.TimePerBlock.TotalMilliseconds : 0)));
+ }
+
+ private void RequestRecovery()
+ {
+ Log($"Sending {nameof(RecoveryRequest)}: height={context.Block.Index} view={context.ViewNumber} nc={context.CountCommitted} nf={context.CountFailed}");
+ localNode.Tell(new LocalNode.SendDirectly(context.MakeRecoveryRequest()));
+ }
+
+ private void RequestChangeView(ChangeViewReason reason)
+ {
+ if (context.WatchOnly) return;
+ // Request for next view is always one view more than the current context.ViewNumber
+ // Nodes will not contribute for changing to a view higher than (context.ViewNumber+1), unless they are recovered
+ // The latter may happen by nodes in higher views with, at least, `M` proofs
+ byte expectedView = context.ViewNumber;
+ expectedView++;
+ ChangeTimer(TimeSpan.FromMilliseconds((int)context.TimePerBlock.TotalMilliseconds << (expectedView + 1)));
+ if ((context.CountCommitted + context.CountFailed) > context.F)
+ {
+ RequestRecovery();
+ }
+ else
+ {
+ Log($"Sending {nameof(ChangeView)}: height={context.Block.Index} view={context.ViewNumber} nv={expectedView} nc={context.CountCommitted} nf={context.CountFailed} reason={reason}");
+ localNode.Tell(new LocalNode.SendDirectly(context.MakeChangeView(reason)));
+ CheckExpectedView(expectedView);
+ }
+ }
+
+ private bool ReverifyAndProcessPayload(ExtensiblePayload payload)
+ {
+ RelayResult relayResult = blockchain.Ask(new Reverify(new IInventory[] { payload })).Result;
+ if (relayResult.Result != VerifyResult.Succeed) return false;
+ OnConsensusPayload(payload);
+ return true;
+ }
+
+ private void OnTransaction(Transaction transaction)
+ {
+ if (!context.IsBackup || context.NotAcceptingPayloadsDueToViewChanging || !context.RequestSentOrReceived || context.ResponseSent || context.BlockSent)
+ return;
+ if (context.Transactions!.ContainsKey(transaction.Hash)) return;
+ if (!context.TransactionHashes.Contains(transaction.Hash)) return;
+ AddTransaction(transaction, true);
+ }
+
+ private bool AddTransaction(Transaction tx, bool verify)
+ {
+ if (verify)
+ {
+ // At this step we're sure that there's no on-chain transaction that conflicts with
+ // the provided tx because of the previous Blockchain's OnReceive check. Thus, we only
+ // need to check that current context doesn't contain conflicting transactions.
+ VerifyResult result;
+
+ // Firstly, check whether tx has Conlicts attribute with the hash of one of the context's transactions.
+ foreach (var h in tx.GetAttributes().Select(attr => attr.Hash))
+ {
+ if (context.TransactionHashes.Contains(h))
+ {
+ result = VerifyResult.HasConflicts;
+ Log($"Rejected tx: {tx.Hash}, {result}{Environment.NewLine}{tx.ToArray().ToHexString()}", LogLevel.Warning);
+ RequestChangeView(ChangeViewReason.TxInvalid);
+ return false;
+ }
+ }
+ // After that, check whether context's transactions have Conflicts attribute with tx's hash.
+ foreach (var pooledTx in context.Transactions!.Values)
+ {
+ if (pooledTx.GetAttributes().Select(attr => attr.Hash).Contains(tx.Hash))
+ {
+ result = VerifyResult.HasConflicts;
+ Log($"Rejected tx: {tx.Hash}, {result}{Environment.NewLine}{tx.ToArray().ToHexString()}", LogLevel.Warning);
+ RequestChangeView(ChangeViewReason.TxInvalid);
+ return false;
+ }
+ }
+
+ // We've ensured that there's no conlicting transactions in the context, thus, can safely provide an empty conflicting list
+ // for futher verification.
+ var conflictingTxs = new List();
+ result = tx.Verify(neoSystem.Settings, context.Snapshot, context.VerificationContext, conflictingTxs);
+ if (result != VerifyResult.Succeed)
+ {
+ Log($"Rejected tx: {tx.Hash}, {result}{Environment.NewLine}{tx.ToArray().ToHexString()}", LogLevel.Warning);
+ RequestChangeView(result == VerifyResult.PolicyFail ? ChangeViewReason.TxRejectedByPolicy : ChangeViewReason.TxInvalid);
+ return false;
+ }
+ }
+ context.Transactions![tx.Hash] = tx;
+ context.VerificationContext.AddTransaction(tx);
+ return CheckPrepareResponse();
+ }
+
+ private void ChangeTimer(TimeSpan delay)
+ {
+ clock_started = TimeProvider.Current.UtcNow;
+ expected_delay = delay;
+ timer_token.CancelIfNotNull();
+ timer_token = Context.System.Scheduler.ScheduleTellOnceCancelable(delay, Self, new Timer
+ {
+ Height = context.Block.Index,
+ ViewNumber = context.ViewNumber
+ }, ActorRefs.NoSender);
+ }
+
+ // this function increases existing timer (never decreases) with a value proportional to `maxDelayInBlockTimes`*`Blockchain.MillisecondsPerBlock`
+ private void ExtendTimerByFactor(int maxDelayInBlockTimes)
+ {
+ TimeSpan nextDelay = expected_delay - (TimeProvider.Current.UtcNow - clock_started)
+ + TimeSpan.FromMilliseconds(maxDelayInBlockTimes * context.TimePerBlock.TotalMilliseconds / (double)context.M);
+ if (!context.WatchOnly && !context.ViewChanging && !context.CommitSent && (nextDelay > TimeSpan.Zero))
+ ChangeTimer(nextDelay);
+ }
+
+ protected override void PostStop()
+ {
+ Log("OnStop");
+ started = false;
+ Context.System.EventStream.Unsubscribe(Self);
+ context.Dispose();
+ base.PostStop();
+ }
+
+ public static Props Props(NeoSystem neoSystem, DbftSettings dbftSettings, ISigner signer)
+ {
+ return Akka.Actor.Props.Create(() => new ConsensusService(neoSystem, dbftSettings, signer));
+ }
+
+ private static void Log(string message, LogLevel level = LogLevel.Info)
+ {
+ Utility.Log(nameof(ConsensusService), level, message);
+ }
+}
diff --git a/plugins/DBFTPlugin/DBFTPlugin.cs b/plugins/DBFTPlugin/DBFTPlugin.cs
new file mode 100644
index 000000000..f0a2db4bf
--- /dev/null
+++ b/plugins/DBFTPlugin/DBFTPlugin.cs
@@ -0,0 +1,124 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// DBFTPlugin.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 Akka.Actor;
+using Neo.ConsoleService;
+using Neo.IEventHandlers;
+using Neo.Network.P2P;
+using Neo.Network.P2P.Payloads;
+using Neo.Plugins.DBFTPlugin.Consensus;
+using Neo.Sign;
+using Neo.Wallets;
+
+namespace Neo.Plugins.DBFTPlugin;
+
+public sealed class DBFTPlugin : Plugin, IMessageReceivedHandler
+{
+ private IWalletProvider? walletProvider;
+ private IActorRef consensus = null!;
+ private bool started = false;
+ private NeoSystem neoSystem = null!;
+ private DbftSettings settings = null!;
+
+ public override string Description => "Consensus plugin with dBFT algorithm.";
+
+ public override string ConfigFile => System.IO.Path.Combine(RootPath, "DBFTPlugin.json");
+
+ protected override UnhandledExceptionPolicy ExceptionPolicy => settings.ExceptionPolicy;
+
+ public DBFTPlugin()
+ {
+ RemoteNode.MessageReceived += ((IMessageReceivedHandler)this).RemoteNode_MessageReceived_Handler;
+ }
+
+ public DBFTPlugin(DbftSettings settings) : this()
+ {
+ this.settings = settings;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ RemoteNode.MessageReceived -= ((IMessageReceivedHandler)this).RemoteNode_MessageReceived_Handler;
+ base.Dispose(disposing);
+ }
+
+ protected override void Configure()
+ {
+ settings ??= new DbftSettings(GetConfiguration());
+ }
+
+ protected override void OnSystemLoaded(NeoSystem system)
+ {
+ if (system.Settings.Network != settings.Network) return;
+ neoSystem = system;
+ neoSystem.ServiceAdded += NeoSystem_ServiceAdded_Handler;
+ }
+
+ void NeoSystem_ServiceAdded_Handler(object? sender, object service)
+ {
+ if (service is not IWalletProvider provider) return;
+ walletProvider = provider;
+ neoSystem.ServiceAdded -= NeoSystem_ServiceAdded_Handler;
+ if (settings.AutoStart)
+ {
+ walletProvider.WalletChanged += IWalletProvider_WalletChanged_Handler;
+ }
+ }
+
+ void IWalletProvider_WalletChanged_Handler(object? sender, Wallet? wallet)
+ {
+ if (wallet != null)
+ {
+ walletProvider!.WalletChanged -= IWalletProvider_WalletChanged_Handler;
+ Start(wallet);
+ }
+ }
+
+ ///
+ /// Starts the consensus service.
+ /// If the signer name is provided, it will start with the specified signer.
+ /// Otherwise, it will start with the WalletProvider's wallet.
+ ///
+ /// The name of the signer to use.
+ [ConsoleCommand("start consensus", Category = "Consensus", Description = "Start consensus service (dBFT)")]
+ private void OnStart(string signerName = "")
+ {
+ var signer = SignerManager.GetSignerOrDefault(signerName)
+ ?? walletProvider?.GetWallet();
+ if (signer == null)
+ {
+ ConsoleHelper.Warning("Please open wallet first!");
+ return;
+ }
+ Start(signer);
+ }
+
+ public void Start(ISigner signer)
+ {
+ if (started) return;
+ started = true;
+ consensus = neoSystem.ActorSystem.ActorOf(ConsensusService.Props(neoSystem, settings, signer));
+ consensus.Tell(new ConsensusService.Start());
+ }
+
+ bool IMessageReceivedHandler.RemoteNode_MessageReceived_Handler(NeoSystem system, Message message)
+ {
+ if (message.Command == MessageCommand.Transaction)
+ {
+ Transaction tx = (Transaction)message.Payload!;
+ if (tx.SystemFee > settings.MaxBlockSystemFee)
+ return false;
+ consensus?.Tell(tx);
+ }
+ return true;
+ }
+}
diff --git a/plugins/DBFTPlugin/DBFTPlugin.csproj b/plugins/DBFTPlugin/DBFTPlugin.csproj
new file mode 100644
index 000000000..cfda61ec5
--- /dev/null
+++ b/plugins/DBFTPlugin/DBFTPlugin.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Neo.Consensus.DBFT
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
diff --git a/plugins/DBFTPlugin/DBFTPlugin.json b/plugins/DBFTPlugin/DBFTPlugin.json
new file mode 100644
index 000000000..705b2b77c
--- /dev/null
+++ b/plugins/DBFTPlugin/DBFTPlugin.json
@@ -0,0 +1,11 @@
+{
+ "PluginConfiguration": {
+ "RecoveryLogs": "ConsensusState",
+ "IgnoreRecoveryLogs": false,
+ "AutoStart": false,
+ "Network": 860833102,
+ "MaxBlockSize": 2097152,
+ "MaxBlockSystemFee": 150000000000,
+ "UnhandledExceptionPolicy": "StopNode"
+ }
+}
diff --git a/plugins/DBFTPlugin/DbftSettings.cs b/plugins/DBFTPlugin/DbftSettings.cs
new file mode 100644
index 000000000..88fc24b6a
--- /dev/null
+++ b/plugins/DBFTPlugin/DbftSettings.cs
@@ -0,0 +1,47 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// DbftSettings.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 Microsoft.Extensions.Configuration;
+
+namespace Neo.Plugins.DBFTPlugin;
+
+public class DbftSettings : IPluginSettings
+{
+ public string RecoveryLogs { get; }
+ public bool IgnoreRecoveryLogs { get; }
+ public bool AutoStart { get; }
+ public uint Network { get; }
+ public uint MaxBlockSize { get; }
+ public long MaxBlockSystemFee { get; }
+
+ public UnhandledExceptionPolicy ExceptionPolicy { get; }
+
+ public DbftSettings()
+ {
+ RecoveryLogs = "ConsensusState";
+ IgnoreRecoveryLogs = false;
+ AutoStart = false;
+ Network = 5195086u;
+ MaxBlockSystemFee = 150000000000L;
+ ExceptionPolicy = UnhandledExceptionPolicy.StopNode;
+ }
+
+ public DbftSettings(IConfigurationSection section)
+ {
+ RecoveryLogs = section.GetValue("RecoveryLogs", "ConsensusState");
+ IgnoreRecoveryLogs = section.GetValue("IgnoreRecoveryLogs", false);
+ AutoStart = section.GetValue("AutoStart", false);
+ Network = section.GetValue("Network", 5195086u);
+ MaxBlockSize = section.GetValue("MaxBlockSize", 262144u);
+ MaxBlockSystemFee = section.GetValue("MaxBlockSystemFee", 150000000000L);
+ ExceptionPolicy = section.GetValue("UnhandledExceptionPolicy", UnhandledExceptionPolicy.StopNode);
+ }
+}
diff --git a/plugins/DBFTPlugin/Messages/ChangeView.cs b/plugins/DBFTPlugin/Messages/ChangeView.cs
new file mode 100644
index 000000000..af2696c84
--- /dev/null
+++ b/plugins/DBFTPlugin/Messages/ChangeView.cs
@@ -0,0 +1,55 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ChangeView.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 Neo.IO;
+using Neo.Plugins.DBFTPlugin.Types;
+
+namespace Neo.Plugins.DBFTPlugin.Messages;
+
+public class ChangeView : ConsensusMessage
+{
+ ///
+ /// NewViewNumber is always set to the current ViewNumber asking changeview + 1
+ ///
+ public byte NewViewNumber => (byte)(ViewNumber + 1);
+
+ ///
+ /// Timestamp of when the ChangeView message was created. This allows receiving nodes to ensure
+ /// they only respond once to a specific ChangeView request (it thus prevents replay of the ChangeView
+ /// message from repeatedly broadcasting RecoveryMessages).
+ ///
+ public ulong Timestamp;
+
+ ///
+ /// Reason
+ ///
+ public ChangeViewReason Reason;
+
+ public override int Size => base.Size +
+ sizeof(ulong) + // Timestamp
+ sizeof(ChangeViewReason); // Reason
+
+ public ChangeView() : base(ConsensusMessageType.ChangeView) { }
+
+ public override void Deserialize(ref MemoryReader reader)
+ {
+ base.Deserialize(ref reader);
+ Timestamp = reader.ReadUInt64();
+ Reason = (ChangeViewReason)reader.ReadByte();
+ }
+
+ public override void Serialize(BinaryWriter writer)
+ {
+ base.Serialize(writer);
+ writer.Write(Timestamp);
+ writer.Write((byte)Reason);
+ }
+}
diff --git a/plugins/DBFTPlugin/Messages/Commit.cs b/plugins/DBFTPlugin/Messages/Commit.cs
new file mode 100644
index 000000000..75a75b120
--- /dev/null
+++ b/plugins/DBFTPlugin/Messages/Commit.cs
@@ -0,0 +1,36 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Commit.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 Neo.IO;
+using Neo.Plugins.DBFTPlugin.Types;
+
+namespace Neo.Plugins.DBFTPlugin.Messages;
+
+public class Commit : ConsensusMessage
+{
+ public ReadOnlyMemory Signature;
+
+ public override int Size => base.Size + Signature.Length;
+
+ public Commit() : base(ConsensusMessageType.Commit) { }
+
+ public override void Deserialize(ref MemoryReader reader)
+ {
+ base.Deserialize(ref reader);
+ Signature = reader.ReadMemory(64);
+ }
+
+ public override void Serialize(BinaryWriter writer)
+ {
+ base.Serialize(writer);
+ writer.Write(Signature.Span);
+ }
+}
diff --git a/plugins/DBFTPlugin/Messages/ConsensusMessage.cs b/plugins/DBFTPlugin/Messages/ConsensusMessage.cs
new file mode 100644
index 000000000..fa35dee15
--- /dev/null
+++ b/plugins/DBFTPlugin/Messages/ConsensusMessage.cs
@@ -0,0 +1,69 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ConsensusMessage.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 Neo.Extensions;
+using Neo.IO;
+using Neo.Plugins.DBFTPlugin.Types;
+
+namespace Neo.Plugins.DBFTPlugin.Messages;
+
+public abstract class ConsensusMessage : ISerializable
+{
+ public readonly ConsensusMessageType Type;
+ public uint BlockIndex;
+ public byte ValidatorIndex;
+ public byte ViewNumber;
+
+ public virtual int Size =>
+ sizeof(ConsensusMessageType) + //Type
+ sizeof(uint) + //BlockIndex
+ sizeof(byte) + //ValidatorIndex
+ sizeof(byte); //ViewNumber
+
+ protected ConsensusMessage(ConsensusMessageType type)
+ {
+ if (!Enum.IsDefined(type))
+ throw new ArgumentOutOfRangeException(nameof(type));
+ Type = type;
+ }
+
+ public virtual void Deserialize(ref MemoryReader reader)
+ {
+ var type = reader.ReadByte();
+ if (Type != (ConsensusMessageType)type)
+ throw new FormatException($"Invalid consensus message type: {type}");
+ BlockIndex = reader.ReadUInt32();
+ ValidatorIndex = reader.ReadByte();
+ ViewNumber = reader.ReadByte();
+ }
+
+ public static ConsensusMessage DeserializeFrom(ReadOnlyMemory data)
+ {
+ ConsensusMessageType type = (ConsensusMessageType)data.Span[0];
+ Type t = typeof(ConsensusMessage);
+ t = t.Assembly.GetType($"{t.Namespace}.{type}", false)!;
+ if (t is null) throw new FormatException($"Invalid consensus message type: {type}");
+ return (ConsensusMessage)data.AsSerializable(t);
+ }
+
+ public virtual bool Verify(ProtocolSettings protocolSettings)
+ {
+ return ValidatorIndex < protocolSettings.ValidatorsCount;
+ }
+
+ public virtual void Serialize(BinaryWriter writer)
+ {
+ writer.Write((byte)Type);
+ writer.Write(BlockIndex);
+ writer.Write(ValidatorIndex);
+ writer.Write(ViewNumber);
+ }
+}
diff --git a/plugins/DBFTPlugin/Messages/PrepareRequest.cs b/plugins/DBFTPlugin/Messages/PrepareRequest.cs
new file mode 100644
index 000000000..5691622e2
--- /dev/null
+++ b/plugins/DBFTPlugin/Messages/PrepareRequest.cs
@@ -0,0 +1,64 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// PrepareRequest.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 Neo.Extensions;
+using Neo.Extensions.Collections;
+using Neo.Extensions.IO;
+using Neo.IO;
+using Neo.Plugins.DBFTPlugin.Types;
+
+namespace Neo.Plugins.DBFTPlugin.Messages;
+
+public class PrepareRequest : ConsensusMessage
+{
+ public uint Version;
+ public required UInt256 PrevHash;
+ public ulong Timestamp;
+ public ulong Nonce;
+ public required UInt256[] TransactionHashes;
+
+ public override int Size => base.Size
+ + sizeof(uint) //Version
+ + UInt256.Length //PrevHash
+ + sizeof(ulong) //Timestamp
+ + sizeof(ulong) // Nonce
+ + TransactionHashes.GetVarSize(); //TransactionHashes
+
+ public PrepareRequest() : base(ConsensusMessageType.PrepareRequest) { }
+
+ public override void Deserialize(ref MemoryReader reader)
+ {
+ base.Deserialize(ref reader);
+ Version = reader.ReadUInt32();
+ PrevHash = reader.ReadSerializable();
+ Timestamp = reader.ReadUInt64();
+ Nonce = reader.ReadUInt64();
+ TransactionHashes = reader.ReadSerializableArray(ushort.MaxValue);
+ if (TransactionHashes.Distinct().Count() != TransactionHashes.Length)
+ throw new FormatException($"Transaction hashes are duplicate");
+ }
+
+ public override bool Verify(ProtocolSettings protocolSettings)
+ {
+ if (!base.Verify(protocolSettings)) return false;
+ return TransactionHashes.Length <= protocolSettings.MaxTransactionsPerBlock;
+ }
+
+ public override void Serialize(BinaryWriter writer)
+ {
+ base.Serialize(writer);
+ writer.Write(Version);
+ writer.Write(PrevHash);
+ writer.Write(Timestamp);
+ writer.Write(Nonce);
+ writer.Write(TransactionHashes);
+ }
+}
diff --git a/plugins/DBFTPlugin/Messages/PrepareResponse.cs b/plugins/DBFTPlugin/Messages/PrepareResponse.cs
new file mode 100644
index 000000000..55b0f3cfc
--- /dev/null
+++ b/plugins/DBFTPlugin/Messages/PrepareResponse.cs
@@ -0,0 +1,37 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// PrepareResponse.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 Neo.Extensions.IO;
+using Neo.IO;
+using Neo.Plugins.DBFTPlugin.Types;
+
+namespace Neo.Plugins.DBFTPlugin.Messages;
+
+public class PrepareResponse : ConsensusMessage
+{
+ public required UInt256 PreparationHash;
+
+ public override int Size => base.Size + PreparationHash.Size;
+
+ public PrepareResponse() : base(ConsensusMessageType.PrepareResponse) { }
+
+ public override void Deserialize(ref MemoryReader reader)
+ {
+ base.Deserialize(ref reader);
+ PreparationHash = reader.ReadSerializable();
+ }
+
+ public override void Serialize(BinaryWriter writer)
+ {
+ base.Serialize(writer);
+ writer.Write(PreparationHash);
+ }
+}
diff --git a/plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.ChangeViewPayloadCompact.cs b/plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.ChangeViewPayloadCompact.cs
new file mode 100644
index 000000000..117afad3a
--- /dev/null
+++ b/plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.ChangeViewPayloadCompact.cs
@@ -0,0 +1,49 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// RecoveryMessage.ChangeViewPayloadCompact.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 Neo.Extensions;
+using Neo.Extensions.IO;
+using Neo.IO;
+
+namespace Neo.Plugins.DBFTPlugin.Messages;
+
+partial class RecoveryMessage
+{
+ public class ChangeViewPayloadCompact : ISerializable
+ {
+ public byte ValidatorIndex;
+ public byte OriginalViewNumber;
+ public ulong Timestamp;
+ public ReadOnlyMemory InvocationScript;
+
+ int ISerializable.Size =>
+ sizeof(byte) + //ValidatorIndex
+ sizeof(byte) + //OriginalViewNumber
+ sizeof(ulong) + //Timestamp
+ InvocationScript.GetVarSize(); //InvocationScript
+
+ void ISerializable.Deserialize(ref MemoryReader reader)
+ {
+ ValidatorIndex = reader.ReadByte();
+ OriginalViewNumber = reader.ReadByte();
+ Timestamp = reader.ReadUInt64();
+ InvocationScript = reader.ReadVarMemory(1024);
+ }
+
+ void ISerializable.Serialize(BinaryWriter writer)
+ {
+ writer.Write(ValidatorIndex);
+ writer.Write(OriginalViewNumber);
+ writer.Write(Timestamp);
+ writer.WriteVarBytes(InvocationScript.Span);
+ }
+ }
+}
diff --git a/plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.CommitPayloadCompact.cs b/plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.CommitPayloadCompact.cs
new file mode 100644
index 000000000..feacbb625
--- /dev/null
+++ b/plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.CommitPayloadCompact.cs
@@ -0,0 +1,49 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// RecoveryMessage.CommitPayloadCompact.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 Neo.Extensions;
+using Neo.Extensions.IO;
+using Neo.IO;
+
+namespace Neo.Plugins.DBFTPlugin.Messages;
+
+partial class RecoveryMessage
+{
+ public class CommitPayloadCompact : ISerializable
+ {
+ public byte ViewNumber;
+ public byte ValidatorIndex;
+ public ReadOnlyMemory Signature;
+ public ReadOnlyMemory InvocationScript;
+
+ int ISerializable.Size =>
+ sizeof(byte) + //ViewNumber
+ sizeof(byte) + //ValidatorIndex
+ Signature.Length + //Signature
+ InvocationScript.GetVarSize(); //InvocationScript
+
+ void ISerializable.Deserialize(ref MemoryReader reader)
+ {
+ ViewNumber = reader.ReadByte();
+ ValidatorIndex = reader.ReadByte();
+ Signature = reader.ReadMemory(64);
+ InvocationScript = reader.ReadVarMemory(1024);
+ }
+
+ void ISerializable.Serialize(BinaryWriter writer)
+ {
+ writer.Write(ViewNumber);
+ writer.Write(ValidatorIndex);
+ writer.Write(Signature.Span);
+ writer.WriteVarBytes(InvocationScript.Span);
+ }
+ }
+}
diff --git a/plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.PreparationPayloadCompact.cs b/plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.PreparationPayloadCompact.cs
new file mode 100644
index 000000000..6d131580b
--- /dev/null
+++ b/plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.PreparationPayloadCompact.cs
@@ -0,0 +1,41 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// RecoveryMessage.PreparationPayloadCompact.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 Neo.Extensions;
+using Neo.Extensions.IO;
+using Neo.IO;
+
+namespace Neo.Plugins.DBFTPlugin.Messages;
+
+partial class RecoveryMessage
+{
+ public class PreparationPayloadCompact : ISerializable
+ {
+ public byte ValidatorIndex;
+ public ReadOnlyMemory InvocationScript;
+
+ int ISerializable.Size =>
+ sizeof(byte) + //ValidatorIndex
+ InvocationScript.GetVarSize(); //InvocationScript
+
+ void ISerializable.Deserialize(ref MemoryReader reader)
+ {
+ ValidatorIndex = reader.ReadByte();
+ InvocationScript = reader.ReadVarMemory(1024);
+ }
+
+ void ISerializable.Serialize(BinaryWriter writer)
+ {
+ writer.Write(ValidatorIndex);
+ writer.WriteVarBytes(InvocationScript.Span);
+ }
+ }
+}
diff --git a/plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.cs b/plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.cs
new file mode 100644
index 000000000..bc1bb1df9
--- /dev/null
+++ b/plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.cs
@@ -0,0 +1,133 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// RecoveryMessage.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 Neo.Extensions;
+using Neo.Extensions.Collections;
+using Neo.Extensions.IO;
+using Neo.IO;
+using Neo.Network.P2P.Payloads;
+using Neo.Plugins.DBFTPlugin.Consensus;
+using Neo.Plugins.DBFTPlugin.Types;
+
+namespace Neo.Plugins.DBFTPlugin.Messages;
+
+public partial class RecoveryMessage : ConsensusMessage
+{
+ public required Dictionary ChangeViewMessages;
+ public PrepareRequest? PrepareRequestMessage;
+ /// The PreparationHash in case the PrepareRequest hasn't been received yet.
+ /// This can be null if the PrepareRequest information is present, since it can be derived in that case.
+ public UInt256? PreparationHash;
+ public required Dictionary PreparationMessages;
+ public required Dictionary CommitMessages;
+
+ public override int Size => base.Size
+ + (ChangeViewMessages?.Values.GetVarSize() ?? 0) // ChangeViewMessages
+ + (1 + (PrepareRequestMessage?.Size ?? 0)) // PrepareRequestMessage
+ + (PreparationHash?.Size ?? 0) // PreparationHash
+ + (PreparationMessages?.Values.GetVarSize() ?? 0) // PreparationMessages
+ + (CommitMessages?.Values.GetVarSize() ?? 0); // CommitMessages
+
+ public RecoveryMessage() : base(ConsensusMessageType.RecoveryMessage) { }
+
+ public override void Deserialize(ref MemoryReader reader)
+ {
+ base.Deserialize(ref reader);
+ ChangeViewMessages = reader.ReadSerializableArray(byte.MaxValue).ToDictionary(p => p.ValidatorIndex);
+ if (reader.ReadBoolean())
+ {
+ PrepareRequestMessage = reader.ReadSerializable();
+ }
+ else
+ {
+ int preparationHashSize = UInt256.Zero.Size;
+ if (preparationHashSize == (int)reader.ReadVarInt((ulong)preparationHashSize))
+ PreparationHash = new UInt256(reader.ReadMemory(preparationHashSize).Span);
+ }
+
+ PreparationMessages = reader.ReadSerializableArray(byte.MaxValue).ToDictionary(p => p.ValidatorIndex);
+ CommitMessages = reader.ReadSerializableArray(byte.MaxValue).ToDictionary(p => p.ValidatorIndex);
+ }
+
+ public override bool Verify(ProtocolSettings protocolSettings)
+ {
+ if (!base.Verify(protocolSettings)) return false;
+ return (PrepareRequestMessage is null || PrepareRequestMessage.Verify(protocolSettings))
+ && ChangeViewMessages.Values.All(p => p.ValidatorIndex < protocolSettings.ValidatorsCount)
+ && PreparationMessages.Values.All(p => p.ValidatorIndex < protocolSettings.ValidatorsCount)
+ && CommitMessages.Values.All(p => p.ValidatorIndex < protocolSettings.ValidatorsCount);
+ }
+
+ internal ExtensiblePayload[] GetChangeViewPayloads(ConsensusContext context)
+ {
+ return ChangeViewMessages.Values.Select(p => context.CreatePayload(new ChangeView
+ {
+ BlockIndex = BlockIndex,
+ ValidatorIndex = p.ValidatorIndex,
+ ViewNumber = p.OriginalViewNumber,
+ Timestamp = p.Timestamp
+ }, p.InvocationScript)).ToArray();
+ }
+
+ internal ExtensiblePayload[] GetCommitPayloadsFromRecoveryMessage(ConsensusContext context)
+ {
+ return CommitMessages.Values.Select(p => context.CreatePayload(new Commit
+ {
+ BlockIndex = BlockIndex,
+ ValidatorIndex = p.ValidatorIndex,
+ ViewNumber = p.ViewNumber,
+ Signature = p.Signature
+ }, p.InvocationScript)).ToArray();
+ }
+
+ internal ExtensiblePayload? GetPrepareRequestPayload(ConsensusContext context)
+ {
+ if (PrepareRequestMessage == null) return null;
+ if (!PreparationMessages.TryGetValue(context.Block.PrimaryIndex, out PreparationPayloadCompact? compact))
+ return null;
+ return context.CreatePayload(PrepareRequestMessage, compact.InvocationScript);
+ }
+
+ internal ExtensiblePayload[] GetPrepareResponsePayloads(ConsensusContext context)
+ {
+ UInt256? preparationHash = PreparationHash ?? context.PreparationPayloads[context.Block.PrimaryIndex]?.Hash;
+ if (preparationHash is null) return Array.Empty();
+ return PreparationMessages.Values.Where(p => p.ValidatorIndex != context.Block.PrimaryIndex).Select(p => context.CreatePayload(new PrepareResponse
+ {
+ BlockIndex = BlockIndex,
+ ValidatorIndex = p.ValidatorIndex,
+ ViewNumber = ViewNumber,
+ PreparationHash = preparationHash
+ }, p.InvocationScript)).ToArray();
+ }
+
+ public override void Serialize(BinaryWriter writer)
+ {
+ base.Serialize(writer);
+ writer.Write(ChangeViewMessages.Values.ToArray());
+ if (PrepareRequestMessage != null)
+ {
+ writer.Write(true);
+ writer.Write(PrepareRequestMessage);
+ }
+ else
+ {
+ writer.Write(false);
+ if (PreparationHash == null)
+ writer.WriteVarInt(0);
+ else
+ writer.WriteVarBytes(PreparationHash.ToArray());
+ }
+
+ writer.Write(PreparationMessages.Values.ToArray());
+ writer.Write(CommitMessages.Values.ToArray());
+ }
+}
diff --git a/plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryRequest.cs b/plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryRequest.cs
new file mode 100644
index 000000000..324eca0c4
--- /dev/null
+++ b/plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryRequest.cs
@@ -0,0 +1,42 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// RecoveryRequest.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 Neo.IO;
+using Neo.Plugins.DBFTPlugin.Types;
+
+namespace Neo.Plugins.DBFTPlugin.Messages;
+
+public class RecoveryRequest : ConsensusMessage
+{
+ ///
+ /// Timestamp of when the ChangeView message was created. This allows receiving nodes to ensure
+ /// they only respond once to a specific RecoveryRequest request.
+ /// In this sense, it prevents replay of the RecoveryRequest message from the repeatedly broadcast of Recovery's messages.
+ ///
+ public ulong Timestamp;
+
+ public override int Size => base.Size
+ + sizeof(ulong); //Timestamp
+
+ public RecoveryRequest() : base(ConsensusMessageType.RecoveryRequest) { }
+
+ public override void Deserialize(ref MemoryReader reader)
+ {
+ base.Deserialize(ref reader);
+ Timestamp = reader.ReadUInt64();
+ }
+
+ public override void Serialize(BinaryWriter writer)
+ {
+ base.Serialize(writer);
+ writer.Write(Timestamp);
+ }
+}
diff --git a/plugins/DBFTPlugin/Types/ChangeViewReason.cs b/plugins/DBFTPlugin/Types/ChangeViewReason.cs
new file mode 100644
index 000000000..15b4aa493
--- /dev/null
+++ b/plugins/DBFTPlugin/Types/ChangeViewReason.cs
@@ -0,0 +1,22 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ChangeViewReason.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.
+
+namespace Neo.Plugins.DBFTPlugin.Types;
+
+public enum ChangeViewReason : byte
+{
+ Timeout = 0x0,
+ ChangeAgreement = 0x1,
+ TxNotFound = 0x2,
+ TxRejectedByPolicy = 0x3,
+ TxInvalid = 0x4,
+ BlockRejectedByPolicy = 0x5
+}
diff --git a/plugins/DBFTPlugin/Types/ConsensusMessageType.cs b/plugins/DBFTPlugin/Types/ConsensusMessageType.cs
new file mode 100644
index 000000000..7132db1b8
--- /dev/null
+++ b/plugins/DBFTPlugin/Types/ConsensusMessageType.cs
@@ -0,0 +1,24 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ConsensusMessageType.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.
+
+namespace Neo.Plugins.DBFTPlugin.Types;
+
+public enum ConsensusMessageType : byte
+{
+ ChangeView = 0x00,
+
+ PrepareRequest = 0x20,
+ PrepareResponse = 0x21,
+ Commit = 0x30,
+
+ RecoveryRequest = 0x40,
+ RecoveryMessage = 0x41,
+}
diff --git a/plugins/Directory.Build.props b/plugins/Directory.Build.props
new file mode 100644
index 000000000..7c941b5bf
--- /dev/null
+++ b/plugins/Directory.Build.props
@@ -0,0 +1,32 @@
+
+
+
+
+ 2015-2025 The Neo Project
+ The Neo Project
+ Neo.Plugins.$(MSBuildProjectName)
+ https://github.com/neo-project/neo-modules
+ MIT
+ git
+ https://github.com/neo-project/neo-modules.git
+ NEO;Blockchain
+ 4.0.0
+ net10.0
+ $(PackageId)
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+ PreserveNewest
+ PreserveNewest
+
+
+
+
diff --git a/plugins/LevelDBStore/IO/Data/LevelDB/DB.cs b/plugins/LevelDBStore/IO/Data/LevelDB/DB.cs
new file mode 100644
index 000000000..e8cfed577
--- /dev/null
+++ b/plugins/LevelDBStore/IO/Data/LevelDB/DB.cs
@@ -0,0 +1,135 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// DB.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System.Collections;
+
+namespace Neo.IO.Data.LevelDB;
+
+///
+/// A DB is a persistent ordered map from keys to values.
+/// A DB is safe for concurrent access from multiple threads without any external synchronization.
+/// Iterating over the whole dataset can be time-consuming. Depending upon how large the dataset is.
+///
+public class DB : LevelDBHandle, IEnumerable>
+{
+ private DB(nint handle) : base(handle) { }
+
+ protected override void FreeUnManagedObjects()
+ {
+ if (Handle != nint.Zero)
+ {
+ Native.leveldb_close(Handle);
+ }
+ }
+
+ ///
+ /// Remove the database entry (if any) for "key".
+ /// It is not an error if "key" did not exist in the database.
+ /// Note: consider setting new WriteOptions{ Sync = true }.
+ ///
+ public void Delete(WriteOptions options, byte[] key)
+ {
+ Native.leveldb_delete(Handle, options.Handle, key, (nuint)key.Length, out var error);
+ NativeHelper.CheckError(error);
+ }
+
+ ///
+ /// If the database contains an entry for "key" return the value,
+ /// otherwise return null.
+ ///
+ public byte[]? Get(ReadOptions options, byte[] key)
+ {
+ var value = Native.leveldb_get(Handle, options.Handle, key, (nuint)key.Length, out var length, out var error);
+ try
+ {
+ NativeHelper.CheckError(error);
+ return value.ToByteArray(length);
+ }
+ finally
+ {
+ if (value != nint.Zero) Native.leveldb_free(value);
+ }
+ }
+
+ public bool Contains(ReadOptions options, byte[] key)
+ {
+ var value = Native.leveldb_get(Handle, options.Handle, key, (nuint)key.Length, out _, out var error);
+ NativeHelper.CheckError(error);
+
+ if (value != nint.Zero)
+ {
+ Native.leveldb_free(value);
+ return true;
+ }
+
+ return false;
+ }
+
+ public Snapshot CreateSnapshot()
+ {
+ return new Snapshot(Handle);
+ }
+
+ public Iterator CreateIterator(ReadOptions options)
+ {
+ return new Iterator(Native.leveldb_create_iterator(Handle, options.Handle));
+ }
+
+ public static DB Open(string name)
+ {
+ return Open(name, Options.Default);
+ }
+
+ public static DB Open(string name, Options options)
+ {
+ var handle = Native.leveldb_open(options.Handle, Path.GetFullPath(name), out var error);
+ NativeHelper.CheckError(error);
+ return new DB(handle);
+ }
+
+ ///
+ /// Set the database entry for "key" to "value".
+ /// Note: consider setting new WriteOptions{ Sync = true }.
+ ///
+ public void Put(WriteOptions options, byte[] key, byte[] value)
+ {
+ Native.leveldb_put(Handle, options.Handle, key, (nuint)key.Length, value, (nuint)value.Length, out var error);
+ NativeHelper.CheckError(error);
+ }
+
+ ///
+ /// If a DB cannot be opened, you may attempt to call this method to
+ /// resurrect as much of the contents of the database as possible.
+ /// Some data may be lost, so be careful when calling this function
+ /// on a database that contains important information.
+ ///
+ public static void Repair(string name, Options options)
+ {
+ Native.leveldb_repair_db(options.Handle, Path.GetFullPath(name), out var error);
+ NativeHelper.CheckError(error);
+ }
+
+ public void Write(WriteOptions options, WriteBatch write_batch)
+ {
+ Native.leveldb_write(Handle, options.Handle, write_batch.Handle, out var error);
+ NativeHelper.CheckError(error);
+ }
+
+ public IEnumerator> GetEnumerator()
+ {
+ using var iterator = CreateIterator(ReadOptions.Default);
+ for (iterator.SeekToFirst(); iterator.Valid(); iterator.Next())
+ yield return new KeyValuePair(iterator.Key()!, iterator.Value()!);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() =>
+ GetEnumerator();
+}
diff --git a/plugins/LevelDBStore/IO/Data/LevelDB/Helper.cs b/plugins/LevelDBStore/IO/Data/LevelDB/Helper.cs
new file mode 100644
index 000000000..c79d2726a
--- /dev/null
+++ b/plugins/LevelDBStore/IO/Data/LevelDB/Helper.cs
@@ -0,0 +1,50 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Helper.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 Neo.Persistence;
+using System.Runtime.InteropServices;
+
+namespace Neo.IO.Data.LevelDB;
+
+public static class Helper
+{
+ public static IEnumerable<(byte[], byte[])> Seek(this DB db, ReadOptions options, byte[]? keyOrPrefix, SeekDirection direction)
+ {
+ keyOrPrefix ??= [];
+
+ using var it = db.CreateIterator(options);
+ if (direction == SeekDirection.Forward)
+ {
+ for (it.Seek(keyOrPrefix); it.Valid(); it.Next())
+ yield return new(it.Key()!, it.Value()!);
+ }
+ else
+ {
+ // SeekForPrev
+ it.Seek(keyOrPrefix);
+ if (!it.Valid())
+ it.SeekToLast();
+ else if (it.Key().AsSpan().SequenceCompareTo(keyOrPrefix) > 0)
+ it.Prev();
+
+ for (; it.Valid(); it.Prev())
+ yield return new(it.Key()!, it.Value()!);
+ }
+ }
+
+ internal static byte[]? ToByteArray(this IntPtr data, UIntPtr length)
+ {
+ if (data == IntPtr.Zero) return null;
+ var buffer = new byte[(int)length];
+ Marshal.Copy(data, buffer, 0, (int)length);
+ return buffer;
+ }
+}
diff --git a/plugins/LevelDBStore/IO/Data/LevelDB/Iterator.cs b/plugins/LevelDBStore/IO/Data/LevelDB/Iterator.cs
new file mode 100644
index 000000000..a65cd4e7b
--- /dev/null
+++ b/plugins/LevelDBStore/IO/Data/LevelDB/Iterator.cs
@@ -0,0 +1,110 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Iterator.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.
+
+
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// Iterator.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.
+
+namespace Neo.IO.Data.LevelDB;
+
+///
+/// An iterator yields a sequence of key/value pairs from a database.
+///
+public class Iterator : LevelDBHandle
+{
+ internal Iterator(nint handle) : base(handle) { }
+
+ private void CheckError()
+ {
+ Native.leveldb_iter_get_error(Handle, out var error);
+ NativeHelper.CheckError(error);
+ }
+
+ protected override void FreeUnManagedObjects()
+ {
+ if (Handle != nint.Zero)
+ {
+ Native.leveldb_iter_destroy(Handle);
+ }
+ }
+
+ ///
+ /// Return the key for the current entry.
+ /// REQUIRES: Valid()
+ ///
+ public byte[]? Key()
+ {
+ var key = Native.leveldb_iter_key(Handle, out var length);
+ CheckError();
+ return key.ToByteArray(length);
+ }
+
+ ///
+ /// Moves to the next entry in the source.
+ /// After this call, Valid() is true if the iterator was not positioned at the last entry in the source.
+ /// REQUIRES: Valid()
+ ///
+ public void Next()
+ {
+ Native.leveldb_iter_next(Handle);
+ CheckError();
+ }
+
+ public void Prev()
+ {
+ Native.leveldb_iter_prev(Handle);
+ CheckError();
+ }
+
+ ///
+ /// Position at the first key in the source that at or past target
+ /// The iterator is Valid() after this call if the source contains
+ /// an entry that comes at or past target.
+ ///
+ public void Seek(byte[] key)
+ {
+ Native.leveldb_iter_seek(Handle, key, (nuint)key.Length);
+ }
+
+ public void SeekToFirst()
+ {
+ Native.leveldb_iter_seek_to_first(Handle);
+ }
+
+ ///
+ /// Position at the last key in the source.
+ /// The iterator is Valid() after this call if the source is not empty.
+ ///
+ public void SeekToLast()
+ {
+ Native.leveldb_iter_seek_to_last(Handle);
+ }
+
+ public bool Valid()
+ {
+ return Native.leveldb_iter_valid(Handle);
+ }
+
+ public byte[]? Value()
+ {
+ var value = Native.leveldb_iter_value(Handle, out var length);
+ CheckError();
+ return value.ToByteArray(length);
+ }
+}
diff --git a/plugins/LevelDBStore/IO/Data/LevelDB/LevelDBException.cs b/plugins/LevelDBStore/IO/Data/LevelDB/LevelDBException.cs
new file mode 100644
index 000000000..77bbd36cc
--- /dev/null
+++ b/plugins/LevelDBStore/IO/Data/LevelDB/LevelDBException.cs
@@ -0,0 +1,22 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// LevelDBException.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System.Data.Common;
+
+namespace Neo.IO.Data.LevelDB;
+
+public class LevelDBException : DbException
+{
+ internal LevelDBException(string message)
+ : base(message)
+ {
+ }
+}
diff --git a/plugins/LevelDBStore/IO/Data/LevelDB/LevelDBHandle.cs b/plugins/LevelDBStore/IO/Data/LevelDB/LevelDBHandle.cs
new file mode 100644
index 000000000..b40a77121
--- /dev/null
+++ b/plugins/LevelDBStore/IO/Data/LevelDB/LevelDBHandle.cs
@@ -0,0 +1,53 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// LevelDBHandle.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.
+
+namespace Neo.IO.Data.LevelDB;
+
+///
+/// Base class for all LevelDB objects
+///
+public abstract class LevelDBHandle(nint handle) : IDisposable
+{
+ private bool _disposed = false;
+
+ public nint Handle { get; private set; } = handle;
+
+ ///
+ /// Return true if haven't got valid handle
+ ///
+ public bool IsDisposed => _disposed || Handle == IntPtr.Zero;
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected abstract void FreeUnManagedObjects();
+
+ void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ if (Handle != nint.Zero)
+ {
+ FreeUnManagedObjects();
+ Handle = nint.Zero;
+ }
+ }
+ }
+
+ ~LevelDBHandle()
+ {
+ Dispose(false);
+ }
+}
diff --git a/plugins/LevelDBStore/IO/Data/LevelDB/Native.cs b/plugins/LevelDBStore/IO/Data/LevelDB/Native.cs
new file mode 100644
index 000000000..69d3ab604
--- /dev/null
+++ b/plugins/LevelDBStore/IO/Data/LevelDB/Native.cs
@@ -0,0 +1,334 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Native.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.Marshalling;
+
+namespace Neo.IO.Data.LevelDB;
+
+public enum CompressionType : byte
+{
+ NoCompression = 0x0,
+ SnappyCompression = 0x1
+}
+
+internal static partial class Native
+{
+ #region Logger
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint leveldb_logger_create(nint /* Action */ logger);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_logger_destroy(nint /* logger*/ option);
+
+ #endregion
+
+ #region DB
+
+ [LibraryImport("libleveldb", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(AnsiStringMarshaller))]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint leveldb_open(nint /* Options*/ options, string name, out nint error);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_close(nint /*DB */ db);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_put(nint /* DB */ db, nint /* WriteOptions*/ options,
+ [In] byte[] key, nuint keylen, [In] byte[] val, nuint vallen, out nint errptr);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_delete(nint /* DB */ db, nint /* WriteOptions*/ options,
+ [In] byte[] key, nuint keylen, out nint errptr);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_write(nint /* DB */ db, nint /* WriteOptions*/ options, nint /* WriteBatch */ batch, out nint errptr);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint leveldb_get(nint /* DB */ db, nint /* ReadOptions*/ options,
+ [In] byte[] key, nuint keylen, out nuint vallen, out nint errptr);
+
+ // [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ // static extern void leveldb_approximate_sizes(nint /* DB */ db, int num_ranges,
+ // byte[] range_start_key, long range_start_key_len, byte[] range_limit_key, long range_limit_key_len, out long sizes);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint leveldb_create_iterator(nint /* DB */ db, nint /* ReadOption */ options);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint leveldb_create_snapshot(nint /* DB */ db);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_release_snapshot(nint /* DB */ db, nint /* SnapShot*/ snapshot);
+
+ [LibraryImport("libleveldb", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(AnsiStringMarshaller))]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint leveldb_property_value(nint /* DB */ db, string propname);
+
+ [LibraryImport("libleveldb", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(AnsiStringMarshaller))]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_repair_db(nint /* Options*/ options, string name, out nint error);
+
+ [LibraryImport("libleveldb", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(AnsiStringMarshaller))]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_destroy_db(nint /* Options*/ options, string name, out nint error);
+
+ #region extensions
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_free(nint /* void */ ptr);
+
+ #endregion
+ #endregion
+
+ #region Env
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint leveldb_create_default_env();
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_env_destroy(nint /*Env*/ cache);
+
+ #endregion
+
+ #region Iterator
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_iter_destroy(nint /*Iterator*/ iterator);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ [return: MarshalAs(UnmanagedType.U1)]
+ internal static partial bool leveldb_iter_valid(nint /*Iterator*/ iterator);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_iter_seek_to_first(nint /*Iterator*/ iterator);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_iter_seek_to_last(nint /*Iterator*/ iterator);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_iter_seek(nint /*Iterator*/ iterator, [In] byte[] key, nuint length);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_iter_next(nint /*Iterator*/ iterator);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_iter_prev(nint /*Iterator*/ iterator);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint leveldb_iter_key(nint /*Iterator*/ iterator, out nuint length);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint leveldb_iter_value(nint /*Iterator*/ iterator, out nuint length);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_iter_get_error(nint /*Iterator*/ iterator, out nint error);
+
+ #endregion
+
+ #region Options
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint leveldb_options_create();
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_options_destroy(nint /*Options*/ options);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_options_set_create_if_missing(nint /*Options*/ options, [MarshalAs(UnmanagedType.U1)] bool o);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_options_set_error_if_exists(nint /*Options*/ options, [MarshalAs(UnmanagedType.U1)] bool o);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_options_set_info_log(nint /*Options*/ options, nint /* Logger */ logger);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_options_set_paranoid_checks(nint /*Options*/ options, [MarshalAs(UnmanagedType.U1)] bool o);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_options_set_env(nint /*Options*/ options, nint /*Env*/ env);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_options_set_write_buffer_size(nint /*Options*/ options, nuint size);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_options_set_max_open_files(nint /*Options*/ options, int max);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_options_set_cache(nint /*Options*/ options, nint /*Cache*/ cache);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_options_set_block_size(nint /*Options*/ options, nuint size);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_options_set_block_restart_interval(nint /*Options*/ options, int interval);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_options_set_compression(nint /*Options*/ options, CompressionType level);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_options_set_comparator(nint /*Options*/ options, nint /*Comparator*/ comparer);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_options_set_filter_policy(nint /*Options*/ options, nint /*FilterPolicy*/ policy);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint leveldb_filterpolicy_create_bloom(int bits_per_key);
+
+ #endregion
+
+ #region ReadOptions
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint leveldb_readoptions_create();
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_readoptions_destroy(nint /*ReadOptions*/ options);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_readoptions_set_verify_checksums(nint /*ReadOptions*/ options, [MarshalAs(UnmanagedType.U1)] bool o);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_readoptions_set_fill_cache(nint /*ReadOptions*/ options, [MarshalAs(UnmanagedType.U1)] bool o);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_readoptions_set_snapshot(nint /*ReadOptions*/ options, nint /*SnapShot*/ snapshot);
+
+ #endregion
+
+ #region WriteBatch
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint leveldb_writebatch_create();
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_writebatch_destroy(nint /* WriteBatch */ batch);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_writebatch_clear(nint /* WriteBatch */ batch);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_writebatch_put(nint /* WriteBatch */ batch, [In] byte[] key, nuint keylen, [In] byte[] val, nuint vallen);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_writebatch_delete(nint /* WriteBatch */ batch, [In] byte[] key, nuint keylen);
+
+ #endregion
+
+ #region WriteOptions
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint leveldb_writeoptions_create();
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_writeoptions_destroy(nint /*WriteOptions*/ options);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_writeoptions_set_sync(nint /*WriteOptions*/ options, [MarshalAs(UnmanagedType.U1)] bool o);
+
+ #endregion
+
+ #region Cache
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint leveldb_cache_create_lru(int capacity);
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_cache_destroy(nint /*Cache*/ cache);
+
+ #endregion
+
+ #region Comparator
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial nint /* leveldb_comparator_t* */ leveldb_comparator_create(
+ nint state, // void* state
+ nint destructor, // void (*destructor)(void*)
+ nint compare, // int (*compare)(void*, const char* a, size_t alen,const char* b, size_t blen)
+ nint name); // const char* (*name)(void*)
+
+ [LibraryImport("libleveldb")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })]
+ internal static partial void leveldb_comparator_destroy(nint /* leveldb_comparator_t* */ cmp);
+
+ #endregion
+}
+
+internal static class NativeHelper
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void CheckError(nint error)
+ {
+ if (error != nint.Zero)
+ {
+ var message = Marshal.PtrToStringAnsi(error);
+ Native.leveldb_free(error);
+ throw new LevelDBException(message ?? string.Empty);
+ }
+ }
+}
diff --git a/plugins/LevelDBStore/IO/Data/LevelDB/Options.cs b/plugins/LevelDBStore/IO/Data/LevelDB/Options.cs
new file mode 100644
index 000000000..162279b85
--- /dev/null
+++ b/plugins/LevelDBStore/IO/Data/LevelDB/Options.cs
@@ -0,0 +1,153 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Options.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.
+
+
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// Options.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.
+
+namespace Neo.IO.Data.LevelDB;
+
+///
+/// Options to control the behavior of a database (passed to Open)
+///
+/// the setter methods for InfoLogger, Env, and Cache only "safe to clean up guarantee". Do not
+/// use Option object if throws.
+///
+public class Options : LevelDBHandle
+{
+ public static readonly Options Default = new();
+
+ public Options() : base(Native.leveldb_options_create()) { }
+
+ ///
+ /// If true, the database will be created if it is missing.
+ ///
+ public bool CreateIfMissing
+ {
+ set { Native.leveldb_options_set_create_if_missing(Handle, value); }
+ }
+
+ ///
+ /// If true, an error is raised if the database already exists.
+ ///
+ public bool ErrorIfExists
+ {
+ set { Native.leveldb_options_set_error_if_exists(Handle, value); }
+ }
+
+ ///
+ /// If true, the implementation will do aggressive checking of the
+ /// data it is processing and will stop early if it detects any
+ /// errors. This may have unforeseen ramifications: for example, a
+ /// corruption of one DB entry may cause a large number of entries to
+ /// become unreadable or for the entire DB to become unopenable.
+ ///
+ public bool ParanoidChecks
+ {
+ set { Native.leveldb_options_set_paranoid_checks(Handle, value); }
+ }
+
+ // Any internal progress/error information generated by the db will
+ // be written to info_log if it is non-NULL, or to a file stored
+ // in the same directory as the DB contents if info_log is NULL.
+
+ ///
+ /// Amount of data to build up in memory (backed by an unsorted log
+ /// on disk) before converting to a sorted on-disk file.
+ ///
+ /// Larger values increase performance, especially during bulk loads.
+ /// Up to two write buffers may be held in memory at the same time,
+ /// so you may wish to adjust this parameter to control memory usage.
+ /// Also, a larger write buffer will result in a longer recovery time
+ /// the next time the database is opened.
+ ///
+ /// Default: 4MB
+ ///
+ public int WriteBufferSize
+ {
+ set { Native.leveldb_options_set_write_buffer_size(Handle, (UIntPtr)value); }
+ }
+
+ ///
+ /// Number of open files that can be used by the DB. You may need to
+ /// increase this if your database has a large working set (budget
+ /// one open file per 2MB of working set).
+ ///
+ /// Default: 1000
+ ///
+ public int MaxOpenFiles
+ {
+ set { Native.leveldb_options_set_max_open_files(Handle, value); }
+ }
+
+ ///
+ /// Approximate size of user data packed per block. Note that the
+ /// block size specified here corresponds to uncompressed data. The
+ /// actual size of the unit read from disk may be smaller if
+ /// compression is enabled. This parameter can be changed dynamically.
+ ///
+ /// Default: 4K
+ ///
+ public int BlockSize
+ {
+ set { Native.leveldb_options_set_block_size(Handle, (UIntPtr)value); }
+ }
+
+ ///
+ /// Number of keys between restart points for delta encoding of keys.
+ /// This parameter can be changed dynamically.
+ /// Most clients should leave this parameter alone.
+ ///
+ /// Default: 16
+ ///
+ public int BlockRestartInterval
+ {
+ set { Native.leveldb_options_set_block_restart_interval(Handle, value); }
+ }
+
+ ///
+ /// Compress blocks using the specified compression algorithm.
+ /// This parameter can be changed dynamically.
+ ///
+ /// Default: kSnappyCompression, which gives lightweight but fast compression.
+ ///
+ /// Typical speeds of kSnappyCompression on an Intel(R) Core(TM)2 2.4GHz:
+ /// ~200-500MB/s compression
+ /// ~400-800MB/s decompression
+ /// Note that these speeds are significantly faster than most
+ /// persistent storage speeds, and therefore it is typically never
+ /// worth switching to kNoCompression. Even if the input data is
+ /// incompressible, the kSnappyCompression implementation will
+ /// efficiently detect that and will switch to uncompressed mode.
+ ///
+ public CompressionType CompressionLevel
+ {
+ set { Native.leveldb_options_set_compression(Handle, value); }
+ }
+
+ public nint FilterPolicy
+ {
+ set { Native.leveldb_options_set_filter_policy(Handle, value); }
+ }
+
+ protected override void FreeUnManagedObjects()
+ {
+ Native.leveldb_options_destroy(Handle);
+ }
+}
diff --git a/plugins/LevelDBStore/IO/Data/LevelDB/ReadOptions.cs b/plugins/LevelDBStore/IO/Data/LevelDB/ReadOptions.cs
new file mode 100644
index 000000000..740077148
--- /dev/null
+++ b/plugins/LevelDBStore/IO/Data/LevelDB/ReadOptions.cs
@@ -0,0 +1,70 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ReadOptions.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.
+
+
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// ReadOptions.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.
+
+namespace Neo.IO.Data.LevelDB;
+
+///
+/// Options that control read operations.
+///
+public class ReadOptions : LevelDBHandle
+{
+ public static readonly ReadOptions Default = new();
+
+ public ReadOptions() : base(Native.leveldb_readoptions_create()) { }
+
+ ///
+ /// If true, all data read from underlying storage will be
+ /// verified against corresponding checksums.
+ ///
+ public bool VerifyChecksums
+ {
+ set { Native.leveldb_readoptions_set_verify_checksums(Handle, value); }
+ }
+
+ ///
+ /// Should the data read for this iteration be cached in memory?
+ /// Callers may wish to set this field to false for bulk scans.
+ /// Default: true
+ ///
+ public bool FillCache
+ {
+ set { Native.leveldb_readoptions_set_fill_cache(Handle, value); }
+ }
+
+ ///
+ /// If "snapshot" is provided, read as of the supplied snapshot
+ /// (which must belong to the DB that is being read and which must
+ /// not have been released).
+ /// If "snapshot" is not set, use an implicit
+ /// snapshot of the state at the beginning of this read operation.
+ ///
+ public Snapshot Snapshot
+ {
+ set { Native.leveldb_readoptions_set_snapshot(Handle, value.Handle); }
+ }
+
+ protected override void FreeUnManagedObjects()
+ {
+ Native.leveldb_readoptions_destroy(Handle);
+ }
+}
diff --git a/plugins/LevelDBStore/IO/Data/LevelDB/Snapshot.cs b/plugins/LevelDBStore/IO/Data/LevelDB/Snapshot.cs
new file mode 100644
index 000000000..7269d0505
--- /dev/null
+++ b/plugins/LevelDBStore/IO/Data/LevelDB/Snapshot.cs
@@ -0,0 +1,46 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Snapshot.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.
+
+
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// Snapshot.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.
+
+namespace Neo.IO.Data.LevelDB;
+
+///
+/// A Snapshot is an immutable object and can therefore be safely
+/// accessed from multiple threads without any external synchronization.
+///
+public class Snapshot : LevelDBHandle
+{
+ internal nint _db;
+
+ internal Snapshot(nint db) : base(Native.leveldb_create_snapshot(db))
+ {
+ _db = db;
+ }
+
+ protected override void FreeUnManagedObjects()
+ {
+ if (Handle != nint.Zero)
+ {
+ Native.leveldb_release_snapshot(_db, Handle);
+ }
+ }
+}
diff --git a/plugins/LevelDBStore/IO/Data/LevelDB/WriteBatch.cs b/plugins/LevelDBStore/IO/Data/LevelDB/WriteBatch.cs
new file mode 100644
index 000000000..9e3625f80
--- /dev/null
+++ b/plugins/LevelDBStore/IO/Data/LevelDB/WriteBatch.cs
@@ -0,0 +1,71 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// WriteBatch.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.
+
+
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// WriteBatch.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.
+
+namespace Neo.IO.Data.LevelDB;
+
+///
+/// WriteBatch holds a collection of updates to apply atomically to a DB.
+///
+/// The updates are applied in the order in which they are added
+/// to the WriteBatch. For example, the value of "key" will be "v3"
+/// after the following batch is written:
+///
+/// batch.Put("key", "v1");
+/// batch.Delete("key");
+/// batch.Put("key", "v2");
+/// batch.Put("key", "v3");
+///
+public class WriteBatch : LevelDBHandle
+{
+ public WriteBatch() : base(Native.leveldb_writebatch_create()) { }
+
+ ///
+ /// Clear all updates buffered in this batch.
+ ///
+ public void Clear()
+ {
+ Native.leveldb_writebatch_clear(Handle);
+ }
+
+ ///
+ /// Store the mapping "key->value" in the database.
+ ///
+ public void Put(byte[] key, byte[] value)
+ {
+ Native.leveldb_writebatch_put(Handle, key, (UIntPtr)key.Length, value, (UIntPtr)value.Length);
+ }
+
+ ///
+ /// If the database contains a mapping for "key", erase it.
+ /// Else do nothing.
+ ///
+ public void Delete(byte[] key)
+ {
+ Native.leveldb_writebatch_delete(Handle, key, (UIntPtr)key.Length);
+ }
+
+ protected override void FreeUnManagedObjects()
+ {
+ Native.leveldb_writebatch_destroy(Handle);
+ }
+}
diff --git a/plugins/LevelDBStore/IO/Data/LevelDB/WriteOptions.cs b/plugins/LevelDBStore/IO/Data/LevelDB/WriteOptions.cs
new file mode 100644
index 000000000..71ee61213
--- /dev/null
+++ b/plugins/LevelDBStore/IO/Data/LevelDB/WriteOptions.cs
@@ -0,0 +1,61 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// WriteOptions.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.
+
+
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// WriteOptions.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.
+
+namespace Neo.IO.Data.LevelDB;
+
+///
+/// Options that control write operations.
+///
+public class WriteOptions : LevelDBHandle
+{
+ public static readonly WriteOptions Default = new();
+ public static readonly WriteOptions SyncWrite = new() { Sync = true };
+
+ public WriteOptions() : base(Native.leveldb_writeoptions_create()) { }
+
+ ///
+ /// If true, the write will be flushed from the operating system
+ /// buffer cache (by calling WritableFile::Sync()) before the write
+ /// is considered complete. If this flag is true, writes will be
+ /// slower.
+ ///
+ /// If this flag is false, and the machine crashes, some recent
+ /// writes may be lost. Note that if it is just the process that
+ /// crashes (i.e., the machine does not reboot), no writes will be
+ /// lost even if sync==false.
+ ///
+ /// In other words, a DB write with sync==false has similar
+ /// crash semantics as the "write()" system call. A DB write
+ /// with sync==true has similar crash semantics to a "write()"
+ /// system call followed by "fsync()".
+ ///
+ public bool Sync
+ {
+ set { Native.leveldb_writeoptions_set_sync(Handle, value); }
+ }
+
+ protected override void FreeUnManagedObjects()
+ {
+ Native.leveldb_writeoptions_destroy(Handle);
+ }
+}
diff --git a/plugins/LevelDBStore/LevelDBStore.csproj b/plugins/LevelDBStore/LevelDBStore.csproj
new file mode 100644
index 000000000..e73cfeb10
--- /dev/null
+++ b/plugins/LevelDBStore/LevelDBStore.csproj
@@ -0,0 +1,32 @@
+
+
+
+ false
+ Neo.Plugins.Storage.LevelDBStore
+ Neo
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ false
+ Always
+
+
+
+
diff --git a/plugins/LevelDBStore/Plugins/Storage/LevelDBStore.cs b/plugins/LevelDBStore/Plugins/Storage/LevelDBStore.cs
new file mode 100644
index 000000000..906e3b092
--- /dev/null
+++ b/plugins/LevelDBStore/Plugins/Storage/LevelDBStore.cs
@@ -0,0 +1,33 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// LevelDBStore.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 Neo.IO.Data.LevelDB;
+using Neo.Persistence;
+
+namespace Neo.Plugins.Storage;
+
+public class LevelDBStore : Plugin, IStoreProvider
+{
+ public override string Description => "Uses LevelDB to store the blockchain data";
+
+ public LevelDBStore()
+ {
+ StoreFactory.RegisterProvider(this);
+ }
+
+ public IStore GetStore(string? path)
+ {
+ ArgumentNullException.ThrowIfNull(path);
+ if (Environment.CommandLine.Split(' ').Any(p => p == "/repair" || p == "--repair"))
+ DB.Repair(path, Options.Default);
+ return new Store(path);
+ }
+}
diff --git a/plugins/LevelDBStore/Plugins/Storage/Snapshot.cs b/plugins/LevelDBStore/Plugins/Storage/Snapshot.cs
new file mode 100644
index 000000000..57c65935a
--- /dev/null
+++ b/plugins/LevelDBStore/Plugins/Storage/Snapshot.cs
@@ -0,0 +1,101 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Snapshot.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 Neo.IO.Data.LevelDB;
+using Neo.Persistence;
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+using LSnapshot = Neo.IO.Data.LevelDB.Snapshot;
+
+namespace Neo.Plugins.Storage;
+
+///
+/// Iterating over the whole dataset can be time-consuming. Depending upon how large the dataset is.
+/// On-chain write operations on a snapshot cannot be concurrent.
+///
+internal class Snapshot : IStoreSnapshot, IEnumerable>
+{
+ private readonly DB _db;
+ private readonly LSnapshot _snapshot;
+ private readonly ReadOptions _readOptions;
+ private readonly WriteBatch _batch;
+ private readonly Lock _lock = new();
+
+ public IStore Store { get; }
+
+ internal Snapshot(Store store, DB db)
+ {
+ Store = store;
+ _db = db;
+ _snapshot = db.CreateSnapshot();
+ _readOptions = new ReadOptions { FillCache = false, Snapshot = _snapshot };
+ _batch = new WriteBatch();
+ }
+
+ ///
+ public void Commit()
+ {
+ lock (_lock)
+ _db.Write(WriteOptions.Default, _batch);
+ }
+
+ ///
+ public void Delete(byte[] key)
+ {
+ lock (_lock)
+ _batch.Delete(key);
+ }
+
+ ///
+ public void Put(byte[] key, byte[] value)
+ {
+ lock (_lock)
+ _batch.Put(key, value);
+ }
+
+ public void Dispose()
+ {
+ _snapshot.Dispose();
+ _readOptions.Dispose();
+ }
+
+ ///
+ public IEnumerable<(byte[] Key, byte[] Value)> Find(byte[]? keyOrPrefix, SeekDirection direction = SeekDirection.Forward)
+ {
+ return _db.Seek(_readOptions, keyOrPrefix, direction);
+ }
+
+ public bool Contains(byte[] key)
+ {
+ return _db.Contains(_readOptions, key);
+ }
+
+ public byte[]? TryGet(byte[] key)
+ {
+ return _db.Get(_readOptions, key);
+ }
+
+ public bool TryGet(byte[] key, [NotNullWhen(true)] out byte[]? value)
+ {
+ value = _db.Get(_readOptions, key);
+ return value != null;
+ }
+
+ public IEnumerator> GetEnumerator()
+ {
+ using var iterator = _db.CreateIterator(_readOptions);
+ for (iterator.SeekToFirst(); iterator.Valid(); iterator.Next())
+ yield return new KeyValuePair(iterator.Key()!, iterator.Value()!);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() =>
+ GetEnumerator();
+}
diff --git a/plugins/LevelDBStore/Plugins/Storage/Store.cs b/plugins/LevelDBStore/Plugins/Storage/Store.cs
new file mode 100644
index 000000000..3b4f93ae2
--- /dev/null
+++ b/plugins/LevelDBStore/Plugins/Storage/Store.cs
@@ -0,0 +1,86 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Store.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 Neo.IO.Data.LevelDB;
+using Neo.Persistence;
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Neo.Plugins.Storage;
+
+///
+/// Iterating over the whole dataset can be time-consuming. Depending upon how large the dataset is.
+///
+internal class Store : IStore, IEnumerable>
+{
+ private readonly DB _db;
+ private readonly Options _options;
+
+ ///
+ public event IStore.OnNewSnapshotDelegate? OnNewSnapshot;
+
+ public Store(string path)
+ {
+ _options = new Options
+ {
+ CreateIfMissing = true,
+ FilterPolicy = Native.leveldb_filterpolicy_create_bloom(15),
+ CompressionLevel = CompressionType.SnappyCompression,
+ };
+ _db = DB.Open(path, _options);
+ }
+
+ public void Delete(byte[] key)
+ {
+ _db.Delete(WriteOptions.Default, key);
+ }
+
+ public void Dispose()
+ {
+ _db.Dispose();
+ _options.Dispose();
+ }
+
+ public IStoreSnapshot GetSnapshot()
+ {
+ var snapshot = new Snapshot(this, _db);
+ OnNewSnapshot?.Invoke(this, snapshot);
+ return snapshot;
+ }
+
+ public void Put(byte[] key, byte[] value) =>
+ _db.Put(WriteOptions.Default, key, value);
+
+ public void PutSync(byte[] key, byte[] value) =>
+ _db.Put(WriteOptions.SyncWrite, key, value);
+
+ public bool Contains(byte[] key) =>
+ _db.Contains(ReadOptions.Default, key);
+
+ public byte[]? TryGet(byte[] key) =>
+ _db.Get(ReadOptions.Default, key);
+
+ public bool TryGet(byte[] key, [NotNullWhen(true)] out byte[]? value)
+ {
+ value = _db.Get(ReadOptions.Default, key);
+ return value != null;
+ }
+
+ ///
+ public IEnumerable<(byte[], byte[])> Find(byte[]? keyOrPrefix, SeekDirection direction = SeekDirection.Forward) =>
+ _db.Seek(ReadOptions.Default, keyOrPrefix, direction);
+
+ public IEnumerator> GetEnumerator() =>
+ _db.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() =>
+ GetEnumerator();
+}
diff --git a/plugins/MPTTrie/Cache.cs b/plugins/MPTTrie/Cache.cs
new file mode 100644
index 000000000..437ca6958
--- /dev/null
+++ b/plugins/MPTTrie/Cache.cs
@@ -0,0 +1,113 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// 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 Neo.Extensions;
+using Neo.Extensions.IO;
+using Neo.Persistence;
+
+namespace Neo.Cryptography.MPTTrie;
+
+public class Cache
+{
+ private enum TrackState : byte
+ {
+ None,
+ Added,
+ Changed,
+ Deleted
+ }
+
+ private class Trackable(Node? node, TrackState state)
+ {
+ public Node? Node { get; internal set; } = node;
+ public TrackState State { get; internal set; } = state;
+ }
+
+ private readonly IStoreSnapshot _store;
+ private readonly byte _prefix;
+ private readonly Dictionary _cache = [];
+
+ public Cache(IStoreSnapshot store, byte prefix)
+ {
+ _store = store;
+ _prefix = prefix;
+ }
+
+ private byte[] Key(UInt256 hash)
+ {
+ var buffer = new byte[UInt256.Length + 1];
+ buffer[0] = _prefix;
+ hash.Serialize(buffer.AsSpan(1));
+ return buffer;
+ }
+
+ public Node? Resolve(UInt256 hash) => ResolveInternal(hash).Node?.Clone();
+
+ private Trackable ResolveInternal(UInt256 hash)
+ {
+ if (_cache.TryGetValue(hash, out var t))
+ {
+ return t;
+ }
+
+ var n = _store.TryGet(Key(hash), out var data) ? data.AsSerializable() : null;
+
+ t = new Trackable(n, TrackState.None);
+ _cache.Add(hash, t);
+ return t;
+ }
+
+ public void PutNode(Node np)
+ {
+ var entry = ResolveInternal(np.Hash);
+ if (entry.Node is null)
+ {
+ np.Reference = 1;
+ entry.Node = np.Clone();
+ entry.State = TrackState.Added;
+ return;
+ }
+ entry.Node.Reference++;
+ entry.State = TrackState.Changed;
+ }
+
+ public void DeleteNode(UInt256 hash)
+ {
+ var entry = ResolveInternal(hash);
+ if (entry.Node is null) return;
+ if (1 < entry.Node.Reference)
+ {
+ entry.Node.Reference--;
+ entry.State = TrackState.Changed;
+ return;
+ }
+ entry.Node = null;
+ entry.State = TrackState.Deleted;
+ }
+
+ public void Commit()
+ {
+ foreach (var item in _cache)
+ {
+ switch (item.Value.State)
+ {
+ case TrackState.Added:
+ case TrackState.Changed:
+ _store.Put(Key(item.Key), item.Value.Node!.ToArray());
+ break;
+ case TrackState.Deleted:
+ _store.Delete(Key(item.Key));
+ break;
+ }
+ }
+ _cache.Clear();
+ }
+}
diff --git a/plugins/MPTTrie/MPTTrie.csproj b/plugins/MPTTrie/MPTTrie.csproj
new file mode 100644
index 000000000..80c43771e
--- /dev/null
+++ b/plugins/MPTTrie/MPTTrie.csproj
@@ -0,0 +1,8 @@
+
+
+
+ Neo.Cryptography.MPT
+ Neo.Cryptography.MPTTrie
+
+
+
diff --git a/plugins/MPTTrie/Node.Branch.cs b/plugins/MPTTrie/Node.Branch.cs
new file mode 100644
index 000000000..f6a6bdf82
--- /dev/null
+++ b/plugins/MPTTrie/Node.Branch.cs
@@ -0,0 +1,67 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Node.Branch.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 Neo.IO;
+
+namespace Neo.Cryptography.MPTTrie;
+
+partial class Node
+{
+ public const int BranchChildCount = 17;
+ public Node[] Children { get; internal set; } = [];
+
+ public static Node NewBranch()
+ {
+ var n = new Node
+ {
+ Type = NodeType.BranchNode,
+ Reference = 1,
+ Children = new Node[BranchChildCount],
+ };
+ for (var i = 0; i < BranchChildCount; i++)
+ {
+ n.Children[i] = new Node();
+ }
+ return n;
+ }
+
+ protected int BranchSize
+ {
+ get
+ {
+ var size = 0;
+ for (var i = 0; i < BranchChildCount; i++)
+ {
+ size += Children[i].SizeAsChild;
+ }
+ return size;
+ }
+ }
+
+ private void SerializeBranch(BinaryWriter writer)
+ {
+ for (var i = 0; i < BranchChildCount; i++)
+ {
+ Children[i].SerializeAsChild(writer);
+ }
+ }
+
+ private void DeserializeBranch(ref MemoryReader reader)
+ {
+ Children = new Node[BranchChildCount];
+ for (var i = 0; i < BranchChildCount; i++)
+ {
+ var n = new Node();
+ n.Deserialize(ref reader);
+ Children[i] = n;
+ }
+ }
+}
diff --git a/plugins/MPTTrie/Node.Extension.cs b/plugins/MPTTrie/Node.Extension.cs
new file mode 100644
index 000000000..c6941b8f9
--- /dev/null
+++ b/plugins/MPTTrie/Node.Extension.cs
@@ -0,0 +1,75 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Node.Extension.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 Neo.Extensions;
+using Neo.Extensions.IO;
+using Neo.IO;
+using Neo.SmartContract;
+
+namespace Neo.Cryptography.MPTTrie;
+
+partial class Node
+{
+ public const int MaxKeyLength = (ApplicationEngine.MaxStorageKeySize + sizeof(int)) * 2;
+ public ReadOnlyMemory Key { get; set; } = ReadOnlyMemory.Empty;
+
+ // Not null when Type is ExtensionNode, null if not ExtensionNode
+ internal Node? _next;
+
+ // Not null when Type is ExtensionNode, null if not ExtensionNode
+ public Node? Next
+ {
+ get => _next;
+ set { _next = value; }
+ }
+
+ public static Node NewExtension(byte[] key, Node next)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+ ArgumentNullException.ThrowIfNull(next);
+
+ if (key.Length == 0) throw new InvalidOperationException(nameof(NewExtension));
+
+ return new Node
+ {
+ Type = NodeType.ExtensionNode,
+ Key = key,
+ Next = next,
+ Reference = 1,
+ };
+ }
+
+ protected int ExtensionSize
+ {
+ get
+ {
+ if (Next is null)
+ throw new InvalidOperationException("ExtensionSize but not extension node");
+ return Key.GetVarSize() + Next.SizeAsChild;
+ }
+ }
+
+ private void SerializeExtension(BinaryWriter writer)
+ {
+ if (Next is null)
+ throw new InvalidOperationException("SerializeExtension but not extension node");
+ writer.WriteVarBytes(Key.Span);
+ Next.SerializeAsChild(writer);
+ }
+
+ private void DeserializeExtension(ref MemoryReader reader)
+ {
+ Key = reader.ReadVarMemory();
+ var n = new Node();
+ n.Deserialize(ref reader);
+ Next = n;
+ }
+}
diff --git a/plugins/MPTTrie/Node.Hash.cs b/plugins/MPTTrie/Node.Hash.cs
new file mode 100644
index 000000000..a84b752f5
--- /dev/null
+++ b/plugins/MPTTrie/Node.Hash.cs
@@ -0,0 +1,42 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Node.Hash.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 Neo.Extensions.IO;
+using Neo.IO;
+
+namespace Neo.Cryptography.MPTTrie;
+
+partial class Node
+{
+ public static Node NewHash(UInt256 hash)
+ {
+ ArgumentNullException.ThrowIfNull(hash);
+ return new Node
+ {
+ Type = NodeType.HashNode,
+ _hash = hash,
+ };
+ }
+
+ protected static int HashSize => UInt256.Length;
+
+ private void SerializeHash(BinaryWriter writer)
+ {
+ if (_hash is null)
+ throw new InvalidOperationException("SerializeHash but not hash node");
+ writer.Write(_hash);
+ }
+
+ private void DeserializeHash(ref MemoryReader reader)
+ {
+ _hash = reader.ReadSerializable();
+ }
+}
diff --git a/plugins/MPTTrie/Node.Leaf.cs b/plugins/MPTTrie/Node.Leaf.cs
new file mode 100644
index 000000000..6fcfa64c1
--- /dev/null
+++ b/plugins/MPTTrie/Node.Leaf.cs
@@ -0,0 +1,46 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Node.Leaf.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 Neo.Extensions;
+using Neo.Extensions.IO;
+using Neo.IO;
+using Neo.SmartContract;
+
+namespace Neo.Cryptography.MPTTrie;
+
+partial class Node
+{
+ public const int MaxValueLength = 3 + ApplicationEngine.MaxStorageValueSize + sizeof(bool);
+ public ReadOnlyMemory Value { get; set; } = ReadOnlyMemory.Empty;
+
+ public static Node NewLeaf(byte[] value)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+ return new Node
+ {
+ Type = NodeType.LeafNode,
+ Value = value,
+ Reference = 1,
+ };
+ }
+
+ protected int LeafSize => Value.GetVarSize();
+
+ private void SerializeLeaf(BinaryWriter writer)
+ {
+ writer.WriteVarBytes(Value.Span);
+ }
+
+ private void DeserializeLeaf(ref MemoryReader reader)
+ {
+ Value = reader.ReadVarMemory();
+ }
+}
diff --git a/plugins/MPTTrie/Node.cs b/plugins/MPTTrie/Node.cs
new file mode 100644
index 000000000..7d6240e1f
--- /dev/null
+++ b/plugins/MPTTrie/Node.cs
@@ -0,0 +1,209 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Node.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 Neo.Extensions.IO;
+using Neo.IO;
+
+namespace Neo.Cryptography.MPTTrie;
+
+public partial class Node : ISerializable
+{
+ private UInt256? _hash;
+ public int Reference { get; internal set; }
+ public UInt256 Hash => _hash ??= new UInt256(Crypto.Hash256(ToArrayWithoutReference()));
+ public NodeType Type { get; internal set; }
+ public bool IsEmpty => Type == NodeType.Empty;
+ public int Size
+ {
+ get
+ {
+ var size = sizeof(NodeType);
+ return Type switch
+ {
+ NodeType.BranchNode => size + BranchSize + Reference.GetVarSize(),
+ NodeType.ExtensionNode => size + ExtensionSize + Reference.GetVarSize(),
+ NodeType.LeafNode => size + LeafSize + Reference.GetVarSize(),
+ NodeType.HashNode => size + HashSize,
+ NodeType.Empty => size,
+ _ => throw new InvalidOperationException($"{nameof(Node)} Cannt get size, unsupport type"),
+ };
+ }
+ }
+
+ public Node()
+ {
+ Type = NodeType.Empty;
+ }
+
+ public void SetDirty()
+ {
+ _hash = null;
+ }
+
+ public int SizeAsChild
+ {
+ get
+ {
+ return Type switch
+ {
+ NodeType.BranchNode or NodeType.ExtensionNode or NodeType.LeafNode => NewHash(Hash).Size,
+ NodeType.HashNode or NodeType.Empty => Size,
+ _ => throw new InvalidOperationException(nameof(Node)),
+ };
+ }
+ }
+
+ public void SerializeAsChild(BinaryWriter writer)
+ {
+ switch (Type)
+ {
+ case NodeType.BranchNode:
+ case NodeType.ExtensionNode:
+ case NodeType.LeafNode:
+ var n = NewHash(Hash);
+ n.Serialize(writer);
+ break;
+ case NodeType.HashNode:
+ case NodeType.Empty:
+ Serialize(writer);
+ break;
+ default:
+ throw new FormatException(nameof(SerializeAsChild));
+ }
+ }
+
+ private void SerializeWithoutReference(BinaryWriter writer)
+ {
+ writer.Write((byte)Type);
+ switch (Type)
+ {
+ case NodeType.BranchNode:
+ SerializeBranch(writer);
+ break;
+ case NodeType.ExtensionNode:
+ SerializeExtension(writer);
+ break;
+ case NodeType.LeafNode:
+ SerializeLeaf(writer);
+ break;
+ case NodeType.HashNode:
+ SerializeHash(writer);
+ break;
+ case NodeType.Empty:
+ break;
+ default:
+ throw new FormatException(nameof(SerializeWithoutReference));
+ }
+ }
+
+ public void Serialize(BinaryWriter writer)
+ {
+ SerializeWithoutReference(writer);
+ if (Type == NodeType.BranchNode || Type == NodeType.ExtensionNode || Type == NodeType.LeafNode)
+ writer.WriteVarInt(Reference);
+ }
+
+ public byte[] ToArrayWithoutReference()
+ {
+ using var ms = new MemoryStream();
+ using var writer = new BinaryWriter(ms, Utility.StrictUTF8, true);
+
+ SerializeWithoutReference(writer);
+ writer.Flush();
+ return ms.ToArray();
+ }
+
+ public void Deserialize(ref MemoryReader reader)
+ {
+ Type = (NodeType)reader.ReadByte();
+ switch (Type)
+ {
+ case NodeType.BranchNode:
+ DeserializeBranch(ref reader);
+ Reference = (int)reader.ReadVarInt();
+ break;
+ case NodeType.ExtensionNode:
+ DeserializeExtension(ref reader);
+ Reference = (int)reader.ReadVarInt();
+ break;
+ case NodeType.LeafNode:
+ DeserializeLeaf(ref reader);
+ Reference = (int)reader.ReadVarInt();
+ break;
+ case NodeType.Empty:
+ break;
+ case NodeType.HashNode:
+ DeserializeHash(ref reader);
+ break;
+ default:
+ throw new FormatException(nameof(Deserialize));
+ }
+ }
+
+ private Node CloneAsChild()
+ {
+ return Type switch
+ {
+ NodeType.BranchNode or NodeType.ExtensionNode or NodeType.LeafNode => new Node
+ {
+ Type = NodeType.HashNode,
+ _hash = Hash,
+ },
+ NodeType.HashNode or NodeType.Empty => Clone(),
+ _ => throw new InvalidOperationException(nameof(Clone)),
+ };
+ }
+
+ public Node Clone()
+ {
+ switch (Type)
+ {
+ case NodeType.BranchNode:
+ var n = new Node
+ {
+ Type = Type,
+ Reference = Reference,
+ Children = new Node[BranchChildCount],
+ };
+ for (var i = 0; i < BranchChildCount; i++)
+ {
+ n.Children[i] = Children[i].CloneAsChild();
+ }
+ return n;
+ case NodeType.ExtensionNode:
+ return new Node
+ {
+ Type = Type,
+ Key = Key,
+ Next = Next!.CloneAsChild(), // Next not null if ExtensionNode
+ Reference = Reference,
+ };
+ case NodeType.LeafNode:
+ return new Node
+ {
+ Type = Type,
+ Value = Value,
+ Reference = Reference,
+ };
+ case NodeType.HashNode:
+ case NodeType.Empty:
+ return this;
+ default:
+ throw new InvalidOperationException(nameof(Clone));
+ }
+ }
+
+ public void FromReplica(Node n)
+ {
+ MemoryReader reader = new(n.ToArray());
+ Deserialize(ref reader);
+ }
+}
diff --git a/plugins/MPTTrie/NodeType.cs b/plugins/MPTTrie/NodeType.cs
new file mode 100644
index 000000000..a963eb4b7
--- /dev/null
+++ b/plugins/MPTTrie/NodeType.cs
@@ -0,0 +1,21 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// NodeType.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.
+
+namespace Neo.Cryptography.MPTTrie;
+
+public enum NodeType : byte
+{
+ BranchNode = 0x00,
+ ExtensionNode = 0x01,
+ LeafNode = 0x02,
+ HashNode = 0x03,
+ Empty = 0x04
+}
diff --git a/plugins/MPTTrie/Trie.Delete.cs b/plugins/MPTTrie/Trie.Delete.cs
new file mode 100644
index 000000000..0540dcc22
--- /dev/null
+++ b/plugins/MPTTrie/Trie.Delete.cs
@@ -0,0 +1,131 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Trie.Delete.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.
+
+namespace Neo.Cryptography.MPTTrie;
+
+partial class Trie
+{
+ public bool Delete(byte[] key)
+ {
+ var path = ToNibbles(key);
+ if (path.Length == 0)
+ throw new ArgumentException("The key cannot be empty. A valid key must contain at least one nibble.", nameof(key));
+ if (path.Length > Node.MaxKeyLength)
+ throw new ArgumentException($"Key length {path.Length} exceeds the maximum allowed length of {Node.MaxKeyLength} nibbles.", nameof(key));
+ return TryDelete(ref _root, path);
+ }
+
+ private bool TryDelete(ref Node node, ReadOnlySpan path)
+ {
+ switch (node.Type)
+ {
+ case NodeType.LeafNode:
+ {
+ if (path.IsEmpty)
+ {
+ if (!_full) _cache.DeleteNode(node.Hash);
+ node = new Node();
+ return true;
+ }
+ return false;
+ }
+ case NodeType.ExtensionNode:
+ {
+ if (path.StartsWith(node.Key.Span))
+ {
+ var oldHash = node.Hash;
+ var result = TryDelete(ref node._next!, path[node.Key.Length..]);
+ if (!result) return false;
+ if (!_full) _cache.DeleteNode(oldHash);
+ if (node.Next!.IsEmpty)
+ {
+ node = node.Next;
+ return true;
+ }
+ if (node.Next.Type == NodeType.ExtensionNode)
+ {
+ if (!_full) _cache.DeleteNode(node.Next.Hash);
+ node.Key = new([.. node.Key.Span, .. node.Next.Key.Span]);
+ node.Next = node.Next.Next;
+ }
+ node.SetDirty();
+ _cache.PutNode(node);
+ return true;
+ }
+ return false;
+ }
+ case NodeType.BranchNode:
+ {
+ bool result;
+ var oldHash = node.Hash;
+ if (path.IsEmpty)
+ {
+ result = TryDelete(ref node.Children[Node.BranchChildCount - 1], path);
+ }
+ else
+ {
+ result = TryDelete(ref node.Children[path[0]], path[1..]);
+ }
+ if (!result) return false;
+ if (!_full) _cache.DeleteNode(oldHash);
+ var childrenIndexes = new List(Node.BranchChildCount);
+ for (var i = 0; i < Node.BranchChildCount; i++)
+ {
+ if (node.Children[i].IsEmpty) continue;
+ childrenIndexes.Add((byte)i);
+ }
+ if (childrenIndexes.Count > 1)
+ {
+ node.SetDirty();
+ _cache.PutNode(node);
+ return true;
+ }
+ var lastChildIndex = childrenIndexes[0];
+ var lastChild = node.Children[lastChildIndex];
+ if (lastChildIndex == Node.BranchChildCount - 1)
+ {
+ node = lastChild;
+ return true;
+ }
+ if (lastChild.Type == NodeType.HashNode)
+ {
+ lastChild = _cache.Resolve(lastChild.Hash);
+ if (lastChild is null) throw new InvalidOperationException("Internal error, can't resolve hash");
+ }
+ if (lastChild.Type == NodeType.ExtensionNode)
+ {
+ if (!_full) _cache.DeleteNode(lastChild.Hash);
+ lastChild.Key = new([.. childrenIndexes.ToArray(), .. lastChild.Key.Span]);
+ lastChild.SetDirty();
+ _cache.PutNode(lastChild);
+ node = lastChild;
+ return true;
+ }
+ node = Node.NewExtension([.. childrenIndexes], lastChild);
+ _cache.PutNode(node);
+ return true;
+ }
+ case NodeType.Empty:
+ {
+ return false;
+ }
+ case NodeType.HashNode:
+ {
+ var newNode = _cache.Resolve(node.Hash)
+ ?? throw new InvalidOperationException("Internal error, can't resolve hash when mpt delete");
+ node = newNode;
+ return TryDelete(ref node, path);
+ }
+ default:
+ return false;
+ }
+ }
+}
diff --git a/plugins/MPTTrie/Trie.Find.cs b/plugins/MPTTrie/Trie.Find.cs
new file mode 100644
index 000000000..0699d6d94
--- /dev/null
+++ b/plugins/MPTTrie/Trie.Find.cs
@@ -0,0 +1,172 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Trie.Find.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.
+
+namespace Neo.Cryptography.MPTTrie;
+
+partial class Trie
+{
+ private ReadOnlySpan Seek(ref Node node, ReadOnlySpan path, out Node? start)
+ {
+ switch (node.Type)
+ {
+ case NodeType.LeafNode:
+ {
+ if (path.IsEmpty)
+ {
+ start = node;
+ return [];
+ }
+ break;
+ }
+ case NodeType.Empty:
+ break;
+ case NodeType.HashNode:
+ {
+ var newNode = _cache.Resolve(node.Hash)
+ ?? throw new InvalidOperationException("Internal error, can't resolve hash when mpt seek");
+ node = newNode;
+ return Seek(ref node, path, out start);
+ }
+ case NodeType.BranchNode:
+ {
+ if (path.IsEmpty)
+ {
+ start = node;
+ return [];
+ }
+ return new([.. path[..1], .. Seek(ref node.Children[path[0]], path[1..], out start)]);
+ }
+ case NodeType.ExtensionNode:
+ {
+ if (path.IsEmpty)
+ {
+ start = node.Next;
+ return node.Key.Span;
+ }
+ if (path.StartsWith(node.Key.Span))
+ {
+ return new([.. node.Key.Span, .. Seek(ref node._next!, path[node.Key.Length..], out start)]);
+ }
+ if (node.Key.Span.StartsWith(path))
+ {
+ start = node.Next;
+ return node.Key.Span;
+ }
+ break;
+ }
+ }
+ start = null;
+ return [];
+ }
+
+ public IEnumerable<(ReadOnlyMemory Key, ReadOnlyMemory Value)> Find(ReadOnlySpan prefix, byte[]? from = null)
+ {
+ var path = ToNibbles(prefix);
+ var offset = 0;
+ from ??= [];
+ if (0 < from.Length)
+ {
+ if (!from.AsSpan().StartsWith(prefix))
+ throw new InvalidOperationException("invalid from key");
+ from = ToNibbles(from.AsSpan());
+ }
+ if (path.Length > Node.MaxKeyLength || from.Length > Node.MaxKeyLength)
+ throw new ArgumentException($"Key length exceeds the maximum allowed length of {Node.MaxKeyLength} nibbles. Path length: {path.Length}, from length: {from.Length}.");
+ path = Seek(ref _root, path, out var start).ToArray();
+ if (from.Length > 0)
+ {
+ for (var i = 0; i < from.Length && i < path.Length; i++)
+ {
+ if (path[i] < from[i]) return [];
+ if (path[i] > from[i])
+ {
+ offset = from.Length;
+ break;
+ }
+ }
+ if (offset == 0)
+ {
+ offset = Math.Min(path.Length, from.Length);
+ }
+ }
+ return Travers(start, path, from, offset).Select(p => (new ReadOnlyMemory(FromNibbles(p.Key.Span)), p.Value));
+ }
+
+ private IEnumerable<(ReadOnlyMemory Key, ReadOnlyMemory Value)> Travers(Node? node, byte[] path, byte[] from, int offset)
+ {
+ if (node is null) yield break;
+ if (offset < 0) throw new InvalidOperationException("invalid offset");
+ switch (node.Type)
+ {
+ case NodeType.LeafNode:
+ {
+ if (from.Length <= offset && !path.SequenceEqual(from))
+ yield return (path, node.Value);
+ break;
+ }
+ case NodeType.Empty:
+ break;
+ case NodeType.HashNode:
+ {
+ var newNode = _cache.Resolve(node.Hash)
+ ?? throw new InvalidOperationException("Internal error, can't resolve hash when mpt find");
+ node = newNode;
+ foreach (var item in Travers(node, path, from, offset))
+ yield return item;
+ break;
+ }
+ case NodeType.BranchNode:
+ {
+ if (offset < from.Length)
+ {
+ for (var i = 0; i < Node.BranchChildCount - 1; i++)
+ {
+ if (from[offset] < i)
+ {
+ foreach (var item in Travers(node.Children[i], [.. path, .. new byte[] { (byte)i }], from, from.Length))
+ yield return item;
+ }
+ else if (i == from[offset])
+ {
+ foreach (var item in Travers(node.Children[i], [.. path, .. new byte[] { (byte)i }], from, offset + 1))
+ yield return item;
+ }
+ }
+ }
+ else
+ {
+ foreach (var item in Travers(node.Children[Node.BranchChildCount - 1], path, from, offset))
+ yield return item;
+ for (var i = 0; i < Node.BranchChildCount - 1; i++)
+ {
+ foreach (var item in Travers(node.Children[i], [.. path, .. new byte[] { (byte)i }], from, offset))
+ yield return item;
+ }
+ }
+ break;
+ }
+ case NodeType.ExtensionNode:
+ {
+ if (offset < from.Length && from.AsSpan()[offset..].StartsWith(node.Key.Span))
+ {
+ foreach (var item in Travers(node.Next, [.. path, .. node.Key.Span], from, offset + node.Key.Length))
+ yield return item;
+ }
+ else if (from.Length <= offset || 0 < node.Key.Span.SequenceCompareTo(from.AsSpan(offset)))
+ {
+ foreach (var item in Travers(node.Next, [.. path, .. node.Key.Span], from, from.Length))
+ yield return item;
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/plugins/MPTTrie/Trie.Get.cs b/plugins/MPTTrie/Trie.Get.cs
new file mode 100644
index 000000000..30551a457
--- /dev/null
+++ b/plugins/MPTTrie/Trie.Get.cs
@@ -0,0 +1,88 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Trie.Get.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Neo.Cryptography.MPTTrie;
+
+partial class Trie
+{
+ public byte[] this[byte[] key]
+ {
+ get
+ {
+ var path = ToNibbles(key);
+ if (path.Length == 0)
+ throw new ArgumentException("The key cannot be empty. A valid key must contain at least one nibble.", nameof(key));
+ if (path.Length > Node.MaxKeyLength)
+ throw new ArgumentException($"Key length {path.Length} exceeds the maximum allowed length of {Node.MaxKeyLength} nibbles.", nameof(key));
+ var result = TryGet(ref _root, path, out var value);
+ return result ? value.ToArray() : throw new KeyNotFoundException();
+ }
+ }
+
+ public bool TryGetValue(byte[] key, [NotNullWhen(true)] out byte[]? value)
+ {
+ value = default;
+ var path = ToNibbles(key);
+ if (path.Length == 0)
+ throw new ArgumentException("The key cannot be empty. A valid key must contain at least one nibble.", nameof(key));
+ if (path.Length > Node.MaxKeyLength)
+ throw new ArgumentException($"Key length {path.Length} exceeds the maximum allowed length of {Node.MaxKeyLength} nibbles.", nameof(key));
+ var result = TryGet(ref _root, path, out var val);
+ if (result)
+ value = val.ToArray();
+ return result;
+ }
+
+ private bool TryGet(ref Node node, ReadOnlySpan path, out ReadOnlySpan value)
+ {
+ switch (node.Type)
+ {
+ case NodeType.LeafNode:
+ {
+ if (path.IsEmpty)
+ {
+ value = node.Value.Span;
+ return true;
+ }
+ break;
+ }
+ case NodeType.Empty:
+ break;
+ case NodeType.HashNode:
+ {
+ var newNode = _cache.Resolve(node.Hash)
+ ?? throw new InvalidOperationException("Internal error, can't resolve hash when mpt get");
+ node = newNode;
+ return TryGet(ref node, path, out value);
+ }
+ case NodeType.BranchNode:
+ {
+ if (path.IsEmpty)
+ {
+ return TryGet(ref node.Children[Node.BranchChildCount - 1], path, out value);
+ }
+ return TryGet(ref node.Children[path[0]], path[1..], out value);
+ }
+ case NodeType.ExtensionNode:
+ {
+ if (path.StartsWith(node.Key.Span))
+ {
+ return TryGet(ref node._next!, path[node.Key.Length..], out value);
+ }
+ break;
+ }
+ }
+ value = default;
+ return false;
+ }
+}
diff --git a/plugins/MPTTrie/Trie.Proof.cs b/plugins/MPTTrie/Trie.Proof.cs
new file mode 100644
index 000000000..b169bf944
--- /dev/null
+++ b/plugins/MPTTrie/Trie.Proof.cs
@@ -0,0 +1,91 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Trie.Proof.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 Neo.Persistence.Providers;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Neo.Cryptography.MPTTrie;
+
+partial class Trie
+{
+ public bool TryGetProof(byte[] key, [NotNull] out HashSet proof)
+ {
+ var path = ToNibbles(key);
+ if (path.Length == 0)
+ throw new ArgumentException("The key cannot be empty. A valid key must contain at least one nibble.", nameof(key));
+ if (path.Length > Node.MaxKeyLength)
+ throw new ArgumentException($"Key length {path.Length} exceeds the maximum allowed length of {Node.MaxKeyLength} nibbles.", nameof(key));
+ proof = new HashSet(ByteArrayEqualityComparer.Default);
+ return GetProof(ref _root, path, proof);
+ }
+
+ private bool GetProof(ref Node node, ReadOnlySpan path, HashSet set)
+ {
+ switch (node.Type)
+ {
+ case NodeType.LeafNode:
+ {
+ if (path.IsEmpty)
+ {
+ set.Add(node.ToArrayWithoutReference());
+ return true;
+ }
+ break;
+ }
+ case NodeType.Empty:
+ break;
+ case NodeType.HashNode:
+ {
+ var newNode = _cache.Resolve(node.Hash)
+ ?? throw new InvalidOperationException("Internal error, can't resolve hash when mpt getproof");
+ node = newNode;
+ return GetProof(ref node, path, set);
+ }
+ case NodeType.BranchNode:
+ {
+ set.Add(node.ToArrayWithoutReference());
+ if (path.IsEmpty)
+ {
+ return GetProof(ref node.Children[Node.BranchChildCount - 1], path, set);
+ }
+ return GetProof(ref node.Children[path[0]], path[1..], set);
+ }
+ case NodeType.ExtensionNode:
+ {
+ if (path.StartsWith(node.Key.Span))
+ {
+ set.Add(node.ToArrayWithoutReference());
+ return GetProof(ref node._next!, path[node.Key.Length..], set);
+ }
+ break;
+ }
+ }
+ return false;
+ }
+
+ private static byte[] Key(byte[] hash)
+ {
+ var buffer = new byte[hash.Length + 1];
+ buffer[0] = Prefix;
+ Buffer.BlockCopy(hash, 0, buffer, 1, hash.Length);
+ return buffer;
+ }
+
+ public static byte[] VerifyProof(UInt256 root, byte[] key, HashSet proof)
+ {
+ using var memoryStore = new MemoryStore();
+ foreach (var data in proof)
+ memoryStore.Put(Key(Crypto.Hash256(data)), [.. data, .. new byte[] { 1 }]);
+ using var snapshot = memoryStore.GetSnapshot();
+ var trie = new Trie(snapshot, root, false);
+ return trie[key];
+ }
+}
diff --git a/plugins/MPTTrie/Trie.Put.cs b/plugins/MPTTrie/Trie.Put.cs
new file mode 100644
index 000000000..7c3f1f674
--- /dev/null
+++ b/plugins/MPTTrie/Trie.Put.cs
@@ -0,0 +1,151 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Trie.Put.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System.Runtime.CompilerServices;
+
+namespace Neo.Cryptography.MPTTrie;
+
+partial class Trie
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ReadOnlySpan CommonPrefix(ReadOnlySpan a, ReadOnlySpan b)
+ {
+ var offset = a.CommonPrefixLength(b);
+ return a[..offset];
+ }
+
+ public void Put(byte[] key, byte[] value)
+ {
+ var path = ToNibbles(key);
+ var val = value;
+ if (path.Length == 0 || path.Length > Node.MaxKeyLength)
+ throw new ArgumentException($"Invalid key length: {path.Length}. The key length must be between 1 and {Node.MaxKeyLength} nibbles.", nameof(key));
+ if (val.Length > Node.MaxValueLength)
+ throw new ArgumentException($"Value length {val.Length} exceeds the maximum allowed length of {Node.MaxValueLength} bytes.", nameof(value));
+ var n = Node.NewLeaf(val);
+ Put(ref _root, path, n);
+ }
+
+ private void Put(ref Node node, ReadOnlySpan path, Node val)
+ {
+ switch (node.Type)
+ {
+ case NodeType.LeafNode:
+ {
+ if (path.IsEmpty)
+ {
+ if (!_full) _cache.DeleteNode(node.Hash);
+ node = val;
+ _cache.PutNode(node);
+ return;
+ }
+ var branch = Node.NewBranch();
+ branch.Children[Node.BranchChildCount - 1] = node;
+ Put(ref branch.Children[path[0]], path[1..], val);
+ _cache.PutNode(branch);
+ node = branch;
+ break;
+ }
+ case NodeType.ExtensionNode:
+ {
+ if (path.StartsWith(node.Key.Span))
+ {
+ var oldHash = node.Hash;
+ Put(ref node._next!, path[node.Key.Length..], val);
+ if (!_full) _cache.DeleteNode(oldHash);
+ node.SetDirty();
+ _cache.PutNode(node);
+ return;
+ }
+ if (!_full) _cache.DeleteNode(node.Hash);
+ var prefix = CommonPrefix(node.Key.Span, path);
+ var pathRemain = path[prefix.Length..];
+ var keyRemain = node.Key.Span[prefix.Length..];
+ var child = Node.NewBranch();
+ var grandChild = new Node();
+ if (keyRemain.Length == 1)
+ {
+ child.Children[keyRemain[0]] = node.Next!;
+ }
+ else
+ {
+ var exNode = Node.NewExtension(keyRemain[1..].ToArray(), node.Next!);
+ _cache.PutNode(exNode);
+ child.Children[keyRemain[0]] = exNode;
+ }
+ if (pathRemain.IsEmpty)
+ {
+ Put(ref grandChild, pathRemain, val);
+ child.Children[Node.BranchChildCount - 1] = grandChild;
+ }
+ else
+ {
+ Put(ref grandChild, pathRemain[1..], val);
+ child.Children[pathRemain[0]] = grandChild;
+ }
+ _cache.PutNode(child);
+ if (prefix.Length > 0)
+ {
+ var exNode = Node.NewExtension(prefix.ToArray(), child);
+ _cache.PutNode(exNode);
+ node = exNode;
+ }
+ else
+ {
+ node = child;
+ }
+ break;
+ }
+ case NodeType.BranchNode:
+ {
+ var oldHash = node.Hash;
+ if (path.IsEmpty)
+ {
+ Put(ref node.Children[Node.BranchChildCount - 1], path, val);
+ }
+ else
+ {
+ Put(ref node.Children[path[0]], path[1..], val);
+ }
+ if (!_full) _cache.DeleteNode(oldHash);
+ node.SetDirty();
+ _cache.PutNode(node);
+ break;
+ }
+ case NodeType.Empty:
+ {
+ Node newNode;
+ if (path.IsEmpty)
+ {
+ newNode = val;
+ }
+ else
+ {
+ newNode = Node.NewExtension(path.ToArray(), val);
+ _cache.PutNode(newNode);
+ }
+ node = newNode;
+ if (val.Type == NodeType.LeafNode) _cache.PutNode(val);
+ break;
+ }
+ case NodeType.HashNode:
+ {
+ var newNode = _cache.Resolve(node.Hash)
+ ?? throw new InvalidOperationException("Internal error, can't resolve hash when mpt put");
+ node = newNode;
+ Put(ref node, path, val);
+ break;
+ }
+ default:
+ throw new InvalidOperationException("unsupport node type");
+ }
+ }
+}
diff --git a/plugins/MPTTrie/Trie.cs b/plugins/MPTTrie/Trie.cs
new file mode 100644
index 000000000..f4073fab0
--- /dev/null
+++ b/plugins/MPTTrie/Trie.cs
@@ -0,0 +1,59 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Trie.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 Neo.Persistence;
+
+namespace Neo.Cryptography.MPTTrie;
+
+public partial class Trie
+{
+ private const byte Prefix = 0xf0;
+ private readonly bool _full;
+ private Node _root;
+ private readonly Cache _cache;
+ public Node Root => _root;
+
+ public Trie(IStoreSnapshot store, UInt256? root, bool fullState = false)
+ {
+ ArgumentNullException.ThrowIfNull(store);
+ _cache = new Cache(store, Prefix);
+ _root = root is null ? new Node() : Node.NewHash(root);
+ _full = fullState;
+ }
+
+ private static byte[] ToNibbles(ReadOnlySpan path)
+ {
+ var result = new byte[path.Length * 2];
+ for (var i = 0; i < path.Length; i++)
+ {
+ result[i * 2] = (byte)(path[i] >> 4);
+ result[i * 2 + 1] = (byte)(path[i] & 0x0F);
+ }
+ return result;
+ }
+
+ private static byte[] FromNibbles(ReadOnlySpan path)
+ {
+ if (path.Length % 2 != 0) throw new FormatException($"MPTTrie.FromNibbles invalid path.");
+ var key = new byte[path.Length / 2];
+ for (var i = 0; i < key.Length; i++)
+ {
+ key[i] = (byte)(path[i * 2] << 4);
+ key[i] |= path[i * 2 + 1];
+ }
+ return key;
+ }
+
+ public void Commit()
+ {
+ _cache.Commit();
+ }
+}
diff --git a/plugins/OracleService/Helper.cs b/plugins/OracleService/Helper.cs
new file mode 100644
index 000000000..34d486ec4
--- /dev/null
+++ b/plugins/OracleService/Helper.cs
@@ -0,0 +1,49 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Helper.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System.Net;
+
+namespace Neo.Plugins.OracleService;
+
+static class Helper
+{
+ public static bool IsInternal(this IPHostEntry entry)
+ {
+ return entry.AddressList.Any(p => IsInternal(p));
+ }
+
+ ///
+ /// ::1 - IPv6 loopback
+ /// 10.0.0.0 - 10.255.255.255 (10/8 prefix)
+ /// 127.0.0.0 - 127.255.255.255 (127/8 prefix)
+ /// 172.16.0.0 - 172.31.255.255 (172.16/12 prefix)
+ /// 192.168.0.0 - 192.168.255.255 (192.168/16 prefix)
+ ///
+ /// Address
+ /// True if it was an internal address
+ public static bool IsInternal(this IPAddress ipAddress)
+ {
+ if (IPAddress.IsLoopback(ipAddress)) return true;
+ if (IPAddress.Broadcast.Equals(ipAddress)) return true;
+ if (IPAddress.Any.Equals(ipAddress)) return true;
+ if (IPAddress.IPv6Any.Equals(ipAddress)) return true;
+ if (IPAddress.IPv6Loopback.Equals(ipAddress)) return true;
+
+ var ip = ipAddress.GetAddressBytes();
+ return ip[0] switch
+ {
+ 10 or 127 => true,
+ 172 => ip[1] >= 16 && ip[1] < 32,
+ 192 => ip[1] == 168,
+ _ => false,
+ };
+ }
+}
diff --git a/plugins/OracleService/OracleService.cs b/plugins/OracleService/OracleService.cs
new file mode 100644
index 000000000..72c65ad7e
--- /dev/null
+++ b/plugins/OracleService/OracleService.cs
@@ -0,0 +1,597 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// OracleService.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 Akka.Actor;
+using Akka.Util.Internal;
+using Neo.ConsoleService;
+using Neo.Cryptography;
+using Neo.Cryptography.ECC;
+using Neo.Extensions;
+using Neo.Extensions.Collections;
+using Neo.Extensions.IO;
+using Neo.IEventHandlers;
+using Neo.Json;
+using Neo.Ledger;
+using Neo.Network.P2P;
+using Neo.Network.P2P.Payloads;
+using Neo.Persistence;
+using Neo.Plugins.OracleService.Protocols;
+using Neo.Plugins.RpcServer;
+using Neo.Sign;
+using Neo.SmartContract;
+using Neo.SmartContract.Manifest;
+using Neo.SmartContract.Native;
+using Neo.VM;
+using Neo.Wallets;
+using System.Collections.Concurrent;
+using System.Text;
+
+namespace Neo.Plugins.OracleService;
+
+public sealed class OracleService : Plugin, ICommittingHandler
+{
+ private const int RefreshIntervalMilliSeconds = 1000 * 60 * 3;
+
+ private static readonly HttpClient httpClient = new()
+ {
+ Timeout = TimeSpan.FromSeconds(5),
+ MaxResponseContentBufferSize = ushort.MaxValue
+ };
+
+ private Wallet wallet = null!;
+ private readonly ConcurrentDictionary pendingQueue = new();
+ private readonly ConcurrentDictionary finishedCache = new();
+ private Timer? timer;
+ internal readonly CancellationTokenSource cancelSource = new();
+ private OracleStatus status = OracleStatus.Unstarted;
+ private IWalletProvider? walletProvider;
+ private int counter;
+ private NeoSystem _system = null!;
+
+ private readonly Dictionary protocols = new();
+
+ public override string Description => "Built-in oracle plugin";
+
+ protected override UnhandledExceptionPolicy ExceptionPolicy => OracleSettings.Default.ExceptionPolicy;
+
+ public override string ConfigFile => System.IO.Path.Combine(RootPath, "OracleService.json");
+
+ public OracleService()
+ {
+ Blockchain.Committing += ((ICommittingHandler)this).Blockchain_Committing_Handler;
+ }
+
+ protected override void Configure()
+ {
+ OracleSettings.Load(GetConfiguration());
+ foreach (var (_, p) in protocols)
+ p.Configure();
+ }
+
+ protected override void OnSystemLoaded(NeoSystem system)
+ {
+ if (system.Settings.Network != OracleSettings.Default.Network) return;
+ _system = system;
+ _system.ServiceAdded += NeoSystem_ServiceAdded_Handler;
+ RpcServerPlugin.RegisterMethods(this, OracleSettings.Default.Network);
+ }
+
+
+ void NeoSystem_ServiceAdded_Handler(object? sender, object service)
+ {
+ if (service is IWalletProvider provider)
+ {
+ walletProvider = provider;
+ _system.ServiceAdded -= NeoSystem_ServiceAdded_Handler;
+ if (OracleSettings.Default.AutoStart)
+ {
+ walletProvider.WalletChanged += IWalletProvider_WalletChanged_Handler;
+ }
+ }
+ }
+
+ void IWalletProvider_WalletChanged_Handler(object? sender, Wallet? wallet)
+ {
+ if (wallet != null)
+ {
+ walletProvider!.WalletChanged -= IWalletProvider_WalletChanged_Handler;
+ Start(wallet);
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ Blockchain.Committing -= ((ICommittingHandler)this).Blockchain_Committing_Handler;
+ OnStop();
+ while (status != OracleStatus.Stopped)
+ Thread.Sleep(100);
+ foreach (var p in protocols)
+ p.Value.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ [ConsoleCommand("start oracle", Category = "Oracle", Description = "Start oracle service")]
+ private void OnStart()
+ {
+ Start(walletProvider?.GetWallet());
+ }
+
+ public Task Start(Wallet? wallet)
+ {
+ if (status == OracleStatus.Running) return Task.CompletedTask;
+
+ if (wallet is null)
+ {
+ ConsoleHelper.Warning("Please open wallet first!");
+ return Task.CompletedTask;
+ }
+
+ if (!CheckOracleAvailable(_system.StoreView, out ECPoint[] oracles))
+ {
+ ConsoleHelper.Warning("The oracle service is unavailable");
+ return Task.CompletedTask;
+ }
+ if (!CheckOracleAccount(wallet, oracles))
+ {
+ ConsoleHelper.Warning("There is no oracle account in wallet");
+ return Task.CompletedTask;
+ }
+
+ this.wallet = wallet;
+ protocols["https"] = new OracleHttpsProtocol();
+ protocols["neofs"] = new OracleNeoFSProtocol(wallet, oracles);
+ status = OracleStatus.Running;
+ timer = new Timer(OnTimer, null, RefreshIntervalMilliSeconds, Timeout.Infinite);
+ ConsoleHelper.Info($"Oracle started");
+ return ProcessRequestsAsync();
+ }
+
+ [ConsoleCommand("stop oracle", Category = "Oracle", Description = "Stop oracle service")]
+ private void OnStop()
+ {
+ cancelSource.Cancel();
+ if (timer != null)
+ {
+ timer.Dispose();
+ timer = null;
+ }
+ status = OracleStatus.Stopped;
+ }
+
+ [ConsoleCommand("oracle status", Category = "Oracle", Description = "Show oracle status")]
+ private void OnShow()
+ {
+ ConsoleHelper.Info($"Oracle status: ", $"{status}");
+ }
+
+ void ICommittingHandler.Blockchain_Committing_Handler(NeoSystem system, Block block, DataCache snapshot,
+ IReadOnlyList applicationExecutedList)
+ {
+ if (system.Settings.Network != OracleSettings.Default.Network) return;
+
+ if (OracleSettings.Default.AutoStart && status == OracleStatus.Unstarted)
+ {
+ OnStart();
+ }
+ if (status != OracleStatus.Running) return;
+ if (!CheckOracleAvailable(snapshot, out ECPoint[] oracles) || !CheckOracleAccount(wallet, oracles))
+ OnStop();
+ }
+
+ private async void OnTimer(object? state)
+ {
+ try
+ {
+ List outOfDate = new();
+ List tasks = new();
+ foreach (var (id, task) in pendingQueue)
+ {
+ var span = TimeProvider.Current.UtcNow - task.Timestamp;
+ if (span > OracleSettings.Default.MaxTaskTimeout)
+ {
+ outOfDate.Add(id);
+ continue;
+ }
+
+ if (span > TimeSpan.FromMilliseconds(RefreshIntervalMilliSeconds))
+ {
+ foreach (var account in wallet.GetAccounts())
+ if (task.BackupSigns.TryGetValue(account.GetKey()!.PublicKey, out byte[]? sign))
+ tasks.Add(SendResponseSignatureAsync(id, sign, account.GetKey()!));
+ }
+ }
+
+ await Task.WhenAll(tasks);
+
+ foreach (ulong requestId in outOfDate)
+ pendingQueue.TryRemove(requestId, out _);
+ foreach (var (key, value) in finishedCache)
+ if (TimeProvider.Current.UtcNow - value > TimeSpan.FromDays(3))
+ finishedCache.TryRemove(key, out _);
+ }
+ catch (Exception e)
+ {
+ Log(e, LogLevel.Error);
+ }
+ finally
+ {
+ if (!cancelSource.IsCancellationRequested)
+ timer?.Change(RefreshIntervalMilliSeconds, Timeout.Infinite);
+ }
+ }
+
+ ///
+ /// Submit oracle response
+ ///
+ /// Oracle public key, base64-encoded if access from json-rpc
+ /// Request id
+ /// Transaction signature, base64-encoded if access from json-rpc
+ /// Message signature, base64-encoded if access from json-rpc
+ /// JObject
+ [RpcMethod]
+ public JObject SubmitOracleResponse(byte[] oraclePubkey, ulong requestId, byte[] txSign, byte[] msgSign)
+ {
+ status.Equals(OracleStatus.Running).True_Or(RpcError.OracleDisabled);
+
+ var oraclePub = ECPoint.DecodePoint(oraclePubkey, ECCurve.Secp256r1);
+ finishedCache.ContainsKey(requestId).False_Or(RpcError.OracleRequestFinished);
+
+ using (var snapshot = _system.GetSnapshotCache())
+ {
+ var height = NativeContract.Ledger.CurrentIndex(snapshot) + 1;
+ var oracles = NativeContract.RoleManagement.GetDesignatedByRole(snapshot, Role.Oracle, height);
+
+ // Check if the oracle is designated
+ oracles.Any(p => p.Equals(oraclePub)).True_Or(RpcErrorFactory.OracleNotDesignatedNode(oraclePub));
+
+ // Check if the request exists
+ NativeContract.Oracle.GetRequest(snapshot, requestId).NotNull_Or(RpcError.OracleRequestNotFound);
+
+ // Check if the transaction signature is valid
+ byte[] data = [.. oraclePub.ToArray(), .. BitConverter.GetBytes(requestId), .. txSign];
+ Crypto.VerifySignature(data, msgSign, oraclePub)
+ .True_Or(RpcErrorFactory.InvalidSignature($"Invalid oracle response transaction signature from '{oraclePub}'."));
+ AddResponseTxSign(snapshot, requestId, oraclePub, txSign);
+ }
+ return new JObject();
+ }
+
+ private static async Task SendContentAsync(Uri url, string content)
+ {
+ try
+ {
+ using HttpResponseMessage response = await httpClient.PostAsync(url, new StringContent(content, Encoding.UTF8, "application/json"));
+ response.EnsureSuccessStatusCode();
+ }
+ catch (Exception e)
+ {
+ Log($"Failed to send the response signature to {url}, as {e.Message}", LogLevel.Warning);
+ }
+ }
+
+ private async Task SendResponseSignatureAsync(ulong requestId, byte[] txSign, KeyPair keyPair)
+ {
+ byte[] message = [.. keyPair.PublicKey.ToArray(), .. BitConverter.GetBytes(requestId), .. txSign];
+ var sign = Crypto.Sign(message, keyPair.PrivateKey);
+ var param = "\"" + Convert.ToBase64String(keyPair.PublicKey.ToArray()) + "\", " + requestId + ", \"" + Convert.ToBase64String(txSign) + "\",\"" + Convert.ToBase64String(sign) + "\"";
+ var content = "{\"id\":" + Interlocked.Increment(ref counter) + ",\"jsonrpc\":\"2.0\",\"method\":\"submitoracleresponse\",\"params\":[" + param + "]}";
+
+ var tasks = OracleSettings.Default.Nodes.Select(p => SendContentAsync(p, content));
+ await Task.WhenAll(tasks);
+ }
+
+ private async Task ProcessRequestAsync(DataCache snapshot, OracleRequest req)
+ {
+ Log($"[{req.OriginalTxid}] Process oracle request start:<{req.Url}>");
+
+ uint height = NativeContract.Ledger.CurrentIndex(snapshot) + 1;
+
+ (OracleResponseCode code, string? data) = await ProcessUrlAsync(req.Url);
+
+ Log($"[{req.OriginalTxid}] Process oracle request end:<{req.Url}>, responseCode:{code}, response:{data}");
+
+ var oracleNodes = NativeContract.RoleManagement.GetDesignatedByRole(snapshot, Role.Oracle, height);
+ foreach (var (requestId, request) in NativeContract.Oracle.GetRequestsByUrl(snapshot, req.Url))
+ {
+ var result = Array.Empty();
+ if (code == OracleResponseCode.Success)
+ {
+ try
+ {
+ result = Filter(data!, request.Filter);
+ }
+ catch (Exception ex)
+ {
+ code = OracleResponseCode.Error;
+ Log($"[{req.OriginalTxid}] Filter '{request.Filter}' error:{ex.Message}");
+ }
+ }
+ var response = new OracleResponse() { Id = requestId, Code = code, Result = result };
+ var responseTx = CreateResponseTx(snapshot, request, response, oracleNodes, _system.Settings);
+ var backupTx = CreateResponseTx(snapshot, request, new OracleResponse() { Code = OracleResponseCode.ConsensusUnreachable, Id = requestId, Result = Array.Empty() }, oracleNodes, _system.Settings, true);
+
+ Log($"[{req.OriginalTxid}]-({requestId}) Built response tx[[{responseTx.Hash}]], responseCode:{code}, result:{result.ToHexString()}, validUntilBlock:{responseTx.ValidUntilBlock}, backupTx:{backupTx.Hash}-{backupTx.ValidUntilBlock}");
+
+ var tasks = new List();
+ ECPoint[] oraclePublicKeys = NativeContract.RoleManagement.GetDesignatedByRole(snapshot, Role.Oracle, height);
+ foreach (var account in wallet.GetAccounts())
+ {
+ if (!account.HasKey || account.Lock) continue;
+ var key = account.GetKey()!;
+ if (!oraclePublicKeys.Contains(key.PublicKey)) continue;
+
+ var txSign = responseTx.Sign(key, _system.Settings.Network);
+ var backTxSign = backupTx.Sign(key, _system.Settings.Network);
+
+ AddResponseTxSign(snapshot, requestId, key.PublicKey, txSign, responseTx, backupTx, backTxSign);
+ tasks.Add(SendResponseSignatureAsync(requestId, txSign, key));
+
+ Log($"[{request.OriginalTxid}]-[[{responseTx.Hash}]] Send oracle sign data, Oracle node: {key.PublicKey}, Sign: {txSign.ToHexString()}");
+ }
+ await Task.WhenAll(tasks);
+ }
+ }
+
+ private async Task ProcessRequestsAsync()
+ {
+ while (!cancelSource.IsCancellationRequested)
+ {
+ using (var snapshot = _system.GetSnapshotCache())
+ {
+ SyncPendingQueue(snapshot);
+ foreach (var (id, request) in NativeContract.Oracle.GetRequests(snapshot))
+ {
+ if (cancelSource.IsCancellationRequested) break;
+ if (!finishedCache.ContainsKey(id) && (!pendingQueue.TryGetValue(id, out OracleTask? task) || task.Tx is null))
+ await ProcessRequestAsync(snapshot, request);
+ }
+ }
+ if (cancelSource.IsCancellationRequested) break;
+ await Task.Delay(500);
+ }
+
+ status = OracleStatus.Stopped;
+ }
+
+
+ private void SyncPendingQueue(DataCache snapshot)
+ {
+ var offChainRequests = NativeContract.Oracle.GetRequests(snapshot).ToDictionary(r => r.Item1, r => r.Item2);
+ var onChainRequests = pendingQueue.Keys.Except(offChainRequests.Keys);
+ foreach (var onChainRequest in onChainRequests)
+ {
+ pendingQueue.TryRemove(onChainRequest, out _);
+ }
+ }
+
+ private async Task<(OracleResponseCode, string?)> ProcessUrlAsync(string url)
+ {
+ if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
+ return (OracleResponseCode.Error, $"Invalid url:<{url}>");
+ if (!protocols.TryGetValue(uri.Scheme, out IOracleProtocol? protocol))
+ return (OracleResponseCode.ProtocolNotSupported, $"Invalid Protocol:<{url}>");
+
+ using CancellationTokenSource ctsTimeout = new(OracleSettings.Default.MaxOracleTimeout);
+ using CancellationTokenSource ctsLinked = CancellationTokenSource.CreateLinkedTokenSource(cancelSource.Token, ctsTimeout.Token);
+
+ try
+ {
+ return await protocol.ProcessAsync(uri, ctsLinked.Token);
+ }
+ catch (Exception ex)
+ {
+ return (OracleResponseCode.Error, $"Request <{url}> Error:{ex.Message}");
+ }
+ }
+
+ public static Transaction CreateResponseTx(DataCache snapshot, OracleRequest request, OracleResponse response, ECPoint[] oracleNodes, ProtocolSettings settings, bool useCurrentHeight = false)
+ {
+ var requestTx = NativeContract.Ledger.GetTransactionState(snapshot, request.OriginalTxid)!;
+ var n = oracleNodes.Length;
+ var m = n - (n - 1) / 3;
+ var oracleSignContract = Contract.CreateMultiSigContract(m, oracleNodes);
+ uint height = NativeContract.Ledger.CurrentIndex(snapshot);
+ var maxVUB = settings.MaxValidUntilBlockIncrement;
+ var validUntilBlock = requestTx.BlockIndex + maxVUB;
+ while (useCurrentHeight && validUntilBlock <= height)
+ {
+ validUntilBlock += maxVUB;
+ }
+ var tx = new Transaction()
+ {
+ Version = 0,
+ Nonce = unchecked((uint)response.Id),
+ ValidUntilBlock = validUntilBlock,
+ Signers = [
+ new() { Account = NativeContract.Oracle.Hash, Scopes = WitnessScope.None },
+ new() { Account = oracleSignContract.ScriptHash, Scopes = WitnessScope.None }
+ ],
+ Attributes = [response],
+ Script = OracleResponse.FixedScript,
+ Witnesses = new Witness[2]
+ };
+
+ var witnessDict = new Dictionary
+ {
+ [oracleSignContract.ScriptHash] = new Witness
+ {
+ InvocationScript = ReadOnlyMemory.Empty,
+ VerificationScript = oracleSignContract.Script,
+ },
+ [NativeContract.Oracle.Hash] = Witness.Empty,
+ };
+
+ UInt160[] hashes = tx.GetScriptHashesForVerifying(snapshot);
+ tx.Witnesses[0] = witnessDict[hashes[0]];
+ tx.Witnesses[1] = witnessDict[hashes[1]];
+
+ // Calculate network fee
+
+ var oracleContract = NativeContract.ContractManagement.GetContract(snapshot, NativeContract.Oracle.Hash)!;
+ var engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshot.CloneCache(), settings: settings);
+ ContractMethodDescriptor md = oracleContract.Manifest.Abi.GetMethod(ContractBasicMethod.Verify, ContractBasicMethod.VerifyPCount)!;
+ engine.LoadContract(oracleContract, md, CallFlags.None);
+ engine.Execute(); //FAULT is impossible
+ tx.NetworkFee += engine.FeeConsumed;
+
+ var executionFactor = NativeContract.Policy.GetExecFeeFactor(snapshot);
+ var networkFee = executionFactor * SmartContract.Helper.MultiSignatureContractCost(m, n);
+ tx.NetworkFee += networkFee;
+
+ // Base size for transaction: includes const_header + signers + script + hashes + witnesses, except attributes
+
+ int sizeInv = 66 * m;
+ int size = Transaction.HeaderSize + tx.Signers.GetVarSize() + tx.Script.GetVarSize()
+ + hashes.Length.GetVarSize() + witnessDict[NativeContract.Oracle.Hash].Size
+ + sizeInv.GetVarSize() + sizeInv + oracleSignContract.Script.GetVarSize();
+
+ var feePerByte = NativeContract.Policy.GetFeePerByte(snapshot);
+ if (response.Result.Length > OracleResponse.MaxResultSize)
+ {
+ response.Code = OracleResponseCode.ResponseTooLarge;
+ response.Result = Array.Empty();
+ }
+ else if (tx.NetworkFee + (size + tx.Attributes.GetVarSize()) * feePerByte > request.GasForResponse)
+ {
+ response.Code = OracleResponseCode.InsufficientFunds;
+ response.Result = Array.Empty();
+ }
+ size += tx.Attributes.GetVarSize();
+ tx.NetworkFee += size * feePerByte;
+
+ // Calcualte system fee
+
+ tx.SystemFee = request.GasForResponse - tx.NetworkFee;
+
+ return tx;
+ }
+
+ private void AddResponseTxSign(DataCache snapshot, ulong requestId, ECPoint oraclePub, byte[] sign, Transaction? responseTx = null, Transaction? backupTx = null, byte[]? backupSign = null)
+ {
+ var task = pendingQueue.GetOrAdd(requestId, _ => new OracleTask
+ {
+ Id = requestId,
+ Request = NativeContract.Oracle.GetRequest(snapshot, requestId)!,
+ Signs = new ConcurrentDictionary(),
+ BackupSigns = new ConcurrentDictionary()
+ });
+
+ if (responseTx != null)
+ {
+ task.Tx = responseTx;
+ var data = task.Tx.GetSignData(_system.Settings.Network);
+ task.Signs.Where(p => !Crypto.VerifySignature(data, p.Value, p.Key)).ForEach(p => task.Signs.Remove(p.Key, out _));
+ }
+ if (backupTx != null)
+ {
+ task.BackupTx = backupTx;
+ var data = task.BackupTx.GetSignData(_system.Settings.Network);
+ task.BackupSigns.Where(p => !Crypto.VerifySignature(data, p.Value, p.Key)).ForEach(p => task.BackupSigns.Remove(p.Key, out _));
+ task.BackupSigns.TryAdd(oraclePub, backupSign!);
+ }
+ if (task.Tx == null)
+ {
+ task.Signs.TryAdd(oraclePub, sign);
+ task.BackupSigns.TryAdd(oraclePub, sign);
+ return;
+ }
+
+ if (Crypto.VerifySignature(task.Tx.GetSignData(_system.Settings.Network), sign, oraclePub))
+ task.Signs.TryAdd(oraclePub, sign);
+ else if (Crypto.VerifySignature(task.BackupTx!.GetSignData(_system.Settings.Network), sign, oraclePub))
+ task.BackupSigns.TryAdd(oraclePub, sign);
+ else
+ throw new RpcException(RpcErrorFactory.InvalidSignature($"Invalid oracle response transaction signature from '{oraclePub}'."));
+
+ if (CheckTxSign(snapshot, task.Tx, task.Signs) || CheckTxSign(snapshot, task.BackupTx!, task.BackupSigns))
+ {
+ finishedCache.TryAdd(requestId, new DateTime());
+ pendingQueue.TryRemove(requestId, out _);
+ }
+ }
+
+ public static byte[] Filter(string input, string? filterArgs)
+ {
+ if (string.IsNullOrEmpty(filterArgs))
+ return input.ToStrictUtf8Bytes();
+
+ JToken? beforeObject = JToken.Parse(input);
+ JArray afterObjects = beforeObject?.JsonPath(filterArgs) ?? new();
+ return afterObjects.ToByteArray(false);
+ }
+
+ private bool CheckTxSign(DataCache snapshot, Transaction tx, ConcurrentDictionary OracleSigns)
+ {
+ uint height = NativeContract.Ledger.CurrentIndex(snapshot) + 1;
+ if (tx.ValidUntilBlock <= height)
+ {
+ return false;
+ }
+ ECPoint[] oraclesNodes = NativeContract.RoleManagement.GetDesignatedByRole(snapshot, Role.Oracle, height);
+ int neededThreshold = oraclesNodes.Length - (oraclesNodes.Length - 1) / 3;
+ if (OracleSigns.Count >= neededThreshold)
+ {
+ var contract = Contract.CreateMultiSigContract(neededThreshold, oraclesNodes);
+ var sb = new ScriptBuilder();
+ foreach (var (_, sign) in OracleSigns.OrderBy(p => p.Key))
+ {
+ sb.EmitPush(sign);
+ if (--neededThreshold == 0) break;
+ }
+ var idx = tx.GetScriptHashesForVerifying(snapshot)[0] == contract.ScriptHash ? 0 : 1;
+ tx.Witnesses[idx].InvocationScript = sb.ToArray();
+
+ Log($"Send response tx: responseTx={tx.Hash}");
+
+ _system.Blockchain.Tell(tx);
+ return true;
+ }
+ return false;
+ }
+
+ private static bool CheckOracleAvailable(DataCache snapshot, out ECPoint[] oracles)
+ {
+ uint height = NativeContract.Ledger.CurrentIndex(snapshot) + 1;
+ oracles = NativeContract.RoleManagement.GetDesignatedByRole(snapshot, Role.Oracle, height);
+ return oracles.Length > 0;
+ }
+
+ private static bool CheckOracleAccount(ISigner signer, ECPoint[] oracles)
+ {
+ return signer is not null && oracles.Any(p => signer.ContainsSignable(p));
+ }
+
+ private static void Log(string message, LogLevel level = LogLevel.Info)
+ {
+ Utility.Log(nameof(OracleService), level, message);
+ }
+
+ class OracleTask
+ {
+ public ulong Id;
+ public required OracleRequest Request;
+ public Transaction? Tx;
+ public Transaction? BackupTx;
+ public required ConcurrentDictionary Signs;
+ public required ConcurrentDictionary BackupSigns;
+ public readonly DateTime Timestamp = TimeProvider.Current.UtcNow;
+ }
+
+ enum OracleStatus
+ {
+ Unstarted,
+ Running,
+ Stopped,
+ }
+}
diff --git a/plugins/OracleService/OracleService.csproj b/plugins/OracleService/OracleService.csproj
new file mode 100644
index 000000000..ba7d22e71
--- /dev/null
+++ b/plugins/OracleService/OracleService.csproj
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+ false
+ runtime
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
diff --git a/plugins/OracleService/OracleService.json b/plugins/OracleService/OracleService.json
new file mode 100644
index 000000000..49bf1153b
--- /dev/null
+++ b/plugins/OracleService/OracleService.json
@@ -0,0 +1,22 @@
+{
+ "PluginConfiguration": {
+ "Network": 860833102,
+ "Nodes": [],
+ "MaxTaskTimeout": 432000000,
+ "MaxOracleTimeout": 10000,
+ "AllowPrivateHost": false,
+ "AllowedContentTypes": [ "application/json" ],
+ "UnhandledExceptionPolicy": "Ignore",
+ "Https": {
+ "Timeout": 5000
+ },
+ "NeoFS": {
+ "EndPoint": "http://127.0.0.1:8080",
+ "Timeout": 15000
+ },
+ "AutoStart": false
+ },
+ "Dependency": [
+ "RpcServer"
+ ]
+}
diff --git a/plugins/OracleService/OracleSettings.cs b/plugins/OracleService/OracleSettings.cs
new file mode 100644
index 000000000..c60b6d315
--- /dev/null
+++ b/plugins/OracleService/OracleSettings.cs
@@ -0,0 +1,75 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// OracleSettings.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 Akka.Util.Internal;
+using Microsoft.Extensions.Configuration;
+
+namespace Neo.Plugins.OracleService;
+
+class HttpsSettings
+{
+ public TimeSpan Timeout { get; }
+
+ public HttpsSettings(IConfigurationSection section)
+ {
+ Timeout = TimeSpan.FromMilliseconds(section.GetValue("Timeout", 5000));
+ }
+}
+
+class NeoFSSettings
+{
+ public string EndPoint { get; }
+ public TimeSpan Timeout { get; }
+
+ public NeoFSSettings(IConfigurationSection section)
+ {
+ EndPoint = section.GetValue("EndPoint", "127.0.0.1:8080");
+ Timeout = TimeSpan.FromMilliseconds(section.GetValue("Timeout", 15000));
+ }
+}
+
+class OracleSettings : IPluginSettings
+{
+ public uint Network { get; }
+ public Uri[] Nodes { get; }
+ public TimeSpan MaxTaskTimeout { get; }
+ public TimeSpan MaxOracleTimeout { get; }
+ public bool AllowPrivateHost { get; }
+ public string[] AllowedContentTypes { get; }
+ public HttpsSettings Https { get; }
+ public NeoFSSettings NeoFS { get; }
+ public bool AutoStart { get; }
+
+ public static OracleSettings Default { get; private set; } = null!;
+
+ public UnhandledExceptionPolicy ExceptionPolicy { get; }
+
+ private OracleSettings(IConfigurationSection section)
+ {
+ Network = section.GetValue("Network", 5195086u);
+ Nodes = section.GetSection("Nodes").GetChildren().Select(p => new Uri(p.Get()!, UriKind.Absolute)).ToArray();
+ MaxTaskTimeout = TimeSpan.FromMilliseconds(section.GetValue("MaxTaskTimeout", 432000000));
+ MaxOracleTimeout = TimeSpan.FromMilliseconds(section.GetValue("MaxOracleTimeout", 15000));
+ AllowPrivateHost = section.GetValue("AllowPrivateHost", false);
+ AllowedContentTypes = section.GetSection("AllowedContentTypes").GetChildren().Select(p => p.Get()!).ToArray();
+ ExceptionPolicy = section.GetValue("UnhandledExceptionPolicy", UnhandledExceptionPolicy.Ignore);
+ if (AllowedContentTypes.Length == 0)
+ AllowedContentTypes = AllowedContentTypes.Concat("application/json").ToArray();
+ Https = new HttpsSettings(section.GetSection("Https"));
+ NeoFS = new NeoFSSettings(section.GetSection("NeoFS"));
+ AutoStart = section.GetValue("AutoStart", false);
+ }
+
+ public static void Load(IConfigurationSection section)
+ {
+ Default = new OracleSettings(section);
+ }
+}
diff --git a/plugins/OracleService/Protocols/IOracleProtocol.cs b/plugins/OracleService/Protocols/IOracleProtocol.cs
new file mode 100644
index 000000000..5520e99ba
--- /dev/null
+++ b/plugins/OracleService/Protocols/IOracleProtocol.cs
@@ -0,0 +1,20 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// IOracleProtocol.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 Neo.Network.P2P.Payloads;
+
+namespace Neo.Plugins.OracleService.Protocols;
+
+interface IOracleProtocol : IDisposable
+{
+ void Configure();
+ Task<(OracleResponseCode, string?)> ProcessAsync(Uri uri, CancellationToken cancellation);
+}
diff --git a/plugins/OracleService/Protocols/OracleHttpsProtocol.cs b/plugins/OracleService/Protocols/OracleHttpsProtocol.cs
new file mode 100644
index 000000000..73715b0de
--- /dev/null
+++ b/plugins/OracleService/Protocols/OracleHttpsProtocol.cs
@@ -0,0 +1,111 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// OracleHttpsProtocol.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 Neo.Network.P2P.Payloads;
+using System.Net;
+using System.Net.Http.Headers;
+using System.Reflection;
+using System.Text;
+
+namespace Neo.Plugins.OracleService.Protocols;
+
+class OracleHttpsProtocol : IOracleProtocol
+{
+ private readonly HttpClient client = new(new HttpClientHandler() { AllowAutoRedirect = false });
+
+ public OracleHttpsProtocol()
+ {
+ CustomAttributeData attribute = Assembly.GetExecutingAssembly().CustomAttributes.First(p => p.AttributeType == typeof(AssemblyInformationalVersionAttribute));
+ string version = (string)attribute.ConstructorArguments[0].Value!;
+ client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("NeoOracleService", version));
+ }
+
+ public void Configure()
+ {
+ client.DefaultRequestHeaders.Accept.Clear();
+ foreach (string type in OracleSettings.Default.AllowedContentTypes)
+ client.DefaultRequestHeaders.Accept.ParseAdd(type);
+ client.Timeout = OracleSettings.Default.Https.Timeout;
+ }
+
+ public void Dispose()
+ {
+ client.Dispose();
+ }
+
+ public async Task<(OracleResponseCode, string?)> ProcessAsync(Uri uri, CancellationToken cancellation)
+ {
+ Utility.Log(nameof(OracleHttpsProtocol), LogLevel.Debug, $"Request: {uri.AbsoluteUri}");
+
+ HttpResponseMessage? message;
+ try
+ {
+ int redirects = 2;
+ do
+ {
+ if (!OracleSettings.Default.AllowPrivateHost)
+ {
+ IPHostEntry entry = await Dns.GetHostEntryAsync(uri.Host, cancellation);
+ if (entry.IsInternal())
+ return (OracleResponseCode.Forbidden, null);
+ }
+ message = await client.GetAsync(uri, HttpCompletionOption.ResponseContentRead, cancellation);
+ if (message.Headers.Location is not null)
+ {
+ uri = message.Headers.Location;
+ message = null;
+ }
+ } while (message == null && redirects-- > 0);
+ }
+ catch
+ {
+ return (OracleResponseCode.Timeout, null);
+ }
+ if (message is null)
+ return (OracleResponseCode.Timeout, null);
+ if (message.StatusCode == HttpStatusCode.NotFound)
+ return (OracleResponseCode.NotFound, null);
+ if (message.StatusCode == HttpStatusCode.Forbidden)
+ return (OracleResponseCode.Forbidden, null);
+ if (!message.IsSuccessStatusCode)
+ return (OracleResponseCode.Error, message.StatusCode.ToString());
+ if (message.Content.Headers.ContentType is null)
+ return (OracleResponseCode.ContentTypeNotSupported, null);
+ if (!OracleSettings.Default.AllowedContentTypes.Contains(message.Content.Headers.ContentType.MediaType))
+ return (OracleResponseCode.ContentTypeNotSupported, null);
+ if (message.Content.Headers.ContentLength.HasValue && message.Content.Headers.ContentLength > OracleResponse.MaxResultSize)
+ return (OracleResponseCode.ResponseTooLarge, null);
+
+ byte[] buffer = new byte[OracleResponse.MaxResultSize + 1];
+ var stream = message.Content.ReadAsStream(cancellation);
+ var read = await stream.ReadAsync(buffer, cancellation);
+
+ if (read > OracleResponse.MaxResultSize)
+ return (OracleResponseCode.ResponseTooLarge, null);
+
+ var encoding = GetEncoding(message.Content.Headers);
+ if (!encoding.Equals(Encoding.UTF8))
+ return (OracleResponseCode.Error, null);
+
+ return (OracleResponseCode.Success, buffer.ToStrictUtf8String(0, read));
+ }
+
+ private static Encoding GetEncoding(HttpContentHeaders headers)
+ {
+ Encoding? encoding = null;
+ if ((headers.ContentType != null) && (headers.ContentType.CharSet != null))
+ {
+ encoding = Encoding.GetEncoding(headers.ContentType.CharSet);
+ }
+
+ return encoding ?? Encoding.UTF8;
+ }
+}
diff --git a/plugins/OracleService/Protocols/OracleNeoFSProtocol.cs b/plugins/OracleService/Protocols/OracleNeoFSProtocol.cs
new file mode 100644
index 000000000..a1d399638
--- /dev/null
+++ b/plugins/OracleService/Protocols/OracleNeoFSProtocol.cs
@@ -0,0 +1,152 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// OracleNeoFSProtocol.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 Neo.FileStorage.API.Client;
+using Neo.FileStorage.API.Cryptography;
+using Neo.FileStorage.API.Refs;
+using Neo.Network.P2P.Payloads;
+using Neo.Wallets;
+using System.Security.Cryptography;
+using System.Web;
+using ECPoint = Neo.Cryptography.ECC.ECPoint;
+using Object = Neo.FileStorage.API.Object.Object;
+using Range = Neo.FileStorage.API.Object.Range;
+
+namespace Neo.Plugins.OracleService.Protocols;
+
+class OracleNeoFSProtocol : IOracleProtocol
+{
+ private readonly ECDsa privateKey;
+
+ public OracleNeoFSProtocol(Wallet wallet, ECPoint[] oracles)
+ {
+ byte[] key = oracles.Select(wallet.GetAccount)
+ .Where(p => p is not null && p.HasKey && !p.Lock)
+ .FirstOrDefault()?
+ .GetKey()?
+ .PrivateKey ?? throw new InvalidOperationException("No available account found for oracle");
+ privateKey = key.LoadPrivateKey();
+ }
+
+ public void Configure() { }
+
+ public void Dispose()
+ {
+ privateKey.Dispose();
+ }
+
+ public async Task<(OracleResponseCode, string?)> ProcessAsync(Uri uri, CancellationToken cancellation)
+ {
+ Utility.Log(nameof(OracleNeoFSProtocol), LogLevel.Debug, $"Request: {uri.AbsoluteUri}");
+ try
+ {
+ (OracleResponseCode code, string data) = await GetAsync(uri, OracleSettings.Default.NeoFS.EndPoint, cancellation);
+ Utility.Log(nameof(OracleNeoFSProtocol), LogLevel.Debug, $"NeoFS result, code: {code}, data: {data}");
+ return (code, data);
+ }
+ catch (Exception e)
+ {
+ Utility.Log(nameof(OracleNeoFSProtocol), LogLevel.Debug, $"NeoFS result: error,{e.Message}");
+ return (OracleResponseCode.Error, null);
+ }
+ }
+
+
+ ///
+ /// GetAsync returns neofs object from the provided url.
+ /// If Command is not provided, full object is requested.
+ ///
+ /// URI scheme is "neofs:ContainerID/ObjectID/Command/offset|length".
+ /// Client host.
+ /// Cancellation token object.
+ /// Returns neofs object.
+ private async Task<(OracleResponseCode, string)> GetAsync(Uri uri, string host, CancellationToken cancellation)
+ {
+ string[] ps = uri.AbsolutePath.Split("/");
+ if (ps.Length < 2) throw new FormatException("Invalid neofs url");
+ ContainerID containerID = ContainerID.FromString(ps[0]);
+ ObjectID objectID = ObjectID.FromString(ps[1]);
+ Address objectAddr = new()
+ {
+ ContainerId = containerID,
+ ObjectId = objectID
+ };
+ using Client client = new(privateKey, host);
+ var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
+ tokenSource.CancelAfter(OracleSettings.Default.NeoFS.Timeout);
+ if (ps.Length == 2)
+ return GetPayload(client, objectAddr, tokenSource.Token);
+ return ps[2] switch
+ {
+ "range" => await GetRangeAsync(client, objectAddr, ps.Skip(3).ToArray(), tokenSource.Token),
+ "header" => (OracleResponseCode.Success, await GetHeaderAsync(client, objectAddr, tokenSource.Token)),
+ "hash" => (OracleResponseCode.Success, await GetHashAsync(client, objectAddr, ps.Skip(3).ToArray(), tokenSource.Token)),
+ _ => throw new Exception("invalid command")
+ };
+ }
+
+ private static (OracleResponseCode, string) GetPayload(Client client, Address addr, CancellationToken cancellation)
+ {
+ var objReader = client.GetObjectInit(addr, options: new CallOptions { Ttl = 2 }, context: cancellation);
+ var obj = objReader.ReadHeader();
+ if (obj.PayloadSize > OracleResponse.MaxResultSize)
+ return (OracleResponseCode.ResponseTooLarge, "");
+ var payload = new byte[obj.PayloadSize];
+ int offset = 0;
+ while (true)
+ {
+ if ((ulong)offset > obj.PayloadSize) return (OracleResponseCode.ResponseTooLarge, "");
+ (byte[] chunk, bool ok) = objReader.ReadChunk();
+ if (!ok) break;
+ Array.Copy(chunk, 0, payload, offset, chunk.Length);
+ offset += chunk.Length;
+ }
+ return (OracleResponseCode.Success, payload.ToStrictUtf8String());
+ }
+
+ private static async Task<(OracleResponseCode, string)> GetRangeAsync(Client client, Address addr, string[] ps, CancellationToken cancellation)
+ {
+ if (ps.Length == 0) throw new FormatException("missing object range (expected 'Offset|Length')");
+ Range range = ParseRange(ps[0]);
+ if (range.Length > OracleResponse.MaxResultSize) return (OracleResponseCode.ResponseTooLarge, "");
+ var res = await client.GetObjectPayloadRangeData(addr, range, options: new CallOptions { Ttl = 2 }, context: cancellation);
+ return (OracleResponseCode.Success, res.ToStrictUtf8String());
+ }
+
+ private static async Task GetHeaderAsync(Client client, Address addr, CancellationToken cancellation)
+ {
+ var obj = await client.GetObjectHeader(addr, options: new CallOptions { Ttl = 2 }, context: cancellation);
+ return obj.ToString();
+ }
+
+ private static async Task GetHashAsync(Client client, Address addr, string[] ps, CancellationToken cancellation)
+ {
+ if (ps.Length == 0 || ps[0] == "")
+ {
+ Object obj = await client.GetObjectHeader(addr, options: new CallOptions { Ttl = 2 }, context: cancellation);
+ return $"\"{new UInt256(obj.PayloadChecksum.Sum.ToByteArray())}\"";
+ }
+ Range range = ParseRange(ps[0]);
+ List hashes = await client.GetObjectPayloadRangeHash(addr, new List() { range }, ChecksumType.Sha256, Array.Empty(), new CallOptions { Ttl = 2 }, cancellation);
+ if (hashes.Count == 0) throw new Exception("empty response, object range is invalid (expected 'Offset|Length')");
+ return $"\"{new UInt256(hashes[0])}\"";
+ }
+
+ private static Range ParseRange(string s)
+ {
+ string url = HttpUtility.UrlDecode(s);
+ int sepIndex = url.IndexOf('|');
+ if (sepIndex < 0) throw new Exception("object range is invalid (expected 'Offset|Length')");
+ ulong offset = ulong.Parse(url[..sepIndex]);
+ ulong length = ulong.Parse(url[(sepIndex + 1)..]);
+ return new Range() { Offset = offset, Length = length };
+ }
+}
diff --git a/plugins/RestServer/Authentication/BasicAuthenticationHandler.cs b/plugins/RestServer/Authentication/BasicAuthenticationHandler.cs
new file mode 100644
index 000000000..3f24890e9
--- /dev/null
+++ b/plugins/RestServer/Authentication/BasicAuthenticationHandler.cs
@@ -0,0 +1,61 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// BasicAuthenticationHandler.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 Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Neo.Plugins.RestServer;
+using System.Net.Http.Headers;
+using System.Security.Claims;
+using System.Text;
+using System.Text.Encodings.Web;
+
+namespace RestServer.Authentication;
+
+internal class BasicAuthenticationHandler : AuthenticationHandler
+{
+ public BasicAuthenticationHandler(
+ IOptionsMonitor options,
+ ILoggerFactory logger,
+ UrlEncoder encoder) : base(options, logger, encoder)
+ {
+ }
+
+ protected override Task HandleAuthenticateAsync()
+ {
+ var authHeader = Request.Headers.Authorization;
+ if (string.IsNullOrEmpty(authHeader) == false && AuthenticationHeaderValue.TryParse(authHeader, out var authValue))
+ {
+ if (authValue.Scheme.Equals("basic", StringComparison.OrdinalIgnoreCase) && authValue.Parameter != null)
+ {
+ try
+ {
+ var decodedParams = Encoding.UTF8.GetString(Convert.FromBase64String(authValue.Parameter));
+ var creds = decodedParams.Split(':', 2);
+
+ if (creds.Length == 2 && creds[0] == RestServerSettings.Current.RestUser && creds[1] == RestServerSettings.Current.RestPass)
+ {
+ var claims = new[] { new Claim(ClaimTypes.NameIdentifier, creds[0]) };
+ var identity = new ClaimsIdentity(claims, Scheme.Name);
+ var principal = new ClaimsPrincipal(identity);
+ var ticket = new AuthenticationTicket(principal, Scheme.Name);
+
+ return Task.FromResult(AuthenticateResult.Success(ticket));
+ }
+ }
+ catch (FormatException)
+ {
+ }
+ }
+ }
+ return Task.FromResult(AuthenticateResult.Fail("Authentication Failed!"));
+ }
+}
diff --git a/plugins/RestServer/Binder/UInt160Binder.cs b/plugins/RestServer/Binder/UInt160Binder.cs
new file mode 100644
index 000000000..e51b48e42
--- /dev/null
+++ b/plugins/RestServer/Binder/UInt160Binder.cs
@@ -0,0 +1,46 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// UInt160Binder.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 Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Neo.Plugins.RestServer.Binder;
+
+internal class UInt160Binder : IModelBinder
+{
+ public Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ _ = bindingContext ?? throw new ArgumentNullException(nameof(bindingContext));
+
+ if (bindingContext.BindingSource == BindingSource.Path ||
+ bindingContext.BindingSource == BindingSource.Query)
+ {
+ var modelName = bindingContext.ModelName;
+
+ // Try to fetch the value of the argument by name
+ var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
+
+ if (valueProviderResult == ValueProviderResult.None)
+ return Task.CompletedTask;
+
+ bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
+
+ var value = valueProviderResult.FirstValue;
+
+ // Check if the argument value is null or empty
+ if (string.IsNullOrEmpty(value))
+ return Task.CompletedTask;
+
+ var model = RestServerUtility.ConvertToScriptHash(value, RestServerPlugin.NeoSystem!.Settings);
+ bindingContext.Result = ModelBindingResult.Success(model);
+ }
+ return Task.CompletedTask;
+ }
+}
diff --git a/plugins/RestServer/Binder/UInt160BinderProvider.cs b/plugins/RestServer/Binder/UInt160BinderProvider.cs
new file mode 100644
index 000000000..6104d8ac2
--- /dev/null
+++ b/plugins/RestServer/Binder/UInt160BinderProvider.cs
@@ -0,0 +1,30 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// UInt160BinderProvider.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 Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
+
+namespace Neo.Plugins.RestServer.Binder;
+
+internal class NeoBinderProvider : IModelBinderProvider
+{
+ public IModelBinder? GetBinder(ModelBinderProviderContext context)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+
+ if (context.Metadata.ModelType == typeof(UInt160))
+ {
+ return new BinderTypeModelBinder(typeof(UInt160Binder));
+ }
+
+ return null;
+ }
+}
diff --git a/plugins/RestServer/Controllers/v1/ContractsController.cs b/plugins/RestServer/Controllers/v1/ContractsController.cs
new file mode 100644
index 000000000..ddb0be562
--- /dev/null
+++ b/plugins/RestServer/Controllers/v1/ContractsController.cs
@@ -0,0 +1,212 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractsController.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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Neo.Extensions.SmartContract;
+using Neo.Plugins.RestServer.Exceptions;
+using Neo.Plugins.RestServer.Extensions;
+using Neo.Plugins.RestServer.Helpers;
+using Neo.Plugins.RestServer.Models;
+using Neo.Plugins.RestServer.Models.Contract;
+using Neo.Plugins.RestServer.Models.Error;
+using Neo.SmartContract;
+using Neo.SmartContract.Manifest;
+using Neo.SmartContract.Native;
+using System.Net.Mime;
+
+namespace Neo.Plugins.RestServer.Controllers.v1;
+
+[Route("/api/v{version:apiVersion}/contracts")]
+[Produces(MediaTypeNames.Application.Json)]
+[Consumes(MediaTypeNames.Application.Json)]
+[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorModel))]
+[ApiVersion("1.0")]
+[ApiController]
+public class ContractsController : ControllerBase
+{
+ private readonly NeoSystem _neoSystem;
+
+ public ContractsController()
+ {
+ _neoSystem = RestServerPlugin.NeoSystem ?? throw new NodeNetworkException();
+ }
+
+ ///
+ /// Get all the smart contracts from the blockchain.
+ ///
+ /// Page
+ /// Page Size
+ /// An array of Contract object.
+ /// No more pages.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet(Name = "GetContracts")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ContractState[]))]
+ public IActionResult Get(
+ [FromQuery(Name = "page")]
+ int skip = 1,
+ [FromQuery(Name = "size")]
+ int take = 50)
+ {
+ if (skip < 1 || take < 1 || take > RestServerSettings.Current.MaxPageSize)
+ throw new InvalidParameterRangeException();
+ var contracts = NativeContract.ContractManagement.ListContracts(_neoSystem.StoreView);
+ if (contracts.Any() == false)
+ return NoContent();
+ var contractRequestList = contracts.OrderBy(o => o.Id).Skip((skip - 1) * take).Take(take);
+ if (contractRequestList.Any() == false)
+ return NoContent();
+ return Ok(contractRequestList);
+ }
+
+ ///
+ /// Gets count of total smart contracts on blockchain.
+ ///
+ /// Count Object
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("count", Name = "GetContractCount")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(CountModel))]
+ public IActionResult GetCount()
+ {
+ var contracts = NativeContract.ContractManagement.ListContracts(_neoSystem.StoreView);
+ return Ok(new CountModel() { Count = contracts.Count() });
+ }
+
+ ///
+ /// Get a smart contract's storage.
+ ///
+ /// ScriptHash
+ /// An array of the Key (Base64) Value (Base64) Pairs objects.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("{hash:required}/storage", Name = "GetContractStorage")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(KeyValuePair, ReadOnlyMemory>[]))]
+ public IActionResult GetContractStorage(
+ [FromRoute(Name = "hash")]
+ UInt160 scriptHash)
+ {
+ if (NativeContract.IsNative(scriptHash))
+ return NoContent();
+ var contract = NativeContract.ContractManagement.GetContract(_neoSystem.StoreView, scriptHash)
+ ?? throw new ContractNotFoundException(scriptHash);
+ var contractStorage = contract.FindStorage(_neoSystem.StoreView);
+ return Ok(contractStorage.Select(s => new KeyValuePair, ReadOnlyMemory>(s.Key.Key, s.Value.Value)));
+ }
+
+ ///
+ /// Get a smart contract.
+ ///
+ /// ScriptHash
+ /// Contract Object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("{hash:required}", Name = "GetContract")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ContractState))]
+ public IActionResult GetByScriptHash(
+ [FromRoute(Name = "hash")]
+ UInt160 scriptHash)
+ {
+ var contract = NativeContract.ContractManagement.GetContract(_neoSystem.StoreView, scriptHash)
+ ?? throw new ContractNotFoundException(scriptHash);
+ return Ok(contract);
+ }
+
+ ///
+ /// Get abi of a smart contract.
+ ///
+ /// ScriptHash
+ /// Contract Abi Object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("{hash:required}/abi", Name = "GetContractAbi")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ContractAbi))]
+ public IActionResult GetContractAbi(
+ [FromRoute(Name = "hash")]
+ UInt160 scriptHash)
+ {
+ var contract = NativeContract.ContractManagement.GetContract(_neoSystem.StoreView, scriptHash)
+ ?? throw new ContractNotFoundException(scriptHash);
+ return Ok(contract.Manifest.Abi);
+ }
+
+ ///
+ /// Get manifest of a smart contract.
+ ///
+ /// ScriptHash
+ /// Contract Manifest object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("{hash:required}/manifest", Name = "GetContractManifest")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ContractManifest))]
+ public IActionResult GetContractManifest(
+ [FromRoute(Name = "hash")]
+ UInt160 scriptHash)
+ {
+ var contract = NativeContract.ContractManagement.GetContract(_neoSystem.StoreView, scriptHash)
+ ?? throw new ContractNotFoundException(scriptHash);
+ return Ok(contract.Manifest);
+ }
+
+ ///
+ /// Get nef of a smart contract.
+ ///
+ /// ScriptHash
+ /// Contract Nef object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("{hash:required}/nef", Name = "GetContractNefFile")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(NefFile))]
+ public IActionResult GetContractNef(
+ [FromRoute(Name = "hash")]
+ UInt160 scriptHash)
+ {
+ var contract = NativeContract.ContractManagement.GetContract(_neoSystem.StoreView, scriptHash)
+ ?? throw new ContractNotFoundException(scriptHash);
+ return Ok(contract.Nef);
+ }
+
+ ///
+ /// Invoke a method as ReadOnly Flag on a smart contract.
+ ///
+ /// ScriptHash
+ /// method name
+ /// JArray of the contract parameters.
+ /// Execution Engine object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpPost("{hash:required}/invoke", Name = "InvokeContractMethod")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ExecutionEngineModel))]
+ public IActionResult InvokeContract(
+ [FromRoute(Name = "hash")]
+ UInt160 scriptHash,
+ [FromQuery(Name = "method")]
+ string method,
+ [FromBody]
+ InvokeParams invokeParameters)
+ {
+ var contract = NativeContract.ContractManagement.GetContract(_neoSystem.StoreView, scriptHash)
+ ?? throw new ContractNotFoundException(scriptHash);
+ if (string.IsNullOrEmpty(method))
+ throw new QueryParameterNotFoundException(nameof(method));
+ try
+ {
+ var engine = ScriptHelper.InvokeMethod(_neoSystem.Settings, _neoSystem.StoreView, contract.Hash, method, invokeParameters.ContractParameters, invokeParameters.Signers, out var script);
+ return Ok(engine.ToModel());
+ }
+ catch (Exception ex)
+ {
+ throw ex.InnerException ?? ex;
+ }
+ }
+}
diff --git a/plugins/RestServer/Controllers/v1/LedgerController.cs b/plugins/RestServer/Controllers/v1/LedgerController.cs
new file mode 100644
index 000000000..a6c46e2e6
--- /dev/null
+++ b/plugins/RestServer/Controllers/v1/LedgerController.cs
@@ -0,0 +1,385 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// LedgerController.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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Neo.Network.P2P.Payloads;
+using Neo.Plugins.RestServer.Exceptions;
+using Neo.Plugins.RestServer.Extensions;
+using Neo.Plugins.RestServer.Models.Blockchain;
+using Neo.Plugins.RestServer.Models.Error;
+using Neo.SmartContract.Native;
+using System.Net.Mime;
+
+namespace Neo.Plugins.RestServer.Controllers.v1;
+
+[Route("/api/v{version:apiVersion}/ledger")]
+[Produces(MediaTypeNames.Application.Json)]
+[Consumes(MediaTypeNames.Application.Json)]
+[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorModel))]
+[ApiVersion("1.0")]
+[ApiController]
+public class LedgerController : ControllerBase
+{
+ private readonly NeoSystem _neoSystem;
+
+ public LedgerController()
+ {
+ _neoSystem = RestServerPlugin.NeoSystem ?? throw new NodeNetworkException();
+ }
+
+ #region Accounts
+
+ ///
+ /// Gets all the accounts that hold gas on the blockchain.
+ ///
+ /// An array of account details object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("gas/accounts", Name = "GetGasAccounts")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AccountDetails[]))]
+ public IActionResult ShowGasAccounts(
+ [FromQuery(Name = "page")]
+ int skip = 1,
+ [FromQuery(Name = "size")]
+ int take = 50)
+ {
+ if (skip < 1 || take < 1 || take > RestServerSettings.Current.MaxPageSize)
+ throw new InvalidParameterRangeException();
+ var accounts = NativeContract.GAS.ListAccounts(_neoSystem.StoreView, _neoSystem.Settings);
+ if (accounts.Any() == false)
+ return NoContent();
+ var accountsList = accounts.OrderByDescending(o => o.Balance).Skip((skip - 1) * take).Take(take);
+ if (accountsList.Any() == false)
+ return NoContent();
+ return Ok(accountsList);
+ }
+
+ ///
+ /// Get all the accounts that hold neo on the blockchain.
+ ///
+ /// An array of account details object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("neo/accounts", Name = "GetNeoAccounts")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AccountDetails[]))]
+ public IActionResult ShowNeoAccounts(
+ [FromQuery(Name = "page")]
+ int skip = 1,
+ [FromQuery(Name = "size")]
+ int take = 50)
+ {
+ if (skip < 1 || take < 1 || take > RestServerSettings.Current.MaxPageSize)
+ throw new InvalidParameterRangeException();
+ var accounts = NativeContract.NEO.ListAccounts(_neoSystem.StoreView, _neoSystem.Settings);
+ if (accounts.Any() == false)
+ return NoContent();
+ var accountsList = accounts.OrderByDescending(o => o.Balance).Skip((skip - 1) * take).Take(take);
+ if (accountsList.Any() == false)
+ return NoContent();
+ return Ok(accountsList);
+ }
+
+ #endregion
+
+ #region Blocks
+
+ ///
+ /// Get blocks from the blockchain.
+ ///
+ /// Page
+ /// Page Size
+ /// An array of Block Header Objects
+ /// No more pages.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("blocks", Name = "GetBlocks")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Header[]))]
+ public IActionResult GetBlocks(
+ [FromQuery(Name = "page")]
+ uint skip = 1,
+ [FromQuery(Name = "size")]
+ uint take = 50)
+ {
+ if (skip < 1 || take < 1 || take > RestServerSettings.Current.MaxPageSize)
+ throw new InvalidParameterRangeException();
+ //var start = (skip - 1) * take + startIndex;
+ //var end = start + take;
+ var start = NativeContract.Ledger.CurrentIndex(_neoSystem.StoreView) - (skip - 1) * take;
+ var end = start - take;
+ var lstOfBlocks = new List();
+ for (var i = start; i > end; i--)
+ {
+ var block = NativeContract.Ledger.GetBlock(_neoSystem.StoreView, i);
+ if (block == null)
+ break;
+ lstOfBlocks.Add(block.Header);
+ }
+ if (lstOfBlocks.Count == 0)
+ return NoContent();
+ return Ok(lstOfBlocks);
+ }
+
+ ///
+ /// Gets the current block header of the connected node.
+ ///
+ /// Full Block Header Object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("blockheader/current", Name = "GetCurrnetBlockHeader")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Header))]
+ public IActionResult GetCurrentBlockHeader()
+ {
+ var currentIndex = NativeContract.Ledger.CurrentIndex(_neoSystem.StoreView);
+ var blockHeader = NativeContract.Ledger.GetHeader(_neoSystem.StoreView, currentIndex);
+ return Ok(blockHeader);
+ }
+
+ ///
+ /// Gets a block by an its index.
+ ///
+ /// Block Index
+ /// Full Block Object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("blocks/{index:min(0)}", Name = "GetBlock")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Block))]
+ public IActionResult GetBlock(
+ [FromRoute(Name = "index")]
+ uint blockIndex)
+ {
+ var block = NativeContract.Ledger.GetBlock(_neoSystem.StoreView, blockIndex)
+ ?? throw new BlockNotFoundException(blockIndex);
+ return Ok(block);
+ }
+
+ ///
+ /// Gets a block header by block index.
+ ///
+ /// Blocks index.
+ /// Block Header Object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("blocks/{index:min(0)}/header", Name = "GetBlockHeader")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Header))]
+ public IActionResult GetBlockHeader(
+ [FromRoute(Name = "index")]
+ uint blockIndex)
+ {
+ var block = NativeContract.Ledger.GetBlock(_neoSystem.StoreView, blockIndex)
+ ?? throw new BlockNotFoundException(blockIndex);
+ return Ok(block.Header);
+ }
+
+ ///
+ /// Gets the witness of the block
+ ///
+ /// Block Index.
+ /// Witness Object
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("blocks/{index:min(0)}/witness", Name = "GetBlockWitness")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Witness))]
+ public IActionResult GetBlockWitness(
+ [FromRoute(Name = "index")]
+ uint blockIndex)
+ {
+ var block = NativeContract.Ledger.GetBlock(_neoSystem.StoreView, blockIndex)
+ ?? throw new BlockNotFoundException(blockIndex);
+ return Ok(block.Witness);
+ }
+
+ ///
+ /// Gets the transactions of the block.
+ ///
+ /// Block Index.
+ /// Page
+ /// Page Size
+ /// An array of transaction object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("blocks/{index:min(0)}/transactions", Name = "GetBlockTransactions")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Transaction[]))]
+ public IActionResult GetBlockTransactions(
+ [FromRoute(Name = "index")]
+ uint blockIndex,
+ [FromQuery(Name = "page")]
+ int skip = 1,
+ [FromQuery(Name = "size")]
+ int take = 50)
+ {
+ if (skip < 1 || take < 1 || take > RestServerSettings.Current.MaxPageSize)
+ throw new InvalidParameterRangeException();
+ var block = NativeContract.Ledger.GetBlock(_neoSystem.StoreView, blockIndex)
+ ?? throw new BlockNotFoundException(blockIndex);
+ if (block.Transactions == null || block.Transactions.Length == 0)
+ return NoContent();
+ return Ok(block.Transactions.Skip((skip - 1) * take).Take(take));
+ }
+
+ #endregion
+
+ #region Transactions
+
+ ///
+ /// Gets a transaction
+ ///
+ /// Hash256
+ /// Transaction object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("transactions/{hash:required}", Name = "GetTransaction")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Transaction))]
+ public IActionResult GetTransaction(
+ [FromRoute(Name = "hash")]
+ UInt256 hash)
+ {
+ if (NativeContract.Ledger.ContainsTransaction(_neoSystem.StoreView, hash) == false)
+ throw new TransactionNotFoundException(hash);
+ var txst = NativeContract.Ledger.GetTransaction(_neoSystem.StoreView, hash);
+ return Ok(txst);
+ }
+
+ ///
+ /// Gets the witness of a transaction.
+ ///
+ /// Hash256
+ /// An array of witness object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("transactions/{hash:required}/witnesses", Name = "GetTransactionWitnesses")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Witness[]))]
+ public IActionResult GetTransactionWitnesses(
+ [FromRoute( Name = "hash")]
+ UInt256 hash)
+ {
+ var tx = NativeContract.Ledger.GetTransaction(_neoSystem.StoreView, hash)
+ ?? throw new TransactionNotFoundException(hash);
+ return Ok(tx.Witnesses);
+ }
+
+ ///
+ /// Gets the signers of a transaction.
+ ///
+ /// Hash256
+ /// An array of Signer object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("transactions/{hash:required}/signers", Name = "GetTransactionSigners")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Signer[]))]
+ public IActionResult GetTransactionSigners(
+ [FromRoute( Name = "hash")]
+ UInt256 hash)
+ {
+ var tx = NativeContract.Ledger.GetTransaction(_neoSystem.StoreView, hash)
+ ?? throw new TransactionNotFoundException(hash);
+ return Ok(tx.Signers);
+ }
+
+ ///
+ /// Gets the transaction attributes of a transaction.
+ ///
+ /// Hash256
+ /// An array of the transaction attributes object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("transactions/{hash:required}/attributes", Name = "GetTransactionAttributes")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TransactionAttribute[]))]
+ public IActionResult GetTransactionAttributes(
+ [FromRoute( Name = "hash")]
+ UInt256 hash)
+ {
+ var tx = NativeContract.Ledger.GetTransaction(_neoSystem.StoreView, hash)
+ ?? throw new TransactionNotFoundException(hash);
+ return Ok(tx.Attributes);
+ }
+
+ #endregion
+
+ #region Memory Pool
+
+ ///
+ /// Gets memory pool.
+ ///
+ /// Page
+ /// Page Size.
+ /// An array of the Transaction object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("memorypool", Name = "GetMemoryPoolTransactions")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Transaction[]))]
+ public IActionResult GetMemoryPool(
+ [FromQuery(Name = "page")]
+ int skip = 1,
+ [FromQuery(Name = "size")]
+ int take = 50)
+ {
+ if (skip < 0 || take < 0 || take > RestServerSettings.Current.MaxPageSize)
+ throw new InvalidParameterRangeException();
+ return Ok(_neoSystem.MemPool.Skip((skip - 1) * take).Take(take));
+ }
+
+ ///
+ /// Gets verified memory pool.
+ ///
+ /// Page
+ /// Page Size.
+ /// An array of the Transaction object.
+ /// No more pages.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("memorypool/verified", Name = "GetMemoryPoolVeridiedTransactions")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Transaction[]))]
+ public IActionResult GetMemoryPoolVerified(
+ [FromQuery(Name = "page")]
+ int skip = 1,
+ [FromQuery(Name = "size")]
+ int take = 50)
+ {
+ if (skip < 0 || take < 0 || take > RestServerSettings.Current.MaxPageSize)
+ throw new InvalidParameterRangeException();
+ if (_neoSystem.MemPool.Count == 0)
+ return NoContent();
+ var vTx = _neoSystem.MemPool.GetVerifiedTransactions();
+ return Ok(vTx.Skip((skip - 1) * take).Take(take));
+ }
+
+ ///
+ /// Gets unverified memory pool.
+ ///
+ /// Page
+ /// Page Size.
+ /// An array of the Transaction object.
+ /// No more pages.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("memorypool/unverified", Name = "GetMemoryPoolUnveridiedTransactions")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Transaction[]))]
+ public IActionResult GetMemoryPoolUnVerified(
+ [FromQuery(Name = "page")]
+ int skip = 1,
+ [FromQuery(Name = "size")]
+ int take = 50)
+ {
+ if (skip < 0 || take < 0 || take > RestServerSettings.Current.MaxPageSize)
+ throw new InvalidParameterRangeException();
+ if (_neoSystem.MemPool.Count == 0)
+ return NoContent();
+ _neoSystem.MemPool.GetVerifiedAndUnverifiedTransactions(out _, out var unVerifiedTransactions);
+ return Ok(unVerifiedTransactions.Skip((skip - 1) * take).Take(take));
+ }
+
+ #endregion
+}
diff --git a/plugins/RestServer/Controllers/v1/NodeController.cs b/plugins/RestServer/Controllers/v1/NodeController.cs
new file mode 100644
index 000000000..ebdbdfe27
--- /dev/null
+++ b/plugins/RestServer/Controllers/v1/NodeController.cs
@@ -0,0 +1,84 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// NodeController.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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Neo.Network.P2P;
+using Neo.Plugins.RestServer.Extensions;
+using Neo.Plugins.RestServer.Models.Error;
+using Neo.Plugins.RestServer.Models.Node;
+using System.Net.Mime;
+
+namespace Neo.Plugins.RestServer.Controllers.v1;
+
+[Route("/api/v{version:apiVersion}/node")]
+[Produces(MediaTypeNames.Application.Json)]
+[Consumes(MediaTypeNames.Application.Json)]
+[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorModel))]
+[ApiVersion("1.0")]
+[ApiController]
+public class NodeController : ControllerBase
+{
+ private readonly LocalNode _neoLocalNode;
+ private readonly NeoSystem _neoSystem;
+
+ public NodeController()
+ {
+ _neoLocalNode = RestServerPlugin.LocalNode ?? throw new InvalidOperationException();
+ _neoSystem = RestServerPlugin.NeoSystem ?? throw new InvalidOperationException();
+ }
+
+ ///
+ /// Gets the connected remote nodes.
+ ///
+ /// An array of the Remote Node Objects.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("peers", Name = "GetPeers")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RemoteNodeModel[]))]
+ public IActionResult GetPeers()
+ {
+ var rNodes = _neoLocalNode
+ .GetRemoteNodes()
+ .OrderByDescending(o => o.LastBlockIndex)
+ .ToArray();
+
+ return Ok(rNodes.Select(s => s.ToModel()));
+ }
+
+ ///
+ /// Gets all the loaded plugins of the current connected node.
+ ///
+ /// An array of the Plugin objects.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("plugins", Name = "GetPlugins")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PluginModel[]))]
+ public IActionResult GetPlugins() =>
+ Ok(Plugin.Plugins.Select(s =>
+ new PluginModel()
+ {
+ Name = s.Name,
+ Version = s.Version.ToString(3),
+ Description = s.Description,
+ }));
+
+ ///
+ /// Gets the ProtocolSettings of the currently connected node.
+ ///
+ /// Protocol Settings Object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("settings", Name = "GetProtocolSettings")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ProtocolSettingsModel))]
+ public IActionResult GetSettings() =>
+ Ok(_neoSystem.Settings.ToModel());
+}
diff --git a/plugins/RestServer/Controllers/v1/TokensController.cs b/plugins/RestServer/Controllers/v1/TokensController.cs
new file mode 100644
index 000000000..a09df6038
--- /dev/null
+++ b/plugins/RestServer/Controllers/v1/TokensController.cs
@@ -0,0 +1,272 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// TokensController.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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Neo.Plugins.RestServer.Exceptions;
+using Neo.Plugins.RestServer.Extensions;
+using Neo.Plugins.RestServer.Helpers;
+using Neo.Plugins.RestServer.Models.Error;
+using Neo.Plugins.RestServer.Models.Token;
+using Neo.Plugins.RestServer.Tokens;
+using Neo.SmartContract.Native;
+using System.Net.Mime;
+
+namespace Neo.Plugins.RestServer.Controllers.v1;
+
+[Route("/api/v{version:apiVersion}/tokens")]
+[Produces(MediaTypeNames.Application.Json)]
+[Consumes(MediaTypeNames.Application.Json)]
+[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorModel))]
+[ApiVersion("1.0")]
+[ApiController]
+public class TokensController : ControllerBase
+{
+ private readonly NeoSystem _neoSystem;
+
+ public TokensController()
+ {
+ _neoSystem = RestServerPlugin.NeoSystem ?? throw new NodeNetworkException();
+ }
+
+ #region NEP-17
+
+ ///
+ /// Gets all Nep-17 valid contracts from the blockchain.
+ ///
+ /// Page
+ /// Page Size
+ /// An array of the Nep-17 Token Object.
+ /// No more pages.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("nep-17", Name = "GetNep17Tokens")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(NEP17TokenModel[]))]
+ public IActionResult GetNEP17(
+ [FromQuery(Name = "page")]
+ int skip = 1,
+ [FromQuery(Name = "size")]
+ int take = 50)
+ {
+ if (skip < 1 || take < 1 || take > RestServerSettings.Current.MaxPageSize)
+ throw new InvalidParameterRangeException();
+ var tokenList = NativeContract.ContractManagement.ListContracts(_neoSystem.StoreView);
+ var vaildContracts = tokenList
+ .Where(ContractHelper.IsNep17Supported)
+ .OrderBy(o => o.Id)
+ .Skip((skip - 1) * take)
+ .Take(take);
+ if (vaildContracts.Any() == false)
+ return NoContent();
+ var listResults = new List();
+ foreach (var contract in vaildContracts)
+ {
+ try
+ {
+ var token = new NEP17Token(_neoSystem, contract.Hash);
+ listResults.Add(token.ToModel());
+ }
+ catch
+ {
+ }
+ }
+ if (listResults.Count == 0)
+ return NoContent();
+ return Ok(listResults);
+ }
+
+ ///
+ /// Gets the balance of the Nep-17 contract by an address.
+ ///
+ /// Nep-17 ScriptHash
+ /// Neo Address ScriptHash
+ /// Token Balance Object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("nep-17/{scripthash:required}/balanceof/{address:required}", Name = "GetNep17TokenBalanceOf")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TokenBalanceModel))]
+ public IActionResult GetNEP17(
+ [FromRoute(Name = "scripthash")]
+ UInt160 tokenAddessOrScripthash,
+ [FromRoute(Name = "address")]
+ UInt160 lookupAddressOrScripthash)
+ {
+ var contract = NativeContract.ContractManagement.GetContract(_neoSystem.StoreView, tokenAddessOrScripthash) ??
+ throw new ContractNotFoundException(tokenAddessOrScripthash);
+ if (ContractHelper.IsNep17Supported(contract) == false)
+ throw new Nep17NotSupportedException(tokenAddessOrScripthash);
+ try
+ {
+ var token = new NEP17Token(_neoSystem, tokenAddessOrScripthash);
+ return Ok(new TokenBalanceModel()
+ {
+ Name = token.Name,
+ ScriptHash = token.ScriptHash,
+ Symbol = token.Symbol,
+ Decimals = token.Decimals,
+ Balance = token.BalanceOf(lookupAddressOrScripthash).Value,
+ TotalSupply = token.TotalSupply().Value,
+ });
+ }
+ catch
+ {
+ throw new Nep17NotSupportedException(tokenAddessOrScripthash);
+ }
+ }
+
+ #endregion
+
+ #region NEP-11
+
+ ///
+ /// Gets all the Nep-11 valid contracts on from the blockchain.
+ ///
+ /// Page
+ /// Page Size
+ /// Nep-11 Token Object.
+ /// No more pages.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("nep-11", Name = "GetNep11Tokens")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(NEP11TokenModel[]))]
+ public IActionResult GetNEP11(
+ [FromQuery(Name = "page")]
+ int skip = 1,
+ [FromQuery(Name = "size")]
+ int take = 50)
+ {
+ if (skip < 1 || take < 1 || take > RestServerSettings.Current.MaxPageSize)
+ throw new InvalidParameterRangeException();
+ var tokenList = NativeContract.ContractManagement.ListContracts(_neoSystem.StoreView);
+ var validContracts = tokenList
+ .Where(ContractHelper.IsNep11Supported)
+ .OrderBy(o => o.Id)
+ .Skip((skip - 1) * take)
+ .Take(take);
+ if (validContracts.Any() == false)
+ return NoContent();
+ var listResults = new List();
+ foreach (var contract in validContracts)
+ {
+ try
+ {
+ var token = new NEP11Token(_neoSystem, contract.Hash);
+ listResults.Add(token.ToModel());
+ }
+ catch
+ {
+ }
+ }
+ if (listResults.Count == 0)
+ return NoContent();
+ return Ok(listResults);
+ }
+
+ ///
+ /// Gets the balance of the Nep-11 contract by an address.
+ ///
+ /// Nep-11 ScriptHash
+ /// Neo Address ScriptHash
+ /// Token Balance Object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("nep-11/{scripthash:required}/balanceof/{address:required}", Name = "GetNep11TokenBalanceOf")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TokenBalanceModel))]
+ public IActionResult GetNEP11(
+ [FromRoute(Name = "scripthash")]
+ UInt160 sAddressHash,
+ [FromRoute(Name = "address")]
+ UInt160 addressHash)
+ {
+ var contract = NativeContract.ContractManagement.GetContract(_neoSystem.StoreView, sAddressHash) ??
+ throw new ContractNotFoundException(sAddressHash);
+ if (ContractHelper.IsNep11Supported(contract) == false)
+ throw new Nep11NotSupportedException(sAddressHash);
+ try
+ {
+ var token = new NEP11Token(_neoSystem, sAddressHash);
+ return Ok(new TokenBalanceModel()
+ {
+ Name = token.Name,
+ ScriptHash = token.ScriptHash,
+ Symbol = token.Symbol,
+ Decimals = token.Decimals,
+ Balance = token.BalanceOf(addressHash).Value,
+ TotalSupply = token.TotalSupply().Value,
+ });
+ }
+ catch
+ {
+ throw new Nep11NotSupportedException(sAddressHash);
+ }
+ }
+
+ #endregion
+
+ ///
+ /// Gets every single NEP17/NEP11 on the blockchain's balance by ScriptHash
+ ///
+ ///
+ /// Token Balance Object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("balanceof/{address:required}", Name = "GetAllTokensBalanceOf")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TokenBalanceModel))]
+ public IActionResult GetBalances(
+ [FromRoute(Name = "address")]
+ UInt160 addressOrScripthash)
+ {
+ var tokenList = NativeContract.ContractManagement.ListContracts(_neoSystem.StoreView);
+ var validContracts = tokenList
+ .Where(w => ContractHelper.IsNep17Supported(w) || ContractHelper.IsNep11Supported(w))
+ .OrderBy(o => o.Id);
+ var listResults = new List();
+ foreach (var contract in validContracts)
+ {
+ try
+ {
+ var token = new NEP17Token(_neoSystem, contract.Hash);
+ var balance = token.BalanceOf(addressOrScripthash).Value;
+ if (balance == 0)
+ continue;
+ listResults.Add(new()
+ {
+ Name = token.Name,
+ ScriptHash = token.ScriptHash,
+ Symbol = token.Symbol,
+ Decimals = token.Decimals,
+ Balance = balance,
+ TotalSupply = token.TotalSupply().Value,
+ });
+
+ var nft = new NEP11Token(_neoSystem, contract.Hash);
+ balance = nft.BalanceOf(addressOrScripthash).Value;
+ if (balance == 0)
+ continue;
+ listResults.Add(new()
+ {
+ Name = nft.Name,
+ ScriptHash = nft.ScriptHash,
+ Symbol = nft.Symbol,
+ Balance = balance,
+ Decimals = nft.Decimals,
+ TotalSupply = nft.TotalSupply().Value,
+ });
+ }
+ catch (NotSupportedException)
+ {
+ }
+ }
+ return Ok(listResults);
+ }
+}
diff --git a/plugins/RestServer/Controllers/v1/UtilsController.cs b/plugins/RestServer/Controllers/v1/UtilsController.cs
new file mode 100644
index 000000000..9df815d31
--- /dev/null
+++ b/plugins/RestServer/Controllers/v1/UtilsController.cs
@@ -0,0 +1,106 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// UtilsController.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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Neo.Plugins.RestServer.Exceptions;
+using Neo.Plugins.RestServer.Models.Error;
+using Neo.Plugins.RestServer.Models.Utils;
+using Neo.Wallets;
+using System.Net.Mime;
+
+namespace Neo.Plugins.RestServer.Controllers.v1;
+
+[Route("/api/v{version:apiVersion}/utils")]
+[Produces(MediaTypeNames.Application.Json)]
+[Consumes(MediaTypeNames.Application.Json)]
+[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorModel))]
+[ApiVersion("1.0")]
+[ApiController]
+public class UtilsController : ControllerBase
+{
+ private readonly NeoSystem _neoSystem;
+
+ public UtilsController()
+ {
+ _neoSystem = RestServerPlugin.NeoSystem ?? throw new NodeNetworkException();
+ }
+
+ #region Validation
+
+ ///
+ /// Converts script to Neo address.
+ ///
+ /// ScriptHash
+ /// Util Address Object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("{hash:required}/address", Name = "GetAddressByScripthash")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UtilsAddressModel))]
+ public IActionResult ScriptHashToWalletAddress(
+ [FromRoute(Name = "hash")]
+ UInt160 ScriptHash)
+ {
+ try
+ {
+ return Ok(new UtilsAddressModel() { Address = ScriptHash.ToAddress(_neoSystem.Settings.AddressVersion) });
+ }
+ catch (FormatException)
+ {
+ throw new ScriptHashFormatException();
+ }
+ }
+
+ ///
+ /// Converts Neo address to ScriptHash
+ ///
+ /// Neo Address
+ /// Util ScriptHash Object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("{address:required}/scripthash", Name = "GetScripthashByAddress")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UtilsScriptHashModel))]
+ public IActionResult WalletAddressToScriptHash(
+ [FromRoute(Name = "address")]
+ string address)
+ {
+ try
+ {
+ return Ok(new UtilsScriptHashModel() { ScriptHash = address.ToScriptHash(_neoSystem.Settings.AddressVersion) });
+ }
+ catch (FormatException)
+ {
+ throw new AddressFormatException();
+ }
+ }
+
+ ///
+ /// Get whether or not a Neo address or ScriptHash is valid.
+ ///
+ ///
+ /// Util Address Valid Object.
+ /// Successful
+ /// An error occurred. See Response for details.
+ [HttpGet("{address:required}/validate", Name = "IsValidAddressOrScriptHash")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UtilsAddressIsValidModel))]
+ public IActionResult ValidateAddress(
+ [FromRoute(Name = "address")]
+ string AddressOrScriptHash)
+ {
+ return Ok(new UtilsAddressIsValidModel()
+ {
+ Address = AddressOrScriptHash,
+ IsValid = RestServerUtility.TryConvertToScriptHash(AddressOrScriptHash, _neoSystem.Settings, out _),
+ });
+ }
+
+ #endregion
+}
diff --git a/plugins/RestServer/Exceptions/AddressFormatException.cs b/plugins/RestServer/Exceptions/AddressFormatException.cs
new file mode 100644
index 000000000..a90dfa93f
--- /dev/null
+++ b/plugins/RestServer/Exceptions/AddressFormatException.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// AddressFormatException.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.
+
+namespace Neo.Plugins.RestServer.Exceptions;
+
+internal class AddressFormatException : Exception
+{
+ public AddressFormatException() : base() { }
+ public AddressFormatException(string message) : base(message) { }
+}
diff --git a/plugins/RestServer/Exceptions/ApplicationEngineException.cs b/plugins/RestServer/Exceptions/ApplicationEngineException.cs
new file mode 100644
index 000000000..bd04b1059
--- /dev/null
+++ b/plugins/RestServer/Exceptions/ApplicationEngineException.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ApplicationEngineException.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.
+
+namespace Neo.Plugins.RestServer.Exceptions;
+
+internal class ApplicationEngineException : Exception
+{
+ public ApplicationEngineException() : base() { }
+ public ApplicationEngineException(string message) : base(message) { }
+}
diff --git a/plugins/RestServer/Exceptions/BlockNotFoundException.cs b/plugins/RestServer/Exceptions/BlockNotFoundException.cs
new file mode 100644
index 000000000..c806f93b9
--- /dev/null
+++ b/plugins/RestServer/Exceptions/BlockNotFoundException.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// BlockNotFoundException.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.
+
+namespace Neo.Plugins.RestServer.Exceptions;
+
+internal class BlockNotFoundException : Exception
+{
+ public BlockNotFoundException() { }
+ public BlockNotFoundException(uint index) : base($"block '{index}' as not found.") { }
+}
diff --git a/plugins/RestServer/Exceptions/ContractNotFoundException.cs b/plugins/RestServer/Exceptions/ContractNotFoundException.cs
new file mode 100644
index 000000000..a0cd6aefe
--- /dev/null
+++ b/plugins/RestServer/Exceptions/ContractNotFoundException.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractNotFoundException.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.
+
+namespace Neo.Plugins.RestServer.Exceptions;
+
+internal class ContractNotFoundException : Exception
+{
+ public ContractNotFoundException() : base() { }
+ public ContractNotFoundException(UInt160 scriptHash) : base($"Contract '{scriptHash}' was not found.") { }
+}
diff --git a/plugins/RestServer/Exceptions/InvalidParameterRangeException.cs b/plugins/RestServer/Exceptions/InvalidParameterRangeException.cs
new file mode 100644
index 000000000..f818595b3
--- /dev/null
+++ b/plugins/RestServer/Exceptions/InvalidParameterRangeException.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// InvalidParameterRangeException.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.
+
+namespace Neo.Plugins.RestServer.Exceptions;
+
+internal class InvalidParameterRangeException : Exception
+{
+ public InvalidParameterRangeException() : base() { }
+ public InvalidParameterRangeException(string message) : base(message) { }
+}
diff --git a/plugins/RestServer/Exceptions/JsonPropertyNullOrEmptyException.cs b/plugins/RestServer/Exceptions/JsonPropertyNullOrEmptyException.cs
new file mode 100644
index 000000000..4339e110a
--- /dev/null
+++ b/plugins/RestServer/Exceptions/JsonPropertyNullOrEmptyException.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// JsonPropertyNullOrEmptyException.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.
+
+namespace Neo.Plugins.RestServer.Exceptions;
+
+internal class JsonPropertyNullOrEmptyException : Exception
+{
+ public JsonPropertyNullOrEmptyException() : base() { }
+ public JsonPropertyNullOrEmptyException(string paramName) : base($"Value cannot be null or empty. (Parameter '{paramName}')") { }
+}
diff --git a/plugins/RestServer/Exceptions/Nep11NotSupportedException.cs b/plugins/RestServer/Exceptions/Nep11NotSupportedException.cs
new file mode 100644
index 000000000..5028055bc
--- /dev/null
+++ b/plugins/RestServer/Exceptions/Nep11NotSupportedException.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Nep11NotSupportedException.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.
+
+namespace Neo.Plugins.RestServer.Exceptions;
+
+internal class Nep11NotSupportedException : Exception
+{
+ public Nep11NotSupportedException() { }
+ public Nep11NotSupportedException(UInt160 scriptHash) : base($"Contract '{scriptHash}' does not support NEP-11.") { }
+}
diff --git a/plugins/RestServer/Exceptions/Nep17NotSupportedException.cs b/plugins/RestServer/Exceptions/Nep17NotSupportedException.cs
new file mode 100644
index 000000000..5e76bc954
--- /dev/null
+++ b/plugins/RestServer/Exceptions/Nep17NotSupportedException.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// Nep17NotSupportedException.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.
+
+namespace Neo.Plugins.RestServer.Exceptions;
+
+internal class Nep17NotSupportedException : Exception
+{
+ public Nep17NotSupportedException() { }
+ public Nep17NotSupportedException(UInt160 scriptHash) : base($"Contract '{scriptHash}' does not support NEP-17.") { }
+}
diff --git a/plugins/RestServer/Exceptions/NodeException.cs b/plugins/RestServer/Exceptions/NodeException.cs
new file mode 100644
index 000000000..63ac5677e
--- /dev/null
+++ b/plugins/RestServer/Exceptions/NodeException.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// NodeException.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.
+
+namespace Neo.Plugins.RestServer.Exceptions;
+
+internal class NodeException : Exception
+{
+ public NodeException() : base() { }
+ public NodeException(string message) : base(message) { }
+}
diff --git a/plugins/RestServer/Exceptions/NodeNetworkException.cs b/plugins/RestServer/Exceptions/NodeNetworkException.cs
new file mode 100644
index 000000000..332a9956e
--- /dev/null
+++ b/plugins/RestServer/Exceptions/NodeNetworkException.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// NodeNetworkException.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.
+
+namespace Neo.Plugins.RestServer.Exceptions;
+
+internal class NodeNetworkException : Exception
+{
+ public NodeNetworkException() : base("Network does not match config file's.") { }
+ public NodeNetworkException(string message) : base(message) { }
+}
diff --git a/plugins/RestServer/Exceptions/QueryParameterNotFoundException.cs b/plugins/RestServer/Exceptions/QueryParameterNotFoundException.cs
new file mode 100644
index 000000000..79d7bfe66
--- /dev/null
+++ b/plugins/RestServer/Exceptions/QueryParameterNotFoundException.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// QueryParameterNotFoundException.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.
+
+namespace Neo.Plugins.RestServer.Exceptions;
+
+internal class QueryParameterNotFoundException : Exception
+{
+ public QueryParameterNotFoundException() { }
+ public QueryParameterNotFoundException(string parameterName) : base($"Query parameter '{parameterName}' was not found.") { }
+}
diff --git a/plugins/RestServer/Exceptions/RestErrorCodes.cs b/plugins/RestServer/Exceptions/RestErrorCodes.cs
new file mode 100644
index 000000000..2d2fee448
--- /dev/null
+++ b/plugins/RestServer/Exceptions/RestErrorCodes.cs
@@ -0,0 +1,19 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// RestErrorCodes.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.
+
+namespace Neo.Plugins.RestServer.Exceptions;
+
+internal static class RestErrorCodes
+{
+ //=========================Rest Codes=========================
+ public const int GenericException = 1000;
+ public const int ParameterFormatException = 1001;
+}
diff --git a/plugins/RestServer/Exceptions/ScriptHashFormatException.cs b/plugins/RestServer/Exceptions/ScriptHashFormatException.cs
new file mode 100644
index 000000000..900e25d63
--- /dev/null
+++ b/plugins/RestServer/Exceptions/ScriptHashFormatException.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ScriptHashFormatException.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.
+
+namespace Neo.Plugins.RestServer.Exceptions;
+
+internal class ScriptHashFormatException : Exception
+{
+ public ScriptHashFormatException() : base() { }
+ public ScriptHashFormatException(string message) : base(message) { }
+}
diff --git a/plugins/RestServer/Exceptions/TransactionNotFoundException.cs b/plugins/RestServer/Exceptions/TransactionNotFoundException.cs
new file mode 100644
index 000000000..afc5000db
--- /dev/null
+++ b/plugins/RestServer/Exceptions/TransactionNotFoundException.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// TransactionNotFoundException.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.
+
+namespace Neo.Plugins.RestServer.Exceptions;
+
+internal class TransactionNotFoundException : Exception
+{
+ public TransactionNotFoundException() { }
+ public TransactionNotFoundException(UInt256 txhash) : base($"Transaction '{txhash}' was not found.") { }
+}
diff --git a/plugins/RestServer/Exceptions/UInt256FormatException.cs b/plugins/RestServer/Exceptions/UInt256FormatException.cs
new file mode 100644
index 000000000..369efcc00
--- /dev/null
+++ b/plugins/RestServer/Exceptions/UInt256FormatException.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// UInt256FormatException.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.
+
+namespace Neo.Plugins.RestServer.Exceptions;
+
+internal class UInt256FormatException : Exception
+{
+ public UInt256FormatException() { }
+ public UInt256FormatException(string message) : base(message) { }
+}
diff --git a/plugins/RestServer/Extensions/LedgerContractExtensions.cs b/plugins/RestServer/Extensions/LedgerContractExtensions.cs
new file mode 100644
index 000000000..50e8637e5
--- /dev/null
+++ b/plugins/RestServer/Extensions/LedgerContractExtensions.cs
@@ -0,0 +1,45 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// LedgerContractExtensions.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 Neo.Extensions.SmartContract;
+using Neo.Persistence;
+using Neo.Plugins.RestServer.Models.Blockchain;
+using Neo.SmartContract.Native;
+using Neo.Wallets;
+
+namespace Neo.Plugins.RestServer.Extensions;
+
+internal static class LedgerContractExtensions
+{
+ public static IEnumerable ListAccounts(this GasToken gasToken, DataCache snapshot, ProtocolSettings protocolSettings) =>
+ gasToken
+ .GetAccounts(snapshot)
+ .Select(s =>
+ new AccountDetails
+ {
+ ScriptHash = s.Address,
+ Address = s.Address.ToAddress(protocolSettings.AddressVersion),
+ Balance = s.Balance,
+ Decimals = gasToken.Decimals,
+ });
+
+ public static IEnumerable ListAccounts(this NeoToken neoToken, DataCache snapshot, ProtocolSettings protocolSettings) =>
+ neoToken
+ .GetAccounts(snapshot)
+ .Select(s =>
+ new AccountDetails
+ {
+ ScriptHash = s.Address,
+ Address = s.Address.ToAddress(protocolSettings.AddressVersion),
+ Balance = s.Balance,
+ Decimals = neoToken.Decimals,
+ });
+}
diff --git a/plugins/RestServer/Extensions/ModelExtensions.cs b/plugins/RestServer/Extensions/ModelExtensions.cs
new file mode 100644
index 000000000..ca9aea311
--- /dev/null
+++ b/plugins/RestServer/Extensions/ModelExtensions.cs
@@ -0,0 +1,98 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ModelExtensions.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 Neo.Network.P2P;
+using Neo.Plugins.RestServer.Models;
+using Neo.Plugins.RestServer.Models.Error;
+using Neo.Plugins.RestServer.Models.Node;
+using Neo.Plugins.RestServer.Models.Token;
+using Neo.Plugins.RestServer.Tokens;
+using Neo.SmartContract;
+
+namespace Neo.Plugins.RestServer.Extensions;
+
+internal static class ModelExtensions
+{
+ public static ExecutionEngineModel ToModel(this ApplicationEngine ae) =>
+ new()
+ {
+ GasConsumed = ae.FeeConsumed,
+ State = ae.State,
+ Notifications = ae.Notifications.Select(s =>
+ new BlockchainEventModel()
+ {
+ ScriptHash = s.ScriptHash,
+ EventName = s.EventName,
+ State = [.. s.State],
+ }).ToArray(),
+ ResultStack = [.. ae.ResultStack],
+ FaultException = ae.FaultException == null ?
+ null :
+ new ErrorModel()
+ {
+ Code = ae.FaultException?.InnerException?.HResult ?? ae.FaultException?.HResult ?? -1,
+ Name = ae.FaultException?.InnerException?.GetType().Name ?? ae.FaultException?.GetType().Name ?? string.Empty,
+ Message = ae.FaultException?.InnerException?.Message ?? ae.FaultException?.Message ?? string.Empty,
+ },
+ };
+
+ public static NEP17TokenModel ToModel(this NEP17Token token) =>
+ new()
+ {
+ Name = token.Name,
+ Symbol = token.Symbol,
+ ScriptHash = token.ScriptHash,
+ Decimals = token.Decimals,
+ TotalSupply = token.TotalSupply().Value,
+ };
+
+ public static NEP11TokenModel ToModel(this NEP11Token nep11) =>
+ new()
+ {
+ Name = nep11.Name,
+ ScriptHash = nep11.ScriptHash,
+ Symbol = nep11.Symbol,
+ Decimals = nep11.Decimals,
+ TotalSupply = nep11.TotalSupply().Value,
+ Tokens = nep11.Tokens().Select(s => new
+ {
+ Key = s,
+ Value = nep11.Properties(s),
+ }).ToDictionary(key => Convert.ToHexString(key.Key), value => value.Value),
+ };
+
+ public static ProtocolSettingsModel ToModel(this ProtocolSettings protocolSettings) =>
+ new()
+ {
+ Network = protocolSettings.Network,
+ AddressVersion = protocolSettings.AddressVersion,
+ ValidatorsCount = protocolSettings.ValidatorsCount,
+ MillisecondsPerBlock = protocolSettings.MillisecondsPerBlock,
+ MaxValidUntilBlockIncrement = protocolSettings.MaxValidUntilBlockIncrement,
+ MaxTransactionsPerBlock = protocolSettings.MaxTransactionsPerBlock,
+ MemoryPoolMaxTransactions = protocolSettings.MemoryPoolMaxTransactions,
+ MaxTraceableBlocks = protocolSettings.MaxTraceableBlocks,
+ InitialGasDistribution = protocolSettings.InitialGasDistribution,
+ SeedList = protocolSettings.SeedList,
+ Hardforks = protocolSettings.Hardforks.ToDictionary(k => k.Key.ToString().Replace("HF_", string.Empty), v => v.Value),
+ StandbyValidators = protocolSettings.StandbyValidators,
+ StandbyCommittee = protocolSettings.StandbyCommittee,
+ };
+
+ public static RemoteNodeModel ToModel(this RemoteNode remoteNode) =>
+ new()
+ {
+ RemoteAddress = remoteNode.Remote.Address.ToString(),
+ RemotePort = remoteNode.Remote.Port,
+ ListenTcpPort = remoteNode.ListenerTcpPort,
+ LastBlockIndex = remoteNode.LastBlockIndex,
+ };
+}
diff --git a/plugins/RestServer/Extensions/UInt160Extensions.cs b/plugins/RestServer/Extensions/UInt160Extensions.cs
new file mode 100644
index 000000000..334e28d0e
--- /dev/null
+++ b/plugins/RestServer/Extensions/UInt160Extensions.cs
@@ -0,0 +1,28 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// UInt160Extensions.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 Neo.Plugins.RestServer.Helpers;
+using Neo.SmartContract.Native;
+
+namespace Neo.Plugins.RestServer.Extensions;
+
+internal static class UInt160Extensions
+{
+ public static bool IsValidNep17(this UInt160 scriptHash)
+ {
+ var contractState = NativeContract.ContractManagement.GetContract(RestServerPlugin.NeoSystem!.StoreView, scriptHash);
+ if (contractState is null) return false;
+ return ContractHelper.IsNep17Supported(contractState);
+ }
+
+ public static bool IsValidContract(this UInt160 scriptHash) =>
+ NativeContract.ContractManagement.GetContract(RestServerPlugin.NeoSystem!.StoreView, scriptHash) != null;
+}
diff --git a/plugins/RestServer/Helpers/ContractHelper.cs b/plugins/RestServer/Helpers/ContractHelper.cs
new file mode 100644
index 000000000..ed718290d
--- /dev/null
+++ b/plugins/RestServer/Helpers/ContractHelper.cs
@@ -0,0 +1,178 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractHelper.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 Neo.Persistence;
+using Neo.SmartContract;
+using Neo.SmartContract.Manifest;
+using Neo.SmartContract.Native;
+
+namespace Neo.Plugins.RestServer.Helpers;
+
+public static class ContractHelper
+{
+ public static ContractParameterDefinition[]? GetAbiEventParams(DataCache snapshot, UInt160 scriptHash, string eventName)
+ {
+ var contractState = NativeContract.ContractManagement.GetContract(snapshot, scriptHash);
+ if (contractState == null)
+ return [];
+ return contractState.Manifest.Abi.Events.SingleOrDefault(s => s.Name.Equals(eventName, StringComparison.OrdinalIgnoreCase))?.Parameters;
+ }
+
+ public static bool IsNep17Supported(DataCache snapshot, UInt160 scriptHash)
+ {
+ var contractState = NativeContract.ContractManagement.GetContract(snapshot, scriptHash);
+ if (contractState == null)
+ return false;
+ return IsNep17Supported(contractState);
+ }
+
+ public static bool IsNep11Supported(DataCache snapshot, UInt160 scriptHash)
+ {
+ var contractState = NativeContract.ContractManagement.GetContract(snapshot, scriptHash);
+ if (contractState == null)
+ return false;
+ return IsNep11Supported(contractState);
+ }
+
+ public static bool IsNep17Supported(ContractState contractState)
+ {
+ var manifest = contractState.Manifest;
+ if (manifest.SupportedStandards.Any(a => a.Equals("NEP-17")))
+ {
+ try
+ {
+ var symbolMethod = manifest.Abi.GetMethod("symbol", 0);
+ var decimalsMethod = manifest.Abi.GetMethod("decimals", 0);
+ var totalSupplyMethod = manifest.Abi.GetMethod("totalSupply", 0);
+ var balanceOfMethod = manifest.Abi.GetMethod("balanceOf", 1);
+ var transferMethod = manifest.Abi.GetMethod("transfer", 4);
+
+ var symbolValid = symbolMethod?.Safe == true &&
+ symbolMethod.ReturnType == ContractParameterType.String;
+ var decimalsValid = decimalsMethod?.Safe == true &&
+ decimalsMethod.ReturnType == ContractParameterType.Integer;
+ var totalSupplyValid = totalSupplyMethod?.Safe == true &&
+ totalSupplyMethod.ReturnType == ContractParameterType.Integer;
+ var balanceOfValid = balanceOfMethod?.Safe == true &&
+ balanceOfMethod.ReturnType == ContractParameterType.Integer &&
+ balanceOfMethod.Parameters[0].Type == ContractParameterType.Hash160;
+ var transferValid = transferMethod?.Safe == false &&
+ transferMethod.ReturnType == ContractParameterType.Boolean &&
+ transferMethod.Parameters[0].Type == ContractParameterType.Hash160 &&
+ transferMethod.Parameters[1].Type == ContractParameterType.Hash160 &&
+ transferMethod.Parameters[2].Type == ContractParameterType.Integer &&
+ transferMethod.Parameters[3].Type == ContractParameterType.Any;
+ var transferEvent = manifest.Abi.Events.Any(s =>
+ s.Name == "Transfer" &&
+ s.Parameters.Length == 3 &&
+ s.Parameters[0].Type == ContractParameterType.Hash160 &&
+ s.Parameters[1].Type == ContractParameterType.Hash160 &&
+ s.Parameters[2].Type == ContractParameterType.Integer);
+
+ return (symbolValid &&
+ decimalsValid &&
+ totalSupplyValid &&
+ balanceOfValid &&
+ transferValid &&
+ transferEvent);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ public static bool IsNep11Supported(ContractState contractState)
+ {
+ var manifest = contractState.Manifest;
+ if (manifest.SupportedStandards.Any(a => a.Equals("NEP-11")))
+ {
+ try
+ {
+ var symbolMethod = manifest.Abi.GetMethod("symbol", 0);
+ var decimalsMethod = manifest.Abi.GetMethod("decimals", 0);
+ var totalSupplyMethod = manifest.Abi.GetMethod("totalSupply", 0);
+ var balanceOfMethod1 = manifest.Abi.GetMethod("balanceOf", 1);
+ var balanceOfMethod2 = manifest.Abi.GetMethod("balanceOf", 2);
+ var tokensOfMethod = manifest.Abi.GetMethod("tokensOf", 1);
+ var ownerOfMethod = manifest.Abi.GetMethod("ownerOf", 1);
+ var transferMethod1 = manifest.Abi.GetMethod("transfer", 3);
+ var transferMethod2 = manifest.Abi.GetMethod("transfer", 5);
+
+ var symbolValid = symbolMethod?.Safe == true &&
+ symbolMethod.ReturnType == ContractParameterType.String;
+ var decimalsValid = decimalsMethod?.Safe == true &&
+ decimalsMethod.ReturnType == ContractParameterType.Integer;
+ var totalSupplyValid = totalSupplyMethod?.Safe == true &&
+ totalSupplyMethod.ReturnType == ContractParameterType.Integer;
+ var balanceOfValid1 = balanceOfMethod1?.Safe == true &&
+ balanceOfMethod1.ReturnType == ContractParameterType.Integer &&
+ balanceOfMethod1.Parameters[0].Type == ContractParameterType.Hash160;
+ var balanceOfValid2 = balanceOfMethod2?.Safe == true &&
+ balanceOfMethod2?.ReturnType == ContractParameterType.Integer &&
+ balanceOfMethod2?.Parameters[0].Type == ContractParameterType.Hash160 &&
+ balanceOfMethod2?.Parameters[0].Type == ContractParameterType.ByteArray;
+ var tokensOfValid = tokensOfMethod?.Safe == true &&
+ tokensOfMethod.ReturnType == ContractParameterType.InteropInterface &&
+ tokensOfMethod.Parameters[0].Type == ContractParameterType.Hash160;
+ var ownerOfValid1 = ownerOfMethod?.Safe == true &&
+ ownerOfMethod.ReturnType == ContractParameterType.Hash160 &&
+ ownerOfMethod.Parameters[0].Type == ContractParameterType.ByteArray;
+ var ownerOfValid2 = ownerOfMethod?.Safe == true &&
+ ownerOfMethod.ReturnType == ContractParameterType.InteropInterface &&
+ ownerOfMethod.Parameters[0].Type == ContractParameterType.ByteArray;
+ var transferValid1 = transferMethod1?.Safe == false &&
+ transferMethod1.ReturnType == ContractParameterType.Boolean &&
+ transferMethod1.Parameters[0].Type == ContractParameterType.Hash160 &&
+ transferMethod1.Parameters[1].Type == ContractParameterType.ByteArray &&
+ transferMethod1.Parameters[2].Type == ContractParameterType.Any;
+ var transferValid2 = transferMethod2?.Safe == false &&
+ transferMethod2?.ReturnType == ContractParameterType.Boolean &&
+ transferMethod2?.Parameters[0].Type == ContractParameterType.Hash160 &&
+ transferMethod2?.Parameters[1].Type == ContractParameterType.Hash160 &&
+ transferMethod2?.Parameters[2].Type == ContractParameterType.Integer &&
+ transferMethod2?.Parameters[3].Type == ContractParameterType.ByteArray &&
+ transferMethod2?.Parameters[4].Type == ContractParameterType.Any;
+ var transferEvent = manifest.Abi.Events.Any(a =>
+ a.Name == "Transfer" &&
+ a.Parameters.Length == 4 &&
+ a.Parameters[0].Type == ContractParameterType.Hash160 &&
+ a.Parameters[1].Type == ContractParameterType.Hash160 &&
+ a.Parameters[2].Type == ContractParameterType.Integer &&
+ a.Parameters[3].Type == ContractParameterType.ByteArray);
+
+ return (symbolValid &&
+ decimalsValid &&
+ totalSupplyValid &&
+ (balanceOfValid2 || balanceOfValid1) &&
+ tokensOfValid &&
+ (ownerOfValid2 || ownerOfValid1) &&
+ (transferValid2 || transferValid1) &&
+ transferEvent);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ public static ContractMethodDescriptor? GetContractMethod(DataCache snapshot, UInt160 scriptHash, string method, int pCount)
+ {
+ var contractState = NativeContract.ContractManagement.GetContract(snapshot, scriptHash);
+ if (contractState == null)
+ return null;
+ return contractState.Manifest.Abi.GetMethod(method, pCount);
+ }
+}
diff --git a/plugins/RestServer/Helpers/ScriptHelper.cs b/plugins/RestServer/Helpers/ScriptHelper.cs
new file mode 100644
index 000000000..669431f15
--- /dev/null
+++ b/plugins/RestServer/Helpers/ScriptHelper.cs
@@ -0,0 +1,70 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ScriptHelper.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 Neo.Extensions.VM;
+using Neo.Network.P2P.Payloads;
+using Neo.Persistence;
+using Neo.SmartContract;
+using Neo.SmartContract.Native;
+using Neo.VM;
+using Neo.VM.Types;
+
+namespace Neo.Plugins.RestServer.Helpers;
+
+internal static class ScriptHelper
+{
+ public static bool InvokeMethod(ProtocolSettings protocolSettings, DataCache snapshot, UInt160 scriptHash, string method, out StackItem[] results, params object[] args)
+ {
+ using var scriptBuilder = new ScriptBuilder();
+ scriptBuilder.EmitDynamicCall(scriptHash, method, CallFlags.ReadOnly, args);
+ byte[] script = scriptBuilder.ToArray();
+ using var engine = ApplicationEngine.Run(script, snapshot, settings: protocolSettings, gas: RestServerSettings.Current.MaxGasInvoke);
+ results = engine.State == VMState.FAULT ? [] : [.. engine.ResultStack];
+ return engine.State == VMState.HALT;
+ }
+
+ public static ApplicationEngine InvokeMethod(ProtocolSettings protocolSettings, DataCache snapshot, UInt160 scriptHash, string method, ContractParameter[] args, Signer[]? signers, out byte[] script)
+ {
+ using var scriptBuilder = new ScriptBuilder();
+ scriptBuilder.EmitDynamicCall(scriptHash, method, CallFlags.All, args);
+ script = scriptBuilder.ToArray();
+ var tx = signers == null ? null : new Transaction
+ {
+ Version = 0,
+ Nonce = (uint)Random.Shared.Next(),
+ ValidUntilBlock = NativeContract.Ledger.CurrentIndex(snapshot) + protocolSettings.MaxValidUntilBlockIncrement,
+ Signers = signers,
+ Attributes = [],
+ Script = script,
+ Witnesses = [.. signers.Select(s => new Witness())],
+ };
+ using var engine = ApplicationEngine.Run(script, snapshot, tx, settings: protocolSettings, gas: RestServerSettings.Current.MaxGasInvoke);
+ return engine;
+ }
+
+ public static ApplicationEngine InvokeScript(ReadOnlyMemory script, Signer[]? signers = null, Witness[]? witnesses = null)
+ {
+ var neoSystem = RestServerPlugin.NeoSystem ?? throw new InvalidOperationException();
+
+ var snapshot = neoSystem.GetSnapshotCache();
+ var tx = signers == null ? null : new Transaction
+ {
+ Version = 0,
+ Nonce = (uint)Random.Shared.Next(),
+ ValidUntilBlock = NativeContract.Ledger.CurrentIndex(snapshot) + neoSystem.Settings.MaxValidUntilBlockIncrement,
+ Signers = signers,
+ Attributes = [],
+ Script = script,
+ Witnesses = witnesses ?? []
+ };
+ return ApplicationEngine.Run(script, snapshot, tx, settings: neoSystem.Settings, gas: RestServerSettings.Current.MaxGasInvoke);
+ }
+}
diff --git a/plugins/RestServer/Middleware/RestServerMiddleware.cs b/plugins/RestServer/Middleware/RestServerMiddleware.cs
new file mode 100644
index 000000000..bc2526b8e
--- /dev/null
+++ b/plugins/RestServer/Middleware/RestServerMiddleware.cs
@@ -0,0 +1,71 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// RestServerMiddleware.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 Microsoft.AspNetCore.Http;
+using System.Reflection;
+
+namespace Neo.Plugins.RestServer.Middleware;
+
+internal class RestServerMiddleware
+{
+ private readonly RequestDelegate _next;
+
+ public RestServerMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ var request = context.Request;
+ var response = context.Response;
+
+ SetServerInformationHeader(response);
+
+ await _next(context);
+ }
+
+ public static void SetServerInformationHeader(HttpResponse response)
+ {
+ var neoCliAsm = Assembly.GetEntryAssembly()?.GetName();
+ var restServerAsm = Assembly.GetExecutingAssembly().GetName();
+
+ if (neoCliAsm?.Version is not null && restServerAsm.Version is not null)
+ {
+ if (restServerAsm.Version is not null)
+ {
+ response.Headers.Server = $"{neoCliAsm.Name}/{neoCliAsm.Version.ToString(3)} {restServerAsm.Name}/{restServerAsm.Version.ToString(3)}";
+ }
+ else
+ {
+ response.Headers.Server = $"{neoCliAsm.Name}/{neoCliAsm.Version.ToString(3)} {restServerAsm.Name}";
+ }
+ }
+ else
+ {
+ if (neoCliAsm is not null)
+ {
+ if (restServerAsm is not null)
+ {
+ response.Headers.Server = $"{neoCliAsm.Name} {restServerAsm.Name}";
+ }
+ else
+ {
+ response.Headers.Server = $"{neoCliAsm.Name}";
+ }
+ }
+ else
+ {
+ // Can't get the server name/version
+ }
+ }
+ }
+}
diff --git a/plugins/RestServer/Models/Blockchain/AccountDetails.cs b/plugins/RestServer/Models/Blockchain/AccountDetails.cs
new file mode 100644
index 000000000..8f3272433
--- /dev/null
+++ b/plugins/RestServer/Models/Blockchain/AccountDetails.cs
@@ -0,0 +1,41 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// AccountDetails.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System.Numerics;
+
+namespace Neo.Plugins.RestServer.Models.Blockchain;
+
+internal class AccountDetails
+{
+ ///
+ /// Scripthash
+ ///
+ /// 0xed7cc6f5f2dd842d384f254bc0c2d58fb69a4761
+ public UInt160 ScriptHash { get; set; } = UInt160.Zero;
+
+ ///
+ /// Wallet address.
+ ///
+ /// NNLi44dJNXtDNSBkofB48aTVYtb1zZrNEs
+ public string Address { get; set; } = string.Empty;
+
+ ///
+ /// Balance of the account.
+ ///
+ /// 10000000
+ public BigInteger Balance { get; set; }
+
+ ///
+ /// Decimals of the token.
+ ///
+ /// 8
+ public BigInteger Decimals { get; set; }
+}
diff --git a/plugins/RestServer/Models/Contract/InvokeParams.cs b/plugins/RestServer/Models/Contract/InvokeParams.cs
new file mode 100644
index 000000000..bbec9bec5
--- /dev/null
+++ b/plugins/RestServer/Models/Contract/InvokeParams.cs
@@ -0,0 +1,21 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// InvokeParams.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 Neo.Network.P2P.Payloads;
+using Neo.SmartContract;
+
+namespace Neo.Plugins.RestServer.Models.Contract;
+
+public class InvokeParams
+{
+ public ContractParameter[] ContractParameters { get; set; } = [];
+ public Signer[] Signers { get; set; } = [];
+}
diff --git a/plugins/RestServer/Models/CountModel.cs b/plugins/RestServer/Models/CountModel.cs
new file mode 100644
index 000000000..279713660
--- /dev/null
+++ b/plugins/RestServer/Models/CountModel.cs
@@ -0,0 +1,21 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// CountModel.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.
+
+namespace Neo.Plugins.RestServer.Models;
+
+internal class CountModel
+{
+ ///
+ /// The count of how many objects.
+ ///
+ /// 378
+ public int Count { get; set; }
+}
diff --git a/plugins/RestServer/Models/Error/ErrorModel.cs b/plugins/RestServer/Models/Error/ErrorModel.cs
new file mode 100644
index 000000000..ef16ae79d
--- /dev/null
+++ b/plugins/RestServer/Models/Error/ErrorModel.cs
@@ -0,0 +1,32 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ErrorModel.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.
+
+namespace Neo.Plugins.RestServer.Models.Error;
+
+internal class ErrorModel
+{
+ ///
+ /// Error's HResult Code.
+ ///
+ /// 1000
+ public int Code { get; init; } = 1000;
+ ///
+ /// Error's name of the type.
+ ///
+ /// GeneralException
+ public string Name { get; init; } = "GeneralException";
+ ///
+ /// Error's exception message.
+ ///
+ /// An error occurred.
+ /// Could be InnerException message as well, If exists.
+ public string Message { get; init; } = "An error occurred.";
+}
diff --git a/plugins/RestServer/Models/Error/ParameterFormatExceptionModel.cs b/plugins/RestServer/Models/Error/ParameterFormatExceptionModel.cs
new file mode 100644
index 000000000..6db9fb4b6
--- /dev/null
+++ b/plugins/RestServer/Models/Error/ParameterFormatExceptionModel.cs
@@ -0,0 +1,28 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ParameterFormatExceptionModel.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 Neo.Plugins.RestServer.Exceptions;
+
+namespace Neo.Plugins.RestServer.Models.Error;
+
+internal class ParameterFormatExceptionModel : ErrorModel
+{
+ public ParameterFormatExceptionModel()
+ {
+ Code = RestErrorCodes.ParameterFormatException;
+ Name = nameof(RestErrorCodes.ParameterFormatException);
+ }
+
+ public ParameterFormatExceptionModel(string message) : this()
+ {
+ Message = message;
+ }
+}
diff --git a/plugins/RestServer/Models/ExecutionEngineModel.cs b/plugins/RestServer/Models/ExecutionEngineModel.cs
new file mode 100644
index 000000000..26aa89f4e
--- /dev/null
+++ b/plugins/RestServer/Models/ExecutionEngineModel.cs
@@ -0,0 +1,49 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ExecutionEngineModel.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 Neo.Plugins.RestServer.Models.Error;
+using Neo.SmartContract;
+using Neo.VM;
+using Neo.VM.Types;
+
+namespace Neo.Plugins.RestServer.Models;
+
+internal class ExecutionEngineModel
+{
+ public long GasConsumed { get; set; } = 0L;
+ public VMState State { get; set; } = VMState.NONE;
+ public BlockchainEventModel[] Notifications { get; set; } = System.Array.Empty();
+ public StackItem[] ResultStack { get; set; } = System.Array.Empty();
+ public ErrorModel? FaultException { get; set; }
+}
+
+internal class BlockchainEventModel
+{
+ public UInt160 ScriptHash { get; set; } = new();
+ public string EventName { get; set; } = string.Empty;
+ public StackItem[] State { get; set; } = System.Array.Empty();
+
+ public static BlockchainEventModel Create(UInt160 scriptHash, string eventName, StackItem[] state) =>
+ new()
+ {
+ ScriptHash = scriptHash,
+ EventName = eventName ?? string.Empty,
+ State = state,
+ };
+
+ public static BlockchainEventModel Create(NotifyEventArgs notifyEventArgs, StackItem[] state) =>
+ new()
+ {
+ ScriptHash = notifyEventArgs.ScriptHash,
+ EventName = notifyEventArgs.EventName,
+ State = state,
+ };
+}
diff --git a/plugins/RestServer/Models/Ledger/MemoryPoolCountModel.cs b/plugins/RestServer/Models/Ledger/MemoryPoolCountModel.cs
new file mode 100644
index 000000000..90620452a
--- /dev/null
+++ b/plugins/RestServer/Models/Ledger/MemoryPoolCountModel.cs
@@ -0,0 +1,31 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// MemoryPoolCountModel.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.
+
+namespace Neo.Plugins.RestServer.Models.Ledger;
+
+internal class MemoryPoolCountModel
+{
+ ///
+ /// Total count all transactions.
+ ///
+ /// 110
+ public int Count { get; set; }
+ ///
+ /// Count of unverified transactions
+ ///
+ /// 10
+ public int UnVerifiedCount { get; set; }
+ ///
+ /// Count of verified transactions.
+ ///
+ /// 100
+ public int VerifiedCount { get; set; }
+}
diff --git a/plugins/RestServer/Models/Node/PluginModel.cs b/plugins/RestServer/Models/Node/PluginModel.cs
new file mode 100644
index 000000000..a3044a7cb
--- /dev/null
+++ b/plugins/RestServer/Models/Node/PluginModel.cs
@@ -0,0 +1,33 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// PluginModel.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.
+
+namespace Neo.Plugins.RestServer.Models.Node;
+
+internal class PluginModel
+{
+ ///
+ /// Name
+ ///
+ /// RestServer
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Version
+ ///
+ /// 3.5.0
+ public string Version { get; set; } = string.Empty;
+
+ ///
+ /// Description
+ ///
+ /// Enables REST Web Sevices for the node
+ public string Description { get; set; } = string.Empty;
+}
diff --git a/plugins/RestServer/Models/Node/ProtocolSettingsModel.cs b/plugins/RestServer/Models/Node/ProtocolSettingsModel.cs
new file mode 100644
index 000000000..c7e57fba3
--- /dev/null
+++ b/plugins/RestServer/Models/Node/ProtocolSettingsModel.cs
@@ -0,0 +1,40 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ProtocolSettingsModel.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 Neo.Cryptography.ECC;
+
+namespace Neo.Plugins.RestServer.Models.Node;
+
+internal class ProtocolSettingsModel
+{
+ ///
+ /// Network
+ ///
+ /// 860833102
+ public uint Network { get; set; }
+
+ ///
+ /// AddressVersion
+ ///
+ /// 53
+ public byte AddressVersion { get; set; }
+ public int ValidatorsCount { get; set; }
+ public uint MillisecondsPerBlock { get; set; }
+ public uint MaxValidUntilBlockIncrement { get; set; }
+ public uint MaxTransactionsPerBlock { get; set; }
+ public int MemoryPoolMaxTransactions { get; set; }
+ public uint MaxTraceableBlocks { get; set; }
+ public ulong InitialGasDistribution { get; set; }
+ public IReadOnlyCollection SeedList { get; set; } = [];
+ public IReadOnlyDictionary Hardforks { get; set; } = new Dictionary().AsReadOnly();
+ public IReadOnlyList StandbyValidators { get; set; } = [];
+ public IReadOnlyList StandbyCommittee { get; set; } = [];
+}
diff --git a/plugins/RestServer/Models/Node/RemoteNodeModel.cs b/plugins/RestServer/Models/Node/RemoteNodeModel.cs
new file mode 100644
index 000000000..1b7a9cf5a
--- /dev/null
+++ b/plugins/RestServer/Models/Node/RemoteNodeModel.cs
@@ -0,0 +1,39 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// RemoteNodeModel.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.
+
+namespace Neo.Plugins.RestServer.Models.Node;
+
+public class RemoteNodeModel
+{
+ ///
+ /// Remote peer's ip address.
+ ///
+ /// 10.0.0.100
+ public string RemoteAddress { get; set; } = string.Empty;
+
+ ///
+ /// Remote peer's port number.
+ ///
+ /// 20333
+ public int RemotePort { get; set; }
+
+ ///
+ /// Remote peer's listening tcp port.
+ ///
+ /// 20333
+ public int ListenTcpPort { get; set; }
+
+ ///
+ /// Remote peer's last synced block height.
+ ///
+ /// 2584158
+ public uint LastBlockIndex { get; set; }
+}
diff --git a/plugins/RestServer/Models/Token/NEP11TokenModel.cs b/plugins/RestServer/Models/Token/NEP11TokenModel.cs
new file mode 100644
index 000000000..75eec4f4c
--- /dev/null
+++ b/plugins/RestServer/Models/Token/NEP11TokenModel.cs
@@ -0,0 +1,20 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// NEP11TokenModel.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 Neo.VM.Types;
+
+namespace Neo.Plugins.RestServer.Models.Token;
+
+internal class NEP11TokenModel : NEP17TokenModel
+{
+ public IReadOnlyDictionary?> Tokens { get; set; }
+ = new Dictionary?>().AsReadOnly();
+}
diff --git a/plugins/RestServer/Models/Token/NEP17TokenModel.cs b/plugins/RestServer/Models/Token/NEP17TokenModel.cs
new file mode 100644
index 000000000..bb97f4e8c
--- /dev/null
+++ b/plugins/RestServer/Models/Token/NEP17TokenModel.cs
@@ -0,0 +1,23 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// NEP17TokenModel.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System.Numerics;
+
+namespace Neo.Plugins.RestServer.Models.Token;
+
+internal class NEP17TokenModel
+{
+ public string Name { get; set; } = string.Empty;
+ public UInt160 ScriptHash { get; set; } = UInt160.Zero;
+ public string Symbol { get; set; } = string.Empty;
+ public byte Decimals { get; set; }
+ public BigInteger TotalSupply { get; set; }
+}
diff --git a/plugins/RestServer/Models/Token/TokenBalanceModel.cs b/plugins/RestServer/Models/Token/TokenBalanceModel.cs
new file mode 100644
index 000000000..dfb3b82e6
--- /dev/null
+++ b/plugins/RestServer/Models/Token/TokenBalanceModel.cs
@@ -0,0 +1,24 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// TokenBalanceModel.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System.Numerics;
+
+namespace Neo.Plugins.RestServer.Models.Token;
+
+public class TokenBalanceModel
+{
+ public string Name { get; set; } = string.Empty;
+ public UInt160 ScriptHash { get; set; } = UInt160.Zero;
+ public string Symbol { get; set; } = string.Empty;
+ public byte Decimals { get; set; }
+ public BigInteger Balance { get; set; }
+ public BigInteger TotalSupply { get; set; }
+}
diff --git a/plugins/RestServer/Models/Utils/UtilsAddressIsValidModel.cs b/plugins/RestServer/Models/Utils/UtilsAddressIsValidModel.cs
new file mode 100644
index 000000000..04c3400d3
--- /dev/null
+++ b/plugins/RestServer/Models/Utils/UtilsAddressIsValidModel.cs
@@ -0,0 +1,21 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// UtilsAddressIsValidModel.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.
+
+namespace Neo.Plugins.RestServer.Models.Utils;
+
+internal class UtilsAddressIsValidModel : UtilsAddressModel
+{
+ ///
+ /// Indicates if address can be converted to ScriptHash or Neo Address.
+ ///
+ /// true
+ public bool IsValid { get; set; }
+}
diff --git a/plugins/RestServer/Models/Utils/UtilsAddressModel.cs b/plugins/RestServer/Models/Utils/UtilsAddressModel.cs
new file mode 100644
index 000000000..a10a15e13
--- /dev/null
+++ b/plugins/RestServer/Models/Utils/UtilsAddressModel.cs
@@ -0,0 +1,21 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// UtilsAddressModel.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.
+
+namespace Neo.Plugins.RestServer.Models.Utils;
+
+internal class UtilsAddressModel
+{
+ ///
+ /// Wallet address that was exported.
+ ///
+ /// NNLi44dJNXtDNSBkofB48aTVYtb1zZrNEs
+ public virtual string Address { get; set; } = string.Empty;
+}
diff --git a/plugins/RestServer/Models/Utils/UtilsScriptHashModel.cs b/plugins/RestServer/Models/Utils/UtilsScriptHashModel.cs
new file mode 100644
index 000000000..ff02af2ae
--- /dev/null
+++ b/plugins/RestServer/Models/Utils/UtilsScriptHashModel.cs
@@ -0,0 +1,21 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// UtilsScriptHashModel.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.
+
+namespace Neo.Plugins.RestServer.Models.Utils;
+
+internal class UtilsScriptHashModel
+{
+ ///
+ /// Scripthash of the wallet account exported.
+ ///
+ /// 0xed7cc6f5f2dd842d384f254bc0c2d58fb69a4761
+ public UInt160 ScriptHash { get; set; } = UInt160.Zero;
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/BigDecimalJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/BigDecimalJsonConverter.cs
new file mode 100644
index 000000000..2e30dd496
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/BigDecimalJsonConverter.cs
@@ -0,0 +1,63 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// BigDecimalJsonConverter.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 Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System.Numerics;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class BigDecimalJsonConverter : JsonConverter
+{
+ public override bool CanRead => true;
+ public override bool CanWrite => true;
+
+ public override BigDecimal ReadJson(JsonReader reader, Type objectType, BigDecimal existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ var token = JToken.ReadFrom(reader);
+
+ switch (token.Type)
+ {
+ case JTokenType.Object:
+ {
+ var jobj = (JObject)token;
+ var valueProp = jobj.Properties().SingleOrDefault(p => p.Name.Equals("value", StringComparison.InvariantCultureIgnoreCase));
+ var decimalsProp = jobj.Properties().SingleOrDefault(p => p.Name.Equals("decimals", StringComparison.InvariantCultureIgnoreCase));
+
+ if (valueProp != null && decimalsProp != null)
+ {
+ return new BigDecimal(valueProp.ToObject(), decimalsProp.ToObject());
+ }
+ break;
+ }
+ case JTokenType.Float:
+ {
+ if (token is JValue jval && jval.Value is not null)
+ {
+ return new BigDecimal((decimal)jval.Value);
+ }
+ break;
+ }
+ }
+
+ throw new FormatException();
+ }
+
+ public override void WriteJson(JsonWriter writer, BigDecimal value, JsonSerializer serializer)
+ {
+ var o = JToken.FromObject(new
+ {
+ value.Value,
+ value.Decimals,
+ }, serializer);
+ o.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/BlockHeaderJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/BlockHeaderJsonConverter.cs
new file mode 100644
index 000000000..23cfa0d6f
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/BlockHeaderJsonConverter.cs
@@ -0,0 +1,34 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// BlockHeaderJsonConverter.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 Neo.Network.P2P.Payloads;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class BlockHeaderJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+ public override bool CanWrite => true;
+
+ public override Header ReadJson(JsonReader reader, Type objectType, Header? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void WriteJson(JsonWriter writer, Header? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.BlockHeaderToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/BlockJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/BlockJsonConverter.cs
new file mode 100644
index 000000000..68e48c912
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/BlockJsonConverter.cs
@@ -0,0 +1,33 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// BlockJsonConverter.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 Neo.Network.P2P.Payloads;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class BlockJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+
+ public override bool CanWrite => true;
+
+ public override Block ReadJson(JsonReader reader, Type objectType, Block? existingValue, bool hasExistingValue, JsonSerializer serializer) =>
+ throw new NotImplementedException();
+
+ public override void WriteJson(JsonWriter writer, Block? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.BlockToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/ContractAbiJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/ContractAbiJsonConverter.cs
new file mode 100644
index 000000000..cad4791e6
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/ContractAbiJsonConverter.cs
@@ -0,0 +1,31 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractAbiJsonConverter.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 Neo.SmartContract.Manifest;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class ContractAbiJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+
+ public override bool CanWrite => true;
+
+ public override ContractAbi ReadJson(JsonReader reader, Type objectType, ContractAbi? existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
+ public override void WriteJson(JsonWriter writer, ContractAbi? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.ContractAbiToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/ContractEventDescriptorJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/ContractEventDescriptorJsonConverter.cs
new file mode 100644
index 000000000..c3845d353
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/ContractEventDescriptorJsonConverter.cs
@@ -0,0 +1,31 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractEventDescriptorJsonConverter.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 Neo.SmartContract.Manifest;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class ContractEventDescriptorJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+
+ public override bool CanWrite => true;
+
+ public override ContractEventDescriptor ReadJson(JsonReader reader, Type objectType, ContractEventDescriptor? existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
+ public override void WriteJson(JsonWriter writer, ContractEventDescriptor? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.ContractEventToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/ContractGroupJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/ContractGroupJsonConverter.cs
new file mode 100644
index 000000000..0c387de44
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/ContractGroupJsonConverter.cs
@@ -0,0 +1,31 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractGroupJsonConverter.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 Neo.SmartContract.Manifest;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class ContractGroupJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+
+ public override bool CanWrite => true;
+
+ public override ContractGroup ReadJson(JsonReader reader, Type objectType, ContractGroup? existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
+ public override void WriteJson(JsonWriter writer, ContractGroup? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.ContractGroupToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/ContractInvokeParametersJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/ContractInvokeParametersJsonConverter.cs
new file mode 100644
index 000000000..1c2b927cf
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/ContractInvokeParametersJsonConverter.cs
@@ -0,0 +1,33 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractInvokeParametersJsonConverter.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 Neo.Plugins.RestServer.Models.Contract;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class ContractInvokeParametersJsonConverter : JsonConverter
+{
+ public override bool CanRead => true;
+ public override bool CanWrite => false;
+
+ public override InvokeParams ReadJson(JsonReader reader, Type objectType, InvokeParams? existingValue, bool hasExistingValue, global::Newtonsoft.Json.JsonSerializer serializer)
+ {
+ var token = JToken.ReadFrom(reader);
+ return RestServerUtility.ContractInvokeParametersFromJToken(token);
+ }
+
+ public override void WriteJson(JsonWriter writer, InvokeParams? value, global::Newtonsoft.Json.JsonSerializer serializer)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/ContractJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/ContractJsonConverter.cs
new file mode 100644
index 000000000..8c3b5e037
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/ContractJsonConverter.cs
@@ -0,0 +1,31 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractJsonConverter.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 Neo.SmartContract;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class ContractJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+
+ public override bool CanWrite => true;
+
+ public override ContractState ReadJson(JsonReader reader, Type objectType, ContractState? existingValue, bool hasExistingValue, global::Newtonsoft.Json.JsonSerializer serializer) => throw new NotImplementedException();
+ public override void WriteJson(JsonWriter writer, ContractState? value, global::Newtonsoft.Json.JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.ContractStateToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/ContractManifestJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/ContractManifestJsonConverter.cs
new file mode 100644
index 000000000..d8210aafb
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/ContractManifestJsonConverter.cs
@@ -0,0 +1,31 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractManifestJsonConverter.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 Neo.SmartContract.Manifest;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class ContractManifestJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+
+ public override bool CanWrite => true;
+
+ public override ContractManifest ReadJson(JsonReader reader, Type objectType, ContractManifest? existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
+ public override void WriteJson(JsonWriter writer, ContractManifest? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.ContractManifestToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/ContractMethodJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/ContractMethodJsonConverter.cs
new file mode 100644
index 000000000..49c5af0a1
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/ContractMethodJsonConverter.cs
@@ -0,0 +1,31 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractMethodJsonConverter.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 Neo.SmartContract.Manifest;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class ContractMethodJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+
+ public override bool CanWrite => true;
+
+ public override ContractMethodDescriptor ReadJson(JsonReader reader, Type objectType, ContractMethodDescriptor? existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
+ public override void WriteJson(JsonWriter writer, ContractMethodDescriptor? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.ContractMethodToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/ContractMethodParametersJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/ContractMethodParametersJsonConverter.cs
new file mode 100644
index 000000000..c211504b2
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/ContractMethodParametersJsonConverter.cs
@@ -0,0 +1,32 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractMethodParametersJsonConverter.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 Neo.SmartContract.Manifest;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class ContractMethodParametersJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+ public override bool CanWrite => true;
+
+ public override ContractParameterDefinition ReadJson(JsonReader reader, Type objectType, ContractParameterDefinition? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ => throw new NotImplementedException();
+
+ public override void WriteJson(JsonWriter writer, ContractParameterDefinition? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.ContractMethodParameterToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/ContractParameterDefinitionJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/ContractParameterDefinitionJsonConverter.cs
new file mode 100644
index 000000000..a4f26fe3e
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/ContractParameterDefinitionJsonConverter.cs
@@ -0,0 +1,31 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractParameterDefinitionJsonConverter.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 Neo.SmartContract.Manifest;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class ContractParameterDefinitionJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+
+ public override bool CanWrite => true;
+
+ public override ContractParameterDefinition ReadJson(JsonReader reader, Type objectType, ContractParameterDefinition? existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
+ public override void WriteJson(JsonWriter writer, ContractParameterDefinition? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.ContractParameterDefinitionToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/ContractParameterJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/ContractParameterJsonConverter.cs
new file mode 100644
index 000000000..d02867da3
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/ContractParameterJsonConverter.cs
@@ -0,0 +1,33 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractParameterJsonConverter.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 Neo.SmartContract;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class ContractParameterJsonConverter : JsonConverter
+{
+ public override bool CanRead => true;
+ public override bool CanWrite => false;
+
+ public override ContractParameter ReadJson(JsonReader reader, Type objectType, ContractParameter? existingValue, bool hasExistingValue, global::Newtonsoft.Json.JsonSerializer serializer)
+ {
+ var token = JToken.ReadFrom(reader);
+ return RestServerUtility.ContractParameterFromJToken(token);
+ }
+
+ public override void WriteJson(JsonWriter writer, ContractParameter? value, global::Newtonsoft.Json.JsonSerializer serializer)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/ContractPermissionDescriptorJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/ContractPermissionDescriptorJsonConverter.cs
new file mode 100644
index 000000000..c132118ed
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/ContractPermissionDescriptorJsonConverter.cs
@@ -0,0 +1,31 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractPermissionDescriptorJsonConverter.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 Neo.SmartContract.Manifest;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class ContractPermissionDescriptorJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+
+ public override bool CanWrite => true;
+
+ public override ContractPermissionDescriptor ReadJson(JsonReader reader, Type objectType, ContractPermissionDescriptor? existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
+ public override void WriteJson(JsonWriter writer, ContractPermissionDescriptor? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.ContractPermissionDescriptorToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/ContractPermissionJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/ContractPermissionJsonConverter.cs
new file mode 100644
index 000000000..8d2da96d8
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/ContractPermissionJsonConverter.cs
@@ -0,0 +1,31 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ContractPermissionJsonConverter.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 Neo.SmartContract.Manifest;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+internal class ContractPermissionJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+
+ public override bool CanWrite => true;
+
+ public override ContractPermission ReadJson(JsonReader reader, Type objectType, ContractPermission? existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
+ public override void WriteJson(JsonWriter writer, ContractPermission? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.ContractPermissionToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/ECPointJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/ECPointJsonConverter.cs
new file mode 100644
index 000000000..1d98be83d
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/ECPointJsonConverter.cs
@@ -0,0 +1,39 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ECPointJsonConverter.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 Neo.Cryptography.ECC;
+using Neo.Plugins.RestServer.Exceptions;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class ECPointJsonConverter : JsonConverter
+{
+ public override ECPoint ReadJson(JsonReader reader, Type objectType, ECPoint? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ var value = reader?.Value?.ToString() ?? throw new UInt256FormatException($"'{reader}' is invalid.");
+ try
+ {
+ return ECPoint.Parse(value, ECCurve.Secp256r1);
+ }
+ catch (FormatException)
+ {
+ throw new UInt256FormatException($"'{value}' is invalid.");
+ }
+ }
+
+ public override void WriteJson(JsonWriter writer, ECPoint? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ writer.WriteValue(value.ToString());
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/GuidJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/GuidJsonConverter.cs
new file mode 100644
index 000000000..027cea669
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/GuidJsonConverter.cs
@@ -0,0 +1,30 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// GuidJsonConverter.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 Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+internal class GuidJsonConverter : JsonConverter
+{
+ public override Guid ReadJson(JsonReader reader, Type objectType, Guid existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ var value = reader.Value?.ToString()
+ ?? throw new ArgumentException("reader.Value is null", nameof(reader));
+
+ return Guid.Parse(value);
+ }
+
+ public override void WriteJson(JsonWriter writer, Guid value, JsonSerializer serializer)
+ {
+ writer.WriteValue(value.ToString("n"));
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/InteropInterfaceJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/InteropInterfaceJsonConverter.cs
new file mode 100644
index 000000000..cbfa4e267
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/InteropInterfaceJsonConverter.cs
@@ -0,0 +1,35 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// InteropInterfaceJsonConverter.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 Neo.VM.Types;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class InteropInterfaceJsonConverter : JsonConverter
+{
+ public override InteropInterface ReadJson(JsonReader reader, Type objectType, InteropInterface? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ var t = JToken.Load(reader);
+ if (RestServerUtility.StackItemFromJToken(t) is InteropInterface iface) return iface;
+
+ throw new FormatException();
+ }
+
+ public override void WriteJson(JsonWriter writer, InteropInterface? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var t = RestServerUtility.StackItemToJToken(value, null, serializer);
+ t.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/MethodTokenJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/MethodTokenJsonConverter.cs
new file mode 100644
index 000000000..760738657
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/MethodTokenJsonConverter.cs
@@ -0,0 +1,31 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// MethodTokenJsonConverter.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 Neo.SmartContract;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class MethodTokenJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+
+ public override bool CanWrite => true;
+
+ public override MethodToken ReadJson(JsonReader reader, Type objectType, MethodToken? existingValue, bool hasExistingValue, global::Newtonsoft.Json.JsonSerializer serializer) => throw new NotImplementedException();
+ public override void WriteJson(JsonWriter writer, MethodToken? value, global::Newtonsoft.Json.JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.MethodTokenToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/NefFileJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/NefFileJsonConverter.cs
new file mode 100644
index 000000000..249bb3e49
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/NefFileJsonConverter.cs
@@ -0,0 +1,27 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// NefFileJsonConverter.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 Neo.SmartContract;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class NefFileJsonConverter : JsonConverter
+{
+ public override NefFile ReadJson(JsonReader reader, Type objectType, NefFile? existingValue, bool hasExistingValue, global::Newtonsoft.Json.JsonSerializer serializer) => throw new NotImplementedException();
+ public override void WriteJson(JsonWriter writer, NefFile? value, global::Newtonsoft.Json.JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.ContractNefFileToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/ReadOnlyMemoryBytesJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/ReadOnlyMemoryBytesJsonConverter.cs
new file mode 100644
index 000000000..c5f777efd
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/ReadOnlyMemoryBytesJsonConverter.cs
@@ -0,0 +1,32 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// ReadOnlyMemoryBytesJsonConverter.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 Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class ReadOnlyMemoryBytesJsonConverter : JsonConverter>
+{
+ public override ReadOnlyMemory ReadJson(JsonReader reader, Type objectType, ReadOnlyMemory existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ var o = JToken.Load(reader);
+ var value = o.ToObject();
+ ArgumentNullException.ThrowIfNull(value, nameof(value));
+
+ return Convert.FromBase64String(value);
+ }
+
+ public override void WriteJson(JsonWriter writer, ReadOnlyMemory value, JsonSerializer serializer)
+ {
+ writer.WriteValue(Convert.ToBase64String(value.Span));
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/SignerJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/SignerJsonConverter.cs
new file mode 100644
index 000000000..0588b34d4
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/SignerJsonConverter.cs
@@ -0,0 +1,33 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// SignerJsonConverter.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 Neo.Network.P2P.Payloads;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class SignerJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+
+ public override bool CanWrite => true;
+
+ public override Signer ReadJson(JsonReader reader, Type objectType, Signer? existingValue, bool hasExistingValue, JsonSerializer serializer) =>
+ throw new NotImplementedException();
+
+ public override void WriteJson(JsonWriter writer, Signer? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.SignerToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/StackItemJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/StackItemJsonConverter.cs
new file mode 100644
index 000000000..4c7f3cd18
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/StackItemJsonConverter.cs
@@ -0,0 +1,33 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// StackItemJsonConverter.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 Neo.VM.Types;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class StackItemJsonConverter : JsonConverter
+{
+ public override StackItem ReadJson(JsonReader reader, Type objectType, StackItem? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ var t = JObject.Load(reader);
+ return RestServerUtility.StackItemFromJToken(t);
+ }
+
+ public override void WriteJson(JsonWriter writer, StackItem? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var t = RestServerUtility.StackItemToJToken(value, null, serializer);
+ t.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/TransactionAttributeJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/TransactionAttributeJsonConverter.cs
new file mode 100644
index 000000000..97e1350af
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/TransactionAttributeJsonConverter.cs
@@ -0,0 +1,31 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// TransactionAttributeJsonConverter.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 Neo.Network.P2P.Payloads;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class TransactionAttributeJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+
+ public override bool CanWrite => true;
+
+ public override TransactionAttribute ReadJson(JsonReader reader, Type objectType, TransactionAttribute? existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
+ public override void WriteJson(JsonWriter writer, TransactionAttribute? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.TransactionAttributeToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/TransactionJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/TransactionJsonConverter.cs
new file mode 100644
index 000000000..f55ea1ec4
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/TransactionJsonConverter.cs
@@ -0,0 +1,31 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// TransactionJsonConverter.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 Neo.Network.P2P.Payloads;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class TransactionJsonConverter : JsonConverter
+{
+ public override bool CanRead => false;
+
+ public override bool CanWrite => true;
+
+ public override Transaction ReadJson(JsonReader reader, Type objectType, Transaction? existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
+ public override void WriteJson(JsonWriter writer, Transaction? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var j = RestServerUtility.TransactionToJToken(value, serializer);
+ j.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/UInt160JsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/UInt160JsonConverter.cs
new file mode 100644
index 000000000..6a8909102
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/UInt160JsonConverter.cs
@@ -0,0 +1,43 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// UInt160JsonConverter.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 Neo.Plugins.RestServer.Exceptions;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class UInt160JsonConverter : JsonConverter
+{
+ public override bool CanRead => true;
+ public override bool CanWrite => true;
+
+ public override UInt160 ReadJson(JsonReader reader, Type objectType, UInt160? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ var value = reader.Value?.ToString();
+ ArgumentNullException.ThrowIfNull(value, nameof(value));
+
+ try
+ {
+ return RestServerUtility.ConvertToScriptHash(value, RestServerPlugin.NeoSystem!.Settings);
+ }
+ catch (FormatException)
+ {
+ throw new ScriptHashFormatException($"'{value}' is invalid.");
+ }
+ }
+
+ public override void WriteJson(JsonWriter writer, UInt160? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ writer.WriteValue(value.ToString());
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/UInt256JsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/UInt256JsonConverter.cs
new file mode 100644
index 000000000..14d038a65
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/UInt256JsonConverter.cs
@@ -0,0 +1,40 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// UInt256JsonConverter.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 Neo.Plugins.RestServer.Exceptions;
+using Newtonsoft.Json;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class UInt256JsonConverter : JsonConverter
+{
+ public override UInt256 ReadJson(JsonReader reader, Type objectType, UInt256? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ var value = reader.Value?.ToString()
+ ?? throw new ArgumentException("reader.Value is null", nameof(reader));
+
+ try
+ {
+ return UInt256.Parse(value);
+ }
+ catch (FormatException)
+ {
+ throw new UInt256FormatException($"'{value}' is invalid.");
+ }
+ }
+
+ public override void WriteJson(JsonWriter writer, UInt256? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ writer.WriteValue(value.ToString());
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/VmArrayJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/VmArrayJsonConverter.cs
new file mode 100644
index 000000000..2aab1908f
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/VmArrayJsonConverter.cs
@@ -0,0 +1,35 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// VmArrayJsonConverter.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 Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Array = Neo.VM.Types.Array;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class VmArrayJsonConverter : JsonConverter
+{
+ public override Array ReadJson(JsonReader reader, Type objectType, Array? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ var t = JToken.Load(reader);
+ if (RestServerUtility.StackItemFromJToken(t) is Array a) return a;
+
+ throw new FormatException();
+ }
+
+ public override void WriteJson(JsonWriter writer, Array? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var t = RestServerUtility.StackItemToJToken(value, null, serializer);
+ t.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/VmBooleanJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/VmBooleanJsonConverter.cs
new file mode 100644
index 000000000..842228e90
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/VmBooleanJsonConverter.cs
@@ -0,0 +1,35 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// VmBooleanJsonConverter.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 Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Boolean = Neo.VM.Types.Boolean;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class VmBooleanJsonConverter : JsonConverter
+{
+ public override Boolean ReadJson(JsonReader reader, Type objectType, Boolean? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ var t = JToken.ReadFrom(reader);
+ if (RestServerUtility.StackItemFromJToken(t) is Boolean b) return b;
+
+ throw new FormatException();
+ }
+
+ public override void WriteJson(JsonWriter writer, Boolean? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var t = RestServerUtility.StackItemToJToken(value, null, serializer);
+ t.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/VmBufferJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/VmBufferJsonConverter.cs
new file mode 100644
index 000000000..a56514544
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/VmBufferJsonConverter.cs
@@ -0,0 +1,35 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// VmBufferJsonConverter.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 Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Buffer = Neo.VM.Types.Buffer;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class VmBufferJsonConverter : JsonConverter
+{
+ public override Buffer ReadJson(JsonReader reader, Type objectType, Buffer? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ var t = JToken.ReadFrom(reader);
+ if (RestServerUtility.StackItemFromJToken(t) is Buffer b) return b;
+
+ throw new FormatException();
+ }
+
+ public override void WriteJson(JsonWriter writer, Buffer? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var t = RestServerUtility.StackItemToJToken(value, null, serializer);
+ t.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/VmByteStringJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/VmByteStringJsonConverter.cs
new file mode 100644
index 000000000..b71eb1b1c
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/VmByteStringJsonConverter.cs
@@ -0,0 +1,35 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// VmByteStringJsonConverter.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 Neo.VM.Types;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class VmByteStringJsonConverter : JsonConverter
+{
+ public override ByteString ReadJson(JsonReader reader, Type objectType, ByteString? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ var t = JToken.ReadFrom(reader);
+ if (RestServerUtility.StackItemFromJToken(t) is ByteString bs) return bs;
+
+ throw new FormatException();
+ }
+
+ public override void WriteJson(JsonWriter writer, ByteString? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var t = RestServerUtility.StackItemToJToken(value, null, serializer);
+ t.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/VmIntegerJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/VmIntegerJsonConverter.cs
new file mode 100644
index 000000000..ef085fabf
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/VmIntegerJsonConverter.cs
@@ -0,0 +1,35 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// VmIntegerJsonConverter.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 Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Integer = Neo.VM.Types.Integer;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class VmIntegerJsonConverter : JsonConverter
+{
+ public override Integer ReadJson(JsonReader reader, Type objectType, Integer? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ var t = JToken.ReadFrom(reader);
+ if (RestServerUtility.StackItemFromJToken(t) is Integer i) return i;
+
+ throw new FormatException();
+ }
+
+ public override void WriteJson(JsonWriter writer, Integer? value, JsonSerializer serializer)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var t = RestServerUtility.StackItemToJToken(value, null, serializer);
+ t.WriteTo(writer);
+ }
+}
diff --git a/plugins/RestServer/Newtonsoft/Json/VmMapJsonConverter.cs b/plugins/RestServer/Newtonsoft/Json/VmMapJsonConverter.cs
new file mode 100644
index 000000000..8cc51ec42
--- /dev/null
+++ b/plugins/RestServer/Newtonsoft/Json/VmMapJsonConverter.cs
@@ -0,0 +1,35 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// VmMapJsonConverter.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 Neo.VM.Types;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Neo.Plugins.RestServer.Newtonsoft.Json;
+
+public class VmMapJsonConverter : JsonConverter