diff --git a/.editorconfig b/.editorconfig
index f24ccd45a8..c8247e5496 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -307,4 +307,4 @@ 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
\ No newline at end of file
+dotnet_code_quality.CA1822.api_surface = private
diff --git a/.gitattributes b/.gitattributes
index 3bdf2f7093..95c6696127 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -4,6 +4,8 @@
* text eol=lf
*.cs eol=lf
*.csproj eol=lf
+*.props eol=lf
+*.json eol=lf
###############################################################################
# Set default behavior for command prompt diff.
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5ac5e3dc65..6f7f9867c8 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -34,15 +34,25 @@ jobs:
- name: Test
if: matrix.os != 'ubuntu-latest'
run: |
+ dotnet sln neo.sln remove ./tests/Neo.Plugins.Storage.Tests/Neo.Plugins.Storage.Tests.csproj
dotnet test
- name: Test for coverall
if: matrix.os == 'ubuntu-latest'
run: |
+ sudo apt-get --assume-yes install libleveldb-dev librocksdb-dev
dotnet test ./tests/Neo.Cryptography.BLS12_381.Tests /p:CollectCoverage=true /p:CoverletOutput='${{ github.workspace }}/TestResults/coverage/'
dotnet test ./tests/Neo.ConsoleService.Tests /p:CollectCoverage=true /p:CoverletOutput='${{ github.workspace }}/TestResults/coverage/' /p:MergeWith='${{ github.workspace }}/TestResults/coverage/coverage.json'
dotnet test ./tests/Neo.UnitTests /p:CollectCoverage=true /p:CoverletOutput='${{ github.workspace }}/TestResults/coverage/' /p:MergeWith='${{ github.workspace }}/TestResults/coverage/coverage.json'
dotnet test ./tests/Neo.VM.Tests /p:CollectCoverage=true /p:CoverletOutput='${{ github.workspace }}/TestResults/coverage/' /p:MergeWith='${{ github.workspace }}/TestResults/coverage/coverage.json'
- dotnet test ./tests/Neo.Json.UnitTests /p:CollectCoverage=true /p:CoverletOutput='${{ github.workspace }}/TestResults/coverage/' /p:MergeWith='${{ github.workspace }}/TestResults/coverage/coverage.json' /p:CoverletOutputFormat='lcov'
+ dotnet test ./tests/Neo.Json.UnitTests /p:CollectCoverage=true /p:CoverletOutput='${{ github.workspace }}/TestResults/coverage/' /p:MergeWith='${{ github.workspace }}/TestResults/coverage/coverage.json'
+
+ # Plugins
+ dotnet test ./tests/Neo.Cryptography.MPTTrie.Tests /p:CollectCoverage=true /p:CoverletOutput='${{ github.workspace }}/TestResults/coverage/' /p:MergeWith='${{ github.workspace }}/TestResults/coverage/coverage.json'
+ dotnet test ./tests/Neo.Network.RPC.Tests /p:CollectCoverage=true /p:CoverletOutput='${{ github.workspace }}/TestResults/coverage/' /p:MergeWith='${{ github.workspace }}/TestResults/coverage/coverage.json'
+ dotnet test ./tests/Neo.Plugins.OracleService.Tests /p:CollectCoverage=true /p:CoverletOutput='${{ github.workspace }}/TestResults/coverage/' /p:MergeWith='${{ github.workspace }}/TestResults/coverage/coverage.json'
+ dotnet test ./tests/Neo.Plugins.RpcServer.Tests /p:CollectCoverage=true /p:CoverletOutput='${{ github.workspace }}/TestResults/coverage/' /p:MergeWith='${{ github.workspace }}/TestResults/coverage/coverage.json'
+ dotnet test ./tests/Neo.Plugins.Storage.Tests /p:CollectCoverage=true /p:CoverletOutput='${{ github.workspace }}/TestResults/coverage/' /p:MergeWith='${{ github.workspace }}/TestResults/coverage/coverage.json' /p:CoverletOutputFormat='lcov'
+
- name: Coveralls
if: matrix.os == 'ubuntu-latest'
uses: coverallsapp/github-action@v2.2.3
diff --git a/src/README.md b/.neo/README.md
similarity index 100%
rename from src/README.md
rename to .neo/README.md
diff --git a/src/neo.png b/.neo/neo.png
similarity index 100%
rename from src/neo.png
rename to .neo/neo.png
diff --git a/neo.sln b/neo.sln
index b62de06a6e..46ff7c64e8 100644
--- a/neo.sln
+++ b/neo.sln
@@ -36,9 +36,49 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Cryptography.BLS12_381"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Cryptography.BLS12_381.Tests", "tests\Neo.Cryptography.BLS12_381.Tests\Neo.Cryptography.BLS12_381.Tests.csproj", "{387CCF6C-9A26-43F6-A639-0A82E91E10D8}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.IO", "src\Neo.IO\Neo.IO.csproj", "{4CDAC1AA-45C6-4157-8D8E-199050433048}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.IO", "src\Neo.IO\Neo.IO.csproj", "{4CDAC1AA-45C6-4157-8D8E-199050433048}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Extensions", "src\Neo.Extensions\Neo.Extensions.csproj", "{9C5213D6-3833-4570-8AE2-47E9F9017A8F}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Extensions", "src\Neo.Extensions\Neo.Extensions.csproj", "{9C5213D6-3833-4570-8AE2-47E9F9017A8F}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "plugins", "plugins", "{C2DC830A-327A-42A7-807D-295216D30DBB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Cryptography.MPTTrie.Tests", "tests\Neo.Cryptography.MPTTrie.Tests\Neo.Cryptography.MPTTrie.Tests.csproj", "{FAF5D8AC-B6B3-4CD4-879D-0E5F6211480F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Network.RPC.Tests", "tests\Neo.Network.RPC.Tests\Neo.Network.RPC.Tests.csproj", "{0E92F219-1225-4DD0-8C4A-98840985D59C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Plugins.OracleService.Tests", "tests\Neo.Plugins.OracleService.Tests\Neo.Plugins.OracleService.Tests.csproj", "{5D9764FB-827D-4DDE-84E3-3C05FD8ABC89}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Plugins.RpcServer.Tests", "tests\Neo.Plugins.RpcServer.Tests\Neo.Plugins.RpcServer.Tests.csproj", "{2CBD2311-BA2E-4921-A000-FDDA59B74958}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Plugins.Storage.Tests", "tests\Neo.Plugins.Storage.Tests\Neo.Plugins.Storage.Tests.csproj", "{EF01E062-DBBC-47AF-AF3B-9EDEB00CFF7C}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{7F257712-D033-47FF-B439-9D4320D06599}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationLogs", "src\Plugins\ApplicationLogs\ApplicationLogs.csproj", "{22E2CE64-080B-4138-885F-7FA74A9159FB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DBFTPlugin", "src\Plugins\DBFTPlugin\DBFTPlugin.csproj", "{4C39E872-FC37-4BFD-AE4C-3E3F0546B726}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LevelDBStore", "src\Plugins\LevelDBStore\LevelDBStore.csproj", "{4C4D8180-9326-486C-AECF-8368BBD0766A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MPTTrie", "src\Plugins\MPTTrie\MPTTrie.csproj", "{80DA3CE7-9770-4F00-9179-0DA91DABFDFA}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OracleService", "src\Plugins\OracleService\OracleService.csproj", "{DE0FB77E-3099-4C88-BB7D-BFAED75D813E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RocksDBStore", "src\Plugins\RocksDBStore\RocksDBStore.csproj", "{3DE59148-59D6-4CD3-8086-0BC74E3D4E0B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RpcServer", "src\Plugins\RpcServer\RpcServer.csproj", "{A3941551-E72C-42D7-8C4D-5122CB60D73D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SQLiteWallet", "src\Plugins\SQLiteWallet\SQLiteWallet.csproj", "{F53D5FF0-5D3D-4E8B-A44F-C4C5D9B563B1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatesDumper", "src\Plugins\StatesDumper\StatesDumper.csproj", "{90CCA7D4-C277-4112-A036-BBB90C3FE3BE}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StateService", "src\Plugins\StateService\StateService.csproj", "{88975A8D-4797-45A4-BC3E-15962A425A54}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StorageDumper", "src\Plugins\StorageDumper\StorageDumper.csproj", "{FF76D8A4-356B-461A-8471-BC1B83E57BBC}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokensTracker", "src\Plugins\TokensTracker\TokensTracker.csproj", "{5E4947F3-05D3-4806-B0F3-30DAC71B5986}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RpcClient", "src\Plugins\RpcClient\RpcClient.csproj", "{185ADAFC-BFC6-413D-BC2E-97F9FB0A8AF0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -110,6 +150,78 @@ Global
{9C5213D6-3833-4570-8AE2-47E9F9017A8F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9C5213D6-3833-4570-8AE2-47E9F9017A8F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9C5213D6-3833-4570-8AE2-47E9F9017A8F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FAF5D8AC-B6B3-4CD4-879D-0E5F6211480F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FAF5D8AC-B6B3-4CD4-879D-0E5F6211480F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FAF5D8AC-B6B3-4CD4-879D-0E5F6211480F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FAF5D8AC-B6B3-4CD4-879D-0E5F6211480F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0E92F219-1225-4DD0-8C4A-98840985D59C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0E92F219-1225-4DD0-8C4A-98840985D59C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0E92F219-1225-4DD0-8C4A-98840985D59C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0E92F219-1225-4DD0-8C4A-98840985D59C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5D9764FB-827D-4DDE-84E3-3C05FD8ABC89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5D9764FB-827D-4DDE-84E3-3C05FD8ABC89}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5D9764FB-827D-4DDE-84E3-3C05FD8ABC89}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5D9764FB-827D-4DDE-84E3-3C05FD8ABC89}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2CBD2311-BA2E-4921-A000-FDDA59B74958}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2CBD2311-BA2E-4921-A000-FDDA59B74958}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2CBD2311-BA2E-4921-A000-FDDA59B74958}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2CBD2311-BA2E-4921-A000-FDDA59B74958}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EF01E062-DBBC-47AF-AF3B-9EDEB00CFF7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EF01E062-DBBC-47AF-AF3B-9EDEB00CFF7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EF01E062-DBBC-47AF-AF3B-9EDEB00CFF7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EF01E062-DBBC-47AF-AF3B-9EDEB00CFF7C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {22E2CE64-080B-4138-885F-7FA74A9159FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {22E2CE64-080B-4138-885F-7FA74A9159FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {22E2CE64-080B-4138-885F-7FA74A9159FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {22E2CE64-080B-4138-885F-7FA74A9159FB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4C39E872-FC37-4BFD-AE4C-3E3F0546B726}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4C39E872-FC37-4BFD-AE4C-3E3F0546B726}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4C39E872-FC37-4BFD-AE4C-3E3F0546B726}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4C39E872-FC37-4BFD-AE4C-3E3F0546B726}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4C4D8180-9326-486C-AECF-8368BBD0766A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4C4D8180-9326-486C-AECF-8368BBD0766A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4C4D8180-9326-486C-AECF-8368BBD0766A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4C4D8180-9326-486C-AECF-8368BBD0766A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {80DA3CE7-9770-4F00-9179-0DA91DABFDFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {80DA3CE7-9770-4F00-9179-0DA91DABFDFA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {80DA3CE7-9770-4F00-9179-0DA91DABFDFA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {80DA3CE7-9770-4F00-9179-0DA91DABFDFA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DE0FB77E-3099-4C88-BB7D-BFAED75D813E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DE0FB77E-3099-4C88-BB7D-BFAED75D813E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DE0FB77E-3099-4C88-BB7D-BFAED75D813E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DE0FB77E-3099-4C88-BB7D-BFAED75D813E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3DE59148-59D6-4CD3-8086-0BC74E3D4E0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3DE59148-59D6-4CD3-8086-0BC74E3D4E0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3DE59148-59D6-4CD3-8086-0BC74E3D4E0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3DE59148-59D6-4CD3-8086-0BC74E3D4E0B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A3941551-E72C-42D7-8C4D-5122CB60D73D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A3941551-E72C-42D7-8C4D-5122CB60D73D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A3941551-E72C-42D7-8C4D-5122CB60D73D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A3941551-E72C-42D7-8C4D-5122CB60D73D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F53D5FF0-5D3D-4E8B-A44F-C4C5D9B563B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F53D5FF0-5D3D-4E8B-A44F-C4C5D9B563B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F53D5FF0-5D3D-4E8B-A44F-C4C5D9B563B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F53D5FF0-5D3D-4E8B-A44F-C4C5D9B563B1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {90CCA7D4-C277-4112-A036-BBB90C3FE3BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {90CCA7D4-C277-4112-A036-BBB90C3FE3BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {90CCA7D4-C277-4112-A036-BBB90C3FE3BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {90CCA7D4-C277-4112-A036-BBB90C3FE3BE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {88975A8D-4797-45A4-BC3E-15962A425A54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {88975A8D-4797-45A4-BC3E-15962A425A54}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {88975A8D-4797-45A4-BC3E-15962A425A54}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {88975A8D-4797-45A4-BC3E-15962A425A54}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FF76D8A4-356B-461A-8471-BC1B83E57BBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FF76D8A4-356B-461A-8471-BC1B83E57BBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FF76D8A4-356B-461A-8471-BC1B83E57BBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FF76D8A4-356B-461A-8471-BC1B83E57BBC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5E4947F3-05D3-4806-B0F3-30DAC71B5986}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5E4947F3-05D3-4806-B0F3-30DAC71B5986}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5E4947F3-05D3-4806-B0F3-30DAC71B5986}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5E4947F3-05D3-4806-B0F3-30DAC71B5986}.Release|Any CPU.Build.0 = Release|Any CPU
+ {185ADAFC-BFC6-413D-BC2E-97F9FB0A8AF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {185ADAFC-BFC6-413D-BC2E-97F9FB0A8AF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {185ADAFC-BFC6-413D-BC2E-97F9FB0A8AF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {185ADAFC-BFC6-413D-BC2E-97F9FB0A8AF0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -131,6 +243,25 @@ Global
{387CCF6C-9A26-43F6-A639-0A82E91E10D8} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1}
{4CDAC1AA-45C6-4157-8D8E-199050433048} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
{9C5213D6-3833-4570-8AE2-47E9F9017A8F} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
+ {FAF5D8AC-B6B3-4CD4-879D-0E5F6211480F} = {7F257712-D033-47FF-B439-9D4320D06599}
+ {0E92F219-1225-4DD0-8C4A-98840985D59C} = {7F257712-D033-47FF-B439-9D4320D06599}
+ {5D9764FB-827D-4DDE-84E3-3C05FD8ABC89} = {7F257712-D033-47FF-B439-9D4320D06599}
+ {2CBD2311-BA2E-4921-A000-FDDA59B74958} = {7F257712-D033-47FF-B439-9D4320D06599}
+ {EF01E062-DBBC-47AF-AF3B-9EDEB00CFF7C} = {7F257712-D033-47FF-B439-9D4320D06599}
+ {7F257712-D033-47FF-B439-9D4320D06599} = {C2DC830A-327A-42A7-807D-295216D30DBB}
+ {22E2CE64-080B-4138-885F-7FA74A9159FB} = {C2DC830A-327A-42A7-807D-295216D30DBB}
+ {4C39E872-FC37-4BFD-AE4C-3E3F0546B726} = {C2DC830A-327A-42A7-807D-295216D30DBB}
+ {4C4D8180-9326-486C-AECF-8368BBD0766A} = {C2DC830A-327A-42A7-807D-295216D30DBB}
+ {80DA3CE7-9770-4F00-9179-0DA91DABFDFA} = {C2DC830A-327A-42A7-807D-295216D30DBB}
+ {DE0FB77E-3099-4C88-BB7D-BFAED75D813E} = {C2DC830A-327A-42A7-807D-295216D30DBB}
+ {3DE59148-59D6-4CD3-8086-0BC74E3D4E0B} = {C2DC830A-327A-42A7-807D-295216D30DBB}
+ {A3941551-E72C-42D7-8C4D-5122CB60D73D} = {C2DC830A-327A-42A7-807D-295216D30DBB}
+ {F53D5FF0-5D3D-4E8B-A44F-C4C5D9B563B1} = {C2DC830A-327A-42A7-807D-295216D30DBB}
+ {90CCA7D4-C277-4112-A036-BBB90C3FE3BE} = {C2DC830A-327A-42A7-807D-295216D30DBB}
+ {88975A8D-4797-45A4-BC3E-15962A425A54} = {C2DC830A-327A-42A7-807D-295216D30DBB}
+ {FF76D8A4-356B-461A-8471-BC1B83E57BBC} = {C2DC830A-327A-42A7-807D-295216D30DBB}
+ {5E4947F3-05D3-4806-B0F3-30DAC71B5986} = {C2DC830A-327A-42A7-807D-295216D30DBB}
+ {185ADAFC-BFC6-413D-BC2E-97F9FB0A8AF0} = {C2DC830A-327A-42A7-807D-295216D30DBB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BCBA19D9-F868-4C6D-8061-A2B91E06E3EC}
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 816760b50b..d3de66b478 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -19,8 +19,8 @@
-
-
+
+
diff --git a/src/Plugins/ApplicationLogs/ApplicationLogs.csproj b/src/Plugins/ApplicationLogs/ApplicationLogs.csproj
new file mode 100644
index 0000000000..2079abfcff
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/ApplicationLogs.csproj
@@ -0,0 +1,16 @@
+
+
+ net8.0
+ Neo.Plugins.ApplicationLogs
+ Neo.Plugins
+ enable
+
+
+
+
+
+ false
+ runtime
+
+
+
\ No newline at end of file
diff --git a/src/Plugins/ApplicationLogs/LogReader.cs b/src/Plugins/ApplicationLogs/LogReader.cs
new file mode 100644
index 0000000000..56163ef4f1
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/LogReader.cs
@@ -0,0 +1,457 @@
+// Copyright (C) 2015-2024 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 ApplicationLogs.Store;
+using ApplicationLogs.Store.Models;
+using Neo.ConsoleService;
+using Neo.IO;
+using Neo.Json;
+using Neo.Ledger;
+using Neo.Network.P2P.Payloads;
+using Neo.Persistence;
+using Neo.SmartContract;
+using Neo.SmartContract.Native;
+using Neo.VM;
+using System.Numerics;
+using static System.IO.Path;
+
+namespace Neo.Plugins
+{
+ public class LogReader : Plugin
+ {
+ #region Globals
+
+ private 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 notificatons (NotifyLog) on blockchain.";
+
+ #region Ctor
+
+ public LogReader()
+ {
+ _logEvents = new();
+ Blockchain.Committing += OnCommitting;
+ Blockchain.Committed += OnCommitted;
+ }
+
+ #endregion
+
+ #region Override Methods
+
+ public override void Dispose()
+ {
+ Blockchain.Committing -= OnCommitting;
+ Blockchain.Committed -= OnCommitted;
+ if (Settings.Default.Debug)
+ ApplicationEngine.Log -= OnApplicationEngineLog;
+ GC.SuppressFinalize(this);
+ }
+
+ protected override void Configure()
+ {
+ Settings.Load(GetConfiguration());
+ }
+
+ protected override void OnSystemLoaded(NeoSystem system)
+ {
+ if (system.Settings.Network != Settings.Default.Network)
+ return;
+ string path = string.Format(Settings.Default.Path, Settings.Default.Network.ToString("X8"));
+ var store = system.LoadStore(GetFullPath(path));
+ _neostore = new NeoStore(store);
+ _neosystem = system;
+ RpcServerPlugin.RegisterMethods(this, Settings.Default.Network);
+
+ if (Settings.Default.Debug)
+ ApplicationEngine.Log += OnApplicationEngineLog;
+ }
+
+ #endregion
+
+ #region JSON RPC Methods
+
+ [RpcMethod]
+ public JToken GetApplicationLog(JArray _params)
+ {
+ if (_params == null || _params.Count == 0)
+ throw new RpcException(RpcError.InvalidParams);
+ if (UInt256.TryParse(_params[0].AsString(), out var hash))
+ {
+ var raw = BlockToJObject(hash);
+ if (raw == null)
+ raw = TransactionToJObject(hash);
+ if (raw == null)
+ throw new RpcException(RpcError.InvalidParams.WithData("Unknown transaction/blockhash"));
+
+ if (_params.Count >= 2 && Enum.TryParse(_params[1].AsString(), true, out TriggerType triggerType))
+ {
+ var executions = raw["executions"] as JArray;
+ for (int i = 0; i < executions.Count;)
+ {
+ if (executions[i]["trigger"].AsString().Equals(triggerType.ToString(), StringComparison.OrdinalIgnoreCase) == false)
+ executions.RemoveAt(i);
+ else
+ i++;
+ }
+ }
+
+ return raw ?? JToken.Null;
+ }
+ else
+ throw new RpcException(RpcError.InvalidParams);
+ }
+
+ #endregion
+
+ #region Console Commands
+
+ [ConsoleCommand("log block", Category = "ApplicationLog Commands")]
+ private void OnGetBlockCommand(string blockHashOrIndex, string eventName = null)
+ {
+ UInt256 blockhash;
+ if (uint.TryParse(blockHashOrIndex, out var blockIndex))
+ {
+ blockhash = NativeContract.Ledger.GetBlockHash(_neosystem.StoreView, blockIndex);
+ }
+ else if (UInt256.TryParse(blockHashOrIndex, out blockhash) == false)
+ {
+ 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 && blockOnPersist == null)
+ ConsoleHelper.Error($"No logs.");
+ if (blockOnPersist != null)
+ PrintExecutionToConsole(blockOnPersist);
+ if (blockPostPersist != null)
+ {
+ ConsoleHelper.Info("--------------------------------");
+ PrintExecutionToConsole(blockPostPersist);
+ }
+ }
+
+ [ConsoleCommand("log tx", Category = "ApplicationLog Commands")]
+ private 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")]
+ private 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
+
+ private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList)
+ {
+ if (system.Settings.Network != Settings.Default.Network)
+ return;
+
+ if (_neostore is null)
+ return;
+ _neostore.StartBlockLogBatch();
+ _neostore.PutBlockLog(block, applicationExecutedList);
+ if (Settings.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.Any())
+ _neostore.PutTransactionEngineLogState(appEng.Transaction.Hash, logs);
+ }
+ _logEvents.Clear();
+ }
+ }
+
+ private void OnCommitted(NeoSystem system, Block block)
+ {
+ if (system.Settings.Network != Settings.Default.Network)
+ return;
+ if (_neostore is null)
+ return;
+ _neostore.CommitBlockLog();
+ }
+
+ private void OnApplicationEngineLog(object sender, LogEventArgs e)
+ {
+ if (Settings.Default.Debug == false)
+ return;
+
+ if (_neosystem.Settings.Network != Settings.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:");
+ for (int i = 0; i < notifyItem.State.Length; i++)
+ ConsoleHelper.Info($" {GetMethodParameterName(notifyItem.ScriptHash, notifyItem.EventName, i)}: ", $"{notifyItem.State[i].ToJson()}");
+ }
+ }
+ if (Settings.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:");
+ for (int i = 0; i < notifyItem.State.Length; i++)
+ ConsoleHelper.Info($" {GetMethodParameterName(notifyItem.ScriptHash, notifyItem.EventName, i)}: ", $"{notifyItem.State[i].ToJson()}");
+ ConsoleHelper.Info("--------------------------------");
+ }
+ }
+
+ private string GetMethodParameterName(UInt160 scriptHash, string methodName, 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);
+ return contractEvent.Parameters[parameterIndex].Name;
+ }
+
+ private JObject EventModelToJObject(BlockchainEventModel model)
+ {
+ var root = new JObject();
+ root["contract"] = model.ScriptHash.ToString();
+ root["eventname"] = model.EventName;
+ root["state"] = model.State.Select(s => s.ToJson()).ToArray();
+ return root;
+ }
+
+ private JObject TransactionToJObject(UInt256 txHash)
+ {
+ var appLog = _neostore.GetTransactionLog(txHash);
+ if (appLog == null)
+ return null;
+
+ var raw = new JObject();
+ raw["txid"] = txHash.ToString();
+
+ var trigger = new JObject();
+ trigger["trigger"] = appLog.Trigger;
+ trigger["vmstate"] = appLog.VmState;
+ trigger["exception"] = string.IsNullOrEmpty(appLog.Exception) ? null : appLog.Exception;
+ trigger["gasconsumed"] = appLog.GasConsumed.ToString();
+
+ try
+ {
+ trigger["stack"] = appLog.Stack.Select(s => s.ToJson(Settings.Default.MaxStackSize)).ToArray();
+ }
+ catch (Exception ex)
+ {
+ trigger["exception"] = ex.Message;
+ }
+
+ trigger["notifications"] = appLog.Notifications.Select(s =>
+ {
+ var notification = new JObject();
+ notification["contract"] = s.ScriptHash.ToString();
+ notification["eventname"] = s.EventName;
+
+ try
+ {
+ var state = new JObject();
+ state["type"] = "Array";
+ state["value"] = s.State.Select(ss => ss.ToJson()).ToArray();
+
+ notification["state"] = state;
+ }
+ catch (InvalidOperationException)
+ {
+ notification["state"] = "error: recursive reference";
+ }
+
+ return notification;
+ }).ToArray();
+
+ if (Settings.Default.Debug)
+ {
+ trigger["logs"] = appLog.Logs.Select(s =>
+ {
+ var log = new JObject();
+ log["contract"] = s.ScriptHash.ToString();
+ log["message"] = s.Message;
+ return log;
+ }).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();
+ blockJson["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 JObject BlockItemToJObject(BlockchainExecutionModel blockExecutionModel)
+ {
+ JObject trigger = new();
+ trigger["trigger"] = blockExecutionModel.Trigger;
+ trigger["vmstate"] = blockExecutionModel.VmState;
+ trigger["gasconsumed"] = blockExecutionModel.GasConsumed.ToString();
+ try
+ {
+ trigger["stack"] = blockExecutionModel.Stack.Select(q => q.ToJson(Settings.Default.MaxStackSize)).ToArray();
+ }
+ catch (Exception ex)
+ {
+ trigger["exception"] = ex.Message;
+ }
+ trigger["notifications"] = blockExecutionModel.Notifications.Select(s =>
+ {
+ JObject notification = new();
+ notification["contract"] = s.ScriptHash.ToString();
+ notification["eventname"] = s.EventName;
+ try
+ {
+ var state = new JObject();
+ state["type"] = "Array";
+ state["value"] = s.State.Select(ss => ss.ToJson()).ToArray();
+
+ notification["state"] = state;
+ }
+ catch (InvalidOperationException)
+ {
+ notification["state"] = "error: recursive reference";
+ }
+ return notification;
+ }).ToArray();
+
+ if (Settings.Default.Debug)
+ {
+ trigger["logs"] = blockExecutionModel.Logs.Select(s =>
+ {
+ var log = new JObject();
+ log["contract"] = s.ScriptHash.ToString();
+ log["message"] = s.Message;
+ return log;
+ }).ToArray();
+ }
+
+ return trigger;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Plugins/ApplicationLogs/Settings.cs b/src/Plugins/ApplicationLogs/Settings.cs
new file mode 100644
index 0000000000..1e425f664b
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/Settings.cs
@@ -0,0 +1,39 @@
+// Copyright (C) 2015-2024 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
+{
+ internal class Settings
+ {
+ public string Path { get; }
+ public uint Network { get; }
+ public int MaxStackSize { get; }
+
+ public bool Debug { get; }
+
+ public static Settings Default { get; private set; }
+
+ private Settings(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);
+ }
+
+ public static void Load(IConfigurationSection section)
+ {
+ Default = new Settings(section);
+ }
+ }
+}
diff --git a/src/Plugins/ApplicationLogs/Store/LogStorageStore.cs b/src/Plugins/ApplicationLogs/Store/LogStorageStore.cs
new file mode 100644
index 0000000000..aa0357ffb2
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/Store/LogStorageStore.cs
@@ -0,0 +1,415 @@
+// Copyright (C) 2015-2024 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 ApplicationLogs.Store.States;
+using Neo;
+using Neo.IO;
+using Neo.Persistence;
+using Neo.Plugins;
+using Neo.Plugins.Store.States;
+using Neo.SmartContract;
+using Neo.VM;
+using Neo.VM.Types;
+
+namespace 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 ISnapshot _snapshot;
+
+ #endregion
+
+ #region Ctor
+
+ public LogStorageStore(ISnapshot 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)
+ .AddBigEndian(timestamp)
+ .AddBigEndian(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)Settings.Default.MaxStackSize }));
+ }
+ catch (NotSupportedException)
+ {
+ _snapshot.Put(key, BinarySerializer.Serialize(StackItem.Null, ExecutionEngineLimits.Default with { MaxItemSize = (uint)Settings.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.Seek(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)
+ .AddBigEndian(ulong.MaxValue) // Get newest to oldest (timestamp)
+ .ToArray();
+ uint index = 1;
+ foreach (var (key, value) in _snapshot.Seek(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)
+ .AddBigEndian(ulong.MaxValue) // Get newest to oldest (timestamp)
+ .ToArray();
+ uint index = 1;
+ foreach (var (key, value) in _snapshot.Seek(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)
+ .AddBigEndian(ulong.MaxValue) // Get newest to oldest (timestamp)
+ .ToArray();
+ uint index = 1;
+ foreach (var (key, value) in _snapshot.Seek(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.Seek(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, out EngineLogState state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Engine)
+ .Add(engineStateId.ToByteArray())
+ .ToArray();
+ var data = _snapshot.TryGet(key);
+ state = data?.AsSerializable()!;
+ return data != null && data.Length > 0;
+ }
+
+ public bool TryGetTransactionEngineState(UInt256 hash, out TransactionEngineLogState state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Engine_Transaction)
+ .Add(hash)
+ .ToArray();
+ var data = _snapshot.TryGet(key);
+ state = data?.AsSerializable()!;
+ return data != null && data.Length > 0;
+ }
+
+ public bool TryGetBlockState(UInt256 hash, TriggerType trigger, out BlockLogState state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Block)
+ .Add(hash)
+ .Add((byte)trigger)
+ .ToArray();
+ var data = _snapshot.TryGet(key);
+ state = data?.AsSerializable()!;
+ return data != null && data.Length > 0;
+ }
+
+ public bool TryGetNotifyState(Guid notifyStateId, out NotifyLogState state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Notify)
+ .Add(notifyStateId.ToByteArray())
+ .ToArray();
+ var data = _snapshot.TryGet(key);
+ state = data?.AsSerializable()!;
+ return data != null && data.Length > 0;
+ }
+
+ public bool TryGetContractState(UInt160 scriptHash, ulong timestamp, uint iterIndex, out ContractLogState state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Contract)
+ .Add(scriptHash)
+ .AddBigEndian(timestamp)
+ .AddBigEndian(iterIndex)
+ .ToArray();
+ var data = _snapshot.TryGet(key);
+ state = data?.AsSerializable()!;
+ return data != null && data.Length > 0;
+ }
+
+ public bool TryGetExecutionState(Guid executionStateId, out ExecutionLogState state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Execution)
+ .Add(executionStateId.ToByteArray())
+ .ToArray();
+ var data = _snapshot.TryGet(key);
+ state = data?.AsSerializable()!;
+ 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();
+ var data = _snapshot.TryGet(key);
+ if (data == null)
+ {
+ executionStateId = Guid.Empty;
+ return false;
+ }
+ else
+ {
+ executionStateId = new Guid(data);
+ return true;
+ }
+ }
+
+ public bool TryGetExecutionTransactionState(UInt256 txHash, out Guid executionStateId)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Execution_Transaction)
+ .Add(txHash)
+ .ToArray();
+ var data = _snapshot.TryGet(key);
+ if (data == null)
+ {
+ executionStateId = Guid.Empty;
+ return false;
+ }
+ else
+ {
+ executionStateId = new Guid(data);
+ return true;
+ }
+ }
+
+ public bool TryGetTransactionState(UInt256 hash, out TransactionLogState state)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_Transaction)
+ .Add(hash)
+ .ToArray();
+ var data = _snapshot.TryGet(key);
+ state = data?.AsSerializable()!;
+ return data != null && data.Length > 0;
+ }
+
+ public bool TryGetStackItemState(Guid stackItemId, out StackItem stackItem)
+ {
+ var key = new KeyBuilder(Prefix_Id, Prefix_StackItem)
+ .Add(stackItemId.ToByteArray())
+ .ToArray();
+ var data = _snapshot.TryGet(key);
+ if (data == null)
+ {
+ stackItem = StackItem.Null;
+ return false;
+ }
+ else
+ {
+ stackItem = BinarySerializer.Deserialize(data, ExecutionEngineLimits.Default);
+ return true;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Plugins/ApplicationLogs/Store/Models/ApplicationEngineLogModel.cs b/src/Plugins/ApplicationLogs/Store/Models/ApplicationEngineLogModel.cs
new file mode 100644
index 0000000000..112cc49377
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/Store/Models/ApplicationEngineLogModel.cs
@@ -0,0 +1,28 @@
+// Copyright (C) 2015-2024 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.Store.States;
+
+namespace Neo.Plugins.Store.Models
+{
+ public class ApplicationEngineLogModel
+ {
+ public UInt160 ScriptHash { get; private init; } = new();
+ public string Message { get; private init; } = string.Empty;
+
+ public static ApplicationEngineLogModel Create(EngineLogState logEventState) =>
+ new()
+ {
+ ScriptHash = logEventState.ScriptHash,
+ Message = logEventState.Message,
+ };
+ }
+}
diff --git a/src/Plugins/ApplicationLogs/Store/Models/BlockchainEventModel.cs b/src/Plugins/ApplicationLogs/Store/Models/BlockchainEventModel.cs
new file mode 100644
index 0000000000..09245db295
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/Store/Models/BlockchainEventModel.cs
@@ -0,0 +1,48 @@
+// Copyright (C) 2015-2024 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 ApplicationLogs.Store.States;
+using Neo;
+using Neo.VM.Types;
+
+namespace ApplicationLogs.Store.Models
+{
+ public class BlockchainEventModel
+ {
+ public UInt160 ScriptHash { get; private init; } = new();
+ public string EventName { get; private init; } = string.Empty;
+ public StackItem[] State { get; private init; } = [];
+
+ public static BlockchainEventModel Create(UInt160 scriptHash, string eventName, StackItem[] state) =>
+ new()
+ {
+ ScriptHash = scriptHash,
+ EventName = eventName ?? string.Empty,
+ State = state,
+ };
+
+ public static BlockchainEventModel Create(NotifyLogState notifyLogState, StackItem[] state) =>
+ new()
+ {
+ ScriptHash = notifyLogState.ScriptHash,
+ EventName = notifyLogState.EventName,
+ State = state,
+ };
+
+ public static BlockchainEventModel Create(ContractLogState contractLogState, StackItem[] state) =>
+ new()
+ {
+ ScriptHash = contractLogState.ScriptHash,
+ EventName = contractLogState.EventName,
+ State = state,
+ };
+ }
+}
diff --git a/src/Plugins/ApplicationLogs/Store/Models/BlockchainExecutionModel.cs b/src/Plugins/ApplicationLogs/Store/Models/BlockchainExecutionModel.cs
new file mode 100644
index 0000000000..e31ecd0d0c
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/Store/Models/BlockchainExecutionModel.cs
@@ -0,0 +1,40 @@
+// Copyright (C) 2015-2024 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 ApplicationLogs.Store.States;
+using Neo.Plugins.Store.Models;
+using Neo.SmartContract;
+using Neo.VM;
+using Neo.VM.Types;
+
+namespace ApplicationLogs.Store.Models
+{
+ public class BlockchainExecutionModel
+ {
+ public TriggerType Trigger { get; private init; } = TriggerType.All;
+ public VMState VmState { get; private init; } = VMState.NONE;
+ public string Exception { get; private init; } = string.Empty;
+ public long GasConsumed { get; private init; } = 0L;
+ public StackItem[] Stack { get; private init; } = [];
+ public BlockchainEventModel[] Notifications { get; set; } = [];
+ public ApplicationEngineLogModel[] Logs { get; set; } = [];
+
+ public static BlockchainExecutionModel Create(TriggerType trigger, ExecutionLogState executionLogState, StackItem[] stack) =>
+ new()
+ {
+ Trigger = trigger,
+ VmState = executionLogState.VmState,
+ Exception = executionLogState.Exception ?? string.Empty,
+ GasConsumed = executionLogState.GasConsumed,
+ Stack = stack,
+ };
+ }
+}
diff --git a/src/Plugins/ApplicationLogs/Store/NeoStore.cs b/src/Plugins/ApplicationLogs/Store/NeoStore.cs
new file mode 100644
index 0000000000..496502869b
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/Store/NeoStore.cs
@@ -0,0 +1,308 @@
+// Copyright (C) 2015-2024 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 ApplicationLogs.Store.Models;
+using ApplicationLogs.Store.States;
+using Neo;
+using Neo.Ledger;
+using Neo.Network.P2P.Payloads;
+using Neo.Persistence;
+using Neo.Plugins.Store.Models;
+using Neo.Plugins.Store.States;
+using Neo.SmartContract;
+using Neo.VM.Types;
+
+namespace ApplicationLogs.Store
+{
+ public sealed class NeoStore : IDisposable
+ {
+ #region Globals
+
+ private readonly IStore _store;
+ private ISnapshot _blocklogsnapshot;
+
+ #endregion
+
+ #region ctor
+
+ public NeoStore(
+ IStore store)
+ {
+ _store = store;
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ public void 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)
+ {
+ 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)
+ {
+ 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/src/Plugins/ApplicationLogs/Store/States/BlockLogState.cs b/src/Plugins/ApplicationLogs/Store/States/BlockLogState.cs
new file mode 100644
index 0000000000..dcbb58b6e6
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/Store/States/BlockLogState.cs
@@ -0,0 +1,72 @@
+// Copyright (C) 2015-2024 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;
+using Neo.IO;
+
+namespace 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) =>
+ 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/src/Plugins/ApplicationLogs/Store/States/ContractLogState.cs b/src/Plugins/ApplicationLogs/Store/States/ContractLogState.cs
new file mode 100644
index 0000000000..0987e86e67
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/Store/States/ContractLogState.cs
@@ -0,0 +1,74 @@
+// Copyright (C) 2015-2024 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;
+using Neo.IO;
+using Neo.Ledger;
+using Neo.SmartContract;
+
+namespace 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.Deserialize(ref reader);
+ 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) =>
+ 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/src/Plugins/ApplicationLogs/Store/States/EngineLogState.cs b/src/Plugins/ApplicationLogs/Store/States/EngineLogState.cs
new file mode 100644
index 0000000000..8b07855253
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/Store/States/EngineLogState.cs
@@ -0,0 +1,66 @@
+// Copyright (C) 2015-2024 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.IO;
+
+namespace Neo.Plugins.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.Deserialize(ref reader);
+ // 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) =>
+ 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/src/Plugins/ApplicationLogs/Store/States/ExecutionLogState.cs b/src/Plugins/ApplicationLogs/Store/States/ExecutionLogState.cs
new file mode 100644
index 0000000000..44f8a74b49
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/Store/States/ExecutionLogState.cs
@@ -0,0 +1,94 @@
+// Copyright (C) 2015-2024 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.IO;
+using Neo.Ledger;
+using Neo.VM;
+
+namespace 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) =>
+ 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/src/Plugins/ApplicationLogs/Store/States/NotifyLogState.cs b/src/Plugins/ApplicationLogs/Store/States/NotifyLogState.cs
new file mode 100644
index 0000000000..20fbd9e456
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/Store/States/NotifyLogState.cs
@@ -0,0 +1,87 @@
+// Copyright (C) 2015-2024 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;
+using Neo.IO;
+using Neo.SmartContract;
+
+namespace 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.Deserialize(ref reader);
+ 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) =>
+ 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/src/Plugins/ApplicationLogs/Store/States/TransactionEngineLogState.cs b/src/Plugins/ApplicationLogs/Store/States/TransactionEngineLogState.cs
new file mode 100644
index 0000000000..8b32867b66
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/Store/States/TransactionEngineLogState.cs
@@ -0,0 +1,71 @@
+// Copyright (C) 2015-2024 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.IO;
+
+namespace Neo.Plugins.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) =>
+ 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/src/Plugins/ApplicationLogs/Store/States/TransactionLogState.cs b/src/Plugins/ApplicationLogs/Store/States/TransactionLogState.cs
new file mode 100644
index 0000000000..b40d3244d0
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/Store/States/TransactionLogState.cs
@@ -0,0 +1,72 @@
+// Copyright (C) 2015-2024 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;
+using Neo.IO;
+
+namespace 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) =>
+ 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/src/Plugins/ApplicationLogs/config.json b/src/Plugins/ApplicationLogs/config.json
new file mode 100644
index 0000000000..af601bc81e
--- /dev/null
+++ b/src/Plugins/ApplicationLogs/config.json
@@ -0,0 +1,11 @@
+{
+ "PluginConfiguration": {
+ "Path": "ApplicationLogs_{0}",
+ "Network": 860833102,
+ "MaxStackSize": 65535,
+ "Debug": false
+ },
+ "Dependency": [
+ "RpcServer"
+ ]
+}
diff --git a/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.Get.cs b/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.Get.cs
new file mode 100644
index 0000000000..b54b6eca1e
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.Get.cs
@@ -0,0 +1,117 @@
+// Copyright (C) 2015-2024 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.SmartContract;
+using Neo.Wallets;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using static Neo.Consensus.RecoveryMessage;
+
+namespace Neo.Consensus
+{
+ partial class ConsensusContext
+ {
+ 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;
+ }
+
+ public T GetMessage(ExtensiblePayload payload) where T : ConsensusMessage
+ {
+ return (T)GetMessage(payload);
+ }
+
+ private ChangeViewPayloadCompact GetChangeViewPayloadCompact(ExtensiblePayload payload)
+ {
+ ChangeView message = GetMessage(payload);
+ return new ChangeViewPayloadCompact
+ {
+ ValidatorIndex = message.ValidatorIndex,
+ OriginalViewNumber = message.ViewNumber,
+ Timestamp = message.Timestamp,
+ InvocationScript = payload.Witness.InvocationScript
+ };
+ }
+
+ private CommitPayloadCompact GetCommitPayloadCompact(ExtensiblePayload payload)
+ {
+ Commit message = GetMessage(payload);
+ return new CommitPayloadCompact
+ {
+ ViewNumber = message.ViewNumber,
+ ValidatorIndex = message.ValidatorIndex,
+ Signature = message.Signature,
+ InvocationScript = payload.Witness.InvocationScript
+ };
+ }
+
+ private PreparationPayloadCompact GetPreparationPayloadCompact(ExtensiblePayload payload)
+ {
+ return new 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
+ IO.Helper.GetVarSize(expectedTransactions);
+ }
+ }
+}
diff --git a/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.MakePayload.cs b/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.MakePayload.cs
new file mode 100644
index 0000000000..a761cbafb0
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.MakePayload.cs
@@ -0,0 +1,176 @@
+// Copyright (C) 2015-2024 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.SmartContract;
+using Neo.Wallets;
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.Linq;
+using static Neo.Consensus.RecoveryMessage;
+
+namespace Neo.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()
+ {
+ return CommitPayloads[MyIndex] ?? (CommitPayloads[MyIndex] = MakeSignedPayload(new Commit
+ {
+ Signature = EnsureHeader().Sign(keyPair, neoSystem.Settings.Network)
+ }));
+ }
+
+ 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)
+ {
+ ContractParametersContext sc;
+ try
+ {
+ sc = new ContractParametersContext(neoSystem.StoreView, payload, dbftSettings.Network);
+ wallet.Sign(sc);
+ }
+ catch (InvalidOperationException exception)
+ {
+ Utility.Log(nameof(ConsensusContext), LogLevel.Debug, exception.ToString());
+ return;
+ }
+ payload.Witness = sc.GetWitnesses()[0];
+ }
+
+ ///
+ /// Prevent that block exceed the max size
+ ///
+ /// Ordered transactions
+ internal void EnsureMaxBlockLimitation(IEnumerable txs)
+ {
+ uint maxTransactionsPerBlock = neoSystem.Settings.MaxTransactionsPerBlock;
+
+ // Limit Speaker proposal to the limit `MaxTransactionsPerBlock` or all available transactions of the mempool
+ txs = txs.Take((int)maxTransactionsPerBlock);
+
+ List hashes = new List();
+ Transactions = new Dictionary();
+ VerificationContext = new TransactionVerificationContext();
+
+ // Expected block size
+ var blockSize = GetExpectedBlockSizeWithoutTransactions(txs.Count());
+ 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()
+ {
+ EnsureMaxBlockLimitation(neoSystem.MemPool.GetSortedVerifiedTransactions());
+ 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
+ });
+ }
+
+ private static ulong GetNonce()
+ {
+ Random _random = new();
+ Span buffer = stackalloc byte[8];
+ _random.NextBytes(buffer);
+ return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
+ }
+ }
+}
diff --git a/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.cs b/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.cs
new file mode 100644
index 0000000000..35044e4fb8
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.cs
@@ -0,0 +1,323 @@
+// Copyright (C) 2015-2024 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.IO;
+using Neo.Ledger;
+using Neo.Network.P2P.Payloads;
+using Neo.Persistence;
+using Neo.SmartContract;
+using Neo.SmartContract.Native;
+using Neo.VM;
+using Neo.Wallets;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Neo.Consensus
+{
+ public partial class ConsensusContext : IDisposable, ISerializable
+ {
+ ///
+ /// Key for saving consensus state.
+ ///
+ private static readonly byte[] ConsensusStateKey = { 0xf4 };
+
+ public Block Block;
+ public byte ViewNumber;
+ public ECPoint[] Validators;
+ public int MyIndex;
+ public UInt256[] TransactionHashes;
+ public Dictionary Transactions;
+ public ExtensiblePayload[] PreparationPayloads;
+ public ExtensiblePayload[] CommitPayloads;
+ public ExtensiblePayload[] ChangeViewPayloads;
+ public ExtensiblePayload[] LastChangeViewPayloads;
+ // 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 SnapshotCache Snapshot { get; private set; }
+ private KeyPair keyPair;
+ private int _witnessSize;
+ private readonly NeoSystem neoSystem;
+ private readonly Settings dbftSettings;
+ private readonly Wallet wallet;
+ private readonly IStore store;
+ private Dictionary cachedMessages;
+
+ 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, Settings settings, Wallet wallet)
+ {
+ this.wallet = wallet;
+ this.neoSystem = neoSystem;
+ dbftSettings = settings;
+ store = neoSystem.LoadStore(settings.RecoveryLogs);
+ }
+
+ public Block CreateBlock()
+ {
+ EnsureHeader();
+ Contract contract = Contract.CreateMultiSigContract(M, Validators);
+ ContractParametersContext 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)
+ {
+ ExtensiblePayload 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()
+ {
+ byte[] data = store.TryGet(ConsensusStateKey);
+ if (data is null || 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.GetSnapshot();
+ uint height = NativeContract.Ledger.CurrentIndex(Snapshot);
+ Block = new Block
+ {
+ Header = new Header
+ {
+ PrevHash = NativeContract.Ledger.CurrentHash(Snapshot),
+ 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))
+ }
+ };
+ 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())
+ {
+ for (int x = 0; x < M; x++)
+ {
+ sb.EmitPush(new byte[64]);
+ }
+ _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;
+ }
+ }
+ keyPair = null;
+ for (int i = 0; i < Validators.Length; i++)
+ {
+ WalletAccount account = wallet?.GetAccount(Validators[i]);
+ if (account?.HasKey != true) continue;
+ MyIndex = i;
+ keyPair = account.GetKey();
+ 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);
+ if (reader.ReadUInt32() != Block.Version) throw new FormatException();
+ 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/src/Plugins/DBFTPlugin/Consensus/ConsensusService.Check.cs b/src/Plugins/DBFTPlugin/Consensus/ConsensusService.Check.cs
new file mode 100644
index 0000000000..3b15bcd8fc
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Consensus/ConsensusService.Check.cs
@@ -0,0 +1,102 @@
+// Copyright (C) 2015-2024 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.IO;
+using Neo.Network.P2P;
+using Neo.Network.P2P.Payloads;
+using System;
+using System.Linq;
+
+namespace Neo.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 { Inventory = 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_received_time = TimeProvider.Current.UtcNow;
+ 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 { Inventory = 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 { Inventory = payload });
+ // Set timer, so we will resend the commit in case of a networking issue
+ ChangeTimer(TimeSpan.FromMilliseconds(neoSystem.Settings.MillisecondsPerBlock));
+ CheckCommits();
+ }
+ }
+ }
+}
diff --git a/src/Plugins/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs b/src/Plugins/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs
new file mode 100644
index 0000000000..ecc31f7ba3
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs
@@ -0,0 +1,317 @@
+// Copyright (C) 2015-2024 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.IO;
+using Neo.Ledger;
+using Neo.Network.P2P;
+using Neo.Network.P2P.Payloads;
+using Neo.SmartContract;
+using Neo.SmartContract.Native;
+using Neo.Wallets;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Neo.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 * neoSystem.Settings.MillisecondsPerBlock).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);
+
+ 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);
+ List unverified = new List();
+ 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), neoSystem.Settings.MaxTraceableBlocks))
+ {
+ 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), neoSystem.Settings.MaxTraceableBlocks))
+ {
+ 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
+ {
+ Payload = 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 { Inventory = context.MakeRecoveryMessage() });
+ }
+ }
+}
diff --git a/src/Plugins/DBFTPlugin/Consensus/ConsensusService.cs b/src/Plugins/DBFTPlugin/Consensus/ConsensusService.cs
new file mode 100644
index 0000000000..8a6d75e8b9
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Consensus/ConsensusService.cs
@@ -0,0 +1,344 @@
+// Copyright (C) 2015-2024 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.IO;
+using Neo.Ledger;
+using Neo.Network.P2P;
+using Neo.Network.P2P.Payloads;
+using Neo.Wallets;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using static Neo.Ledger.Blockchain;
+
+namespace Neo.Consensus
+{
+ 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 block_received_time;
+ 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 Settings dbftSettings;
+ private readonly NeoSystem neoSystem;
+
+ public ConsensusService(NeoSystem neoSystem, Settings settings, Wallet wallet)
+ : this(neoSystem, settings, new ConsensusContext(neoSystem, settings, wallet)) { }
+
+ internal ConsensusService(NeoSystem neoSystem, Settings 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(Blockchain.PersistCompleted));
+ Context.System.EventStream.Subscribe(Self, typeof(Blockchain.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(neoSystem.Settings.MillisecondsPerBlock << (viewNumber + 1)));
+ }
+ else
+ {
+ TimeSpan span = neoSystem.Settings.TimePerBlock;
+ if (block_received_index + 1 == context.Block.Index)
+ {
+ var diff = TimeProvider.Current.UtcNow - block_received_time;
+ if (diff >= span)
+ span = TimeSpan.Zero;
+ else
+ span -= diff;
+ }
+ ChangeTimer(span);
+ }
+ }
+ else
+ {
+ ChangeTimer(TimeSpan.FromMilliseconds(neoSystem.Settings.MillisecondsPerBlock << (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 Blockchain.PersistCompleted completed:
+ OnPersistCompleted(completed.Block);
+ break;
+ case Blockchain.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 Blockchain.FillMemoryPool
+ {
+ Transactions = 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 { Inventory = context.MakeRecoveryMessage() });
+ ChangeTimer(TimeSpan.FromMilliseconds(neoSystem.Settings.MillisecondsPerBlock << 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 { Inventory = 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((neoSystem.Settings.MillisecondsPerBlock << (context.ViewNumber + 1)) - (context.ViewNumber == 0 ? neoSystem.Settings.MillisecondsPerBlock : 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 { Inventory = 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(neoSystem.Settings.MillisecondsPerBlock << (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 { Inventory = context.MakeChangeView(reason) });
+ CheckExpectedView(expectedView);
+ }
+ }
+
+ private bool ReverifyAndProcessPayload(ExtensiblePayload payload)
+ {
+ RelayResult relayResult = blockchain.Ask(new Blockchain.Reverify { Inventories = 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 * neoSystem.Settings.MillisecondsPerBlock / (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, Settings dbftSettings, Wallet wallet)
+ {
+ return Akka.Actor.Props.Create(() => new ConsensusService(neoSystem, dbftSettings, wallet));
+ }
+
+ private static void Log(string message, LogLevel level = LogLevel.Info)
+ {
+ Utility.Log(nameof(ConsensusService), level, message);
+ }
+ }
+}
diff --git a/src/Plugins/DBFTPlugin/DBFTPlugin.cs b/src/Plugins/DBFTPlugin/DBFTPlugin.cs
new file mode 100644
index 0000000000..7fa29ce0dd
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/DBFTPlugin.cs
@@ -0,0 +1,101 @@
+// Copyright (C) 2015-2024 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.Network.P2P;
+using Neo.Network.P2P.Payloads;
+using Neo.Plugins;
+using Neo.Wallets;
+
+namespace Neo.Consensus
+{
+ public class DBFTPlugin : Plugin
+ {
+ private IWalletProvider walletProvider;
+ private IActorRef consensus;
+ private bool started = false;
+ private NeoSystem neoSystem;
+ private Settings settings;
+
+ public override string Description => "Consensus plugin with dBFT algorithm.";
+
+ public DBFTPlugin()
+ {
+ RemoteNode.MessageReceived += RemoteNode_MessageReceived;
+ }
+
+ public DBFTPlugin(Settings settings) : this()
+ {
+ this.settings = settings;
+ }
+
+ public override void Dispose()
+ {
+ RemoteNode.MessageReceived -= RemoteNode_MessageReceived;
+ }
+
+ protected override void Configure()
+ {
+ settings ??= new Settings(GetConfiguration());
+ }
+
+ protected override void OnSystemLoaded(NeoSystem system)
+ {
+ if (system.Settings.Network != settings.Network) return;
+ neoSystem = system;
+ neoSystem.ServiceAdded += NeoSystem_ServiceAdded;
+ }
+
+ private void NeoSystem_ServiceAdded(object sender, object service)
+ {
+ if (service is not IWalletProvider provider) return;
+ walletProvider = provider;
+ neoSystem.ServiceAdded -= NeoSystem_ServiceAdded;
+ if (settings.AutoStart)
+ {
+ walletProvider.WalletChanged += WalletProvider_WalletChanged;
+ }
+ }
+
+ private void WalletProvider_WalletChanged(object sender, Wallet wallet)
+ {
+ walletProvider.WalletChanged -= WalletProvider_WalletChanged;
+ Start(wallet);
+ }
+
+ [ConsoleCommand("start consensus", Category = "Consensus", Description = "Start consensus service (dBFT)")]
+ private void OnStart()
+ {
+ Start(walletProvider.GetWallet());
+ }
+
+ public void Start(Wallet wallet)
+ {
+ if (started) return;
+ started = true;
+ consensus = neoSystem.ActorSystem.ActorOf(ConsensusService.Props(neoSystem, settings, wallet));
+ consensus.Tell(new ConsensusService.Start());
+ }
+
+ private bool RemoteNode_MessageReceived(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/src/Plugins/DBFTPlugin/DBFTPlugin.csproj b/src/Plugins/DBFTPlugin/DBFTPlugin.csproj
new file mode 100644
index 0000000000..fe681cf2ed
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/DBFTPlugin.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ Neo.Consensus.DBFT
+ Neo.Consensus
+
+
+
+
+
+
+
diff --git a/src/Plugins/DBFTPlugin/Messages/ChangeView.cs b/src/Plugins/DBFTPlugin/Messages/ChangeView.cs
new file mode 100644
index 0000000000..e7be40075f
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Messages/ChangeView.cs
@@ -0,0 +1,56 @@
+// Copyright (C) 2015-2024 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 System.IO;
+
+namespace Neo.Consensus
+{
+ 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/src/Plugins/DBFTPlugin/Messages/Commit.cs b/src/Plugins/DBFTPlugin/Messages/Commit.cs
new file mode 100644
index 0000000000..6e8fe93d87
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Messages/Commit.cs
@@ -0,0 +1,38 @@
+// Copyright (C) 2015-2024 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 System;
+using System.IO;
+
+namespace Neo.Consensus
+{
+ 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/src/Plugins/DBFTPlugin/Messages/ConsensusMessage.cs b/src/Plugins/DBFTPlugin/Messages/ConsensusMessage.cs
new file mode 100644
index 0000000000..4e136a99e5
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Messages/ConsensusMessage.cs
@@ -0,0 +1,69 @@
+// Copyright (C) 2015-2024 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.IO;
+using System;
+using System.IO;
+
+namespace Neo.Consensus
+{
+ 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(typeof(ConsensusMessageType), type))
+ throw new ArgumentOutOfRangeException(nameof(type));
+ Type = type;
+ }
+
+ public virtual void Deserialize(ref MemoryReader reader)
+ {
+ if (Type != (ConsensusMessageType)reader.ReadByte())
+ throw new FormatException();
+ 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();
+ 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/src/Plugins/DBFTPlugin/Messages/PrepareRequest.cs b/src/Plugins/DBFTPlugin/Messages/PrepareRequest.cs
new file mode 100644
index 0000000000..2bce609f79
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Messages/PrepareRequest.cs
@@ -0,0 +1,64 @@
+// Copyright (C) 2015-2024 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.IO;
+using System;
+using System.IO;
+using System.Linq;
+
+namespace Neo.Consensus
+{
+ public class PrepareRequest : ConsensusMessage
+ {
+ public uint Version;
+ public UInt256 PrevHash;
+ public ulong Timestamp;
+ public ulong Nonce;
+ public 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();
+ }
+
+ 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/src/Plugins/DBFTPlugin/Messages/PrepareResponse.cs b/src/Plugins/DBFTPlugin/Messages/PrepareResponse.cs
new file mode 100644
index 0000000000..7510ff99bb
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Messages/PrepareResponse.cs
@@ -0,0 +1,37 @@
+// Copyright (C) 2015-2024 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.IO;
+using System.IO;
+
+namespace Neo.Consensus
+{
+ public class PrepareResponse : ConsensusMessage
+ {
+ public 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/src/Plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.ChangeViewPayloadCompact.cs b/src/Plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.ChangeViewPayloadCompact.cs
new file mode 100644
index 0000000000..6a7734e569
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.ChangeViewPayloadCompact.cs
@@ -0,0 +1,50 @@
+// Copyright (C) 2015-2024 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.IO;
+using System;
+using System.IO;
+
+namespace Neo.Consensus
+{
+ 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/src/Plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.CommitPayloadCompact.cs b/src/Plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.CommitPayloadCompact.cs
new file mode 100644
index 0000000000..2dfa16597a
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.CommitPayloadCompact.cs
@@ -0,0 +1,50 @@
+// Copyright (C) 2015-2024 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.IO;
+using System;
+using System.IO;
+
+namespace Neo.Consensus
+{
+ 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/src/Plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.PreparationPayloadCompact.cs b/src/Plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.PreparationPayloadCompact.cs
new file mode 100644
index 0000000000..80b2a48b6f
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.PreparationPayloadCompact.cs
@@ -0,0 +1,42 @@
+// Copyright (C) 2015-2024 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.IO;
+using System;
+using System.IO;
+
+namespace Neo.Consensus
+{
+ 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/src/Plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.cs b/src/Plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.cs
new file mode 100644
index 0000000000..fc688d6a1f
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryMessage.cs
@@ -0,0 +1,131 @@
+// Copyright (C) 2015-2024 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.IO;
+using Neo.Network.P2P.Payloads;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Neo.Consensus
+{
+ public partial class RecoveryMessage : ConsensusMessage
+ {
+ public 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 Dictionary PreparationMessages;
+ public Dictionary CommitMessages;
+
+ public override int Size => base.Size
+ + /* ChangeViewMessages */ ChangeViewMessages?.Values.GetVarSize() ?? 0
+ + /* PrepareRequestMessage */ 1 + PrepareRequestMessage?.Size ?? 0
+ + /* PreparationHash */ PreparationHash?.Size ?? 0
+ + /* PreparationMessages */ PreparationMessages?.Values.GetVarSize() ?? 0
+ + /* CommitMessages */ CommitMessages?.Values.GetVarSize() ?? 0;
+
+ 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());
+ bool hasPrepareRequestMessage = PrepareRequestMessage != null;
+ writer.Write(hasPrepareRequestMessage);
+ if (hasPrepareRequestMessage)
+ writer.Write(PrepareRequestMessage);
+ else
+ {
+ if (PreparationHash == null)
+ writer.WriteVarInt(0);
+ else
+ writer.WriteVarBytes(PreparationHash.ToArray());
+ }
+
+ writer.Write(PreparationMessages.Values.ToArray());
+ writer.Write(CommitMessages.Values.ToArray());
+ }
+ }
+}
diff --git a/src/Plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryRequest.cs b/src/Plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryRequest.cs
new file mode 100644
index 0000000000..84cd381add
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Messages/RecoveryMessage/RecoveryRequest.cs
@@ -0,0 +1,43 @@
+// Copyright (C) 2015-2024 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 System.IO;
+
+namespace Neo.Consensus
+{
+ 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/src/Plugins/DBFTPlugin/Settings.cs b/src/Plugins/DBFTPlugin/Settings.cs
new file mode 100644
index 0000000000..d0ecbb63fd
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Settings.cs
@@ -0,0 +1,35 @@
+// Copyright (C) 2015-2024 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.Consensus
+{
+ public class Settings
+ {
+ 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 Settings(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);
+ }
+ }
+}
diff --git a/src/Plugins/DBFTPlugin/Types/ChangeViewReason.cs b/src/Plugins/DBFTPlugin/Types/ChangeViewReason.cs
new file mode 100644
index 0000000000..4c0a3c1100
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Types/ChangeViewReason.cs
@@ -0,0 +1,23 @@
+// Copyright (C) 2015-2024 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.Consensus
+{
+ public enum ChangeViewReason : byte
+ {
+ Timeout = 0x0,
+ ChangeAgreement = 0x1,
+ TxNotFound = 0x2,
+ TxRejectedByPolicy = 0x3,
+ TxInvalid = 0x4,
+ BlockRejectedByPolicy = 0x5
+ }
+}
diff --git a/src/Plugins/DBFTPlugin/Types/ConsensusMessageType.cs b/src/Plugins/DBFTPlugin/Types/ConsensusMessageType.cs
new file mode 100644
index 0000000000..f325133f08
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/Types/ConsensusMessageType.cs
@@ -0,0 +1,25 @@
+// Copyright (C) 2015-2024 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.Consensus
+{
+ public enum ConsensusMessageType : byte
+ {
+ ChangeView = 0x00,
+
+ PrepareRequest = 0x20,
+ PrepareResponse = 0x21,
+ Commit = 0x30,
+
+ RecoveryRequest = 0x40,
+ RecoveryMessage = 0x41,
+ }
+}
diff --git a/src/Plugins/DBFTPlugin/config.json b/src/Plugins/DBFTPlugin/config.json
new file mode 100644
index 0000000000..2e2b710ba3
--- /dev/null
+++ b/src/Plugins/DBFTPlugin/config.json
@@ -0,0 +1,10 @@
+{
+ "PluginConfiguration": {
+ "RecoveryLogs": "ConsensusState",
+ "IgnoreRecoveryLogs": false,
+ "AutoStart": false,
+ "Network": 860833102,
+ "MaxBlockSize": 2097152,
+ "MaxBlockSystemFee": 150000000000
+ }
+}
diff --git a/src/Plugins/Directory.Build.props b/src/Plugins/Directory.Build.props
new file mode 100644
index 0000000000..4eb51eef9b
--- /dev/null
+++ b/src/Plugins/Directory.Build.props
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+ PreserveNewest
+
+
+
diff --git a/src/Plugins/LevelDBStore/IO/Data/LevelDB/DB.cs b/src/Plugins/LevelDBStore/IO/Data/LevelDB/DB.cs
new file mode 100644
index 0000000000..60e0e24e5a
--- /dev/null
+++ b/src/Plugins/LevelDBStore/IO/Data/LevelDB/DB.cs
@@ -0,0 +1,114 @@
+// Copyright (C) 2015-2024 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;
+using System.IO;
+
+namespace Neo.IO.Data.LevelDB
+{
+ public class DB : IDisposable
+ {
+ private IntPtr handle;
+
+ ///
+ /// Return true if haven't got valid handle
+ ///
+ public bool IsDisposed => handle == IntPtr.Zero;
+
+ private DB(IntPtr handle)
+ {
+ this.handle = handle;
+ }
+
+ public void Dispose()
+ {
+ if (handle != IntPtr.Zero)
+ {
+ Native.leveldb_close(handle);
+ handle = IntPtr.Zero;
+ }
+ }
+
+ public void Delete(WriteOptions options, byte[] key)
+ {
+ Native.leveldb_delete(handle, options.handle, key, (UIntPtr)key.Length, out IntPtr error);
+ NativeHelper.CheckError(error);
+ }
+
+ public byte[] Get(ReadOptions options, byte[] key)
+ {
+ IntPtr value = Native.leveldb_get(handle, options.handle, key, (UIntPtr)key.Length, out UIntPtr length, out IntPtr error);
+ try
+ {
+ NativeHelper.CheckError(error);
+ return value.ToByteArray(length);
+ }
+ finally
+ {
+ if (value != IntPtr.Zero) Native.leveldb_free(value);
+ }
+ }
+
+ public bool Contains(ReadOptions options, byte[] key)
+ {
+ IntPtr value = Native.leveldb_get(handle, options.handle, key, (UIntPtr)key.Length, out _, out IntPtr error);
+ NativeHelper.CheckError(error);
+
+ if (value != IntPtr.Zero)
+ {
+ Native.leveldb_free(value);
+ return true;
+ }
+
+ return false;
+ }
+
+ public Snapshot GetSnapshot()
+ {
+ return new Snapshot(handle);
+ }
+
+ public Iterator NewIterator(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)
+ {
+ IntPtr handle = Native.leveldb_open(options.handle, Path.GetFullPath(name), out IntPtr error);
+ NativeHelper.CheckError(error);
+ return new DB(handle);
+ }
+
+ public void Put(WriteOptions options, byte[] key, byte[] value)
+ {
+ Native.leveldb_put(handle, options.handle, key, (UIntPtr)key.Length, value, (UIntPtr)value.Length, out IntPtr error);
+ NativeHelper.CheckError(error);
+ }
+
+ public static void Repair(string name, Options options)
+ {
+ Native.leveldb_repair_db(options.handle, Path.GetFullPath(name), out IntPtr error);
+ NativeHelper.CheckError(error);
+ }
+
+ public void Write(WriteOptions options, WriteBatch write_batch)
+ {
+ Native.leveldb_write(handle, options.handle, write_batch.handle, out IntPtr error);
+ NativeHelper.CheckError(error);
+ }
+ }
+}
diff --git a/src/Plugins/LevelDBStore/IO/Data/LevelDB/Helper.cs b/src/Plugins/LevelDBStore/IO/Data/LevelDB/Helper.cs
new file mode 100644
index 0000000000..2ac3a0005f
--- /dev/null
+++ b/src/Plugins/LevelDBStore/IO/Data/LevelDB/Helper.cs
@@ -0,0 +1,63 @@
+// Copyright (C) 2015-2024 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;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace Neo.IO.Data.LevelDB
+{
+ public static class Helper
+ {
+ public static IEnumerable Seek(this DB db, ReadOptions options, byte[] prefix, SeekDirection direction, Func resultSelector)
+ {
+ using Iterator it = db.NewIterator(options);
+ if (direction == SeekDirection.Forward)
+ {
+ for (it.Seek(prefix); it.Valid(); it.Next())
+ yield return resultSelector(it.Key(), it.Value());
+ }
+ else
+ {
+ // SeekForPrev
+
+ it.Seek(prefix);
+ if (!it.Valid())
+ it.SeekToLast();
+ else if (it.Key().AsSpan().SequenceCompareTo(prefix) > 0)
+ it.Prev();
+
+ for (; it.Valid(); it.Prev())
+ yield return resultSelector(it.Key(), it.Value());
+ }
+ }
+
+ public static IEnumerable FindRange(this DB db, ReadOptions options, byte[] startKey, byte[] endKey, Func resultSelector)
+ {
+ using Iterator it = db.NewIterator(options);
+ for (it.Seek(startKey); it.Valid(); it.Next())
+ {
+ byte[] key = it.Key();
+ if (key.AsSpan().SequenceCompareTo(endKey) > 0) break;
+ yield return resultSelector(key, it.Value());
+ }
+ }
+
+ internal static byte[] ToByteArray(this IntPtr data, UIntPtr length)
+ {
+ if (data == IntPtr.Zero) return null;
+ byte[] buffer = new byte[(int)length];
+ Marshal.Copy(data, buffer, 0, (int)length);
+ return buffer;
+ }
+ }
+}
diff --git a/src/Plugins/LevelDBStore/IO/Data/LevelDB/Iterator.cs b/src/Plugins/LevelDBStore/IO/Data/LevelDB/Iterator.cs
new file mode 100644
index 0000000000..6c025380fb
--- /dev/null
+++ b/src/Plugins/LevelDBStore/IO/Data/LevelDB/Iterator.cs
@@ -0,0 +1,86 @@
+// Copyright (C) 2015-2024 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.
+
+using System;
+
+namespace Neo.IO.Data.LevelDB
+{
+ public class Iterator : IDisposable
+ {
+ private IntPtr handle;
+
+ internal Iterator(IntPtr handle)
+ {
+ this.handle = handle;
+ }
+
+ private void CheckError()
+ {
+ Native.leveldb_iter_get_error(handle, out IntPtr error);
+ NativeHelper.CheckError(error);
+ }
+
+ public void Dispose()
+ {
+ if (handle != IntPtr.Zero)
+ {
+ Native.leveldb_iter_destroy(handle);
+ handle = IntPtr.Zero;
+ }
+ }
+
+ public byte[] Key()
+ {
+ IntPtr key = Native.leveldb_iter_key(handle, out UIntPtr length);
+ CheckError();
+ return key.ToByteArray(length);
+ }
+
+ public void Next()
+ {
+ Native.leveldb_iter_next(handle);
+ CheckError();
+ }
+
+ public void Prev()
+ {
+ Native.leveldb_iter_prev(handle);
+ CheckError();
+ }
+
+ public void Seek(byte[] target)
+ {
+ Native.leveldb_iter_seek(handle, target, (UIntPtr)target.Length);
+ }
+
+ public void SeekToFirst()
+ {
+ Native.leveldb_iter_seek_to_first(handle);
+ }
+
+ public void SeekToLast()
+ {
+ Native.leveldb_iter_seek_to_last(handle);
+ }
+
+ public bool Valid()
+ {
+ return Native.leveldb_iter_valid(handle);
+ }
+
+ public byte[] Value()
+ {
+ IntPtr value = Native.leveldb_iter_value(handle, out UIntPtr length);
+ CheckError();
+ return value.ToByteArray(length);
+ }
+ }
+}
diff --git a/src/Plugins/LevelDBStore/IO/Data/LevelDB/LevelDBException.cs b/src/Plugins/LevelDBStore/IO/Data/LevelDB/LevelDBException.cs
new file mode 100644
index 0000000000..c9cca42070
--- /dev/null
+++ b/src/Plugins/LevelDBStore/IO/Data/LevelDB/LevelDBException.cs
@@ -0,0 +1,23 @@
+// Copyright (C) 2015-2024 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/src/Plugins/LevelDBStore/IO/Data/LevelDB/Native.cs b/src/Plugins/LevelDBStore/IO/Data/LevelDB/Native.cs
new file mode 100644
index 0000000000..acf8fa82b9
--- /dev/null
+++ b/src/Plugins/LevelDBStore/IO/Data/LevelDB/Native.cs
@@ -0,0 +1,264 @@
+// Copyright (C) 2015-2024 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;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Neo.IO.Data.LevelDB
+{
+ public enum CompressionType : byte
+ {
+ kNoCompression = 0x0,
+ kSnappyCompression = 0x1
+ }
+
+ public static class Native
+ {
+ #region Logger
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr leveldb_logger_create(IntPtr /* Action */ logger);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_logger_destroy(IntPtr /* logger*/ option);
+ #endregion
+
+ #region DB
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr leveldb_open(IntPtr /* Options*/ options, string name, out IntPtr error);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_close(IntPtr /*DB */ db);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_put(IntPtr /* DB */ db, IntPtr /* WriteOptions*/ options, byte[] key, UIntPtr keylen, byte[] val, UIntPtr vallen, out IntPtr errptr);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_delete(IntPtr /* DB */ db, IntPtr /* WriteOptions*/ options, byte[] key, UIntPtr keylen, out IntPtr errptr);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_write(IntPtr /* DB */ db, IntPtr /* WriteOptions*/ options, IntPtr /* WriteBatch */ batch, out IntPtr errptr);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr leveldb_get(IntPtr /* DB */ db, IntPtr /* ReadOptions*/ options, byte[] key, UIntPtr keylen, out UIntPtr vallen, out IntPtr errptr);
+
+ //[DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ //static extern void leveldb_approximate_sizes(IntPtr /* 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);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr leveldb_create_iterator(IntPtr /* DB */ db, IntPtr /* ReadOption */ options);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr leveldb_create_snapshot(IntPtr /* DB */ db);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_release_snapshot(IntPtr /* DB */ db, IntPtr /* SnapShot*/ snapshot);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr leveldb_property_value(IntPtr /* DB */ db, string propname);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_repair_db(IntPtr /* Options*/ options, string name, out IntPtr error);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_destroy_db(IntPtr /* Options*/ options, string name, out IntPtr error);
+
+ #region extensions
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_free(IntPtr /* void */ ptr);
+
+ #endregion
+
+
+ #endregion
+
+ #region Env
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr leveldb_create_default_env();
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_env_destroy(IntPtr /*Env*/ cache);
+ #endregion
+
+ #region Iterator
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_iter_destroy(IntPtr /*Iterator*/ iterator);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ [return: MarshalAs(UnmanagedType.U1)]
+ public static extern bool leveldb_iter_valid(IntPtr /*Iterator*/ iterator);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_iter_seek_to_first(IntPtr /*Iterator*/ iterator);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_iter_seek_to_last(IntPtr /*Iterator*/ iterator);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_iter_seek(IntPtr /*Iterator*/ iterator, byte[] key, UIntPtr length);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_iter_next(IntPtr /*Iterator*/ iterator);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_iter_prev(IntPtr /*Iterator*/ iterator);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr leveldb_iter_key(IntPtr /*Iterator*/ iterator, out UIntPtr length);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr leveldb_iter_value(IntPtr /*Iterator*/ iterator, out UIntPtr length);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_iter_get_error(IntPtr /*Iterator*/ iterator, out IntPtr error);
+ #endregion
+
+ #region Options
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr leveldb_options_create();
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_options_destroy(IntPtr /*Options*/ options);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_options_set_create_if_missing(IntPtr /*Options*/ options, [MarshalAs(UnmanagedType.U1)] bool o);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_options_set_error_if_exists(IntPtr /*Options*/ options, [MarshalAs(UnmanagedType.U1)] bool o);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_options_set_info_log(IntPtr /*Options*/ options, IntPtr /* Logger */ logger);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_options_set_paranoid_checks(IntPtr /*Options*/ options, [MarshalAs(UnmanagedType.U1)] bool o);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_options_set_env(IntPtr /*Options*/ options, IntPtr /*Env*/ env);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_options_set_write_buffer_size(IntPtr /*Options*/ options, UIntPtr size);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_options_set_max_open_files(IntPtr /*Options*/ options, int max);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_options_set_cache(IntPtr /*Options*/ options, IntPtr /*Cache*/ cache);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_options_set_block_size(IntPtr /*Options*/ options, UIntPtr size);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_options_set_block_restart_interval(IntPtr /*Options*/ options, int interval);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_options_set_compression(IntPtr /*Options*/ options, CompressionType level);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_options_set_comparator(IntPtr /*Options*/ options, IntPtr /*Comparator*/ comparer);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_options_set_filter_policy(IntPtr /*Options*/ options, IntPtr /*FilterPolicy*/ policy);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr leveldb_filterpolicy_create_bloom(int bits_per_key);
+ #endregion
+
+ #region ReadOptions
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr leveldb_readoptions_create();
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_readoptions_destroy(IntPtr /*ReadOptions*/ options);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_readoptions_set_verify_checksums(IntPtr /*ReadOptions*/ options, [MarshalAs(UnmanagedType.U1)] bool o);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_readoptions_set_fill_cache(IntPtr /*ReadOptions*/ options, [MarshalAs(UnmanagedType.U1)] bool o);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_readoptions_set_snapshot(IntPtr /*ReadOptions*/ options, IntPtr /*SnapShot*/ snapshot);
+ #endregion
+
+ #region WriteBatch
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr leveldb_writebatch_create();
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_writebatch_destroy(IntPtr /* WriteBatch */ batch);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_writebatch_clear(IntPtr /* WriteBatch */ batch);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_writebatch_put(IntPtr /* WriteBatch */ batch, byte[] key, UIntPtr keylen, byte[] val, UIntPtr vallen);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_writebatch_delete(IntPtr /* WriteBatch */ batch, byte[] key, UIntPtr keylen);
+
+ [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void leveldb_writebatch_iterate(IntPtr /* WriteBatch */ batch, object state, Action