diff --git a/.devcontainer/devcontainer.dockerfile b/.devcontainer/devcontainer.dockerfile new file mode 100644 index 0000000000..3b16107e9e --- /dev/null +++ b/.devcontainer/devcontainer.dockerfile @@ -0,0 +1,3 @@ +FROM mcr.microsoft.com/devcontainers/dotnet:8.0-jammy +# Install the libleveldb-dev package +RUN apt-get update && apt-get install -y libleveldb-dev diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 00807d9b64..5e9bdf6374 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,10 @@ // README at: https://github.com/devcontainers/templates/tree/main/src/dotnet { "name": "C# (.NET)", - "image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0-jammy", + "build": { + // Path is relative to the devcontainer.json file. + "dockerfile": "devcontainer.dockerfile" + }, "postCreateCommand": "dotnet build", "customizations": { "vscode": { 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/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7b043f14a7..4ebc00ed19 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,6 +8,8 @@ Fixes # (issue) +- [ ] Optimization (the change is only an optimization) +- [ ] Style (the change is only a code style for better maintenance or standard purpose) - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5ac5e3dc65..3dae25a1df 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,6 +7,8 @@ on: env: DOTNET_VERSION: 8.0.x + COVERALL_COLLECT_OUTPUT: "/p:CollectCoverage=true /p:CoverletOutput='${{ github.workspace }}/TestResults/coverage/'" + COVERALL_MERGE_PATH: "/p:MergeWith='${{ github.workspace }}/TestResults/coverage/coverage.json'" jobs: @@ -34,22 +36,33 @@ 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: | - 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' + sudo apt-get --assume-yes install libleveldb-dev librocksdb-dev + + dotnet test ./tests/Neo.Cryptography.BLS12_381.Tests ${{ env.COVERALL_COLLECT_OUTPUT }} + dotnet test ./tests/Neo.ConsoleService.Tests ${{ env.COVERALL_COLLECT_OUTPUT }} ${{ env.COVERALL_MERGE_PATH }} + dotnet test ./tests/Neo.UnitTests ${{ env.COVERALL_COLLECT_OUTPUT }} ${{ env.COVERALL_MERGE_PATH }} + dotnet test ./tests/Neo.VM.Tests ${{ env.COVERALL_COLLECT_OUTPUT }} ${{ env.COVERALL_MERGE_PATH }} + dotnet test ./tests/Neo.Json.UnitTests ${{ env.COVERALL_COLLECT_OUTPUT }} ${{ env.COVERALL_MERGE_PATH }} + + # Plugins + dotnet test ./tests/Neo.Cryptography.MPTTrie.Tests ${{ env.COVERALL_COLLECT_OUTPUT }} ${{ env.COVERALL_MERGE_PATH }} + dotnet test ./tests/Neo.Network.RPC.Tests ${{ env.COVERALL_COLLECT_OUTPUT }} ${{ env.COVERALL_MERGE_PATH }} + dotnet test ./tests/Neo.Plugins.OracleService.Tests ${{ env.COVERALL_COLLECT_OUTPUT }} ${{ env.COVERALL_MERGE_PATH }} + dotnet test ./tests/Neo.Plugins.RpcServer.Tests ${{ env.COVERALL_COLLECT_OUTPUT }} ${{ env.COVERALL_MERGE_PATH }} + dotnet test ./tests/Neo.Plugins.Storage.Tests ${{ env.COVERALL_COLLECT_OUTPUT }} ${{ env.COVERALL_MERGE_PATH }} /p:CoverletOutputFormat='cobertura' + - name: Coveralls if: matrix.os == 'ubuntu-latest' - uses: coverallsapp/github-action@v2.2.3 + uses: coverallsapp/github-action@v2.3.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} - format: lcov - file: ${{ github.workspace }}/TestResults/coverage/coverage.info + format: cobertura + file: ${{ github.workspace }}/TestResults/coverage/coverage.cobertura.xml PublishPackage: if: github.ref == 'refs/heads/master' && startsWith(github.repository, 'neo-project/') 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 92bc5b006a..c211ee34aa 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,7 @@ 2015-2024 The Neo Project - 3.7.1 + 3.7.4 12.0 The Neo Project neo.png @@ -19,8 +19,8 @@ - - + + diff --git a/src/Neo.CLI/CLI/MainService.Plugins.cs b/src/Neo.CLI/CLI/MainService.Plugins.cs index 9038cacc47..3063fb163c 100644 --- a/src/Neo.CLI/CLI/MainService.Plugins.cs +++ b/src/Neo.CLI/CLI/MainService.Plugins.cs @@ -157,9 +157,7 @@ public async Task InstallPluginAsync( try { - Version v = new Version(3, 7, 5); - - using var stream = await DownloadPluginAsync(pluginName, v, Settings.Default.Plugins.Prerelease); + using var stream = await DownloadPluginAsync(pluginName, Settings.Default.Plugins.Version, Settings.Default.Plugins.Prerelease); using var zip = new ZipArchive(stream, ZipArchiveMode.Read); var entry = zip.Entries.FirstOrDefault(p => p.Name == "config.json"); diff --git a/src/Neo.CLI/CLI/MainService.Tools.cs b/src/Neo.CLI/CLI/MainService.Tools.cs index f4a5b762a9..66723d7df9 100644 --- a/src/Neo.CLI/CLI/MainService.Tools.cs +++ b/src/Neo.CLI/CLI/MainService.Tools.cs @@ -10,12 +10,18 @@ // modifications are permitted. using Neo.ConsoleService; +using Neo.Cryptography.ECC; using Neo.IO; +using Neo.SmartContract; +using Neo.VM; using Neo.Wallets; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Numerics; +using System.Reflection; +using System.Text; namespace Neo.CLI { @@ -27,23 +33,21 @@ partial class MainService [ConsoleCommand("parse", Category = "Base Commands", Description = "Parse a value to its possible conversions.")] private void OnParseCommand(string value) { - var parseFunctions = new Dictionary>() - { - { "Address to ScriptHash", AddressToScripthash }, - { "Address to Base64", AddressToBase64 }, - { "ScriptHash to Address", ScripthashToAddress }, - { "Base64 to Address", Base64ToAddress }, - { "Base64 to String", Base64ToString }, - { "Base64 to Big Integer", Base64ToNumber }, - { "Big Integer to Hex String", NumberToHex }, - { "Big Integer to Base64", NumberToBase64 }, - { "Hex String to String", HexToString }, - { "Hex String to Big Integer", HexToNumber }, - { "String to Hex String", StringToHex }, - { "String to Base64", StringToBase64 } - }; - - bool any = false; + value = Base64Fixed(value); + + var parseFunctions = new Dictionary>(); + var methods = GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance); + + foreach (var method in methods) + { + var attribute = method.GetCustomAttribute(); + if (attribute != null) + { + parseFunctions.Add(attribute.Description, (Func)Delegate.CreateDelegate(typeof(Func), this, method)); + } + } + + var any = false; foreach (var pair in parseFunctions) { @@ -64,50 +68,38 @@ private void OnParseCommand(string value) } /// - /// Converts an hexadecimal value to an UTF-8 string + /// Little-endian to Big-endian + /// input: ce616f7f74617e0fc4b805583af2602a238df63f + /// output: 0x3ff68d232a60f23a5805b8c40f7e61747f6f61ce /// - /// - /// Hexadecimal value to be converted - /// - /// - /// Returns null when is not possible to parse the hexadecimal value to a UTF-8 - /// string or when the converted string is not printable; otherwise, returns - /// the string represented by the hexadecimal value - /// - private string? HexToString(string hexString) + [ParseFunction("Little-endian to Big-endian")] + private string? LittleEndianToBigEndian(string hex) { try { - var clearHexString = ClearHexString(hexString); - var bytes = clearHexString.HexToBytes(); - var utf8String = Utility.StrictUTF8.GetString(bytes); - return IsPrintable(utf8String) ? utf8String : null; + if (!IsHex(hex)) return null; + return "0x" + hex.HexToBytes().Reverse().ToArray().ToHexString(); } - catch + catch (FormatException) { return null; } } /// - /// Converts an hex value to a big integer + /// Big-endian to Little-endian + /// input: 0x3ff68d232a60f23a5805b8c40f7e61747f6f61ce + /// output: ce616f7f74617e0fc4b805583af2602a238df63f /// - /// - /// Hexadecimal value to be converted - /// - /// - /// Returns null when is not possible to parse the hex value to big integer value; - /// otherwise, returns the string that represents the converted big integer. - /// - private string? HexToNumber(string hexString) + [ParseFunction("Big-endian to Little-endian")] + private string? BigEndianToLittleEndian(string hex) { try { - var clearHexString = ClearHexString(hexString); - var bytes = clearHexString.HexToBytes(); - var number = new BigInteger(bytes); - - return number.ToString(); + var hasHexPrefix = hex.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase); + hex = hasHexPrefix ? hex[2..] : hex; + if (!hasHexPrefix || !IsHex(hex)) return null; + return hex.HexToBytes().Reverse().ToArray().ToHexString(); } catch { @@ -116,66 +108,17 @@ private void OnParseCommand(string value) } /// - /// Formats a string value to a default hexadecimal representation of a byte array + /// String to Base64 + /// input: Hello World! + /// output: SGVsbG8gV29ybGQh /// - /// - /// The string value to be formatted - /// - /// - /// Returns the formatted string. - /// - /// - /// Throw when is the string is not a valid hex representation of a byte array. - /// - private string ClearHexString(string hexString) - { - bool hasHexPrefix = hexString.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase); - - try - { - if (hasHexPrefix) - { - hexString = hexString.Substring(2); - } - - if (hexString.Length % 2 == 1) - { - // if the length is an odd number, it cannot be parsed to a byte array - // it may be a valid hex string, so include a leading zero to parse correctly - hexString = "0" + hexString; - } - - if (hasHexPrefix) - { - // if the input value starts with '0x', the first byte is the less significant - // to parse correctly, reverse the byte array - return hexString.HexToBytes().Reverse().ToArray().ToHexString(); - } - } - catch (FormatException) - { - throw new ArgumentException(); - } - - return hexString; - } - - /// - /// Converts a string in a hexadecimal value - /// - /// - /// String value to be converted - /// - /// - /// Returns null when it is not possible to parse the string value to a hexadecimal - /// value; otherwise returns the hexadecimal value that represents the converted string - /// - private string? StringToHex(string strParam) + [ParseFunction("String to Base64")] + private string? StringToBase64(string strParam) { try { - var bytesParam = Utility.StrictUTF8.GetBytes(strParam); - return bytesParam.ToHexString(); + var bytearray = Utility.StrictUTF8.GetBytes(strParam); + return Convert.ToBase64String(bytearray.AsSpan()); } catch { @@ -184,25 +127,21 @@ private string ClearHexString(string hexString) } /// - /// Converts a string in Base64 string + /// Big Integer to Base64 + /// input: 123456 + /// output: QOIB /// - /// - /// String value to be converted - /// - /// - /// Returns null when is not possible to parse the string value to a Base64 value; - /// otherwise returns the Base64 value that represents the converted string - /// - /// - /// Throw . - /// - private string? StringToBase64(string strParam) + [ParseFunction("Big Integer to Base64")] + private string? NumberToBase64(string strParam) { try { - byte[] bytearray = Utility.StrictUTF8.GetBytes(strParam); - string base64 = Convert.ToBase64String(bytearray.AsSpan()); - return base64; + if (!BigInteger.TryParse(strParam, out var number)) + { + return null; + } + var bytearray = number.ToByteArray(); + return Convert.ToBase64String(bytearray.AsSpan()); } catch { @@ -210,56 +149,56 @@ private string ClearHexString(string hexString) } } + private static bool IsHex(string str) => str.Length % 2 == 0 && str.All(c => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')); + /// - /// Converts a string number in hexadecimal format + /// Fix for Base64 strings containing unicode + /// input: DCECbzTesnBofh/Xng1SofChKkBC7jhVmLxCN1vk\u002B49xa2pBVuezJw== + /// output: DCECbzTesnBofh/Xng1SofChKkBC7jhVmLxCN1vk+49xa2pBVuezJw== /// - /// - /// String that represents the number to be converted - /// - /// - /// Returns null when the string does not represent a big integer value or when - /// it is not possible to parse the big integer value to hexadecimal; otherwise, - /// returns the string that represents the converted hexadecimal value - /// - private string? NumberToHex(string strParam) + /// Base64 strings containing unicode + /// Correct Base64 string + private static string Base64Fixed(string str) { - try + var sb = new StringBuilder(); + for (var i = 0; i < str.Length; i++) { - if (!BigInteger.TryParse(strParam, out var numberParam)) + if (str[i] == '\\' && i + 5 < str.Length && str[i + 1] == 'u') { - return null; + var hex = str.Substring(i + 2, 4); + if (IsHex(hex)) + { + var bts = new byte[2]; + bts[0] = (byte)int.Parse(hex.Substring(2, 2), NumberStyles.HexNumber); + bts[1] = (byte)int.Parse(hex.Substring(0, 2), NumberStyles.HexNumber); + sb.Append(Encoding.Unicode.GetString(bts)); + i += 5; + } + else + { + sb.Append(str[i]); + } + } + else + { + sb.Append(str[i]); } - return numberParam.ToByteArray().ToHexString(); - } - catch - { - return null; } + return sb.ToString(); } /// - /// Converts a string number in Base64 byte array + /// Address to ScriptHash (big-endian) + /// input: NejD7DJWzD48ZG4gXKDVZt3QLf1fpNe1PF + /// output: 0x3ff68d232a60f23a5805b8c40f7e61747f6f61ce /// - /// - /// String that represents the number to be converted - /// - /// - /// Returns null when the string does not represent a big integer value or when - /// it is not possible to parse the big integer value to Base64 value; otherwise, - /// returns the string that represents the converted Base64 value - /// - private string? NumberToBase64(string strParam) + [ParseFunction("Address to ScriptHash (big-endian)")] + private string? AddressToScripthash(string address) { try { - if (!BigInteger.TryParse(strParam, out var number)) - { - return null; - } - byte[] bytearray = number.ToByteArray(); - string base64 = Convert.ToBase64String(bytearray.AsSpan()); - - return base64; + var bigEndScript = address.ToScriptHash(NeoSystem.Settings.AddressVersion); + return bigEndScript.ToString(); } catch { @@ -268,23 +207,17 @@ private string ClearHexString(string hexString) } /// - /// Converts an address to its corresponding scripthash + /// Address to ScriptHash (blittleig-endian) + /// input: NejD7DJWzD48ZG4gXKDVZt3QLf1fpNe1PF + /// output: ce616f7f74617e0fc4b805583af2602a238df63f /// - /// - /// String that represents the address to be converted - /// - /// - /// Returns null when the string does not represent an address or when - /// it is not possible to parse the address to scripthash; otherwise returns - /// the string that represents the converted scripthash - /// - private string? AddressToScripthash(string address) + [ParseFunction("Address to ScriptHash (little-endian)")] + private string? AddressToScripthashLE(string address) { try { var bigEndScript = address.ToScriptHash(NeoSystem.Settings.AddressVersion); - - return bigEndScript.ToString(); + return bigEndScript.ToArray().ToHexString(); } catch { @@ -293,24 +226,17 @@ private string ClearHexString(string hexString) } /// - /// Converts an address to Base64 byte array + /// Address to Base64 + /// input: NejD7DJWzD48ZG4gXKDVZt3QLf1fpNe1PF + /// output: zmFvf3Rhfg/EuAVYOvJgKiON9j8= /// - /// - /// String that represents the address to be converted - /// - /// - /// Returns null when the string does not represent an address or when it is - /// not possible to parse the address to Base64 value; otherwise returns - /// the string that represents the converted Base64 value. - /// + [ParseFunction("Address to Base64")] private string? AddressToBase64(string address) { try { var script = address.ToScriptHash(NeoSystem.Settings.AddressVersion); - string base64 = Convert.ToBase64String(script.ToArray().AsSpan()); - - return base64; + return Convert.ToBase64String(script.ToArray().AsSpan()); } catch { @@ -319,15 +245,11 @@ private string ClearHexString(string hexString) } /// - /// Converts a big end script hash to its equivalent address + /// ScriptHash to Address + /// input: 0x3ff68d232a60f23a5805b8c40f7e61747f6f61ce + /// output: NejD7DJWzD48ZG4gXKDVZt3QLf1fpNe1PF /// - /// - /// String that represents the scripthash to be converted - /// - /// - /// Returns null when the string does not represent an scripthash; - /// otherwise, returns the string that represents the converted address - /// + [ParseFunction("ScriptHash to Address")] private string? ScripthashToAddress(string script) { try @@ -346,15 +268,14 @@ private string ClearHexString(string hexString) { return null; } - string bigEndScript = littleEndScript.ToArray().ToHexString(); + var bigEndScript = littleEndScript.ToArray().ToHexString(); if (!UInt160.TryParse(bigEndScript, out scriptHash)) { return null; } } - var hexScript = scriptHash.ToAddress(NeoSystem.Settings.AddressVersion); - return hexScript; + return scriptHash.ToAddress(NeoSystem.Settings.AddressVersion); } catch { @@ -363,30 +284,24 @@ private string ClearHexString(string hexString) } /// - /// Converts an Base64 byte array to address + /// Base64 to Address + /// input: zmFvf3Rhfg/EuAVYOvJgKiON9j8= + /// output: NejD7DJWzD48ZG4gXKDVZt3QLf1fpNe1PF /// - /// - /// String that represents the Base64 value - /// - /// - /// Returns null when the string does not represent an Base64 value or when - /// it is not possible to parse the Base64 value to address; otherwise, - /// returns the string that represents the converted address - /// + [ParseFunction("Base64 to Address")] private string? Base64ToAddress(string bytearray) { try { - byte[] result = Convert.FromBase64String(bytearray).Reverse().ToArray(); - string hex = result.ToHexString(); + var result = Convert.FromBase64String(bytearray).Reverse().ToArray(); + var hex = result.ToHexString(); if (!UInt160.TryParse(hex, out var scripthash)) { return null; } - string address = scripthash.ToAddress(NeoSystem.Settings.AddressVersion); - return address; + return scripthash.ToAddress(NeoSystem.Settings.AddressVersion); } catch { @@ -395,23 +310,17 @@ private string ClearHexString(string hexString) } /// - /// Converts an Base64 hex string to string + /// Base64 to String + /// input: SGVsbG8gV29ybGQh + /// output: Hello World! /// - /// - /// String that represents the Base64 value - /// - /// - /// Returns null when the string does not represent an Base64 value or when - /// it is not possible to parse the Base64 value to string value or the converted - /// string is not printable; otherwise, returns the string that represents - /// the Base64 value. - /// + [ParseFunction("Base64 to String")] private string? Base64ToString(string bytearray) { try { - byte[] result = Convert.FromBase64String(bytearray); - string utf8String = Utility.StrictUTF8.GetString(result); + var result = Convert.FromBase64String(bytearray); + var utf8String = Utility.StrictUTF8.GetString(result); return IsPrintable(utf8String) ? utf8String : null; } catch @@ -421,16 +330,11 @@ private string ClearHexString(string hexString) } /// - /// Converts an Base64 hex string to big integer value + /// Base64 to Big Integer + /// input: QOIB + /// output: 123456 /// - /// - /// String that represents the Base64 value - /// - /// - /// Returns null when the string does not represent an Base64 value or when - /// it is not possible to parse the Base64 value to big integer value; otherwise - /// returns the string that represents the converted big integer - /// + [ParseFunction("Base64 to Big Integer")] private string? Base64ToNumber(string bytearray) { try @@ -445,6 +349,132 @@ private string ClearHexString(string hexString) } } + /// + /// Public Key to Address + /// input: 03dab84c1243ec01ab2500e1a8c7a1546a26d734628180b0cf64e72bf776536997 + /// output: NU7RJrzNgCSnoPLxmcY7C72fULkpaGiSpJ + /// + [ParseFunction("Public Key to Address")] + private string? PublicKeyToAddress(string pubKey) + { + if (ECPoint.TryParse(pubKey, ECCurve.Secp256r1, out var publicKey) == false) + return null; + return Contract.CreateSignatureContract(publicKey) + .ScriptHash + .ToAddress(NeoSystem.Settings.AddressVersion); + } + + /// + /// WIF to Public Key + /// + [ParseFunction("WIF to Public Key")] + private string? WIFToPublicKey(string wif) + { + try + { + var privateKey = Wallet.GetPrivateKeyFromWIF(wif); + var account = new KeyPair(privateKey); + return account.PublicKey.ToArray().ToHexString(); + } + catch (Exception) + { + return null; + } + } + + /// + /// WIF to Address + /// + [ParseFunction("WIF to Address")] + private string? WIFToAddress(string wif) + { + try + { + var pubKey = WIFToPublicKey(wif); + return Contract.CreateSignatureContract(ECPoint.Parse(pubKey, ECCurve.Secp256r1)).ScriptHash.ToAddress(NeoSystem.Settings.AddressVersion); + } + catch (Exception) + { + return null; + } + } + + /// + /// Base64 Smart Contract Script Analysis + /// input: DARkYXRhAgBlzR0MFPdcrAXPVptVduMEs2lf1jQjxKIKDBT3XKwFz1abVXbjBLNpX9Y0I8SiChTAHwwIdHJhbnNmZXIMFKNSbimM12LkFYX/8KGvm2ttFxulQWJ9W1I= + /// output: + /// PUSHDATA1 data + /// PUSHINT32 500000000 + /// PUSHDATA1 0x0aa2c42334d65f69b304e376559b56cf05ac5cf7 + /// PUSHDATA1 0x0aa2c42334d65f69b304e376559b56cf05ac5cf7 + /// PUSH4 + /// PACK + /// PUSH15 + /// PUSHDATA1 transfer + /// PUSHDATA1 0xa51b176d6b9bafa1f0ff8515e462d78c296e52a3 + /// SYSCALL System.Contract.Call + /// + [ParseFunction("Base64 Smart Contract Script Analysis")] + private string? ScriptsToOpCode(string base64) + { + Script script; + try + { + var scriptData = Convert.FromBase64String(base64); + script = new Script(scriptData.ToArray(), true); + } + catch (Exception) + { + return null; + } + return ScriptsToOpCode(script); + } + + private string ScriptsToOpCode(Script script) + { + //Initialize all InteropService + var dic = new Dictionary(); + ApplicationEngine.Services.ToList().ForEach(p => dic.Add(p.Value.Hash, p.Value.Name)); + + //Analyzing Scripts + var ip = 0; + Instruction instruction; + var result = new List(); + while (ip < script.Length && (instruction = script.GetInstruction(ip)) != null) + { + ip += instruction.Size; + + var op = instruction.OpCode; + + if (op.ToString().StartsWith("PUSHINT")) + { + var operand = instruction.Operand.ToArray(); + result.Add($"{op} {new BigInteger(operand)}"); + } + else if (op == OpCode.SYSCALL) + { + var operand = instruction.Operand.ToArray(); + result.Add($"{op} {dic[BitConverter.ToUInt32(operand)]}"); + } + else + { + if (!instruction.Operand.IsEmpty && instruction.Operand.Length > 0) + { + var operand = instruction.Operand.ToArray(); + var ascii = Encoding.Default.GetString(operand); + ascii = ascii.Any(p => p < '0' || p > 'z') ? operand.ToHexString() : ascii; + + result.Add($"{op} {(operand.Length == 20 ? new UInt160(operand).ToString() : ascii)}"); + } + else + { + result.Add($"{op}"); + } + } + } + return Environment.NewLine + string.Join("\r\n", result.ToArray()); + } + /// /// Checks if the string is null or cannot be printed. /// @@ -455,7 +485,7 @@ private string ClearHexString(string hexString) /// Returns false if the string is null, or if it is empty, or if each character cannot be printed; /// otherwise, returns true. /// - private bool IsPrintable(string value) + private static bool IsPrintable(string value) { return !string.IsNullOrWhiteSpace(value) && value.Any(c => !char.IsControl(c)); } diff --git a/src/Neo.CLI/CLI/ParseFunctionAttribute.cs b/src/Neo.CLI/CLI/ParseFunctionAttribute.cs new file mode 100644 index 0000000000..7dc92c661a --- /dev/null +++ b/src/Neo.CLI/CLI/ParseFunctionAttribute.cs @@ -0,0 +1,25 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// ParseFunctionAttribute.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.CLI +{ + internal class ParseFunctionAttribute : Attribute + { + public string Description { get; } + + public ParseFunctionAttribute(string description) + { + Description = description; + } + } +} diff --git a/src/Neo.CLI/config.fs.mainnet.json b/src/Neo.CLI/config.fs.mainnet.json index fad987209a..e79a31c312 100644 --- a/src/Neo.CLI/config.fs.mainnet.json +++ b/src/Neo.CLI/config.fs.mainnet.json @@ -36,6 +36,11 @@ "MaxTraceableBlocks": 2102400, "InitialGasDistribution": 5200000000000000, "ValidatorsCount": 7, + "Hardforks": { + "HF_Aspidochelone": 3000000, + "HF_Basilisk": 4500000, + "HF_Cockatrice": 5800000 + }, "StandbyCommittee": [ "026fa34ec057d74c2fdf1a18e336d0bd597ea401a0b2ad57340d5c220d09f44086", "039a9db2a30942b1843db673aeb0d4fd6433f74cec1d879de6343cb9fcf7628fa4", diff --git a/src/Neo.CLI/config.json b/src/Neo.CLI/config.json index fb73c3f8d8..c9544a337f 100644 --- a/src/Neo.CLI/config.json +++ b/src/Neo.CLI/config.json @@ -37,7 +37,7 @@ "Hardforks": { "HF_Aspidochelone": 1730000, "HF_Basilisk": 4120000, - "HF_Cockatrice": 5420000 + "HF_Cockatrice": 5450000 }, "InitialGasDistribution": 5200000000000000, "ValidatorsCount": 7, diff --git a/src/Neo.CLI/config.mainnet.json b/src/Neo.CLI/config.mainnet.json index fb73c3f8d8..c9544a337f 100644 --- a/src/Neo.CLI/config.mainnet.json +++ b/src/Neo.CLI/config.mainnet.json @@ -37,7 +37,7 @@ "Hardforks": { "HF_Aspidochelone": 1730000, "HF_Basilisk": 4120000, - "HF_Cockatrice": 5420000 + "HF_Cockatrice": 5450000 }, "InitialGasDistribution": 5200000000000000, "ValidatorsCount": 7, diff --git a/src/Neo.Extensions/Neo.Extensions.csproj b/src/Neo.Extensions/Neo.Extensions.csproj index d0d899cec3..71330a05a3 100644 --- a/src/Neo.Extensions/Neo.Extensions.csproj +++ b/src/Neo.Extensions/Neo.Extensions.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Neo.VM/ExecutionEngine.cs b/src/Neo.VM/ExecutionEngine.cs index 52dd057902..c7c3f86ce9 100644 --- a/src/Neo.VM/ExecutionEngine.cs +++ b/src/Neo.VM/ExecutionEngine.cs @@ -166,7 +166,7 @@ protected internal void ExecuteNext() /// Loads the specified context into the invocation stack. /// /// The context to load. - internal virtual void LoadContext(ExecutionContext context) + public virtual void LoadContext(ExecutionContext context) { if (InvocationStack.Count >= Limits.MaxInvocationStackSize) throw new InvalidOperationException($"MaxInvocationStackSize exceed: {InvocationStack.Count}"); diff --git a/src/Neo/Neo.csproj b/src/Neo/Neo.csproj index 02668fda95..6a1ba44504 100644 --- a/src/Neo/Neo.csproj +++ b/src/Neo/Neo.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Neo/ProtocolSettings.cs b/src/Neo/ProtocolSettings.cs index c3d8232afd..f03e5ffaf9 100644 --- a/src/Neo/ProtocolSettings.cs +++ b/src/Neo/ProtocolSettings.cs @@ -144,7 +144,7 @@ public static ProtocolSettings Load(string path, bool optional = true) /// The loaded . public static ProtocolSettings Load(IConfigurationSection section) { - return new ProtocolSettings + Custom = new ProtocolSettings { Network = section.GetValue("Network", Default.Network), AddressVersion = section.GetValue("AddressVersion", Default.AddressVersion), @@ -164,6 +164,7 @@ public static ProtocolSettings Load(IConfigurationSection section) ? EnsureOmmitedHardforks(section.GetSection("Hardforks").GetChildren().ToDictionary(p => Enum.Parse(p.Key, true), p => uint.Parse(p.Value))).ToImmutableDictionary() : Default.Hardforks }; + return Custom; } /// diff --git a/src/Neo/SmartContract/ApplicationEngine.cs b/src/Neo/SmartContract/ApplicationEngine.cs index 0c57b771e5..14dedf8e7c 100644 --- a/src/Neo/SmartContract/ApplicationEngine.cs +++ b/src/Neo/SmartContract/ApplicationEngine.cs @@ -32,7 +32,7 @@ namespace Neo.SmartContract /// public partial class ApplicationEngine : ExecutionEngine { - private static readonly JumpTable DefaultJumpTable = ComposeDefaultJumpTable(); + protected static readonly JumpTable DefaultJumpTable = ComposeDefaultJumpTable(); /// /// The maximum cost that can be spent when a contract is executed in test mode. @@ -194,7 +194,7 @@ private static JumpTable ComposeDefaultJumpTable() return table; } - private static void OnCallT(ExecutionEngine engine, Instruction instruction) + protected static void OnCallT(ExecutionEngine engine, Instruction instruction) { if (engine is ApplicationEngine app) { @@ -218,7 +218,7 @@ private static void OnCallT(ExecutionEngine engine, Instruction instruction) } } - private static void OnSysCall(ExecutionEngine engine, Instruction instruction) + protected static void OnSysCall(ExecutionEngine engine, Instruction instruction) { if (engine is ApplicationEngine app) { @@ -384,7 +384,7 @@ public static ApplicationEngine Create(TriggerType trigger, IVerifiable containe ?? new ApplicationEngine(trigger, container, snapshot, persistingBlock, settings, gas, diagnostic, jumpTable); } - internal override void LoadContext(ExecutionContext context) + public override void LoadContext(ExecutionContext context) { // Set default execution context state var state = context.GetState(); diff --git a/src/Neo/SmartContract/Native/ContractMethodAttribute.cs b/src/Neo/SmartContract/Native/ContractMethodAttribute.cs index cd8f7de59e..7caa27c8b0 100644 --- a/src/Neo/SmartContract/Native/ContractMethodAttribute.cs +++ b/src/Neo/SmartContract/Native/ContractMethodAttribute.cs @@ -23,6 +23,7 @@ internal class ContractMethodAttribute : Attribute public long CpuFee { get; init; } public long StorageFee { get; init; } public Hardfork? ActiveIn { get; init; } = null; + public Hardfork? DeprecatedIn { get; init; } = null; public ContractMethodAttribute() { } @@ -30,5 +31,11 @@ public ContractMethodAttribute(Hardfork activeIn) { ActiveIn = activeIn; } + + public ContractMethodAttribute(bool isDeprecated, Hardfork deprecatedIn) + { + if (!isDeprecated) throw new ArgumentException("isDeprecated must be true", nameof(isDeprecated)); + DeprecatedIn = deprecatedIn; + } } } diff --git a/src/Neo/SmartContract/Native/ContractMethodMetadata.cs b/src/Neo/SmartContract/Native/ContractMethodMetadata.cs index 0688a0dac2..30874efb47 100644 --- a/src/Neo/SmartContract/Native/ContractMethodMetadata.cs +++ b/src/Neo/SmartContract/Native/ContractMethodMetadata.cs @@ -36,6 +36,7 @@ internal class ContractMethodMetadata public CallFlags RequiredCallFlags { get; } public ContractMethodDescriptor Descriptor { get; } public Hardfork? ActiveIn { get; init; } = null; + public Hardfork? DeprecatedIn { get; init; } = null; public ContractMethodMetadata(MemberInfo member, ContractMethodAttribute attribute) { @@ -60,6 +61,7 @@ public ContractMethodMetadata(MemberInfo member, ContractMethodAttribute attribu StorageFee = attribute.StorageFee; RequiredCallFlags = attribute.RequiredCallFlags; ActiveIn = attribute.ActiveIn; + DeprecatedIn = attribute.DeprecatedIn; Descriptor = new ContractMethodDescriptor { Name = Name, diff --git a/src/Neo/SmartContract/Native/CryptoLib.cs b/src/Neo/SmartContract/Native/CryptoLib.cs index 6e2876a860..452dd31faa 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.cs @@ -21,6 +21,12 @@ namespace Neo.SmartContract.Native /// public sealed partial class CryptoLib : NativeContract { + private static readonly Dictionary curves = new() + { + [NamedCurve.secp256k1] = ECCurve.Secp256k1, + [NamedCurve.secp256r1] = ECCurve.Secp256r1, + }; + private static readonly Dictionary s_curves = new() { [NamedCurveHash.secp256k1SHA256] = (ECCurve.Secp256k1, Hasher.SHA256), @@ -85,7 +91,7 @@ public static byte[] Keccak256(byte[] data) /// The signature to be verified. /// A pair of the curve to be used by the ECDSA algorithm and the hasher function to be used to hash message. /// if the signature is valid; otherwise, . - [ContractMethod(CpuFee = 1 << 15)] + [ContractMethod(Hardfork.HF_Cockatrice, CpuFee = 1 << 15)] public static bool VerifyWithECDsa(byte[] message, byte[] pubkey, byte[] signature, NamedCurveHash curveHash) { try @@ -98,5 +104,19 @@ public static bool VerifyWithECDsa(byte[] message, byte[] pubkey, byte[] signatu return false; } } + + // This is for solving the hardfork issue in https://github.com/neo-project/neo/pull/3209 + [ContractMethod(true, Hardfork.HF_Cockatrice, CpuFee = 1 << 15)] + public static bool VerifyWithECDsa(byte[] message, byte[] pubkey, byte[] signature, NamedCurve curve) + { + try + { + return Crypto.VerifySignature(message, signature, pubkey, curves[curve]); + } + catch (ArgumentException) + { + return false; + } + } } } diff --git a/src/Neo/SmartContract/Native/NativeContract.cs b/src/Neo/SmartContract/Native/NativeContract.cs index 70f67de538..03815b3fae 100644 --- a/src/Neo/SmartContract/Native/NativeContract.cs +++ b/src/Neo/SmartContract/Native/NativeContract.cs @@ -160,6 +160,7 @@ protected NativeContract() // Calculate the initializations forks usedHardforks = methodDescriptors.Select(u => u.ActiveIn) + .Concat(methodDescriptors.Select(u => u.DeprecatedIn)) .Concat(eventsDescriptors.Select(u => u.ActiveIn)) .Concat(new Hardfork?[] { ActiveIn }) .Where(u => u is not null) @@ -184,7 +185,15 @@ private NativeContractsCache.CacheEntry GetAllowedMethods(IsHardforkEnabledDeleg byte[] script; using (ScriptBuilder sb = new()) { - foreach (ContractMethodMetadata method in methodDescriptors.Where(u => u.ActiveIn is null || hfChecker(u.ActiveIn.Value, index))) + foreach (ContractMethodMetadata method in methodDescriptors.Where(u + => + // no hardfork is involved + u.ActiveIn is null && u.DeprecatedIn is null || + // deprecated method hardfork is involved + u.DeprecatedIn is not null && hfChecker(u.DeprecatedIn.Value, index) == false || + // active method hardfork is involved + u.ActiveIn is not null && hfChecker(u.ActiveIn.Value, index)) + ) { method.Descriptor.Offset = sb.Length; sb.EmitPush(0); //version @@ -366,6 +375,8 @@ internal async void Invoke(ApplicationEngine engine, byte version) ContractMethodMetadata method = currentAllowedMethods.Methods[context.InstructionPointer]; if (method.ActiveIn is not null && !engine.IsHardforkEnabled(method.ActiveIn.Value)) throw new InvalidOperationException($"Cannot call this method before hardfork {method.ActiveIn}."); + if (method.DeprecatedIn is not null && engine.IsHardforkEnabled(method.DeprecatedIn.Value)) + throw new InvalidOperationException($"Cannot call this method after hardfork {method.DeprecatedIn}."); ExecutionContextState state = context.GetState(); if (!state.CallFlags.HasFlag(method.RequiredCallFlags)) throw new InvalidOperationException($"Cannot call this method with the flag {state.CallFlags}."); diff --git a/src/Neo/SmartContract/Native/NeoToken.cs b/src/Neo/SmartContract/Native/NeoToken.cs index 1ff9aa2fdb..1fbf07204e 100644 --- a/src/Neo/SmartContract/Native/NeoToken.cs +++ b/src/Neo/SmartContract/Native/NeoToken.cs @@ -63,7 +63,7 @@ public sealed class NeoToken : FungibleToken "from", ContractParameterType.PublicKey, "to", ContractParameterType.PublicKey, "amount", ContractParameterType.Integer)] - [ContractEvent(3, name: "CommitteeChanged", + [ContractEvent(Hardfork.HF_Cockatrice, 3, name: "CommitteeChanged", "old", ContractParameterType.Array, "new", ContractParameterType.Array)] internal NeoToken() : base() @@ -203,14 +203,20 @@ internal override ContractTask OnPersistAsync(ApplicationEngine engine) cachedCommittee.Clear(); cachedCommittee.AddRange(ComputeCommitteeMembers(engine.Snapshot, engine.ProtocolSettings)); - var newCommittee = cachedCommittee.Select(u => u.PublicKey).ToArray(); - - if (!newCommittee.SequenceEqual(prevCommittee)) + // Hardfork check for https://github.com/neo-project/neo/pull/3158 + // New notification will case 3.7.0 and 3.6.0 have different behavior + var index = engine.PersistingBlock?.Index ?? Ledger.CurrentIndex(engine.Snapshot); + if (engine.ProtocolSettings.IsHardforkEnabled(Hardfork.HF_Cockatrice, index)) { - engine.SendNotification(Hash, "CommitteeChanged", new VM.Types.Array(engine.ReferenceCounter) { - new VM.Types.Array(engine.ReferenceCounter, prevCommittee.Select(u => (ByteString)u.ToArray())) , - new VM.Types.Array(engine.ReferenceCounter, newCommittee.Select(u => (ByteString)u.ToArray())) - }); + var newCommittee = cachedCommittee.Select(u => u.PublicKey).ToArray(); + + if (!newCommittee.SequenceEqual(prevCommittee)) + { + engine.SendNotification(Hash, "CommitteeChanged", new VM.Types.Array(engine.ReferenceCounter) { + new VM.Types.Array(engine.ReferenceCounter, prevCommittee.Select(u => (ByteString)u.ToArray())) , + new VM.Types.Array(engine.ReferenceCounter, newCommittee.Select(u => (ByteString)u.ToArray())) + }); + } } } return ContractTask.CompletedTask; diff --git a/src/Plugins/ApplicationLogs/ApplicationLogs.csproj b/src/Plugins/ApplicationLogs/ApplicationLogs.csproj new file mode 100644 index 0000000000..89eea3b1bb --- /dev/null +++ b/src/Plugins/ApplicationLogs/ApplicationLogs.csproj @@ -0,0 +1,21 @@ + + + net8.0 + Neo.Plugins.ApplicationLogs + Neo.Plugins + enable + + + + + + false + runtime + + + + + PreserveNewest + + + \ No newline at end of file diff --git a/src/Plugins/ApplicationLogs/ApplicationLogs.json b/src/Plugins/ApplicationLogs/ApplicationLogs.json new file mode 100644 index 0000000000..af601bc81e --- /dev/null +++ b/src/Plugins/ApplicationLogs/ApplicationLogs.json @@ -0,0 +1,11 @@ +{ + "PluginConfiguration": { + "Path": "ApplicationLogs_{0}", + "Network": 860833102, + "MaxStackSize": 65535, + "Debug": false + }, + "Dependency": [ + "RpcServer" + ] +} diff --git a/src/Plugins/ApplicationLogs/LogReader.cs b/src/Plugins/ApplicationLogs/LogReader.cs new file mode 100644 index 0000000000..4de4237acb --- /dev/null +++ b/src/Plugins/ApplicationLogs/LogReader.cs @@ -0,0 +1,459 @@ +// 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 notifications (NotifyLog) on blockchain."; + + #region Ctor + + public LogReader() + { + _logEvents = new(); + Blockchain.Committing += OnCommitting; + Blockchain.Committed += OnCommitted; + } + + #endregion + + #region Override Methods + + public override string ConfigFile => Combine(RootPath, "ApplicationLogs.json"); + + 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..9836db7146 --- /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 required UInt160 ScriptHash { get; init; } + public required string Message { get; init; } + + public static ApplicationEngineLogModel Create(EngineLogState logEventState) => + new() + { + ScriptHash = logEventState.ScriptHash, + Message = logEventState.Message, + }; + } +} diff --git a/src/Plugins/ApplicationLogs/Store/Models/BlockchainEventModel.cs b/src/Plugins/ApplicationLogs/Store/Models/BlockchainEventModel.cs new file mode 100644 index 0000000000..a882ff1a8c --- /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 required UInt160 ScriptHash { get; init; } + public required string EventName { get; init; } + public required StackItem[] State { get; init; } + + public static BlockchainEventModel Create(UInt160 scriptHash, string eventName, params StackItem[] state) => + new() + { + ScriptHash = scriptHash, + EventName = eventName ?? string.Empty, + State = state, + }; + + public static BlockchainEventModel Create(NotifyLogState notifyLogState, params StackItem[] state) => + new() + { + ScriptHash = notifyLogState.ScriptHash, + EventName = notifyLogState.EventName, + State = state, + }; + + public static BlockchainEventModel Create(ContractLogState contractLogState, params StackItem[] state) => + new() + { + ScriptHash = contractLogState.ScriptHash, + EventName = contractLogState.EventName, + State = state, + }; + } +} diff --git a/src/Plugins/ApplicationLogs/Store/Models/BlockchainExecutionModel.cs b/src/Plugins/ApplicationLogs/Store/Models/BlockchainExecutionModel.cs new file mode 100644 index 0000000000..19a3de5749 --- /dev/null +++ b/src/Plugins/ApplicationLogs/Store/Models/BlockchainExecutionModel.cs @@ -0,0 +1,42 @@ +// 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 required TriggerType Trigger { get; init; } + public required VMState VmState { get; init; } + public required string Exception { get; init; } + public required long GasConsumed { get; init; } + public required StackItem[] Stack { get; init; } + public required BlockchainEventModel[] Notifications { get; set; } + public required ApplicationEngineLogModel[] Logs { get; set; } + + public static BlockchainExecutionModel Create(TriggerType trigger, ExecutionLogState executionLogState, params StackItem[] stack) => + new() + { + Trigger = trigger, + VmState = executionLogState.VmState, + Exception = executionLogState.Exception ?? string.Empty, + GasConsumed = executionLogState.GasConsumed, + Stack = stack, + Notifications = [], + Logs = [] + }; + } +} diff --git a/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/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..59191d2881 --- /dev/null +++ b/src/Plugins/DBFTPlugin/DBFTPlugin.cs @@ -0,0 +1,103 @@ +// 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 override string ConfigFile => System.IO.Path.Combine(RootPath, "DBFTPlugin.json"); + + 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..b04e2e5c4f --- /dev/null +++ b/src/Plugins/DBFTPlugin/DBFTPlugin.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + Neo.Consensus.DBFT + Neo.Consensus + + + + + + + + + PreserveNewest + + + + diff --git a/src/Plugins/DBFTPlugin/DBFTPlugin.json b/src/Plugins/DBFTPlugin/DBFTPlugin.json new file mode 100644 index 0000000000..2e2b710ba3 --- /dev/null +++ b/src/Plugins/DBFTPlugin/DBFTPlugin.json @@ -0,0 +1,10 @@ +{ + "PluginConfiguration": { + "RecoveryLogs": "ConsensusState", + "IgnoreRecoveryLogs": false, + "AutoStart": false, + "Network": 860833102, + "MaxBlockSize": 2097152, + "MaxBlockSystemFee": 150000000000 + } +} 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/Directory.Build.props b/src/Plugins/Directory.Build.props new file mode 100644 index 0000000000..f7bcef093b --- /dev/null +++ b/src/Plugins/Directory.Build.props @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + 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 put, Action deleted); + #endregion + + #region WriteOptions + [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr leveldb_writeoptions_create(); + + [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern void leveldb_writeoptions_destroy(IntPtr /*WriteOptions*/ options); + + [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern void leveldb_writeoptions_set_sync(IntPtr /*WriteOptions*/ options, [MarshalAs(UnmanagedType.U1)] bool o); + #endregion + + #region Cache + [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr leveldb_cache_create_lru(int capacity); + + [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern void leveldb_cache_destroy(IntPtr /*Cache*/ cache); + #endregion + + #region Comparator + + [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr /* leveldb_comparator_t* */ + leveldb_comparator_create( + IntPtr /* void* */ state, + IntPtr /* void (*)(void*) */ destructor, + IntPtr + /* int (*compare)(void*, + const char* a, size_t alen, + const char* b, size_t blen) */ + compare, + IntPtr /* const char* (*)(void*) */ name); + + [DllImport("libleveldb", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern void leveldb_comparator_destroy(IntPtr /* leveldb_comparator_t* */ cmp); + + #endregion + } + + internal static class NativeHelper + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CheckError(IntPtr error) + { + if (error != IntPtr.Zero) + { + string message = Marshal.PtrToStringAnsi(error); + Native.leveldb_free(error); + throw new LevelDBException(message); + } + } + } +} diff --git a/src/Plugins/LevelDBStore/IO/Data/LevelDB/Options.cs b/src/Plugins/LevelDBStore/IO/Data/LevelDB/Options.cs new file mode 100644 index 0000000000..989987eeed --- /dev/null +++ b/src/Plugins/LevelDBStore/IO/Data/LevelDB/Options.cs @@ -0,0 +1,98 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Options.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; + +namespace Neo.IO.Data.LevelDB +{ + public class Options + { + public static readonly Options Default = new Options(); + internal readonly IntPtr handle = Native.leveldb_options_create(); + + public bool CreateIfMissing + { + set + { + Native.leveldb_options_set_create_if_missing(handle, value); + } + } + + public bool ErrorIfExists + { + set + { + Native.leveldb_options_set_error_if_exists(handle, value); + } + } + + public bool ParanoidChecks + { + set + { + Native.leveldb_options_set_paranoid_checks(handle, value); + } + } + + public int WriteBufferSize + { + set + { + Native.leveldb_options_set_write_buffer_size(handle, (UIntPtr)value); + } + } + + public int MaxOpenFiles + { + set + { + Native.leveldb_options_set_max_open_files(handle, value); + } + } + + public int BlockSize + { + set + { + Native.leveldb_options_set_block_size(handle, (UIntPtr)value); + } + } + + public int BlockRestartInterval + { + set + { + Native.leveldb_options_set_block_restart_interval(handle, value); + } + } + + public CompressionType Compression + { + set + { + Native.leveldb_options_set_compression(handle, value); + } + } + + public IntPtr FilterPolicy + { + set + { + Native.leveldb_options_set_filter_policy(handle, value); + } + } + + ~Options() + { + Native.leveldb_options_destroy(handle); + } + } +} diff --git a/src/Plugins/LevelDBStore/IO/Data/LevelDB/ReadOptions.cs b/src/Plugins/LevelDBStore/IO/Data/LevelDB/ReadOptions.cs new file mode 100644 index 0000000000..727ae9f02a --- /dev/null +++ b/src/Plugins/LevelDBStore/IO/Data/LevelDB/ReadOptions.cs @@ -0,0 +1,50 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// ReadOptions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; + +namespace Neo.IO.Data.LevelDB +{ + public class ReadOptions + { + public static readonly ReadOptions Default = new ReadOptions(); + internal readonly IntPtr handle = Native.leveldb_readoptions_create(); + + public bool VerifyChecksums + { + set + { + Native.leveldb_readoptions_set_verify_checksums(handle, value); + } + } + + public bool FillCache + { + set + { + Native.leveldb_readoptions_set_fill_cache(handle, value); + } + } + + public Snapshot Snapshot + { + set + { + Native.leveldb_readoptions_set_snapshot(handle, value.handle); + } + } + + ~ReadOptions() + { + Native.leveldb_readoptions_destroy(handle); + } + } +} diff --git a/src/Plugins/LevelDBStore/IO/Data/LevelDB/Snapshot.cs b/src/Plugins/LevelDBStore/IO/Data/LevelDB/Snapshot.cs new file mode 100644 index 0000000000..14280fbc8f --- /dev/null +++ b/src/Plugins/LevelDBStore/IO/Data/LevelDB/Snapshot.cs @@ -0,0 +1,35 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Snapshot.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; + +namespace Neo.IO.Data.LevelDB +{ + public class Snapshot : IDisposable + { + internal IntPtr db, handle; + + internal Snapshot(IntPtr db) + { + this.db = db; + handle = Native.leveldb_create_snapshot(db); + } + + public void Dispose() + { + if (handle != IntPtr.Zero) + { + Native.leveldb_release_snapshot(db, handle); + handle = IntPtr.Zero; + } + } + } +} diff --git a/src/Plugins/LevelDBStore/IO/Data/LevelDB/WriteBatch.cs b/src/Plugins/LevelDBStore/IO/Data/LevelDB/WriteBatch.cs new file mode 100644 index 0000000000..ad82dad450 --- /dev/null +++ b/src/Plugins/LevelDBStore/IO/Data/LevelDB/WriteBatch.cs @@ -0,0 +1,40 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// WriteBatch.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; + +namespace Neo.IO.Data.LevelDB +{ + public class WriteBatch + { + internal readonly IntPtr handle = Native.leveldb_writebatch_create(); + + ~WriteBatch() + { + Native.leveldb_writebatch_destroy(handle); + } + + public void Clear() + { + Native.leveldb_writebatch_clear(handle); + } + + public void Delete(byte[] key) + { + Native.leveldb_writebatch_delete(handle, key, (UIntPtr)key.Length); + } + + public void Put(byte[] key, byte[] value) + { + Native.leveldb_writebatch_put(handle, key, (UIntPtr)key.Length, value, (UIntPtr)value.Length); + } + } +} diff --git a/src/Plugins/LevelDBStore/IO/Data/LevelDB/WriteOptions.cs b/src/Plugins/LevelDBStore/IO/Data/LevelDB/WriteOptions.cs new file mode 100644 index 0000000000..48915ba480 --- /dev/null +++ b/src/Plugins/LevelDBStore/IO/Data/LevelDB/WriteOptions.cs @@ -0,0 +1,36 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// WriteOptions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; + +namespace Neo.IO.Data.LevelDB +{ + public class WriteOptions + { + public static readonly WriteOptions Default = new WriteOptions(); + public static readonly WriteOptions SyncWrite = new WriteOptions { Sync = true }; + + internal readonly IntPtr handle = Native.leveldb_writeoptions_create(); + + public bool Sync + { + set + { + Native.leveldb_writeoptions_set_sync(handle, value); + } + } + + ~WriteOptions() + { + Native.leveldb_writeoptions_destroy(handle); + } + } +} diff --git a/src/Plugins/LevelDBStore/LevelDBStore.csproj b/src/Plugins/LevelDBStore/LevelDBStore.csproj new file mode 100644 index 0000000000..ba82156b18 --- /dev/null +++ b/src/Plugins/LevelDBStore/LevelDBStore.csproj @@ -0,0 +1,10 @@ + + + + net8.0 + Neo.Plugins.Storage.LevelDBStore + Neo.Plugins.Storage + true + + + diff --git a/src/Plugins/LevelDBStore/Plugins/Storage/LevelDBStore.cs b/src/Plugins/LevelDBStore/Plugins/Storage/LevelDBStore.cs new file mode 100644 index 0000000000..9c676e8a7f --- /dev/null +++ b/src/Plugins/LevelDBStore/Plugins/Storage/LevelDBStore.cs @@ -0,0 +1,35 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// LevelDBStore.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.IO.Data.LevelDB; +using Neo.Persistence; +using System; +using System.Linq; + +namespace Neo.Plugins.Storage +{ + public class LevelDBStore : Plugin, IStoreProvider + { + public override string Description => "Uses LevelDB to store the blockchain data"; + + public LevelDBStore() + { + StoreFactory.RegisterProvider(this); + } + + public IStore GetStore(string path) + { + if (Environment.CommandLine.Split(' ').Any(p => p == "/repair" || p == "--repair")) + DB.Repair(path, Options.Default); + return new Store(path); + } + } +} diff --git a/src/Plugins/LevelDBStore/Plugins/Storage/Snapshot.cs b/src/Plugins/LevelDBStore/Plugins/Storage/Snapshot.cs new file mode 100644 index 0000000000..0b0a63b885 --- /dev/null +++ b/src/Plugins/LevelDBStore/Plugins/Storage/Snapshot.cs @@ -0,0 +1,69 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Snapshot.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.IO.Data.LevelDB; +using Neo.Persistence; +using System.Collections.Generic; +using LSnapshot = Neo.IO.Data.LevelDB.Snapshot; + +namespace Neo.Plugins.Storage +{ + internal class Snapshot : ISnapshot + { + private readonly DB db; + private readonly LSnapshot snapshot; + private readonly ReadOptions options; + private readonly WriteBatch batch; + + public Snapshot(DB db) + { + this.db = db; + snapshot = db.GetSnapshot(); + options = new ReadOptions { FillCache = false, Snapshot = snapshot }; + batch = new WriteBatch(); + } + + public void Commit() + { + db.Write(WriteOptions.Default, batch); + } + + public void Delete(byte[] key) + { + batch.Delete(key); + } + + public void Dispose() + { + snapshot.Dispose(); + } + + public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[] prefix, SeekDirection direction = SeekDirection.Forward) + { + return db.Seek(options, prefix, direction, (k, v) => (k, v)); + } + + public void Put(byte[] key, byte[] value) + { + batch.Put(key, value); + } + + public bool Contains(byte[] key) + { + return db.Contains(options, key); + } + + public byte[] TryGet(byte[] key) + { + return db.Get(options, key); + } + } +} diff --git a/src/Plugins/LevelDBStore/Plugins/Storage/Store.cs b/src/Plugins/LevelDBStore/Plugins/Storage/Store.cs new file mode 100644 index 0000000000..27b12a8b64 --- /dev/null +++ b/src/Plugins/LevelDBStore/Plugins/Storage/Store.cs @@ -0,0 +1,67 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Store.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.IO.Data.LevelDB; +using Neo.Persistence; +using System.Collections.Generic; + +namespace Neo.Plugins.Storage +{ + internal class Store : IStore + { + private readonly DB db; + + public Store(string path) + { + db = DB.Open(path, new Options { CreateIfMissing = true, FilterPolicy = Native.leveldb_filterpolicy_create_bloom(15) }); + } + + public void Delete(byte[] key) + { + db.Delete(WriteOptions.Default, key); + } + + public void Dispose() + { + db.Dispose(); + } + + public IEnumerable<(byte[], byte[])> Seek(byte[] prefix, SeekDirection direction = SeekDirection.Forward) + { + return db.Seek(ReadOptions.Default, prefix, direction, (k, v) => (k, v)); + } + + public ISnapshot GetSnapshot() + { + return new Snapshot(db); + } + + public void Put(byte[] key, byte[] value) + { + db.Put(WriteOptions.Default, key, value); + } + + public void PutSync(byte[] key, byte[] value) + { + db.Put(WriteOptions.SyncWrite, key, value); + } + + public bool Contains(byte[] key) + { + return db.Contains(ReadOptions.Default, key); + } + + public byte[] TryGet(byte[] key) + { + return db.Get(ReadOptions.Default, key); + } + } +} diff --git a/src/Plugins/MPTTrie/Cryptography/MPTTrie/Cache.cs b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Cache.cs new file mode 100644 index 0000000000..d8baef8529 --- /dev/null +++ b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Cache.cs @@ -0,0 +1,126 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Cache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.IO; +using Neo.Persistence; +using System.Collections.Generic; +using System.IO; + +namespace Neo.Cryptography.MPTTrie +{ + public class Cache + { + private enum TrackState : byte + { + None, + Added, + Changed, + Deleted + } + + private class Trackable + { + public Node Node; + public TrackState State; + } + + private readonly ISnapshot store; + private readonly byte prefix; + private readonly Dictionary cache = new Dictionary(); + + public Cache(ISnapshot store, byte prefix) + { + this.store = store; + this.prefix = prefix; + } + + private byte[] Key(UInt256 hash) + { + byte[] buffer = new byte[UInt256.Length + 1]; + using (MemoryStream ms = new MemoryStream(buffer, true)) + using (BinaryWriter writer = new BinaryWriter(ms)) + { + writer.Write(prefix); + hash.Serialize(writer); + } + return buffer; + } + + public Node Resolve(UInt256 hash) + { + if (cache.TryGetValue(hash, out Trackable t)) + { + return t.Node?.Clone(); + } + var n = store.TryGet(Key(hash))?.AsSerializable(); + cache.Add(hash, new Trackable + { + Node = n, + State = TrackState.None, + }); + return n?.Clone(); + } + + public void PutNode(Node np) + { + var n = Resolve(np.Hash); + if (n is null) + { + np.Reference = 1; + cache[np.Hash] = new Trackable + { + Node = np.Clone(), + State = TrackState.Added, + }; + return; + } + var entry = cache[np.Hash]; + entry.Node.Reference++; + entry.State = TrackState.Changed; + } + + public void DeleteNode(UInt256 hash) + { + var n = Resolve(hash); + if (n is null) return; + if (1 < n.Reference) + { + var entry = cache[hash]; + entry.Node.Reference--; + entry.State = TrackState.Changed; + return; + } + cache[hash] = new Trackable + { + Node = null, + State = TrackState.Deleted, + }; + } + + public void Commit() + { + foreach (var item in cache) + { + switch (item.Value.State) + { + case TrackState.Added: + case TrackState.Changed: + store.Put(Key(item.Key), item.Value.Node.ToArray()); + break; + case TrackState.Deleted: + store.Delete(Key(item.Key)); + break; + } + } + cache.Clear(); + } + } +} diff --git a/src/Plugins/MPTTrie/Cryptography/MPTTrie/Helper.cs b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Helper.cs new file mode 100644 index 0000000000..5c93afd659 --- /dev/null +++ b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Helper.cs @@ -0,0 +1,28 @@ +// 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 System; + +namespace Neo.Cryptography.MPTTrie +{ + public static class Helper + { + public static int CompareTo(this ReadOnlySpan arr1, ReadOnlySpan arr2) + { + for (int i = 0; i < arr1.Length && i < arr2.Length; i++) + { + var r = arr1[i].CompareTo(arr2[i]); + if (r != 0) return r; + } + return arr2.Length < arr1.Length ? 1 : arr2.Length == arr1.Length ? 0 : -1; + } + } +} diff --git a/src/Plugins/MPTTrie/Cryptography/MPTTrie/Node.Branch.cs b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Node.Branch.cs new file mode 100644 index 0000000000..c8ff04dfc6 --- /dev/null +++ b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Node.Branch.cs @@ -0,0 +1,69 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Node.Branch.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.IO; +using System.IO; + +namespace Neo.Cryptography.MPTTrie +{ + partial class Node + { + public const int BranchChildCount = 17; + public Node[] Children; + + public static Node NewBranch() + { + var n = new Node + { + type = NodeType.BranchNode, + Reference = 1, + Children = new Node[BranchChildCount], + }; + for (int i = 0; i < BranchChildCount; i++) + { + n.Children[i] = new Node(); + } + return n; + } + + protected int BranchSize + { + get + { + int size = 0; + for (int i = 0; i < BranchChildCount; i++) + { + size += Children[i].SizeAsChild; + } + return size; + } + } + + private void SerializeBranch(BinaryWriter writer) + { + for (int i = 0; i < BranchChildCount; i++) + { + Children[i].SerializeAsChild(writer); + } + } + + private void DeserializeBranch(ref MemoryReader reader) + { + Children = new Node[BranchChildCount]; + for (int i = 0; i < BranchChildCount; i++) + { + var n = new Node(); + n.Deserialize(ref reader); + Children[i] = n; + } + } + } +} diff --git a/src/Plugins/MPTTrie/Cryptography/MPTTrie/Node.Extension.cs b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Node.Extension.cs new file mode 100644 index 0000000000..510db49250 --- /dev/null +++ b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Node.Extension.cs @@ -0,0 +1,55 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Node.Extension.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.IO; +using Neo.SmartContract; +using System; +using System.IO; + +namespace Neo.Cryptography.MPTTrie +{ + partial class Node + { + public const int MaxKeyLength = (ApplicationEngine.MaxStorageKeySize + sizeof(int)) * 2; + public ReadOnlyMemory Key; + public Node Next; + + public static Node NewExtension(byte[] key, Node next) + { + if (key is null || next is null) throw new ArgumentNullException(nameof(NewExtension)); + if (key.Length == 0) throw new InvalidOperationException(nameof(NewExtension)); + var n = new Node + { + type = NodeType.ExtensionNode, + Key = key, + Next = next, + Reference = 1, + }; + return n; + } + + protected int ExtensionSize => Key.GetVarSize() + Next.SizeAsChild; + + private void SerializeExtension(BinaryWriter writer) + { + writer.WriteVarBytes(Key.Span); + Next.SerializeAsChild(writer); + } + + private void DeserializeExtension(ref MemoryReader reader) + { + Key = reader.ReadVarMemory(); + var n = new Node(); + n.Deserialize(ref reader); + Next = n; + } + } +} diff --git a/src/Plugins/MPTTrie/Cryptography/MPTTrie/Node.Hash.cs b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Node.Hash.cs new file mode 100644 index 0000000000..e0190dd146 --- /dev/null +++ b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Node.Hash.cs @@ -0,0 +1,43 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Node.Hash.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.IO; +using System; +using System.IO; + +namespace Neo.Cryptography.MPTTrie +{ + partial class Node + { + public static Node NewHash(UInt256 hash) + { + if (hash is null) throw new ArgumentNullException(nameof(NewHash)); + var n = new Node + { + type = NodeType.HashNode, + hash = hash, + }; + return n; + } + + protected int HashSize => hash.Size; + + private void SerializeHash(BinaryWriter writer) + { + writer.Write(hash); + } + + private void DeserializeHash(ref MemoryReader reader) + { + hash = reader.ReadSerializable(); + } + } +} diff --git a/src/Plugins/MPTTrie/Cryptography/MPTTrie/Node.Leaf.cs b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Node.Leaf.cs new file mode 100644 index 0000000000..024a07f8c8 --- /dev/null +++ b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Node.Leaf.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Node.Leaf.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.IO; +using Neo.SmartContract; +using System; +using System.IO; + +namespace Neo.Cryptography.MPTTrie +{ + partial class Node + { + public const int MaxValueLength = 3 + ApplicationEngine.MaxStorageValueSize + sizeof(bool); + public ReadOnlyMemory Value; + + public static Node NewLeaf(byte[] value) + { + if (value is null) throw new ArgumentNullException(nameof(value)); + var n = new Node + { + type = NodeType.LeafNode, + Value = value, + Reference = 1, + }; + return n; + } + + protected int LeafSize => Value.GetVarSize(); + + private void SerializeLeaf(BinaryWriter writer) + { + writer.WriteVarBytes(Value.Span); + } + + private void DeserializeLeaf(ref MemoryReader reader) + { + Value = reader.ReadVarMemory(); + } + } +} diff --git a/src/Plugins/MPTTrie/Cryptography/MPTTrie/Node.cs b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Node.cs new file mode 100644 index 0000000000..ef45548645 --- /dev/null +++ b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Node.cs @@ -0,0 +1,230 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Node.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.IO; +using System; +using System.IO; + +namespace Neo.Cryptography.MPTTrie +{ + public partial class Node : ISerializable + { + private NodeType type; + private UInt256 hash; + public int Reference; + public UInt256 Hash => hash ??= new UInt256(Crypto.Hash256(ToArrayWithoutReference())); + public NodeType Type => type; + public bool IsEmpty => type == NodeType.Empty; + public int Size + { + get + { + int size = sizeof(NodeType); + switch (type) + { + case NodeType.BranchNode: + return size + BranchSize + IO.Helper.GetVarSize(Reference); + case NodeType.ExtensionNode: + return size + ExtensionSize + IO.Helper.GetVarSize(Reference); + case NodeType.LeafNode: + return size + LeafSize + IO.Helper.GetVarSize(Reference); + case NodeType.HashNode: + return size + HashSize; + case NodeType.Empty: + return size; + default: + throw new InvalidOperationException($"{nameof(Node)} Cannt get size, unsupport type"); + }; + } + } + + public Node() + { + type = NodeType.Empty; + } + + public void SetDirty() + { + hash = null; + } + + public int SizeAsChild + { + get + { + switch (type) + { + case NodeType.BranchNode: + case NodeType.ExtensionNode: + case NodeType.LeafNode: + return NewHash(Hash).Size; + case NodeType.HashNode: + case NodeType.Empty: + return Size; + default: + throw new InvalidOperationException(nameof(Node)); + } + } + } + + public void SerializeAsChild(BinaryWriter writer) + { + switch (type) + { + case NodeType.BranchNode: + case NodeType.ExtensionNode: + case NodeType.LeafNode: + var n = NewHash(Hash); + n.Serialize(writer); + break; + case NodeType.HashNode: + case NodeType.Empty: + Serialize(writer); + break; + default: + throw new FormatException(nameof(SerializeAsChild)); + } + } + + private void SerializeWithoutReference(BinaryWriter writer) + { + writer.Write((byte)type); + switch (type) + { + case NodeType.BranchNode: + SerializeBranch(writer); + break; + case NodeType.ExtensionNode: + SerializeExtension(writer); + break; + case NodeType.LeafNode: + SerializeLeaf(writer); + break; + case NodeType.HashNode: + SerializeHash(writer); + break; + case NodeType.Empty: + break; + default: + throw new FormatException(nameof(SerializeWithoutReference)); + } + } + + public void Serialize(BinaryWriter writer) + { + SerializeWithoutReference(writer); + if (type == NodeType.BranchNode || type == NodeType.ExtensionNode || type == NodeType.LeafNode) + writer.WriteVarInt(Reference); + } + + public byte[] ToArrayWithoutReference() + { + using MemoryStream ms = new MemoryStream(); + using BinaryWriter writer = new BinaryWriter(ms, Utility.StrictUTF8, true); + + SerializeWithoutReference(writer); + writer.Flush(); + return ms.ToArray(); + } + + public void Deserialize(ref MemoryReader reader) + { + type = (NodeType)reader.ReadByte(); + switch (type) + { + case NodeType.BranchNode: + DeserializeBranch(ref reader); + Reference = (int)reader.ReadVarInt(); + break; + case NodeType.ExtensionNode: + DeserializeExtension(ref reader); + Reference = (int)reader.ReadVarInt(); + break; + case NodeType.LeafNode: + DeserializeLeaf(ref reader); + Reference = (int)reader.ReadVarInt(); + break; + case NodeType.Empty: + break; + case NodeType.HashNode: + DeserializeHash(ref reader); + break; + default: + throw new FormatException(nameof(Deserialize)); + } + } + + private Node CloneAsChild() + { + switch (type) + { + case NodeType.BranchNode: + case NodeType.ExtensionNode: + case NodeType.LeafNode: + return new Node + { + type = NodeType.HashNode, + hash = Hash, + }; + case NodeType.HashNode: + case NodeType.Empty: + return Clone(); + default: + throw new InvalidOperationException(nameof(Clone)); + } + } + + public Node Clone() + { + switch (type) + { + case NodeType.BranchNode: + var n = new Node + { + type = type, + Reference = Reference, + Children = new Node[BranchChildCount], + }; + for (int i = 0; i < BranchChildCount; i++) + { + n.Children[i] = Children[i].CloneAsChild(); + } + return n; + case NodeType.ExtensionNode: + return new Node + { + type = type, + Key = Key, + Next = Next.CloneAsChild(), + Reference = Reference, + }; + case NodeType.LeafNode: + return new Node + { + type = type, + Value = Value, + Reference = Reference, + }; + case NodeType.HashNode: + case NodeType.Empty: + return this; + default: + throw new InvalidOperationException(nameof(Clone)); + } + } + + public void FromReplica(Node n) + { + MemoryReader reader = new(n.ToArray()); + Deserialize(ref reader); + } + } +} diff --git a/src/Plugins/MPTTrie/Cryptography/MPTTrie/NodeType.cs b/src/Plugins/MPTTrie/Cryptography/MPTTrie/NodeType.cs new file mode 100644 index 0000000000..9c676ff2f7 --- /dev/null +++ b/src/Plugins/MPTTrie/Cryptography/MPTTrie/NodeType.cs @@ -0,0 +1,22 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// NodeType.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.Cryptography.MPTTrie +{ + public enum NodeType : byte + { + BranchNode = 0x00, + ExtensionNode = 0x01, + LeafNode = 0x02, + HashNode = 0x03, + Empty = 0x04 + } +} diff --git a/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.Delete.cs b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.Delete.cs new file mode 100644 index 0000000000..2041100a14 --- /dev/null +++ b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.Delete.cs @@ -0,0 +1,136 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Trie.Delete.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Collections.Generic; +using static Neo.Helper; + +namespace Neo.Cryptography.MPTTrie +{ + partial class Trie + { + public bool Delete(byte[] key) + { + var path = ToNibbles(key); + if (path.Length == 0) + throw new ArgumentException("could not be empty", nameof(key)); + if (path.Length > Node.MaxKeyLength) + throw new ArgumentException("exceeds limit", nameof(key)); + return TryDelete(ref root, path); + } + + private bool TryDelete(ref Node node, ReadOnlySpan path) + { + switch (node.Type) + { + case NodeType.LeafNode: + { + if (path.IsEmpty) + { + if (!full) cache.DeleteNode(node.Hash); + node = new Node(); + return true; + } + return false; + } + case NodeType.ExtensionNode: + { + if (path.StartsWith(node.Key.Span)) + { + var oldHash = node.Hash; + var result = TryDelete(ref node.Next, path[node.Key.Length..]); + if (!result) return false; + if (!full) cache.DeleteNode(oldHash); + if (node.Next.IsEmpty) + { + node = node.Next; + return true; + } + if (node.Next.Type == NodeType.ExtensionNode) + { + if (!full) cache.DeleteNode(node.Next.Hash); + node.Key = Concat(node.Key.Span, node.Next.Key.Span); + node.Next = node.Next.Next; + } + node.SetDirty(); + cache.PutNode(node); + return true; + } + return false; + } + case NodeType.BranchNode: + { + bool result; + var oldHash = node.Hash; + if (path.IsEmpty) + { + result = TryDelete(ref node.Children[Node.BranchChildCount - 1], path); + } + else + { + result = TryDelete(ref node.Children[path[0]], path[1..]); + } + if (!result) return false; + if (!full) cache.DeleteNode(oldHash); + List childrenIndexes = new List(Node.BranchChildCount); + for (int i = 0; i < Node.BranchChildCount; i++) + { + if (node.Children[i].IsEmpty) continue; + childrenIndexes.Add((byte)i); + } + if (childrenIndexes.Count > 1) + { + node.SetDirty(); + cache.PutNode(node); + return true; + } + var lastChildIndex = childrenIndexes[0]; + var lastChild = node.Children[lastChildIndex]; + if (lastChildIndex == Node.BranchChildCount - 1) + { + node = lastChild; + return true; + } + if (lastChild.Type == NodeType.HashNode) + { + lastChild = cache.Resolve(lastChild.Hash); + if (lastChild is null) throw new InvalidOperationException("Internal error, can't resolve hash"); + } + if (lastChild.Type == NodeType.ExtensionNode) + { + if (!full) cache.DeleteNode(lastChild.Hash); + lastChild.Key = Concat(childrenIndexes.ToArray(), lastChild.Key.Span); + lastChild.SetDirty(); + cache.PutNode(lastChild); + node = lastChild; + return true; + } + node = Node.NewExtension(childrenIndexes.ToArray(), lastChild); + cache.PutNode(node); + return true; + } + case NodeType.Empty: + { + return false; + } + case NodeType.HashNode: + { + var newNode = cache.Resolve(node.Hash); + if (newNode is null) throw new InvalidOperationException("Internal error, can't resolve hash when mpt delete"); + node = newNode; + return TryDelete(ref node, path); + } + default: + return false; + } + } + } +} diff --git a/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.Find.cs b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.Find.cs new file mode 100644 index 0000000000..b3922e8ce8 --- /dev/null +++ b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.Find.cs @@ -0,0 +1,170 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Trie.Find.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Collections.Generic; +using System.Linq; +using static Neo.Helper; + +namespace Neo.Cryptography.MPTTrie +{ + partial class Trie + { + private ReadOnlySpan Seek(ref Node node, ReadOnlySpan path, out Node start) + { + switch (node.Type) + { + case NodeType.LeafNode: + { + if (path.IsEmpty) + { + start = node; + return ReadOnlySpan.Empty; + } + break; + } + case NodeType.Empty: + break; + case NodeType.HashNode: + { + var newNode = cache.Resolve(node.Hash); + if (newNode is null) throw new InvalidOperationException("Internal error, can't resolve hash when mpt seek"); + node = newNode; + return Seek(ref node, path, out start); + } + case NodeType.BranchNode: + { + if (path.IsEmpty) + { + start = node; + return ReadOnlySpan.Empty; + } + return Concat(path[..1], Seek(ref node.Children[path[0]], path[1..], out start)); + } + case NodeType.ExtensionNode: + { + if (path.IsEmpty) + { + start = node.Next; + return node.Key.Span; + } + if (path.StartsWith(node.Key.Span)) + { + return Concat(node.Key.Span, Seek(ref node.Next, path[node.Key.Length..], out start)); + } + if (node.Key.Span.StartsWith(path)) + { + start = node.Next; + return node.Key.Span; + } + break; + } + } + start = null; + return ReadOnlySpan.Empty; + } + + public IEnumerable<(ReadOnlyMemory Key, ReadOnlyMemory Value)> Find(ReadOnlySpan prefix, byte[] from = null) + { + var path = ToNibbles(prefix); + int offset = 0; + if (from is null) from = Array.Empty(); + if (0 < from.Length) + { + if (!from.AsSpan().StartsWith(prefix)) + throw new InvalidOperationException("invalid from key"); + from = ToNibbles(from.AsSpan()); + } + if (path.Length > Node.MaxKeyLength || from.Length > Node.MaxKeyLength) + throw new ArgumentException("exceeds limit"); + path = Seek(ref root, path, out Node start).ToArray(); + if (from.Length > 0) + { + for (int i = 0; i < from.Length && i < path.Length; i++) + { + if (path[i] < from[i]) return Enumerable.Empty<(ReadOnlyMemory, ReadOnlyMemory)>(); + if (path[i] > from[i]) + { + offset = from.Length; + break; + } + } + if (offset == 0) + { + offset = Math.Min(path.Length, from.Length); + } + } + return Travers(start, path, from, offset).Select(p => (new ReadOnlyMemory(FromNibbles(p.Key.Span)), p.Value)); + } + + private IEnumerable<(ReadOnlyMemory Key, ReadOnlyMemory Value)> Travers(Node node, byte[] path, byte[] from, int offset) + { + if (node is null) yield break; + if (offset < 0) throw new InvalidOperationException("invalid offset"); + switch (node.Type) + { + case NodeType.LeafNode: + { + if (from.Length <= offset && !path.SequenceEqual(from)) + yield return (path, node.Value); + break; + } + case NodeType.Empty: + break; + case NodeType.HashNode: + { + var newNode = cache.Resolve(node.Hash); + if (newNode is null) throw new InvalidOperationException("Internal error, can't resolve hash when mpt find"); + node = newNode; + foreach (var item in Travers(node, path, from, offset)) + yield return item; + break; + } + case NodeType.BranchNode: + { + if (offset < from.Length) + { + for (int i = 0; i < Node.BranchChildCount - 1; i++) + { + if (from[offset] < i) + foreach (var item in Travers(node.Children[i], Concat(path, new byte[] { (byte)i }), from, from.Length)) + yield return item; + else if (i == from[offset]) + foreach (var item in Travers(node.Children[i], Concat(path, new byte[] { (byte)i }), from, offset + 1)) + yield return item; + } + } + else + { + foreach (var item in Travers(node.Children[Node.BranchChildCount - 1], path, from, offset)) + yield return item; + for (int i = 0; i < Node.BranchChildCount - 1; i++) + { + foreach (var item in Travers(node.Children[i], Concat(path, new byte[] { (byte)i }), from, offset)) + yield return item; + } + } + break; + } + case NodeType.ExtensionNode: + { + if (offset < from.Length && from.AsSpan()[offset..].StartsWith(node.Key.Span)) + foreach (var item in Travers(node.Next, Concat(path, node.Key.Span), from, offset + node.Key.Length)) + yield return item; + else if (from.Length <= offset || 0 < node.Key.Span.CompareTo(from.AsSpan(offset))) + foreach (var item in Travers(node.Next, Concat(path, node.Key.Span), from, from.Length)) + yield return item; + break; + } + } + } + } +} diff --git a/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.Get.cs b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.Get.cs new file mode 100644 index 0000000000..da69407ac3 --- /dev/null +++ b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.Get.cs @@ -0,0 +1,90 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Trie.Get.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Collections.Generic; + +namespace Neo.Cryptography.MPTTrie +{ + partial class Trie + { + public byte[] this[byte[] key] + { + get + { + var path = ToNibbles(key); + if (path.Length == 0) + throw new ArgumentException("could not be empty", nameof(key)); + if (path.Length > Node.MaxKeyLength) + throw new ArgumentException("exceeds limit", nameof(key)); + var result = TryGet(ref root, path, out var value); + return result ? value.ToArray() : throw new KeyNotFoundException(); + } + } + + public bool TryGetValue(byte[] key, out byte[] value) + { + value = default; + var path = ToNibbles(key); + if (path.Length == 0) + throw new ArgumentException("could not be empty", nameof(key)); + if (path.Length > Node.MaxKeyLength) + throw new ArgumentException("exceeds limit", nameof(key)); + var result = TryGet(ref root, path, out var val); + if (result) + value = val.ToArray(); + return result; + } + + private bool TryGet(ref Node node, ReadOnlySpan path, out ReadOnlySpan value) + { + switch (node.Type) + { + case NodeType.LeafNode: + { + if (path.IsEmpty) + { + value = node.Value.Span; + return true; + } + break; + } + case NodeType.Empty: + break; + case NodeType.HashNode: + { + var newNode = cache.Resolve(node.Hash); + if (newNode is null) throw new InvalidOperationException("Internal error, can't resolve hash when mpt get"); + node = newNode; + return TryGet(ref node, path, out value); + } + case NodeType.BranchNode: + { + if (path.IsEmpty) + { + return TryGet(ref node.Children[Node.BranchChildCount - 1], path, out value); + } + return TryGet(ref node.Children[path[0]], path[1..], out value); + } + case NodeType.ExtensionNode: + { + if (path.StartsWith(node.Key.Span)) + { + return TryGet(ref node.Next, path[node.Key.Length..], out value); + } + break; + } + } + value = default; + return false; + } + } +} diff --git a/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.Proof.cs b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.Proof.cs new file mode 100644 index 0000000000..e0925452e3 --- /dev/null +++ b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.Proof.cs @@ -0,0 +1,95 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Trie.Proof.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.IO; +using Neo.Persistence; +using System; +using System.Collections.Generic; +using static Neo.Helper; + +namespace Neo.Cryptography.MPTTrie +{ + partial class Trie + { + public bool TryGetProof(byte[] key, out HashSet proof) + { + var path = ToNibbles(key); + if (path.Length == 0) + throw new ArgumentException("could not be empty", nameof(key)); + if (path.Length > Node.MaxKeyLength) + throw new ArgumentException("exceeds limit", nameof(key)); + proof = new HashSet(ByteArrayEqualityComparer.Default); + return GetProof(ref root, path, proof); + } + + private bool GetProof(ref Node node, ReadOnlySpan path, HashSet set) + { + switch (node.Type) + { + case NodeType.LeafNode: + { + if (path.IsEmpty) + { + set.Add(node.ToArrayWithoutReference()); + return true; + } + break; + } + case NodeType.Empty: + break; + case NodeType.HashNode: + { + var newNode = cache.Resolve(node.Hash); + if (newNode is null) throw new InvalidOperationException("Internal error, can't resolve hash when mpt getproof"); + node = newNode; + return GetProof(ref node, path, set); + } + case NodeType.BranchNode: + { + set.Add(node.ToArrayWithoutReference()); + if (path.IsEmpty) + { + return GetProof(ref node.Children[Node.BranchChildCount - 1], path, set); + } + return GetProof(ref node.Children[path[0]], path[1..], set); + } + case NodeType.ExtensionNode: + { + if (path.StartsWith(node.Key.Span)) + { + set.Add(node.ToArrayWithoutReference()); + return GetProof(ref node.Next, path[node.Key.Length..], set); + } + break; + } + } + return false; + } + + private static byte[] Key(byte[] hash) + { + byte[] buffer = new byte[hash.Length + 1]; + buffer[0] = Prefix; + Buffer.BlockCopy(hash, 0, buffer, 1, hash.Length); + return buffer; + } + + public static byte[] VerifyProof(UInt256 root, byte[] key, HashSet proof) + { + using var memoryStore = new MemoryStore(); + foreach (byte[] data in proof) + memoryStore.Put(Key(Crypto.Hash256(data)), Concat(data, new byte[] { 1 })); + using ISnapshot snapshot = memoryStore.GetSnapshot(); + var trie = new Trie(snapshot, root, false); + return trie[key]; + } + } +} diff --git a/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.Put.cs b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.Put.cs new file mode 100644 index 0000000000..5de6f3fc85 --- /dev/null +++ b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.Put.cs @@ -0,0 +1,159 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Trie.Put.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; + +namespace Neo.Cryptography.MPTTrie +{ + partial class Trie + { + private static ReadOnlySpan CommonPrefix(ReadOnlySpan a, ReadOnlySpan b) + { + var minLen = a.Length <= b.Length ? a.Length : b.Length; + int i = 0; + if (a.Length != 0 && b.Length != 0) + { + for (i = 0; i < minLen; i++) + { + if (a[i] != b[i]) break; + } + } + return a[..i]; + } + + public void Put(byte[] key, byte[] value) + { + var path = ToNibbles(key); + var val = value; + if (path.Length == 0 || path.Length > Node.MaxKeyLength) + throw new ArgumentException("invalid", nameof(key)); + if (val.Length > Node.MaxValueLength) + throw new ArgumentException("exceed limit", nameof(value)); + var n = Node.NewLeaf(val); + Put(ref root, path, n); + } + + private void Put(ref Node node, ReadOnlySpan path, Node val) + { + switch (node.Type) + { + case NodeType.LeafNode: + { + if (path.IsEmpty) + { + if (!full) cache.DeleteNode(node.Hash); + node = val; + cache.PutNode(node); + return; + } + var branch = Node.NewBranch(); + branch.Children[Node.BranchChildCount - 1] = node; + Put(ref branch.Children[path[0]], path[1..], val); + cache.PutNode(branch); + node = branch; + break; + } + case NodeType.ExtensionNode: + { + if (path.StartsWith(node.Key.Span)) + { + var oldHash = node.Hash; + Put(ref node.Next, path[node.Key.Length..], val); + if (!full) cache.DeleteNode(oldHash); + node.SetDirty(); + cache.PutNode(node); + return; + } + if (!full) cache.DeleteNode(node.Hash); + var prefix = CommonPrefix(node.Key.Span, path); + var pathRemain = path[prefix.Length..]; + var keyRemain = node.Key.Span[prefix.Length..]; + var child = Node.NewBranch(); + Node grandChild = new Node(); + if (keyRemain.Length == 1) + { + child.Children[keyRemain[0]] = node.Next; + } + else + { + var exNode = Node.NewExtension(keyRemain[1..].ToArray(), node.Next); + cache.PutNode(exNode); + child.Children[keyRemain[0]] = exNode; + } + if (pathRemain.IsEmpty) + { + Put(ref grandChild, pathRemain, val); + child.Children[Node.BranchChildCount - 1] = grandChild; + } + else + { + Put(ref grandChild, pathRemain[1..], val); + child.Children[pathRemain[0]] = grandChild; + } + cache.PutNode(child); + if (prefix.Length > 0) + { + var exNode = Node.NewExtension(prefix.ToArray(), child); + cache.PutNode(exNode); + node = exNode; + } + else + { + node = child; + } + break; + } + case NodeType.BranchNode: + { + var oldHash = node.Hash; + if (path.IsEmpty) + { + Put(ref node.Children[Node.BranchChildCount - 1], path, val); + } + else + { + Put(ref node.Children[path[0]], path[1..], val); + } + if (!full) cache.DeleteNode(oldHash); + node.SetDirty(); + cache.PutNode(node); + break; + } + case NodeType.Empty: + { + Node newNode; + if (path.IsEmpty) + { + newNode = val; + } + else + { + newNode = Node.NewExtension(path.ToArray(), val); + cache.PutNode(newNode); + } + node = newNode; + if (val.Type == NodeType.LeafNode) cache.PutNode(val); + break; + } + case NodeType.HashNode: + { + Node newNode = cache.Resolve(node.Hash); + if (newNode is null) throw new InvalidOperationException("Internal error, can't resolve hash when mpt put"); + node = newNode; + Put(ref node, path, val); + break; + } + default: + throw new InvalidOperationException("unsupport node type"); + } + } + } +} diff --git a/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.cs b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.cs new file mode 100644 index 0000000000..19ef1c8b4c --- /dev/null +++ b/src/Plugins/MPTTrie/Cryptography/MPTTrie/Trie.cs @@ -0,0 +1,62 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Trie.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; +using System; + +namespace Neo.Cryptography.MPTTrie +{ + public partial class Trie + { + private const byte Prefix = 0xf0; + private readonly bool full; + private readonly ISnapshot store; + private Node root; + private readonly Cache cache; + public Node Root => root; + + public Trie(ISnapshot store, UInt256 root, bool full_state = false) + { + this.store = store ?? throw new ArgumentNullException(nameof(store)); + cache = new Cache(store, Prefix); + this.root = root is null ? new Node() : Node.NewHash(root); + full = full_state; + } + + private static byte[] ToNibbles(ReadOnlySpan path) + { + var result = new byte[path.Length * 2]; + for (int i = 0; i < path.Length; i++) + { + result[i * 2] = (byte)(path[i] >> 4); + result[i * 2 + 1] = (byte)(path[i] & 0x0F); + } + return result; + } + + private static byte[] FromNibbles(ReadOnlySpan path) + { + if (path.Length % 2 != 0) throw new FormatException($"MPTTrie.FromNibbles invalid path."); + var key = new byte[path.Length / 2]; + for (int i = 0; i < key.Length; i++) + { + key[i] = (byte)(path[i * 2] << 4); + key[i] |= path[i * 2 + 1]; + } + return key; + } + + public void Commit() + { + cache.Commit(); + } + } +} diff --git a/src/Plugins/MPTTrie/IO/ByteArrayEqualityComparer.cs b/src/Plugins/MPTTrie/IO/ByteArrayEqualityComparer.cs new file mode 100644 index 0000000000..590306d560 --- /dev/null +++ b/src/Plugins/MPTTrie/IO/ByteArrayEqualityComparer.cs @@ -0,0 +1,58 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// ByteArrayEqualityComparer.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Collections.Generic; + +namespace Neo.IO +{ + public class ByteArrayEqualityComparer : IEqualityComparer + { + public static readonly ByteArrayEqualityComparer Default = new ByteArrayEqualityComparer(); + + public unsafe bool Equals(byte[] x, byte[] y) + { + if (ReferenceEquals(x, y)) return true; + if (x is null || y is null) return false; + int len = x.Length; + if (len != y.Length) return false; + if (len == 0) return true; + fixed (byte* xp = x, yp = y) + { + long* xlp = (long*)xp, ylp = (long*)yp; + for (; len >= 8; len -= 8) + { + if (*xlp != *ylp) return false; + xlp++; + ylp++; + } + byte* xbp = (byte*)xlp, ybp = (byte*)ylp; + for (; len > 0; len--) + { + if (*xbp != *ybp) return false; + xbp++; + ybp++; + } + } + return true; + } + + public int GetHashCode(byte[] obj) + { + unchecked + { + int hash = 17; + foreach (byte element in obj) + hash = hash * 31 + element; + return hash; + } + } + } +} diff --git a/src/Plugins/MPTTrie/MPTTrie.csproj b/src/Plugins/MPTTrie/MPTTrie.csproj new file mode 100644 index 0000000000..a2c3377a16 --- /dev/null +++ b/src/Plugins/MPTTrie/MPTTrie.csproj @@ -0,0 +1,10 @@ + + + + net8.0 + Neo.Cryptography.MPT + Neo.Cryptography + true + + + diff --git a/src/Plugins/OracleService/Helper.cs b/src/Plugins/OracleService/Helper.cs new file mode 100644 index 0000000000..35611e8698 --- /dev/null +++ b/src/Plugins/OracleService/Helper.cs @@ -0,0 +1,52 @@ +// 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 System.Linq; +using System.Net; + +namespace Neo.Plugins +{ + static class Helper + { + public static bool IsInternal(this IPHostEntry entry) + { + return entry.AddressList.Any(p => IsInternal(p)); + } + + /// + /// ::1 - IPv6 loopback + /// 10.0.0.0 - 10.255.255.255 (10/8 prefix) + /// 127.0.0.0 - 127.255.255.255 (127/8 prefix) + /// 172.16.0.0 - 172.31.255.255 (172.16/12 prefix) + /// 192.168.0.0 - 192.168.255.255 (192.168/16 prefix) + /// + /// Address + /// True if it was an internal address + public static bool IsInternal(this IPAddress ipAddress) + { + if (IPAddress.IsLoopback(ipAddress)) return true; + if (IPAddress.Broadcast.Equals(ipAddress)) return true; + if (IPAddress.Any.Equals(ipAddress)) return true; + if (IPAddress.IPv6Any.Equals(ipAddress)) return true; + if (IPAddress.IPv6Loopback.Equals(ipAddress)) return true; + + var ip = ipAddress.GetAddressBytes(); + switch (ip[0]) + { + case 10: + case 127: return true; + case 172: return ip[1] >= 16 && ip[1] < 32; + case 192: return ip[1] == 168; + default: return false; + } + } + } +} diff --git a/src/Plugins/OracleService/OracleService.cs b/src/Plugins/OracleService/OracleService.cs new file mode 100644 index 0000000000..637c06712f --- /dev/null +++ b/src/Plugins/OracleService/OracleService.cs @@ -0,0 +1,588 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// OracleService.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Akka.Actor; +using Akka.Util.Internal; +using Neo.ConsoleService; +using Neo.Cryptography; +using Neo.Cryptography.ECC; +using Neo.IO; +using Neo.Json; +using Neo.Ledger; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Manifest; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.Wallets; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Neo.Plugins +{ + public class OracleService : Plugin + { + private const int RefreshIntervalMilliSeconds = 1000 * 60 * 3; + + private static readonly HttpClient httpClient = new() + { + Timeout = TimeSpan.FromSeconds(5), + MaxResponseContentBufferSize = ushort.MaxValue + }; + + private Wallet wallet; + private readonly ConcurrentDictionary pendingQueue = new ConcurrentDictionary(); + private readonly ConcurrentDictionary finishedCache = new ConcurrentDictionary(); + private Timer timer; + private readonly CancellationTokenSource cancelSource = new CancellationTokenSource(); + private OracleStatus status = OracleStatus.Unstarted; + private IWalletProvider walletProvider; + private int counter; + private NeoSystem _system; + + private readonly Dictionary protocols = new Dictionary(); + + public override string Description => "Built-in oracle plugin"; + + public override string ConfigFile => System.IO.Path.Combine(RootPath, "OracleService.json"); + + public OracleService() + { + Blockchain.Committing += OnCommitting; + } + + protected override void Configure() + { + Settings.Load(GetConfiguration()); + foreach (var (_, p) in protocols) + p.Configure(); + } + + protected override void OnSystemLoaded(NeoSystem system) + { + if (system.Settings.Network != Settings.Default.Network) return; + _system = system; + _system.ServiceAdded += NeoSystem_ServiceAdded; + RpcServerPlugin.RegisterMethods(this, Settings.Default.Network); + } + + private void NeoSystem_ServiceAdded(object sender, object service) + { + if (service is IWalletProvider) + { + walletProvider = service as IWalletProvider; + _system.ServiceAdded -= NeoSystem_ServiceAdded; + if (Settings.Default.AutoStart) + { + walletProvider.WalletChanged += WalletProvider_WalletChanged; + } + } + } + + private void WalletProvider_WalletChanged(object sender, Wallet wallet) + { + walletProvider.WalletChanged -= WalletProvider_WalletChanged; + Start(wallet); + } + + public override void Dispose() + { + Blockchain.Committing -= OnCommitting; + OnStop(); + while (status != OracleStatus.Stopped) + Thread.Sleep(100); + foreach (var p in protocols) + p.Value.Dispose(); + } + + [ConsoleCommand("start oracle", Category = "Oracle", Description = "Start oracle service")] + private void OnStart() + { + Start(walletProvider?.GetWallet()); + } + + public void Start(Wallet wallet) + { + if (status == OracleStatus.Running) return; + + if (wallet is null) + { + ConsoleHelper.Warning("Please open wallet first!"); + return; + } + + if (!CheckOracleAvaiblable(_system.StoreView, out ECPoint[] oracles)) + { + ConsoleHelper.Warning("The oracle service is unavailable"); + return; + } + if (!CheckOracleAccount(wallet, oracles)) + { + ConsoleHelper.Warning("There is no oracle account in wallet"); + return; + } + + this.wallet = wallet; + protocols["https"] = new OracleHttpsProtocol(); + protocols["neofs"] = new OracleNeoFSProtocol(wallet, oracles); + status = OracleStatus.Running; + timer = new Timer(OnTimer, null, RefreshIntervalMilliSeconds, Timeout.Infinite); + ConsoleHelper.Info($"Oracle started"); + ProcessRequestsAsync(); + } + + [ConsoleCommand("stop oracle", Category = "Oracle", Description = "Stop oracle service")] + private void OnStop() + { + cancelSource.Cancel(); + if (timer != null) + { + timer.Dispose(); + timer = null; + } + status = OracleStatus.Stopped; + } + + [ConsoleCommand("oracle status", Category = "Oracle", Description = "Show oracle status")] + private void OnShow() + { + ConsoleHelper.Info($"Oracle status: ", $"{status}"); + } + + private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + if (system.Settings.Network != Settings.Default.Network) return; + + if (Settings.Default.AutoStart && status == OracleStatus.Unstarted) + { + OnStart(); + } + if (status != OracleStatus.Running) return; + if (!CheckOracleAvaiblable(snapshot, out ECPoint[] oracles) || !CheckOracleAccount(wallet, oracles)) + OnStop(); + } + + private async void OnTimer(object state) + { + try + { + List outOfDate = new(); + List tasks = new(); + foreach (var (id, task) in pendingQueue) + { + var span = TimeProvider.Current.UtcNow - task.Timestamp; + if (span > Settings.Default.MaxTaskTimeout) + { + outOfDate.Add(id); + continue; + } + + if (span > TimeSpan.FromMilliseconds(RefreshIntervalMilliSeconds)) + { + foreach (var account in wallet.GetAccounts()) + if (task.BackupSigns.TryGetValue(account.GetKey().PublicKey, out byte[] sign)) + tasks.Add(SendResponseSignatureAsync(id, sign, account.GetKey())); + } + } + + await Task.WhenAll(tasks); + + foreach (ulong requestId in outOfDate) + pendingQueue.TryRemove(requestId, out _); + foreach (var (key, value) in finishedCache) + if (TimeProvider.Current.UtcNow - value > TimeSpan.FromDays(3)) + finishedCache.TryRemove(key, out _); + } + catch (Exception e) + { + Log(e, LogLevel.Error); + } + finally + { + if (!cancelSource.IsCancellationRequested) + timer?.Change(RefreshIntervalMilliSeconds, Timeout.Infinite); + } + } + + [RpcMethod] + public JObject SubmitOracleResponse(JArray _params) + { + status.Equals(OracleStatus.Running).True_Or(RpcError.OracleDisabled); + ECPoint oraclePub = ECPoint.DecodePoint(Convert.FromBase64String(_params[0].AsString()), ECCurve.Secp256r1); + ulong requestId = Result.Ok_Or(() => (ulong)_params[1].AsNumber(), RpcError.InvalidParams.WithData($"Invalid requestId: {_params[1]}")); + byte[] txSign = Result.Ok_Or(() => Convert.FromBase64String(_params[2].AsString()), RpcError.InvalidParams.WithData($"Invalid txSign: {_params[2]}")); + byte[] msgSign = Result.Ok_Or(() => Convert.FromBase64String(_params[3].AsString()), RpcError.InvalidParams.WithData($"Invalid msgSign: {_params[3]}")); + + finishedCache.ContainsKey(requestId).False_Or(RpcError.OracleRequestFinished); + + using (var snapshot = _system.GetSnapshot()) + { + uint height = NativeContract.Ledger.CurrentIndex(snapshot) + 1; + var oracles = NativeContract.RoleManagement.GetDesignatedByRole(snapshot, Role.Oracle, height); + oracles.Any(p => p.Equals(oraclePub)).True_Or(RpcErrorFactory.OracleNotDesignatedNode(oraclePub)); + NativeContract.Oracle.GetRequest(snapshot, requestId).NotNull_Or(RpcError.OracleRequestNotFound); + var data = Neo.Helper.Concat(oraclePub.ToArray(), BitConverter.GetBytes(requestId), txSign); + Crypto.VerifySignature(data, msgSign, oraclePub).True_Or(RpcErrorFactory.InvalidSignature($"Invalid oracle response transaction signature from '{oraclePub}'.")); + AddResponseTxSign(snapshot, requestId, oraclePub, txSign); + } + return new JObject(); + } + + private static async Task SendContentAsync(Uri url, string content) + { + try + { + using HttpResponseMessage response = await httpClient.PostAsync(url, new StringContent(content, Encoding.UTF8, "application/json")); + response.EnsureSuccessStatusCode(); + } + catch (Exception e) + { + Log($"Failed to send the response signature to {url}, as {e.Message}", LogLevel.Warning); + } + } + + private async Task SendResponseSignatureAsync(ulong requestId, byte[] txSign, KeyPair keyPair) + { + var message = Neo.Helper.Concat(keyPair.PublicKey.ToArray(), BitConverter.GetBytes(requestId), txSign); + var sign = Crypto.Sign(message, keyPair.PrivateKey); + var param = "\"" + Convert.ToBase64String(keyPair.PublicKey.ToArray()) + "\", " + requestId + ", \"" + Convert.ToBase64String(txSign) + "\",\"" + Convert.ToBase64String(sign) + "\""; + var content = "{\"id\":" + Interlocked.Increment(ref counter) + ",\"jsonrpc\":\"2.0\",\"method\":\"submitoracleresponse\",\"params\":[" + param + "]}"; + + var tasks = Settings.Default.Nodes.Select(p => SendContentAsync(p, content)); + await Task.WhenAll(tasks); + } + + private async Task ProcessRequestAsync(DataCache snapshot, OracleRequest req) + { + Log($"[{req.OriginalTxid}] Process oracle request start:<{req.Url}>"); + + uint height = NativeContract.Ledger.CurrentIndex(snapshot) + 1; + + (OracleResponseCode code, string data) = await ProcessUrlAsync(req.Url); + + Log($"[{req.OriginalTxid}] Process oracle request end:<{req.Url}>, responseCode:{code}, response:{data}"); + + var oracleNodes = NativeContract.RoleManagement.GetDesignatedByRole(snapshot, Role.Oracle, height); + foreach (var (requestId, request) in NativeContract.Oracle.GetRequestsByUrl(snapshot, req.Url)) + { + var result = Array.Empty(); + if (code == OracleResponseCode.Success) + { + try + { + result = Filter(data, request.Filter); + } + catch (Exception ex) + { + code = OracleResponseCode.Error; + Log($"[{req.OriginalTxid}] Filter '{request.Filter}' error:{ex.Message}"); + } + } + var response = new OracleResponse() { Id = requestId, Code = code, Result = result }; + var responseTx = CreateResponseTx(snapshot, request, response, oracleNodes, _system.Settings); + var backupTx = CreateResponseTx(snapshot, request, new OracleResponse() { Code = OracleResponseCode.ConsensusUnreachable, Id = requestId, Result = Array.Empty() }, oracleNodes, _system.Settings, true); + + Log($"[{req.OriginalTxid}]-({requestId}) Built response tx[[{responseTx.Hash}]], responseCode:{code}, result:{result.ToHexString()}, validUntilBlock:{responseTx.ValidUntilBlock}, backupTx:{backupTx.Hash}-{backupTx.ValidUntilBlock}"); + + List tasks = new List(); + ECPoint[] oraclePublicKeys = NativeContract.RoleManagement.GetDesignatedByRole(snapshot, Role.Oracle, height); + foreach (var account in wallet.GetAccounts()) + { + var oraclePub = account.GetKey()?.PublicKey; + if (!account.HasKey || account.Lock || !oraclePublicKeys.Contains(oraclePub)) continue; + + var txSign = responseTx.Sign(account.GetKey(), _system.Settings.Network); + var backTxSign = backupTx.Sign(account.GetKey(), _system.Settings.Network); + + AddResponseTxSign(snapshot, requestId, oraclePub, txSign, responseTx, backupTx, backTxSign); + tasks.Add(SendResponseSignatureAsync(requestId, txSign, account.GetKey())); + + Log($"[{request.OriginalTxid}]-[[{responseTx.Hash}]] Send oracle sign data, Oracle node: {oraclePub}, Sign: {txSign.ToHexString()}"); + } + await Task.WhenAll(tasks); + } + } + + private async void ProcessRequestsAsync() + { + while (!cancelSource.IsCancellationRequested) + { + using (var snapshot = _system.GetSnapshot()) + { + SyncPendingQueue(snapshot); + foreach (var (id, request) in NativeContract.Oracle.GetRequests(snapshot)) + { + if (cancelSource.IsCancellationRequested) break; + if (!finishedCache.ContainsKey(id) && (!pendingQueue.TryGetValue(id, out OracleTask task) || task.Tx is null)) + await ProcessRequestAsync(snapshot, request); + } + } + if (cancelSource.IsCancellationRequested) break; + await Task.Delay(500); + } + + status = OracleStatus.Stopped; + } + + + private void SyncPendingQueue(DataCache snapshot) + { + var offChainRequests = NativeContract.Oracle.GetRequests(snapshot).ToDictionary(r => r.Item1, r => r.Item2); + var onChainRequests = pendingQueue.Keys.Except(offChainRequests.Keys); + foreach (var onChainRequest in onChainRequests) + { + pendingQueue.TryRemove(onChainRequest, out _); + } + } + + private async Task<(OracleResponseCode, string)> ProcessUrlAsync(string url) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + return (OracleResponseCode.Error, $"Invalid url:<{url}>"); + if (!protocols.TryGetValue(uri.Scheme, out IOracleProtocol protocol)) + return (OracleResponseCode.ProtocolNotSupported, $"Invalid Protocol:<{url}>"); + + using CancellationTokenSource ctsTimeout = new(Settings.Default.MaxOracleTimeout); + using CancellationTokenSource ctsLinked = CancellationTokenSource.CreateLinkedTokenSource(cancelSource.Token, ctsTimeout.Token); + + try + { + return await protocol.ProcessAsync(uri, ctsLinked.Token); + } + catch (Exception ex) + { + return (OracleResponseCode.Error, $"Request <{url}> Error:{ex.Message}"); + } + } + + public static Transaction CreateResponseTx(DataCache snapshot, OracleRequest request, OracleResponse response, ECPoint[] oracleNodes, ProtocolSettings settings, bool useCurrentHeight = false) + { + var requestTx = NativeContract.Ledger.GetTransactionState(snapshot, request.OriginalTxid); + var n = oracleNodes.Length; + var m = n - (n - 1) / 3; + var oracleSignContract = Contract.CreateMultiSigContract(m, oracleNodes); + uint height = NativeContract.Ledger.CurrentIndex(snapshot); + var validUntilBlock = requestTx.BlockIndex + settings.MaxValidUntilBlockIncrement; + while (useCurrentHeight && validUntilBlock <= height) + { + validUntilBlock += settings.MaxValidUntilBlockIncrement; + } + var tx = new Transaction() + { + Version = 0, + Nonce = unchecked((uint)response.Id), + ValidUntilBlock = validUntilBlock, + Signers = new[] + { + new Signer + { + Account = NativeContract.Oracle.Hash, + Scopes = WitnessScope.None + }, + new Signer + { + Account = oracleSignContract.ScriptHash, + Scopes = WitnessScope.None + } + }, + Attributes = new[] { response }, + Script = OracleResponse.FixedScript, + Witnesses = new Witness[2] + }; + Dictionary witnessDict = new Dictionary + { + [oracleSignContract.ScriptHash] = new Witness + { + InvocationScript = Array.Empty(), + VerificationScript = oracleSignContract.Script, + }, + [NativeContract.Oracle.Hash] = new Witness + { + InvocationScript = Array.Empty(), + VerificationScript = Array.Empty(), + } + }; + + UInt160[] hashes = tx.GetScriptHashesForVerifying(snapshot); + tx.Witnesses[0] = witnessDict[hashes[0]]; + tx.Witnesses[1] = witnessDict[hashes[1]]; + + // Calculate network fee + + var oracleContract = NativeContract.ContractManagement.GetContract(snapshot, NativeContract.Oracle.Hash); + var engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshot.CreateSnapshot(), settings: settings); + ContractMethodDescriptor md = oracleContract.Manifest.Abi.GetMethod("verify", -1); + engine.LoadContract(oracleContract, md, CallFlags.None); + if (engine.Execute() != VMState.HALT) return null; + tx.NetworkFee += engine.GasConsumed; + + var executionFactor = NativeContract.Policy.GetExecFeeFactor(snapshot); + var networkFee = executionFactor * SmartContract.Helper.MultiSignatureContractCost(m, n); + tx.NetworkFee += networkFee; + + // Base size for transaction: includes const_header + signers + script + hashes + witnesses, except attributes + + int size_inv = 66 * m; + int size = Transaction.HeaderSize + tx.Signers.GetVarSize() + tx.Script.GetVarSize() + + IO.Helper.GetVarSize(hashes.Length) + witnessDict[NativeContract.Oracle.Hash].Size + + IO.Helper.GetVarSize(size_inv) + size_inv + oracleSignContract.Script.GetVarSize(); + + var feePerByte = NativeContract.Policy.GetFeePerByte(snapshot); + if (response.Result.Length > OracleResponse.MaxResultSize) + { + response.Code = OracleResponseCode.ResponseTooLarge; + response.Result = Array.Empty(); + } + else if (tx.NetworkFee + (size + tx.Attributes.GetVarSize()) * feePerByte > request.GasForResponse) + { + response.Code = OracleResponseCode.InsufficientFunds; + response.Result = Array.Empty(); + } + size += tx.Attributes.GetVarSize(); + tx.NetworkFee += size * feePerByte; + + // Calcualte system fee + + tx.SystemFee = request.GasForResponse - tx.NetworkFee; + + return tx; + } + + private void AddResponseTxSign(DataCache snapshot, ulong requestId, ECPoint oraclePub, byte[] sign, Transaction responseTx = null, Transaction backupTx = null, byte[] backupSign = null) + { + var task = pendingQueue.GetOrAdd(requestId, _ => new OracleTask + { + Id = requestId, + Request = NativeContract.Oracle.GetRequest(snapshot, requestId), + Signs = new ConcurrentDictionary(), + BackupSigns = new ConcurrentDictionary() + }); + + if (responseTx != null) + { + task.Tx = responseTx; + var data = task.Tx.GetSignData(_system.Settings.Network); + task.Signs.Where(p => !Crypto.VerifySignature(data, p.Value, p.Key)).ForEach(p => task.Signs.Remove(p.Key, out _)); + } + if (backupTx != null) + { + task.BackupTx = backupTx; + var data = task.BackupTx.GetSignData(_system.Settings.Network); + task.BackupSigns.Where(p => !Crypto.VerifySignature(data, p.Value, p.Key)).ForEach(p => task.BackupSigns.Remove(p.Key, out _)); + task.BackupSigns.TryAdd(oraclePub, backupSign); + } + if (task.Tx == null) + { + task.Signs.TryAdd(oraclePub, sign); + task.BackupSigns.TryAdd(oraclePub, sign); + return; + } + + if (Crypto.VerifySignature(task.Tx.GetSignData(_system.Settings.Network), sign, oraclePub)) + task.Signs.TryAdd(oraclePub, sign); + else if (Crypto.VerifySignature(task.BackupTx.GetSignData(_system.Settings.Network), sign, oraclePub)) + task.BackupSigns.TryAdd(oraclePub, sign); + else + throw new RpcException(RpcErrorFactory.InvalidSignature($"Invalid oracle response transaction signature from '{oraclePub}'.")); + + if (CheckTxSign(snapshot, task.Tx, task.Signs) || CheckTxSign(snapshot, task.BackupTx, task.BackupSigns)) + { + finishedCache.TryAdd(requestId, new DateTime()); + pendingQueue.TryRemove(requestId, out _); + } + } + + public static byte[] Filter(string input, string filterArgs) + { + if (string.IsNullOrEmpty(filterArgs)) + return Utility.StrictUTF8.GetBytes(input); + + JToken beforeObject = JToken.Parse(input); + JArray afterObjects = beforeObject.JsonPath(filterArgs); + return afterObjects.ToByteArray(false); + } + + private bool CheckTxSign(DataCache snapshot, Transaction tx, ConcurrentDictionary OracleSigns) + { + uint height = NativeContract.Ledger.CurrentIndex(snapshot) + 1; + if (tx.ValidUntilBlock <= height) + { + return false; + } + ECPoint[] oraclesNodes = NativeContract.RoleManagement.GetDesignatedByRole(snapshot, Role.Oracle, height); + int neededThreshold = oraclesNodes.Length - (oraclesNodes.Length - 1) / 3; + if (OracleSigns.Count >= neededThreshold && tx != null) + { + var contract = Contract.CreateMultiSigContract(neededThreshold, oraclesNodes); + ScriptBuilder sb = new ScriptBuilder(); + foreach (var (_, sign) in OracleSigns.OrderBy(p => p.Key)) + { + sb.EmitPush(sign); + if (--neededThreshold == 0) break; + } + var idx = tx.GetScriptHashesForVerifying(snapshot)[0] == contract.ScriptHash ? 0 : 1; + tx.Witnesses[idx].InvocationScript = sb.ToArray(); + + Log($"Send response tx: responseTx={tx.Hash}"); + + _system.Blockchain.Tell(tx); + return true; + } + return false; + } + + private static bool CheckOracleAvaiblable(DataCache snapshot, out ECPoint[] oracles) + { + uint height = NativeContract.Ledger.CurrentIndex(snapshot) + 1; + oracles = NativeContract.RoleManagement.GetDesignatedByRole(snapshot, Role.Oracle, height); + return oracles.Length > 0; + } + + private static bool CheckOracleAccount(Wallet wallet, ECPoint[] oracles) + { + if (wallet is null) return false; + return oracles + .Select(p => wallet.GetAccount(p)) + .Any(p => p is not null && p.HasKey && !p.Lock); + } + + private static void Log(string message, LogLevel level = LogLevel.Info) + { + Utility.Log(nameof(OracleService), level, message); + } + + class OracleTask + { + public ulong Id; + public OracleRequest Request; + public Transaction Tx; + public Transaction BackupTx; + public ConcurrentDictionary Signs; + public ConcurrentDictionary BackupSigns; + public readonly DateTime Timestamp = TimeProvider.Current.UtcNow; + } + + enum OracleStatus + { + Unstarted, + Running, + Stopped, + } + } +} diff --git a/src/Plugins/OracleService/OracleService.csproj b/src/Plugins/OracleService/OracleService.csproj new file mode 100644 index 0000000000..48ca5f3808 --- /dev/null +++ b/src/Plugins/OracleService/OracleService.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + Neo.Plugins.OracleService + + + + + + + + + + false + runtime + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/src/Plugins/OracleService/OracleService.json b/src/Plugins/OracleService/OracleService.json new file mode 100644 index 0000000000..1ab0d93399 --- /dev/null +++ b/src/Plugins/OracleService/OracleService.json @@ -0,0 +1,21 @@ +{ + "PluginConfiguration": { + "Network": 860833102, + "Nodes": [], + "MaxTaskTimeout": 432000000, + "MaxOracleTimeout": 10000, + "AllowPrivateHost": false, + "AllowedContentTypes": [ "application/json" ], + "Https": { + "Timeout": 5000 + }, + "NeoFS": { + "EndPoint": "http://127.0.0.1:8080", + "Timeout": 15000 + }, + "AutoStart": false + }, + "Dependency": [ + "RpcServer" + ] +} diff --git a/src/Plugins/OracleService/Protocols/IOracleProtocol.cs b/src/Plugins/OracleService/Protocols/IOracleProtocol.cs new file mode 100644 index 0000000000..3532a69454 --- /dev/null +++ b/src/Plugins/OracleService/Protocols/IOracleProtocol.cs @@ -0,0 +1,24 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// IOracleProtocol.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Network.P2P.Payloads; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Neo.Plugins +{ + interface IOracleProtocol : IDisposable + { + void Configure(); + Task<(OracleResponseCode, string)> ProcessAsync(Uri uri, CancellationToken cancellation); + } +} diff --git a/src/Plugins/OracleService/Protocols/OracleHttpsProtocol.cs b/src/Plugins/OracleService/Protocols/OracleHttpsProtocol.cs new file mode 100644 index 0000000000..29d3eedc43 --- /dev/null +++ b/src/Plugins/OracleService/Protocols/OracleHttpsProtocol.cs @@ -0,0 +1,115 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// OracleHttpsProtocol.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Network.P2P.Payloads; +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Neo.Plugins +{ + class OracleHttpsProtocol : IOracleProtocol + { + private readonly HttpClient client = new(new HttpClientHandler() { AllowAutoRedirect = false }); + + public OracleHttpsProtocol() + { + CustomAttributeData attribute = Assembly.GetExecutingAssembly().CustomAttributes.First(p => p.AttributeType == typeof(AssemblyInformationalVersionAttribute)); + string version = (string)attribute.ConstructorArguments[0].Value; + client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("NeoOracleService", version)); + } + + public void Configure() + { + client.DefaultRequestHeaders.Accept.Clear(); + foreach (string type in Settings.Default.AllowedContentTypes) + client.DefaultRequestHeaders.Accept.ParseAdd(type); + client.Timeout = Settings.Default.Https.Timeout; + } + + public void Dispose() + { + client.Dispose(); + } + + public async Task<(OracleResponseCode, string)> ProcessAsync(Uri uri, CancellationToken cancellation) + { + Utility.Log(nameof(OracleHttpsProtocol), LogLevel.Debug, $"Request: {uri.AbsoluteUri}"); + + HttpResponseMessage message; + try + { + int redirects = 2; + do + { + if (!Settings.Default.AllowPrivateHost) + { + IPHostEntry entry = await Dns.GetHostEntryAsync(uri.Host, cancellation); + if (entry.IsInternal()) + return (OracleResponseCode.Forbidden, null); + } + message = await client.GetAsync(uri, HttpCompletionOption.ResponseContentRead, cancellation); + if (message.Headers.Location is not null) + { + uri = message.Headers.Location; + message = null; + } + } while (message == null && redirects-- > 0); + } + catch + { + return (OracleResponseCode.Timeout, null); + } + if (message is null) + return (OracleResponseCode.Timeout, null); + if (message.StatusCode == HttpStatusCode.NotFound) + return (OracleResponseCode.NotFound, null); + if (message.StatusCode == HttpStatusCode.Forbidden) + return (OracleResponseCode.Forbidden, null); + if (!message.IsSuccessStatusCode) + return (OracleResponseCode.Error, message.StatusCode.ToString()); + if (!Settings.Default.AllowedContentTypes.Contains(message.Content.Headers.ContentType.MediaType)) + return (OracleResponseCode.ContentTypeNotSupported, null); + if (message.Content.Headers.ContentLength.HasValue && message.Content.Headers.ContentLength > OracleResponse.MaxResultSize) + return (OracleResponseCode.ResponseTooLarge, null); + + byte[] buffer = new byte[OracleResponse.MaxResultSize + 1]; + var stream = message.Content.ReadAsStream(cancellation); + var read = await stream.ReadAsync(buffer, 0, buffer.Length, cancellation); + + if (read > OracleResponse.MaxResultSize) + return (OracleResponseCode.ResponseTooLarge, null); + + var encoding = GetEncoding(message.Content.Headers); + if (!encoding.Equals(Encoding.UTF8)) + return (OracleResponseCode.Error, null); + + return (OracleResponseCode.Success, Utility.StrictUTF8.GetString(buffer, 0, read)); + } + + private static Encoding GetEncoding(HttpContentHeaders headers) + { + Encoding encoding = null; + if ((headers.ContentType != null) && (headers.ContentType.CharSet != null)) + { + encoding = Encoding.GetEncoding(headers.ContentType.CharSet); + } + + return encoding ?? Encoding.UTF8; + } + } +} diff --git a/src/Plugins/OracleService/Protocols/OracleNeoFSProtocol.cs b/src/Plugins/OracleService/Protocols/OracleNeoFSProtocol.cs new file mode 100644 index 0000000000..2bf92fa8f4 --- /dev/null +++ b/src/Plugins/OracleService/Protocols/OracleNeoFSProtocol.cs @@ -0,0 +1,155 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// OracleNeoFSProtocol.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Cryptography.ECC; +using Neo.FileStorage.API.Client; +using Neo.FileStorage.API.Cryptography; +using Neo.FileStorage.API.Refs; +using Neo.Network.P2P.Payloads; +using Neo.Wallets; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using Object = Neo.FileStorage.API.Object.Object; +using Range = Neo.FileStorage.API.Object.Range; + +namespace Neo.Plugins +{ + class OracleNeoFSProtocol : IOracleProtocol + { + private readonly System.Security.Cryptography.ECDsa privateKey; + + public OracleNeoFSProtocol(Wallet wallet, ECPoint[] oracles) + { + byte[] key = oracles.Select(p => wallet.GetAccount(p)).Where(p => p is not null && p.HasKey && !p.Lock).FirstOrDefault().GetKey().PrivateKey; + privateKey = key.LoadPrivateKey(); + } + + public void Configure() + { + } + + public void Dispose() + { + privateKey.Dispose(); + } + + public async Task<(OracleResponseCode, string)> ProcessAsync(Uri uri, CancellationToken cancellation) + { + Utility.Log(nameof(OracleNeoFSProtocol), LogLevel.Debug, $"Request: {uri.AbsoluteUri}"); + try + { + (OracleResponseCode code, string data) = await GetAsync(uri, Settings.Default.NeoFS.EndPoint, cancellation); + Utility.Log(nameof(OracleNeoFSProtocol), LogLevel.Debug, $"NeoFS result, code: {code}, data: {data}"); + return (code, data); + } + catch (Exception e) + { + Utility.Log(nameof(OracleNeoFSProtocol), LogLevel.Debug, $"NeoFS result: error,{e.Message}"); + return (OracleResponseCode.Error, null); + } + } + + + /// + /// GetAsync returns neofs object from the provided url. + /// If Command is not provided, full object is requested. + /// + /// URI scheme is "neofs:ContainerID/ObjectID/Command/offset|length". + /// Client host. + /// Cancellation token object. + /// Returns neofs object. + private async Task<(OracleResponseCode, string)> GetAsync(Uri uri, string host, CancellationToken cancellation) + { + string[] ps = uri.AbsolutePath.Split("/"); + if (ps.Length < 2) throw new FormatException("Invalid neofs url"); + ContainerID containerID = ContainerID.FromString(ps[0]); + ObjectID objectID = ObjectID.FromString(ps[1]); + Address objectAddr = new() + { + ContainerId = containerID, + ObjectId = objectID + }; + using Client client = new(privateKey, host); + var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellation); + tokenSource.CancelAfter(Settings.Default.NeoFS.Timeout); + if (ps.Length == 2) + return GetPayload(client, objectAddr, tokenSource.Token); + return ps[2] switch + { + "range" => await GetRangeAsync(client, objectAddr, ps.Skip(3).ToArray(), tokenSource.Token), + "header" => (OracleResponseCode.Success, await GetHeaderAsync(client, objectAddr, tokenSource.Token)), + "hash" => (OracleResponseCode.Success, await GetHashAsync(client, objectAddr, ps.Skip(3).ToArray(), tokenSource.Token)), + _ => throw new Exception("invalid command") + }; + } + + private static (OracleResponseCode, string) GetPayload(Client client, Address addr, CancellationToken cancellation) + { + var objReader = client.GetObjectInit(addr, options: new CallOptions { Ttl = 2 }, context: cancellation); + var obj = objReader.ReadHeader(); + if (obj.PayloadSize > OracleResponse.MaxResultSize) + return (OracleResponseCode.ResponseTooLarge, ""); + var payload = new byte[obj.PayloadSize]; + int offset = 0; + while (true) + { + if ((ulong)offset > obj.PayloadSize) return (OracleResponseCode.ResponseTooLarge, ""); + (byte[] chunk, bool ok) = objReader.ReadChunk(); + if (!ok) break; + Array.Copy(chunk, 0, payload, offset, chunk.Length); + offset += chunk.Length; + } + return (OracleResponseCode.Success, Utility.StrictUTF8.GetString(payload)); + } + + private static async Task<(OracleResponseCode, string)> GetRangeAsync(Client client, Address addr, string[] ps, CancellationToken cancellation) + { + if (ps.Length == 0) throw new FormatException("missing object range (expected 'Offset|Length')"); + Range range = ParseRange(ps[0]); + if (range.Length > OracleResponse.MaxResultSize) return (OracleResponseCode.ResponseTooLarge, ""); + var res = await client.GetObjectPayloadRangeData(addr, range, options: new CallOptions { Ttl = 2 }, context: cancellation); + return (OracleResponseCode.Success, Utility.StrictUTF8.GetString(res)); + } + + private static async Task GetHeaderAsync(Client client, Address addr, CancellationToken cancellation) + { + var obj = await client.GetObjectHeader(addr, options: new CallOptions { Ttl = 2 }, context: cancellation); + return obj.ToString(); + } + + private static async Task GetHashAsync(Client client, Address addr, string[] ps, CancellationToken cancellation) + { + if (ps.Length == 0 || ps[0] == "") + { + Object obj = await client.GetObjectHeader(addr, options: new CallOptions { Ttl = 2 }, context: cancellation); + return $"\"{new UInt256(obj.PayloadChecksum.Sum.ToByteArray())}\""; + } + Range range = ParseRange(ps[0]); + List hashes = await client.GetObjectPayloadRangeHash(addr, new List() { range }, ChecksumType.Sha256, Array.Empty(), new CallOptions { Ttl = 2 }, cancellation); + if (hashes.Count == 0) throw new Exception("empty response, object range is invalid (expected 'Offset|Length')"); + return $"\"{new UInt256(hashes[0])}\""; + } + + private static Range ParseRange(string s) + { + string url = HttpUtility.UrlDecode(s); + int sepIndex = url.IndexOf("|"); + if (sepIndex < 0) throw new Exception("object range is invalid (expected 'Offset|Length')"); + ulong offset = ulong.Parse(url[..sepIndex]); + ulong length = ulong.Parse(url[(sepIndex + 1)..]); + return new Range() { Offset = offset, Length = length }; + } + } +} diff --git a/src/Plugins/OracleService/Settings.cs b/src/Plugins/OracleService/Settings.cs new file mode 100644 index 0000000000..c66010cb5f --- /dev/null +++ b/src/Plugins/OracleService/Settings.cs @@ -0,0 +1,72 @@ +// 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; +using System; +using System.Linq; + +namespace Neo.Plugins +{ + class HttpsSettings + { + public TimeSpan Timeout { get; } + + public HttpsSettings(IConfigurationSection section) + { + Timeout = TimeSpan.FromMilliseconds(section.GetValue("Timeout", 5000)); + } + } + + class NeoFSSettings + { + public string EndPoint { get; } + public TimeSpan Timeout { get; } + + public NeoFSSettings(IConfigurationSection section) + { + EndPoint = section.GetValue("EndPoint", "127.0.0.1:8080"); + Timeout = TimeSpan.FromMilliseconds(section.GetValue("Timeout", 15000)); + } + } + + class Settings + { + public uint Network { get; } + public Uri[] Nodes { get; } + public TimeSpan MaxTaskTimeout { get; } + public TimeSpan MaxOracleTimeout { get; } + public bool AllowPrivateHost { get; } + public string[] AllowedContentTypes { get; } + public HttpsSettings Https { get; } + public NeoFSSettings NeoFS { get; } + public bool AutoStart { get; } + + public static Settings Default { get; private set; } + + private Settings(IConfigurationSection section) + { + Network = section.GetValue("Network", 5195086u); + Nodes = section.GetSection("Nodes").GetChildren().Select(p => new Uri(p.Get(), UriKind.Absolute)).ToArray(); + MaxTaskTimeout = TimeSpan.FromMilliseconds(section.GetValue("MaxTaskTimeout", 432000000)); + MaxOracleTimeout = TimeSpan.FromMilliseconds(section.GetValue("MaxOracleTimeout", 15000)); + AllowPrivateHost = section.GetValue("AllowPrivateHost", false); + AllowedContentTypes = section.GetSection("AllowedContentTypes").GetChildren().Select(p => p.Get()).ToArray(); + Https = new HttpsSettings(section.GetSection("Https")); + NeoFS = new NeoFSSettings(section.GetSection("NeoFS")); + AutoStart = section.GetValue("AutoStart", false); + } + + public static void Load(IConfigurationSection section) + { + Default = new Settings(section); + } + } +} diff --git a/src/Plugins/RocksDBStore/Plugins/Storage/Options.cs b/src/Plugins/RocksDBStore/Plugins/Storage/Options.cs new file mode 100644 index 0000000000..26dd6c63aa --- /dev/null +++ b/src/Plugins/RocksDBStore/Plugins/Storage/Options.cs @@ -0,0 +1,36 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Options.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using RocksDbSharp; + +namespace Neo.Plugins.Storage +{ + public static class Options + { + public static readonly DbOptions Default = CreateDbOptions(); + public static readonly ReadOptions ReadDefault = new ReadOptions(); + public static readonly WriteOptions WriteDefault = new WriteOptions(); + public static readonly WriteOptions WriteDefaultSync = new WriteOptions().SetSync(true); + + public static DbOptions CreateDbOptions() + { + DbOptions options = new DbOptions(); + options.SetCreateMissingColumnFamilies(true); + options.SetCreateIfMissing(true); + options.SetErrorIfExists(false); + options.SetMaxOpenFiles(1000); + options.SetParanoidChecks(false); + options.SetWriteBufferSize(4 << 20); + options.SetBlockBasedTableFactory(new BlockBasedTableOptions().SetBlockSize(4096)); + return options; + } + } +} diff --git a/src/Plugins/RocksDBStore/Plugins/Storage/RocksDBStore.cs b/src/Plugins/RocksDBStore/Plugins/Storage/RocksDBStore.cs new file mode 100644 index 0000000000..079a012f67 --- /dev/null +++ b/src/Plugins/RocksDBStore/Plugins/Storage/RocksDBStore.cs @@ -0,0 +1,34 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RocksDBStore.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; + +namespace Neo.Plugins.Storage +{ + public class RocksDBStore : Plugin, IStoreProvider + { + public override string Description => "Uses RocksDBStore to store the blockchain data"; + + public RocksDBStore() + { + StoreFactory.RegisterProvider(this); + } + + /// + /// Get store + /// + /// RocksDbStore + public IStore GetStore(string path) + { + return new Store(path); + } + } +} diff --git a/src/Plugins/RocksDBStore/Plugins/Storage/Snapshot.cs b/src/Plugins/RocksDBStore/Plugins/Storage/Snapshot.cs new file mode 100644 index 0000000000..7423f6ae4a --- /dev/null +++ b/src/Plugins/RocksDBStore/Plugins/Storage/Snapshot.cs @@ -0,0 +1,82 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Snapshot.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; +using RocksDbSharp; +using System; +using System.Collections.Generic; + +namespace Neo.Plugins.Storage +{ + internal class Snapshot : ISnapshot + { + private readonly RocksDb db; + private readonly RocksDbSharp.Snapshot snapshot; + private readonly WriteBatch batch; + private readonly ReadOptions options; + + public Snapshot(RocksDb db) + { + this.db = db; + snapshot = db.CreateSnapshot(); + batch = new WriteBatch(); + + options = new ReadOptions(); + options.SetFillCache(false); + options.SetSnapshot(snapshot); + } + + public void Commit() + { + db.Write(batch, Options.WriteDefault); + } + + public void Delete(byte[] key) + { + batch.Delete(key); + } + + public void Put(byte[] key, byte[] value) + { + batch.Put(key, value); + } + + public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[] keyOrPrefix, SeekDirection direction) + { + if (keyOrPrefix == null) keyOrPrefix = Array.Empty(); + + using var it = db.NewIterator(readOptions: options); + + if (direction == SeekDirection.Forward) + for (it.Seek(keyOrPrefix); it.Valid(); it.Next()) + yield return (it.Key(), it.Value()); + else + for (it.SeekForPrev(keyOrPrefix); it.Valid(); it.Prev()) + yield return (it.Key(), it.Value()); + } + + public bool Contains(byte[] key) + { + return db.Get(key, Array.Empty(), 0, 0, readOptions: options) >= 0; + } + + public byte[] TryGet(byte[] key) + { + return db.Get(key, readOptions: options); + } + + public void Dispose() + { + snapshot.Dispose(); + batch.Dispose(); + } + } +} diff --git a/src/Plugins/RocksDBStore/Plugins/Storage/Store.cs b/src/Plugins/RocksDBStore/Plugins/Storage/Store.cs new file mode 100644 index 0000000000..ebf160dab0 --- /dev/null +++ b/src/Plugins/RocksDBStore/Plugins/Storage/Store.cs @@ -0,0 +1,77 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Store.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; +using RocksDbSharp; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Neo.Plugins.Storage +{ + internal class Store : IStore + { + private readonly RocksDb db; + + public Store(string path) + { + db = RocksDb.Open(Options.Default, Path.GetFullPath(path)); + } + + public void Dispose() + { + db.Dispose(); + } + + public ISnapshot GetSnapshot() + { + return new Snapshot(db); + } + + public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[] keyOrPrefix, SeekDirection direction = SeekDirection.Forward) + { + if (keyOrPrefix == null) keyOrPrefix = Array.Empty(); + + using var it = db.NewIterator(); + if (direction == SeekDirection.Forward) + for (it.Seek(keyOrPrefix); it.Valid(); it.Next()) + yield return (it.Key(), it.Value()); + else + for (it.SeekForPrev(keyOrPrefix); it.Valid(); it.Prev()) + yield return (it.Key(), it.Value()); + } + + public bool Contains(byte[] key) + { + return db.Get(key, Array.Empty(), 0, 0) >= 0; + } + + public byte[] TryGet(byte[] key) + { + return db.Get(key); + } + + public void Delete(byte[] key) + { + db.Remove(key); + } + + public void Put(byte[] key, byte[] value) + { + db.Put(key, value); + } + + public void PutSync(byte[] key, byte[] value) + { + db.Put(key, value, writeOptions: Options.WriteDefaultSync); + } + } +} diff --git a/src/Plugins/RocksDBStore/RocksDBStore.csproj b/src/Plugins/RocksDBStore/RocksDBStore.csproj new file mode 100644 index 0000000000..57037c68cf --- /dev/null +++ b/src/Plugins/RocksDBStore/RocksDBStore.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + Neo.Plugins.Storage.RocksDBStore + Neo.Plugins.Storage + + + + + + + diff --git a/src/Plugins/RpcClient/ContractClient.cs b/src/Plugins/RpcClient/ContractClient.cs new file mode 100644 index 0000000000..f4ec020abc --- /dev/null +++ b/src/Plugins/RpcClient/ContractClient.cs @@ -0,0 +1,77 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// ContractClient.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.Network.RPC.Models; +using Neo.SmartContract; +using Neo.SmartContract.Manifest; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.Wallets; +using System.Threading.Tasks; + +namespace Neo.Network.RPC +{ + /// + /// Contract related operations through RPC API + /// + public class ContractClient + { + protected readonly RpcClient rpcClient; + + /// + /// ContractClient Constructor + /// + /// the RPC client to call NEO RPC methods + public ContractClient(RpcClient rpc) + { + rpcClient = rpc; + } + + /// + /// Use RPC method to test invoke operation. + /// + /// contract script hash + /// contract operation + /// operation arguments + /// + public Task TestInvokeAsync(UInt160 scriptHash, string operation, params object[] args) + { + byte[] script = scriptHash.MakeScript(operation, args); + return rpcClient.InvokeScriptAsync(script); + } + + /// + /// Deploy Contract, return signed transaction + /// + /// neo contract executable file + /// contract manifest + /// sender KeyPair + /// + public async Task CreateDeployContractTxAsync(byte[] nefFile, ContractManifest manifest, KeyPair key) + { + byte[] script; + using (ScriptBuilder sb = new ScriptBuilder()) + { + sb.EmitDynamicCall(NativeContract.ContractManagement.Hash, "deploy", nefFile, manifest.ToJson().ToString()); + script = sb.ToArray(); + } + UInt160 sender = Contract.CreateSignatureRedeemScript(key.PublicKey).ToScriptHash(); + Signer[] signers = new[] { new Signer { Scopes = WitnessScope.CalledByEntry, Account = sender } }; + + TransactionManagerFactory factory = new TransactionManagerFactory(rpcClient); + TransactionManager manager = await factory.MakeTransactionAsync(script, signers).ConfigureAwait(false); + return await manager + .AddSignature(key) + .SignAsync().ConfigureAwait(false); + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcAccount.cs b/src/Plugins/RpcClient/Models/RpcAccount.cs new file mode 100644 index 0000000000..2afe18b0e9 --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcAccount.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcAccount.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.Json; + +namespace Neo.Network.RPC.Models +{ + public class RpcAccount + { + public string Address { get; set; } + + public bool HasKey { get; set; } + + public string Label { get; set; } + + public bool WatchOnly { get; set; } + + public JObject ToJson() + { + return new JObject + { + ["address"] = Address, + ["haskey"] = HasKey, + ["label"] = Label, + ["watchonly"] = WatchOnly + }; + } + + public static RpcAccount FromJson(JObject json) + { + return new RpcAccount + { + Address = json["address"].AsString(), + HasKey = json["haskey"].AsBoolean(), + Label = json["label"]?.AsString(), + WatchOnly = json["watchonly"].AsBoolean(), + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcApplicationLog.cs b/src/Plugins/RpcClient/Models/RpcApplicationLog.cs new file mode 100644 index 0000000000..b641f9df06 --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcApplicationLog.cs @@ -0,0 +1,118 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcApplicationLog.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.Json; +using Neo.SmartContract; +using Neo.VM; +using Neo.VM.Types; +using System.Collections.Generic; +using System.Linq; + +namespace Neo.Network.RPC.Models +{ + public class RpcApplicationLog + { + public UInt256 TxId { get; set; } + + public UInt256 BlockHash { get; set; } + + public List Executions { get; set; } + + public JObject ToJson() + { + JObject json = new JObject(); + if (TxId != null) + json["txid"] = TxId.ToString(); + if (BlockHash != null) + json["blockhash"] = BlockHash.ToString(); + json["executions"] = Executions.Select(p => p.ToJson()).ToArray(); + return json; + } + + public static RpcApplicationLog FromJson(JObject json, ProtocolSettings protocolSettings) + { + return new RpcApplicationLog + { + TxId = json["txid"] is null ? null : UInt256.Parse(json["txid"].AsString()), + BlockHash = json["blockhash"] is null ? null : UInt256.Parse(json["blockhash"].AsString()), + Executions = ((JArray)json["executions"]).Select(p => Execution.FromJson((JObject)p, protocolSettings)).ToList(), + }; + } + } + + public class Execution + { + public TriggerType Trigger { get; set; } + + public VMState VMState { get; set; } + + public long GasConsumed { get; set; } + + public string ExceptionMessage { get; set; } + + public List Stack { get; set; } + + public List Notifications { get; set; } + + public JObject ToJson() + { + JObject json = new(); + json["trigger"] = Trigger; + json["vmstate"] = VMState; + json["gasconsumed"] = GasConsumed.ToString(); + json["exception"] = ExceptionMessage; + json["stack"] = Stack.Select(q => q.ToJson()).ToArray(); + json["notifications"] = Notifications.Select(q => q.ToJson()).ToArray(); + return json; + } + + public static Execution FromJson(JObject json, ProtocolSettings protocolSettings) + { + return new Execution + { + Trigger = json["trigger"].GetEnum(), + VMState = json["vmstate"].GetEnum(), + GasConsumed = long.Parse(json["gasconsumed"].AsString()), + ExceptionMessage = json["exception"]?.AsString(), + Stack = ((JArray)json["stack"]).Select(p => Utility.StackItemFromJson((JObject)p)).ToList(), + Notifications = ((JArray)json["notifications"]).Select(p => RpcNotifyEventArgs.FromJson((JObject)p, protocolSettings)).ToList() + }; + } + } + + public class RpcNotifyEventArgs + { + public UInt160 Contract { get; set; } + + public string EventName { get; set; } + + public StackItem State { get; set; } + + public JObject ToJson() + { + JObject json = new(); + json["contract"] = Contract.ToString(); + json["eventname"] = EventName; + json["state"] = State.ToJson(); + return json; + } + + public static RpcNotifyEventArgs FromJson(JObject json, ProtocolSettings protocolSettings) + { + return new RpcNotifyEventArgs + { + Contract = json["contract"].ToScriptHash(protocolSettings), + EventName = json["eventname"].AsString(), + State = Utility.StackItemFromJson((JObject)json["state"]) + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcBlock.cs b/src/Plugins/RpcClient/Models/RpcBlock.cs new file mode 100644 index 0000000000..46f54a7230 --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcBlock.cs @@ -0,0 +1,43 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcBlock.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.Json; +using Neo.Network.P2P.Payloads; + +namespace Neo.Network.RPC.Models +{ + public class RpcBlock + { + public Block Block { get; set; } + + public uint Confirmations { get; set; } + + public UInt256 NextBlockHash { get; set; } + + public JObject ToJson(ProtocolSettings protocolSettings) + { + JObject json = Utility.BlockToJson(Block, protocolSettings); + json["confirmations"] = Confirmations; + json["nextblockhash"] = NextBlockHash?.ToString(); + return json; + } + + public static RpcBlock FromJson(JObject json, ProtocolSettings protocolSettings) + { + return new RpcBlock + { + Block = Utility.BlockFromJson(json, protocolSettings), + Confirmations = (uint)json["confirmations"].AsNumber(), + NextBlockHash = json["nextblockhash"] is null ? null : UInt256.Parse(json["nextblockhash"].AsString()) + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcBlockHeader.cs b/src/Plugins/RpcClient/Models/RpcBlockHeader.cs new file mode 100644 index 0000000000..e30a6a64a1 --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcBlockHeader.cs @@ -0,0 +1,43 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcBlockHeader.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.Json; +using Neo.Network.P2P.Payloads; + +namespace Neo.Network.RPC.Models +{ + public class RpcBlockHeader + { + public Header Header { get; set; } + + public uint Confirmations { get; set; } + + public UInt256 NextBlockHash { get; set; } + + public JObject ToJson(ProtocolSettings protocolSettings) + { + JObject json = Header.ToJson(protocolSettings); + json["confirmations"] = Confirmations; + json["nextblockhash"] = NextBlockHash?.ToString(); + return json; + } + + public static RpcBlockHeader FromJson(JObject json, ProtocolSettings protocolSettings) + { + return new RpcBlockHeader + { + Header = Utility.HeaderFromJson(json, protocolSettings), + Confirmations = (uint)json["confirmations"].AsNumber(), + NextBlockHash = json["nextblockhash"] is null ? null : UInt256.Parse(json["nextblockhash"].AsString()) + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcContractState.cs b/src/Plugins/RpcClient/Models/RpcContractState.cs new file mode 100644 index 0000000000..b77a2d3a89 --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcContractState.cs @@ -0,0 +1,42 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcContractState.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.Json; +using Neo.SmartContract; +using Neo.SmartContract.Manifest; + +namespace Neo.Network.RPC.Models +{ + public class RpcContractState + { + public ContractState ContractState { get; set; } + + public JObject ToJson() + { + return ContractState.ToJson(); + } + + public static RpcContractState FromJson(JObject json) + { + return new RpcContractState + { + ContractState = new ContractState + { + Id = (int)json["id"].AsNumber(), + UpdateCounter = (ushort)json["updatecounter"].AsNumber(), + Hash = UInt160.Parse(json["hash"].AsString()), + Nef = RpcNefFile.FromJson((JObject)json["nef"]), + Manifest = ContractManifest.FromJson((JObject)json["manifest"]) + } + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcFoundStates.cs b/src/Plugins/RpcClient/Models/RpcFoundStates.cs new file mode 100644 index 0000000000..a3a1c1f10a --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcFoundStates.cs @@ -0,0 +1,44 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcFoundStates.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.Json; +using System; +using System.Linq; + +namespace Neo.Network.RPC.Models +{ + public class RpcFoundStates + { + public bool Truncated; + public (byte[] key, byte[] value)[] Results; + public byte[] FirstProof; + public byte[] LastProof; + + public static RpcFoundStates FromJson(JObject json) + { + return new RpcFoundStates + { + Truncated = json["truncated"].AsBoolean(), + Results = ((JArray)json["results"]) + .Select(j => ( + Convert.FromBase64String(j["key"].AsString()), + Convert.FromBase64String(j["value"].AsString()) + )) + .ToArray(), + FirstProof = ProofFromJson((JString)json["firstProof"]), + LastProof = ProofFromJson((JString)json["lastProof"]), + }; + } + + static byte[] ProofFromJson(JString json) + => json == null ? null : Convert.FromBase64String(json.AsString()); + } +} diff --git a/src/Plugins/RpcClient/Models/RpcInvokeResult.cs b/src/Plugins/RpcClient/Models/RpcInvokeResult.cs new file mode 100644 index 0000000000..6bd661e1ba --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcInvokeResult.cs @@ -0,0 +1,100 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcInvokeResult.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.Json; +using Neo.VM; +using Neo.VM.Types; +using System; +using System.Linq; + +namespace Neo.Network.RPC.Models +{ + public class RpcInvokeResult + { + public string Script { get; set; } + + public VM.VMState State { get; set; } + + public long GasConsumed { get; set; } + + public StackItem[] Stack { get; set; } + + public string Tx { get; set; } + + public string Exception { get; set; } + + public string Session { get; set; } + + public JObject ToJson() + { + JObject json = new(); + json["script"] = Script; + json["state"] = State; + json["gasconsumed"] = GasConsumed.ToString(); + if (!string.IsNullOrEmpty(Exception)) + json["exception"] = Exception; + try + { + json["stack"] = new JArray(Stack.Select(p => p.ToJson())); + } + catch (InvalidOperationException) + { + // ContractParameter.ToJson() may cause InvalidOperationException + json["stack"] = "error: recursive reference"; + } + if (!string.IsNullOrEmpty(Tx)) json["tx"] = Tx; + return json; + } + + public static RpcInvokeResult FromJson(JObject json) + { + RpcInvokeResult invokeScriptResult = new() + { + Script = json["script"].AsString(), + State = json["state"].GetEnum(), + GasConsumed = long.Parse(json["gasconsumed"].AsString()), + Exception = json["exception"]?.AsString(), + Session = json["session"]?.AsString() + }; + try + { + invokeScriptResult.Stack = ((JArray)json["stack"]).Select(p => Utility.StackItemFromJson((JObject)p)).ToArray(); + } + catch { } + invokeScriptResult.Tx = json["tx"]?.AsString(); + return invokeScriptResult; + } + } + + public class RpcStack + { + public string Type { get; set; } + + public string Value { get; set; } + + public JObject ToJson() + { + JObject json = new(); + json["type"] = Type; + json["value"] = Value; + return json; + } + + public static RpcStack FromJson(JObject json) + { + return new RpcStack + { + Type = json["type"].AsString(), + Value = json["value"].AsString() + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcMethodToken.cs b/src/Plugins/RpcClient/Models/RpcMethodToken.cs new file mode 100644 index 0000000000..f426950deb --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcMethodToken.cs @@ -0,0 +1,32 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcMethodToken.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.Json; +using Neo.SmartContract; +using System; + +namespace Neo.Network.RPC.Models +{ + class RpcMethodToken + { + public static MethodToken FromJson(JObject json) + { + return new MethodToken + { + Hash = UInt160.Parse(json["hash"].AsString()), + Method = json["method"].AsString(), + ParametersCount = (ushort)json["paramcount"].AsNumber(), + HasReturnValue = json["hasreturnvalue"].AsBoolean(), + CallFlags = (CallFlags)Enum.Parse(typeof(CallFlags), json["callflags"].AsString()) + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcNefFile.cs b/src/Plugins/RpcClient/Models/RpcNefFile.cs new file mode 100644 index 0000000000..4b33f2b6ac --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcNefFile.cs @@ -0,0 +1,33 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcNefFile.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.Json; +using Neo.SmartContract; +using System; +using System.Linq; + +namespace Neo.Network.RPC.Models +{ + class RpcNefFile + { + public static NefFile FromJson(JObject json) + { + return new NefFile + { + Compiler = json["compiler"].AsString(), + Source = json["source"].AsString(), + Tokens = ((JArray)json["tokens"]).Select(p => RpcMethodToken.FromJson((JObject)p)).ToArray(), + Script = Convert.FromBase64String(json["script"].AsString()), + CheckSum = (uint)json["checksum"].AsNumber() + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcNep17Balances.cs b/src/Plugins/RpcClient/Models/RpcNep17Balances.cs new file mode 100644 index 0000000000..f7f8b00dbe --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcNep17Balances.cs @@ -0,0 +1,73 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcNep17Balances.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.Json; +using Neo.Wallets; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Neo.Network.RPC.Models +{ + public class RpcNep17Balances + { + public UInt160 UserScriptHash { get; set; } + + public List Balances { get; set; } + + public JObject ToJson(ProtocolSettings protocolSettings) + { + JObject json = new(); + json["balance"] = Balances.Select(p => p.ToJson()).ToArray(); + json["address"] = UserScriptHash.ToAddress(protocolSettings.AddressVersion); + return json; + } + + public static RpcNep17Balances FromJson(JObject json, ProtocolSettings protocolSettings) + { + RpcNep17Balances nep17Balance = new() + { + Balances = ((JArray)json["balance"]).Select(p => RpcNep17Balance.FromJson((JObject)p, protocolSettings)).ToList(), + UserScriptHash = json["address"].ToScriptHash(protocolSettings) + }; + return nep17Balance; + } + } + + public class RpcNep17Balance + { + public UInt160 AssetHash { get; set; } + + public BigInteger Amount { get; set; } + + public uint LastUpdatedBlock { get; set; } + + public JObject ToJson() + { + JObject json = new(); + json["assethash"] = AssetHash.ToString(); + json["amount"] = Amount.ToString(); + json["lastupdatedblock"] = LastUpdatedBlock; + return json; + } + + public static RpcNep17Balance FromJson(JObject json, ProtocolSettings protocolSettings) + { + RpcNep17Balance balance = new() + { + AssetHash = json["assethash"].ToScriptHash(protocolSettings), + Amount = BigInteger.Parse(json["amount"].AsString()), + LastUpdatedBlock = (uint)json["lastupdatedblock"].AsNumber() + }; + return balance; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcNep17TokenInfo.cs b/src/Plugins/RpcClient/Models/RpcNep17TokenInfo.cs new file mode 100644 index 0000000000..a7cb6d0ef4 --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcNep17TokenInfo.cs @@ -0,0 +1,26 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcNep17TokenInfo.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Numerics; + +namespace Neo.Network.RPC.Models +{ + public class RpcNep17TokenInfo + { + public string Name { get; set; } + + public string Symbol { get; set; } + + public byte Decimals { get; set; } + + public BigInteger TotalSupply { get; set; } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcNep17Transfers.cs b/src/Plugins/RpcClient/Models/RpcNep17Transfers.cs new file mode 100644 index 0000000000..3a3226b9a9 --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcNep17Transfers.cs @@ -0,0 +1,92 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcNep17Transfers.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.Json; +using Neo.Wallets; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Neo.Network.RPC.Models +{ + public class RpcNep17Transfers + { + public UInt160 UserScriptHash { get; set; } + + public List Sent { get; set; } + + public List Received { get; set; } + + public JObject ToJson(ProtocolSettings protocolSettings) + { + JObject json = new(); + json["sent"] = Sent.Select(p => p.ToJson(protocolSettings)).ToArray(); + json["received"] = Received.Select(p => p.ToJson(protocolSettings)).ToArray(); + json["address"] = UserScriptHash.ToAddress(protocolSettings.AddressVersion); + return json; + } + + public static RpcNep17Transfers FromJson(JObject json, ProtocolSettings protocolSettings) + { + RpcNep17Transfers transfers = new() + { + Sent = ((JArray)json["sent"]).Select(p => RpcNep17Transfer.FromJson((JObject)p, protocolSettings)).ToList(), + Received = ((JArray)json["received"]).Select(p => RpcNep17Transfer.FromJson((JObject)p, protocolSettings)).ToList(), + UserScriptHash = json["address"].ToScriptHash(protocolSettings) + }; + return transfers; + } + } + + public class RpcNep17Transfer + { + public ulong TimestampMS { get; set; } + + public UInt160 AssetHash { get; set; } + + public UInt160 UserScriptHash { get; set; } + + public BigInteger Amount { get; set; } + + public uint BlockIndex { get; set; } + + public ushort TransferNotifyIndex { get; set; } + + public UInt256 TxHash { get; set; } + + public JObject ToJson(ProtocolSettings protocolSettings) + { + JObject json = new(); + json["timestamp"] = TimestampMS; + json["assethash"] = AssetHash.ToString(); + json["transferaddress"] = UserScriptHash?.ToAddress(protocolSettings.AddressVersion); + json["amount"] = Amount.ToString(); + json["blockindex"] = BlockIndex; + json["transfernotifyindex"] = TransferNotifyIndex; + json["txhash"] = TxHash.ToString(); + return json; + } + + public static RpcNep17Transfer FromJson(JObject json, ProtocolSettings protocolSettings) + { + return new RpcNep17Transfer + { + TimestampMS = (ulong)json["timestamp"].AsNumber(), + AssetHash = json["assethash"].ToScriptHash(protocolSettings), + UserScriptHash = json["transferaddress"]?.ToScriptHash(protocolSettings), + Amount = BigInteger.Parse(json["amount"].AsString()), + BlockIndex = (uint)json["blockindex"].AsNumber(), + TransferNotifyIndex = (ushort)json["transfernotifyindex"].AsNumber(), + TxHash = UInt256.Parse(json["txhash"].AsString()) + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcPeers.cs b/src/Plugins/RpcClient/Models/RpcPeers.cs new file mode 100644 index 0000000000..6659ffe0dd --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcPeers.cs @@ -0,0 +1,68 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcPeers.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.Json; +using System.Linq; + +namespace Neo.Network.RPC.Models +{ + public class RpcPeers + { + public RpcPeer[] Unconnected { get; set; } + + public RpcPeer[] Bad { get; set; } + + public RpcPeer[] Connected { get; set; } + + public JObject ToJson() + { + JObject json = new(); + json["unconnected"] = new JArray(Unconnected.Select(p => p.ToJson())); + json["bad"] = new JArray(Bad.Select(p => p.ToJson())); + json["connected"] = new JArray(Connected.Select(p => p.ToJson())); + return json; + } + + public static RpcPeers FromJson(JObject json) + { + return new RpcPeers + { + Unconnected = ((JArray)json["unconnected"]).Select(p => RpcPeer.FromJson((JObject)p)).ToArray(), + Bad = ((JArray)json["bad"]).Select(p => RpcPeer.FromJson((JObject)p)).ToArray(), + Connected = ((JArray)json["connected"]).Select(p => RpcPeer.FromJson((JObject)p)).ToArray() + }; + } + } + + public class RpcPeer + { + public string Address { get; set; } + + public int Port { get; set; } + + public JObject ToJson() + { + JObject json = new(); + json["address"] = Address; + json["port"] = Port; + return json; + } + + public static RpcPeer FromJson(JObject json) + { + return new RpcPeer + { + Address = json["address"].AsString(), + Port = int.Parse(json["port"].AsString()) + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcPlugin.cs b/src/Plugins/RpcClient/Models/RpcPlugin.cs new file mode 100644 index 0000000000..12a8669dea --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcPlugin.cs @@ -0,0 +1,44 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcPlugin.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.Json; +using System.Linq; + +namespace Neo.Network.RPC.Models +{ + public class RpcPlugin + { + public string Name { get; set; } + + public string Version { get; set; } + + public string[] Interfaces { get; set; } + + public JObject ToJson() + { + JObject json = new(); + json["name"] = Name; + json["version"] = Version; + json["interfaces"] = new JArray(Interfaces.Select(p => (JToken)p)); + return json; + } + + public static RpcPlugin FromJson(JObject json) + { + return new RpcPlugin + { + Name = json["name"].AsString(), + Version = json["version"].AsString(), + Interfaces = ((JArray)json["interfaces"]).Select(p => p.AsString()).ToArray() + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcRawMemPool.cs b/src/Plugins/RpcClient/Models/RpcRawMemPool.cs new file mode 100644 index 0000000000..4474e0b6be --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcRawMemPool.cs @@ -0,0 +1,45 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcRawMemPool.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.Json; +using System.Collections.Generic; +using System.Linq; + +namespace Neo.Network.RPC.Models +{ + public class RpcRawMemPool + { + public uint Height { get; set; } + + public List Verified { get; set; } + + public List UnVerified { get; set; } + + public JObject ToJson() + { + JObject json = new(); + json["height"] = Height; + json["verified"] = new JArray(Verified.Select(p => (JToken)p.ToString())); + json["unverified"] = new JArray(UnVerified.Select(p => (JToken)p.ToString())); + return json; + } + + public static RpcRawMemPool FromJson(JObject json) + { + return new RpcRawMemPool + { + Height = uint.Parse(json["height"].AsString()), + Verified = ((JArray)json["verified"]).Select(p => UInt256.Parse(p.AsString())).ToList(), + UnVerified = ((JArray)json["unverified"]).Select(p => UInt256.Parse(p.AsString())).ToList() + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcRequest.cs b/src/Plugins/RpcClient/Models/RpcRequest.cs new file mode 100644 index 0000000000..1c4b67415a --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcRequest.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcRequest.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.Json; +using System.Linq; + +namespace Neo.Network.RPC.Models +{ + public class RpcRequest + { + public JToken Id { get; set; } + + public string JsonRpc { get; set; } + + public string Method { get; set; } + + public JToken[] Params { get; set; } + + public static RpcRequest FromJson(JObject json) + { + return new RpcRequest + { + Id = json["id"], + JsonRpc = json["jsonrpc"].AsString(), + Method = json["method"].AsString(), + Params = ((JArray)json["params"]).ToArray() + }; + } + + public JObject ToJson() + { + var json = new JObject(); + json["id"] = Id; + json["jsonrpc"] = JsonRpc; + json["method"] = Method; + json["params"] = new JArray(Params); + return json; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcResponse.cs b/src/Plugins/RpcClient/Models/RpcResponse.cs new file mode 100644 index 0000000000..25e3212fc3 --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcResponse.cs @@ -0,0 +1,83 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcResponse.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.Json; + +namespace Neo.Network.RPC.Models +{ + public class RpcResponse + { + public JToken Id { get; set; } + + public string JsonRpc { get; set; } + + public RpcResponseError Error { get; set; } + + public JToken Result { get; set; } + + public string RawResponse { get; set; } + + public static RpcResponse FromJson(JObject json) + { + RpcResponse response = new() + { + Id = json["id"], + JsonRpc = json["jsonrpc"].AsString(), + Result = json["result"] + }; + + if (json["error"] != null) + { + response.Error = RpcResponseError.FromJson((JObject)json["error"]); + } + + return response; + } + + public JObject ToJson() + { + JObject json = new(); + json["id"] = Id; + json["jsonrpc"] = JsonRpc; + json["error"] = Error?.ToJson(); + json["result"] = Result; + return json; + } + } + + public class RpcResponseError + { + public int Code { get; set; } + + public string Message { get; set; } + + public JToken Data { get; set; } + + public static RpcResponseError FromJson(JObject json) + { + return new RpcResponseError + { + Code = (int)json["code"].AsNumber(), + Message = json["message"].AsString(), + Data = json["data"], + }; + } + + public JObject ToJson() + { + JObject json = new(); + json["code"] = Code; + json["message"] = Message; + json["data"] = Data; + return json; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcStateRoot.cs b/src/Plugins/RpcClient/Models/RpcStateRoot.cs new file mode 100644 index 0000000000..095b054a33 --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcStateRoot.cs @@ -0,0 +1,36 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcStateRoot.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.Json; +using Neo.Network.P2P.Payloads; +using System.Linq; + +namespace Neo.Network.RPC.Models +{ + public class RpcStateRoot + { + public byte Version; + public uint Index; + public UInt256 RootHash; + public Witness Witness; + + public static RpcStateRoot FromJson(JObject json) + { + return new RpcStateRoot + { + Version = (byte)json["version"].AsNumber(), + Index = (uint)json["index"].AsNumber(), + RootHash = UInt256.Parse(json["roothash"].AsString()), + Witness = ((JArray)json["witnesses"]).Select(p => Utility.WitnessFromJson((JObject)p)).FirstOrDefault() + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcTransaction.cs b/src/Plugins/RpcClient/Models/RpcTransaction.cs new file mode 100644 index 0000000000..cb674316d5 --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcTransaction.cs @@ -0,0 +1,62 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcTransaction.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.Json; +using Neo.Network.P2P.Payloads; +using Neo.VM; + +namespace Neo.Network.RPC.Models +{ + public class RpcTransaction + { + public Transaction Transaction { get; set; } + + public UInt256 BlockHash { get; set; } + + public uint? Confirmations { get; set; } + + public ulong? BlockTime { get; set; } + + public VMState? VMState { get; set; } + + public JObject ToJson(ProtocolSettings protocolSettings) + { + JObject json = Utility.TransactionToJson(Transaction, protocolSettings); + if (Confirmations != null) + { + json["blockhash"] = BlockHash.ToString(); + json["confirmations"] = Confirmations; + json["blocktime"] = BlockTime; + if (VMState != null) + { + json["vmstate"] = VMState; + } + } + return json; + } + + public static RpcTransaction FromJson(JObject json, ProtocolSettings protocolSettings) + { + RpcTransaction transaction = new RpcTransaction + { + Transaction = Utility.TransactionFromJson(json, protocolSettings) + }; + if (json["confirmations"] != null) + { + transaction.BlockHash = UInt256.Parse(json["blockhash"].AsString()); + transaction.Confirmations = (uint)json["confirmations"].AsNumber(); + transaction.BlockTime = (ulong)json["blocktime"].AsNumber(); + transaction.VMState = json["vmstate"]?.GetEnum(); + } + return transaction; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcTransferOut.cs b/src/Plugins/RpcClient/Models/RpcTransferOut.cs new file mode 100644 index 0000000000..d5e82a8468 --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcTransferOut.cs @@ -0,0 +1,45 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcTransferOut.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.Json; +using Neo.Wallets; + +namespace Neo.Network.RPC.Models +{ + public class RpcTransferOut + { + public UInt160 Asset { get; set; } + + public UInt160 ScriptHash { get; set; } + + public string Value { get; set; } + + public JObject ToJson(ProtocolSettings protocolSettings) + { + return new JObject + { + ["asset"] = Asset.ToString(), + ["value"] = Value, + ["address"] = ScriptHash.ToAddress(protocolSettings.AddressVersion), + }; + } + + public static RpcTransferOut FromJson(JObject json, ProtocolSettings protocolSettings) + { + return new RpcTransferOut + { + Asset = json["asset"].ToScriptHash(protocolSettings), + Value = json["value"].AsString(), + ScriptHash = json["address"].ToScriptHash(protocolSettings), + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcUnclaimedGas.cs b/src/Plugins/RpcClient/Models/RpcUnclaimedGas.cs new file mode 100644 index 0000000000..c25f527d33 --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcUnclaimedGas.cs @@ -0,0 +1,39 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcUnclaimedGas.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.Json; + +namespace Neo.Network.RPC.Models +{ + public class RpcUnclaimedGas + { + public long Unclaimed { get; set; } + + public string Address { get; set; } + + public JObject ToJson() + { + JObject json = new(); + json["unclaimed"] = Unclaimed.ToString(); + json["address"] = Address; + return json; + } + + public static RpcUnclaimedGas FromJson(JObject json) + { + return new RpcUnclaimedGas + { + Unclaimed = long.Parse(json["unclaimed"].AsString()), + Address = json["address"].AsString() + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcValidateAddressResult.cs b/src/Plugins/RpcClient/Models/RpcValidateAddressResult.cs new file mode 100644 index 0000000000..6f49e08f06 --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcValidateAddressResult.cs @@ -0,0 +1,39 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcValidateAddressResult.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.Json; + +namespace Neo.Network.RPC.Models +{ + public class RpcValidateAddressResult + { + public string Address { get; set; } + + public bool IsValid { get; set; } + + public JObject ToJson() + { + JObject json = new(); + json["address"] = Address; + json["isvalid"] = IsValid; + return json; + } + + public static RpcValidateAddressResult FromJson(JObject json) + { + return new RpcValidateAddressResult + { + Address = json["address"].AsString(), + IsValid = json["isvalid"].AsBoolean() + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcValidator.cs b/src/Plugins/RpcClient/Models/RpcValidator.cs new file mode 100644 index 0000000000..27031631e9 --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcValidator.cs @@ -0,0 +1,40 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcValidator.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.Json; +using System.Numerics; + +namespace Neo.Network.RPC.Models +{ + public class RpcValidator + { + public string PublicKey { get; set; } + + public BigInteger Votes { get; set; } + + public JObject ToJson() + { + JObject json = new(); + json["publickey"] = PublicKey; + json["votes"] = Votes.ToString(); + return json; + } + + public static RpcValidator FromJson(JObject json) + { + return new RpcValidator + { + PublicKey = json["publickey"].AsString(), + Votes = BigInteger.Parse(json["votes"].AsString()), + }; + } + } +} diff --git a/src/Plugins/RpcClient/Models/RpcVersion.cs b/src/Plugins/RpcClient/Models/RpcVersion.cs new file mode 100644 index 0000000000..430d659f7c --- /dev/null +++ b/src/Plugins/RpcClient/Models/RpcVersion.cs @@ -0,0 +1,113 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcVersion.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.Json; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Neo.Network.RPC.Models +{ + public class RpcVersion + { + public class RpcProtocol + { + public uint Network { get; set; } + public int ValidatorsCount { get; set; } + public uint MillisecondsPerBlock { get; set; } + public uint MaxValidUntilBlockIncrement { get; set; } + public uint MaxTraceableBlocks { get; set; } + public byte AddressVersion { get; set; } + public uint MaxTransactionsPerBlock { get; set; } + public int MemoryPoolMaxTransactions { get; set; } + public ulong InitialGasDistribution { get; set; } + public IReadOnlyDictionary Hardforks { get; set; } + + public JObject ToJson() + { + JObject json = new(); + json["network"] = Network; + json["validatorscount"] = ValidatorsCount; + json["msperblock"] = MillisecondsPerBlock; + json["maxvaliduntilblockincrement"] = MaxValidUntilBlockIncrement; + json["maxtraceableblocks"] = MaxTraceableBlocks; + json["addressversion"] = AddressVersion; + json["maxtransactionsperblock"] = MaxTransactionsPerBlock; + json["memorypoolmaxtransactions"] = MemoryPoolMaxTransactions; + json["initialgasdistribution"] = InitialGasDistribution; + json["hardforks"] = new JArray(Hardforks.Select(s => new JObject() + { + // Strip HF_ prefix. + ["name"] = StripPrefix(s.Key.ToString(), "HF_"), + ["blockheight"] = s.Value, + })); + return json; + } + + public static RpcProtocol FromJson(JObject json) + { + return new() + { + Network = (uint)json["network"].AsNumber(), + ValidatorsCount = (int)json["validatorscount"].AsNumber(), + MillisecondsPerBlock = (uint)json["msperblock"].AsNumber(), + MaxValidUntilBlockIncrement = (uint)json["maxvaliduntilblockincrement"].AsNumber(), + MaxTraceableBlocks = (uint)json["maxtraceableblocks"].AsNumber(), + AddressVersion = (byte)json["addressversion"].AsNumber(), + MaxTransactionsPerBlock = (uint)json["maxtransactionsperblock"].AsNumber(), + MemoryPoolMaxTransactions = (int)json["memorypoolmaxtransactions"].AsNumber(), + InitialGasDistribution = (ulong)json["initialgasdistribution"].AsNumber(), + Hardforks = new Dictionary(((JArray)json["hardforks"]).Select(s => + { + var name = s["name"].AsString(); + // Add HF_ prefix to the hardfork response for proper Hardfork enum parsing. + return new KeyValuePair(Enum.Parse(name.StartsWith("HF_") ? name : $"HF_{name}"), (uint)s["blockheight"].AsNumber()); + })), + }; + } + + private static string StripPrefix(string s, string prefix) + { + return s.StartsWith(prefix) ? s.Substring(prefix.Length) : s; + } + } + + public int TcpPort { get; set; } + + public uint Nonce { get; set; } + + public string UserAgent { get; set; } + + public RpcProtocol Protocol { get; set; } = new(); + + public JObject ToJson() + { + JObject json = new(); + json["network"] = Protocol.Network; // Obsolete + json["tcpport"] = TcpPort; + json["nonce"] = Nonce; + json["useragent"] = UserAgent; + json["protocol"] = Protocol.ToJson(); + return json; + } + + public static RpcVersion FromJson(JObject json) + { + return new() + { + TcpPort = (int)json["tcpport"].AsNumber(), + Nonce = (uint)json["nonce"].AsNumber(), + UserAgent = json["useragent"].AsString(), + Protocol = RpcProtocol.FromJson((JObject)json["protocol"]) + }; + } + } +} diff --git a/src/Plugins/RpcClient/Nep17API.cs b/src/Plugins/RpcClient/Nep17API.cs new file mode 100644 index 0000000000..518f470924 --- /dev/null +++ b/src/Plugins/RpcClient/Nep17API.cs @@ -0,0 +1,182 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Nep17API.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Cryptography.ECC; +using Neo.Network.P2P.Payloads; +using Neo.Network.RPC.Models; +using Neo.SmartContract; +using Neo.VM; +using Neo.Wallets; +using System; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using static Neo.Helper; + +namespace Neo.Network.RPC +{ + /// + /// Call NEP17 methods with RPC API + /// + public class Nep17API : ContractClient + { + /// + /// Nep17API Constructor + /// + /// the RPC client to call NEO RPC methods + public Nep17API(RpcClient rpcClient) : base(rpcClient) { } + + /// + /// Get balance of NEP17 token + /// + /// contract script hash + /// account script hash + /// + public async Task BalanceOfAsync(UInt160 scriptHash, UInt160 account) + { + var result = await TestInvokeAsync(scriptHash, "balanceOf", account).ConfigureAwait(false); + BigInteger balance = result.Stack.Single().GetInteger(); + return balance; + } + + /// + /// Get symbol of NEP17 token + /// + /// contract script hash + /// + public async Task SymbolAsync(UInt160 scriptHash) + { + var result = await TestInvokeAsync(scriptHash, "symbol").ConfigureAwait(false); + return result.Stack.Single().GetString(); + } + + /// + /// Get decimals of NEP17 token + /// + /// contract script hash + /// + public async Task DecimalsAsync(UInt160 scriptHash) + { + var result = await TestInvokeAsync(scriptHash, "decimals").ConfigureAwait(false); + return (byte)result.Stack.Single().GetInteger(); + } + + /// + /// Get total supply of NEP17 token + /// + /// contract script hash + /// + public async Task TotalSupplyAsync(UInt160 scriptHash) + { + var result = await TestInvokeAsync(scriptHash, "totalSupply").ConfigureAwait(false); + return result.Stack.Single().GetInteger(); + } + + /// + /// Get token information in one rpc call + /// + /// contract script hash + /// + public async Task GetTokenInfoAsync(UInt160 scriptHash) + { + var contractState = await rpcClient.GetContractStateAsync(scriptHash.ToString()).ConfigureAwait(false); + byte[] script = Concat( + scriptHash.MakeScript("symbol"), + scriptHash.MakeScript("decimals"), + scriptHash.MakeScript("totalSupply")); + var name = contractState.Manifest.Name; + var result = await rpcClient.InvokeScriptAsync(script).ConfigureAwait(false); + var stack = result.Stack; + + return new RpcNep17TokenInfo + { + Name = name, + Symbol = stack[0].GetString(), + Decimals = (byte)stack[1].GetInteger(), + TotalSupply = stack[2].GetInteger() + }; + } + + public async Task GetTokenInfoAsync(string contractHash) + { + var contractState = await rpcClient.GetContractStateAsync(contractHash).ConfigureAwait(false); + byte[] script = Concat( + contractState.Hash.MakeScript("symbol"), + contractState.Hash.MakeScript("decimals"), + contractState.Hash.MakeScript("totalSupply")); + var name = contractState.Manifest.Name; + var result = await rpcClient.InvokeScriptAsync(script).ConfigureAwait(false); + var stack = result.Stack; + + return new RpcNep17TokenInfo + { + Name = name, + Symbol = stack[0].GetString(), + Decimals = (byte)stack[1].GetInteger(), + TotalSupply = stack[2].GetInteger() + }; + } + + /// + /// Create NEP17 token transfer transaction + /// + /// contract script hash + /// from KeyPair + /// to account script hash + /// transfer amount + /// onPayment data + /// Add assert at the end of the script + /// + public async Task CreateTransferTxAsync(UInt160 scriptHash, KeyPair fromKey, UInt160 to, BigInteger amount, object data = null, bool addAssert = true) + { + var sender = Contract.CreateSignatureRedeemScript(fromKey.PublicKey).ToScriptHash(); + Signer[] signers = new[] { new Signer { Scopes = WitnessScope.CalledByEntry, Account = sender } }; + byte[] script = scriptHash.MakeScript("transfer", sender, to, amount, data); + if (addAssert) script = script.Concat(new[] { (byte)OpCode.ASSERT }).ToArray(); + + TransactionManagerFactory factory = new(rpcClient); + TransactionManager manager = await factory.MakeTransactionAsync(script, signers).ConfigureAwait(false); + + return await manager + .AddSignature(fromKey) + .SignAsync().ConfigureAwait(false); + } + + /// + /// Create NEP17 token transfer transaction from multi-sig account + /// + /// contract script hash + /// multi-sig min signature count + /// multi-sig pubKeys + /// sign keys + /// to account + /// transfer amount + /// onPayment data + /// Add assert at the end of the script + /// + public async Task CreateTransferTxAsync(UInt160 scriptHash, int m, ECPoint[] pubKeys, KeyPair[] fromKeys, UInt160 to, BigInteger amount, object data = null, bool addAssert = true) + { + if (m > fromKeys.Length) + throw new ArgumentException($"Need at least {m} KeyPairs for signing!"); + var sender = Contract.CreateMultiSigContract(m, pubKeys).ScriptHash; + Signer[] signers = new[] { new Signer { Scopes = WitnessScope.CalledByEntry, Account = sender } }; + byte[] script = scriptHash.MakeScript("transfer", sender, to, amount, data); + if (addAssert) script = script.Concat(new[] { (byte)OpCode.ASSERT }).ToArray(); + + TransactionManagerFactory factory = new(rpcClient); + TransactionManager manager = await factory.MakeTransactionAsync(script, signers).ConfigureAwait(false); + + return await manager + .AddMultiSig(fromKeys, m, pubKeys) + .SignAsync().ConfigureAwait(false); + } + } +} diff --git a/src/Plugins/RpcClient/PolicyAPI.cs b/src/Plugins/RpcClient/PolicyAPI.cs new file mode 100644 index 0000000000..60e749a79c --- /dev/null +++ b/src/Plugins/RpcClient/PolicyAPI.cs @@ -0,0 +1,71 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PolicyAPI.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.SmartContract.Native; +using System.Linq; +using System.Threading.Tasks; + +namespace Neo.Network.RPC +{ + /// + /// Get Policy info by RPC API + /// + public class PolicyAPI : ContractClient + { + readonly UInt160 scriptHash = NativeContract.Policy.Hash; + + /// + /// PolicyAPI Constructor + /// + /// the RPC client to call NEO RPC methods + public PolicyAPI(RpcClient rpcClient) : base(rpcClient) { } + + /// + /// Get Fee Factor + /// + /// + public async Task GetExecFeeFactorAsync() + { + var result = await TestInvokeAsync(scriptHash, "getExecFeeFactor").ConfigureAwait(false); + return (uint)result.Stack.Single().GetInteger(); + } + + /// + /// Get Storage Price + /// + /// + public async Task GetStoragePriceAsync() + { + var result = await TestInvokeAsync(scriptHash, "getStoragePrice").ConfigureAwait(false); + return (uint)result.Stack.Single().GetInteger(); + } + + /// + /// Get Network Fee Per Byte + /// + /// + public async Task GetFeePerByteAsync() + { + var result = await TestInvokeAsync(scriptHash, "getFeePerByte").ConfigureAwait(false); + return (long)result.Stack.Single().GetInteger(); + } + + /// + /// Get Ploicy Blocked Accounts + /// + /// + public async Task IsBlockedAsync(UInt160 account) + { + var result = await TestInvokeAsync(scriptHash, "isBlocked", new object[] { account }).ConfigureAwait(false); + return result.Stack.Single().GetBoolean(); + } + } +} diff --git a/src/Plugins/RpcClient/Properties/AssemblyInfo.cs b/src/Plugins/RpcClient/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..d464f67da1 --- /dev/null +++ b/src/Plugins/RpcClient/Properties/AssemblyInfo.cs @@ -0,0 +1,14 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// AssemblyInfo.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Neo.Network.RPC.Tests")] diff --git a/src/Plugins/RpcClient/RpcClient.cs b/src/Plugins/RpcClient/RpcClient.cs new file mode 100644 index 0000000000..27b0023ec0 --- /dev/null +++ b/src/Plugins/RpcClient/RpcClient.cs @@ -0,0 +1,712 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcClient.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.Json; +using Neo.Network.P2P.Payloads; +using Neo.Network.RPC.Models; +using Neo.SmartContract; +using Neo.SmartContract.Manifest; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Neo.Network.RPC +{ + /// + /// The RPC client to call NEO RPC methods + /// + public class RpcClient : IDisposable + { + private readonly HttpClient httpClient; + private readonly Uri baseAddress; + internal readonly ProtocolSettings protocolSettings; + + public RpcClient(Uri url, string rpcUser = default, string rpcPass = default, ProtocolSettings protocolSettings = null) + { + httpClient = new HttpClient(); + baseAddress = url; + if (!string.IsNullOrEmpty(rpcUser) && !string.IsNullOrEmpty(rpcPass)) + { + string token = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{rpcUser}:{rpcPass}")); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", token); + } + this.protocolSettings = protocolSettings ?? ProtocolSettings.Default; + } + + public RpcClient(HttpClient client, Uri url, ProtocolSettings protocolSettings = null) + { + httpClient = client; + baseAddress = url; + this.protocolSettings = protocolSettings ?? ProtocolSettings.Default; + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + httpClient.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + } + #endregion + + static RpcRequest AsRpcRequest(string method, params JToken[] paraArgs) + { + return new RpcRequest + { + Id = 1, + JsonRpc = "2.0", + Method = method, + Params = paraArgs + }; + } + + static RpcResponse AsRpcResponse(string content, bool throwOnError) + { + var response = RpcResponse.FromJson((JObject)JToken.Parse(content)); + response.RawResponse = content; + + if (response.Error != null && throwOnError) + { + throw new RpcException(response.Error.Code, response.Error.Message); + } + + return response; + } + + HttpRequestMessage AsHttpRequest(RpcRequest request) + { + var requestJson = request.ToJson().ToString(); + return new HttpRequestMessage(HttpMethod.Post, baseAddress) + { + Content = new StringContent(requestJson, Neo.Utility.StrictUTF8) + }; + } + + public RpcResponse Send(RpcRequest request, bool throwOnError = true) + { + if (disposedValue) throw new ObjectDisposedException(nameof(RpcClient)); + + using var requestMsg = AsHttpRequest(request); + using var responseMsg = httpClient.Send(requestMsg); + using var contentStream = responseMsg.Content.ReadAsStream(); + using var contentReader = new StreamReader(contentStream); + return AsRpcResponse(contentReader.ReadToEnd(), throwOnError); + } + + public async Task SendAsync(RpcRequest request, bool throwOnError = true) + { + if (disposedValue) throw new ObjectDisposedException(nameof(RpcClient)); + + using var requestMsg = AsHttpRequest(request); + using var responseMsg = await httpClient.SendAsync(requestMsg).ConfigureAwait(false); + var content = await responseMsg.Content.ReadAsStringAsync(); + return AsRpcResponse(content, throwOnError); + } + + public virtual JToken RpcSend(string method, params JToken[] paraArgs) + { + var request = AsRpcRequest(method, paraArgs); + var response = Send(request); + return response.Result; + } + + public virtual async Task RpcSendAsync(string method, params JToken[] paraArgs) + { + var request = AsRpcRequest(method, paraArgs); + var response = await SendAsync(request).ConfigureAwait(false); + return response.Result; + } + + public static string GetRpcName([CallerMemberName] string methodName = null) + { + return new Regex("(.*?)(Hex|Both)?(Async)?").Replace(methodName, "$1").ToLowerInvariant(); + } + + #region Blockchain + + /// + /// Returns the hash of the tallest block in the main chain. + /// + public async Task GetBestBlockHashAsync() + { + var result = await RpcSendAsync(GetRpcName()).ConfigureAwait(false); + return result.AsString(); + } + + /// + /// Returns the hash of the tallest block in the main chain. + /// The serialized information of the block is returned, represented by a hexadecimal string. + /// + public async Task GetBlockHexAsync(string hashOrIndex) + { + var result = int.TryParse(hashOrIndex, out int index) + ? await RpcSendAsync(GetRpcName(), index).ConfigureAwait(false) + : await RpcSendAsync(GetRpcName(), hashOrIndex).ConfigureAwait(false); + return result.AsString(); + } + + /// + /// Returns the hash of the tallest block in the main chain. + /// + public async Task GetBlockAsync(string hashOrIndex) + { + var result = int.TryParse(hashOrIndex, out int index) + ? await RpcSendAsync(GetRpcName(), index, true).ConfigureAwait(false) + : await RpcSendAsync(GetRpcName(), hashOrIndex, true).ConfigureAwait(false); + + return RpcBlock.FromJson((JObject)result, protocolSettings); + } + + /// + /// Gets the number of block header in the main chain. + /// + public async Task GetBlockHeaderCountAsync() + { + var result = await RpcSendAsync(GetRpcName()).ConfigureAwait(false); + return (uint)result.AsNumber(); + } + + /// + /// Gets the number of blocks in the main chain. + /// + public async Task GetBlockCountAsync() + { + var result = await RpcSendAsync(GetRpcName()).ConfigureAwait(false); + return (uint)result.AsNumber(); + } + + /// + /// Returns the hash value of the corresponding block, based on the specified index. + /// + public async Task GetBlockHashAsync(uint index) + { + var result = await RpcSendAsync(GetRpcName(), index).ConfigureAwait(false); + return result.AsString(); + } + + /// + /// Returns the corresponding block header information according to the specified script hash. + /// + public async Task GetBlockHeaderHexAsync(string hashOrIndex) + { + var result = int.TryParse(hashOrIndex, out int index) + ? await RpcSendAsync(GetRpcName(), index).ConfigureAwait(false) + : await RpcSendAsync(GetRpcName(), hashOrIndex).ConfigureAwait(false); + return result.AsString(); + } + + /// + /// Returns the corresponding block header information according to the specified script hash. + /// + public async Task GetBlockHeaderAsync(string hashOrIndex) + { + var result = int.TryParse(hashOrIndex, out int index) + ? await RpcSendAsync(GetRpcName(), index, true).ConfigureAwait(false) + : await RpcSendAsync(GetRpcName(), hashOrIndex, true).ConfigureAwait(false); + + return RpcBlockHeader.FromJson((JObject)result, protocolSettings); + } + + /// + /// Queries contract information, according to the contract script hash. + /// + public async Task GetContractStateAsync(string hash) + { + var result = await RpcSendAsync(GetRpcName(), hash).ConfigureAwait(false); + return ContractStateFromJson((JObject)result); + } + + /// + /// Queries contract information, according to the contract id. + /// + public async Task GetContractStateAsync(int id) + { + var result = await RpcSendAsync(GetRpcName(), id).ConfigureAwait(false); + return ContractStateFromJson((JObject)result); + } + + public static ContractState ContractStateFromJson(JObject json) + { + return new ContractState + { + Id = (int)json["id"].AsNumber(), + UpdateCounter = (ushort)(json["updatecounter"]?.AsNumber() ?? 0), + Hash = UInt160.Parse(json["hash"].AsString()), + Nef = RpcNefFile.FromJson((JObject)json["nef"]), + Manifest = ContractManifest.FromJson((JObject)json["manifest"]) + }; + } + + /// + /// Get all native contracts. + /// + public async Task GetNativeContractsAsync() + { + var result = await RpcSendAsync(GetRpcName()).ConfigureAwait(false); + return ((JArray)result).Select(p => ContractStateFromJson((JObject)p)).ToArray(); + } + + /// + /// Obtains the list of unconfirmed transactions in memory. + /// + public async Task GetRawMempoolAsync() + { + var result = await RpcSendAsync(GetRpcName()).ConfigureAwait(false); + return ((JArray)result).Select(p => p.AsString()).ToArray(); + } + + /// + /// Obtains the list of unconfirmed transactions in memory. + /// shouldGetUnverified = true + /// + public async Task GetRawMempoolBothAsync() + { + var result = await RpcSendAsync(GetRpcName(), true).ConfigureAwait(false); + return RpcRawMemPool.FromJson((JObject)result); + } + + /// + /// Returns the corresponding transaction information, based on the specified hash value. + /// + public async Task GetRawTransactionHexAsync(string txHash) + { + var result = await RpcSendAsync(GetRpcName(), txHash).ConfigureAwait(false); + return result.AsString(); + } + + /// + /// Returns the corresponding transaction information, based on the specified hash value. + /// verbose = true + /// + public async Task GetRawTransactionAsync(string txHash) + { + var result = await RpcSendAsync(GetRpcName(), txHash, true).ConfigureAwait(false); + return RpcTransaction.FromJson((JObject)result, protocolSettings); + } + + /// + /// Calculate network fee + /// + /// Transaction + /// NetworkFee + public async Task CalculateNetworkFeeAsync(Transaction tx) + { + var json = await RpcSendAsync(GetRpcName(), Convert.ToBase64String(tx.ToArray())) + .ConfigureAwait(false); + return (long)json["networkfee"].AsNumber(); + } + + /// + /// Returns the stored value, according to the contract script hash (or Id) and the stored key. + /// + public async Task GetStorageAsync(string scriptHashOrId, string key) + { + var result = int.TryParse(scriptHashOrId, out int id) + ? await RpcSendAsync(GetRpcName(), id, key).ConfigureAwait(false) + : await RpcSendAsync(GetRpcName(), scriptHashOrId, key).ConfigureAwait(false); + return result.AsString(); + } + + /// + /// Returns the block index in which the transaction is found. + /// + public async Task GetTransactionHeightAsync(string txHash) + { + var result = await RpcSendAsync(GetRpcName(), txHash).ConfigureAwait(false); + return uint.Parse(result.AsString()); + } + + /// + /// Returns the next NEO consensus nodes information and voting status. + /// + public async Task GetNextBlockValidatorsAsync() + { + var result = await RpcSendAsync(GetRpcName()).ConfigureAwait(false); + return ((JArray)result).Select(p => RpcValidator.FromJson((JObject)p)).ToArray(); + } + + /// + /// Returns the current NEO committee members. + /// + public async Task GetCommitteeAsync() + { + var result = await RpcSendAsync(GetRpcName()).ConfigureAwait(false); + return ((JArray)result).Select(p => p.AsString()).ToArray(); + } + + #endregion Blockchain + + #region Node + + /// + /// Gets the current number of connections for the node. + /// + public async Task GetConnectionCountAsync() + { + var result = await RpcSendAsync(GetRpcName()).ConfigureAwait(false); + return (int)result.AsNumber(); + } + + /// + /// Gets the list of nodes that the node is currently connected/disconnected from. + /// + public async Task GetPeersAsync() + { + var result = await RpcSendAsync(GetRpcName()).ConfigureAwait(false); + return RpcPeers.FromJson((JObject)result); + } + + /// + /// Returns the version information about the queried node. + /// + public async Task GetVersionAsync() + { + var result = await RpcSendAsync(GetRpcName()).ConfigureAwait(false); + return RpcVersion.FromJson((JObject)result); + } + + /// + /// Broadcasts a serialized transaction over the NEO network. + /// + public async Task SendRawTransactionAsync(byte[] rawTransaction) + { + var result = await RpcSendAsync(GetRpcName(), Convert.ToBase64String(rawTransaction)).ConfigureAwait(false); + return UInt256.Parse(result["hash"].AsString()); + } + + /// + /// Broadcasts a transaction over the NEO network. + /// + public Task SendRawTransactionAsync(Transaction transaction) + { + return SendRawTransactionAsync(transaction.ToArray()); + } + + /// + /// Broadcasts a serialized block over the NEO network. + /// + public async Task SubmitBlockAsync(byte[] block) + { + var result = await RpcSendAsync(GetRpcName(), Convert.ToBase64String(block)).ConfigureAwait(false); + return UInt256.Parse(result["hash"].AsString()); + } + + #endregion Node + + #region SmartContract + + /// + /// Returns the result after calling a smart contract at scripthash with the given operation and parameters. + /// This RPC call does not affect the blockchain in any way. + /// + public async Task InvokeFunctionAsync(string scriptHash, string operation, RpcStack[] stacks, params Signer[] signer) + { + List parameters = new() { scriptHash.AsScriptHash(), operation, stacks.Select(p => p.ToJson()).ToArray() }; + if (signer.Length > 0) + { + parameters.Add(signer.Select(p => p.ToJson()).ToArray()); + } + var result = await RpcSendAsync(GetRpcName(), parameters.ToArray()).ConfigureAwait(false); + return RpcInvokeResult.FromJson((JObject)result); + } + + /// + /// Returns the result after passing a script through the VM. + /// This RPC call does not affect the blockchain in any way. + /// + public async Task InvokeScriptAsync(ReadOnlyMemory script, params Signer[] signers) + { + List parameters = new() { Convert.ToBase64String(script.Span) }; + if (signers.Length > 0) + { + parameters.Add(signers.Select(p => p.ToJson()).ToArray()); + } + var result = await RpcSendAsync(GetRpcName(), parameters.ToArray()).ConfigureAwait(false); + return RpcInvokeResult.FromJson((JObject)result); + } + + public async Task GetUnclaimedGasAsync(string address) + { + var result = await RpcSendAsync(GetRpcName(), address.AsScriptHash()).ConfigureAwait(false); + return RpcUnclaimedGas.FromJson((JObject)result); + } + + + public async IAsyncEnumerable TraverseIteratorAsync(string sessionId, string id) + { + const int count = 100; + while (true) + { + var result = await RpcSendAsync(GetRpcName(), sessionId, id, count).ConfigureAwait(false); + var array = (JArray)result; + foreach (JObject jObject in array) + { + yield return jObject; + } + if (array.Count < count) break; + } + } + + /// + /// Returns limit results from Iterator. + /// This RPC call does not affect the blockchain in any way. + /// + /// + /// + /// + /// + public async IAsyncEnumerable TraverseIteratorAsync(string sessionId, string id, int count) + { + var result = await RpcSendAsync(GetRpcName(), sessionId, id, count).ConfigureAwait(false); + if (result is JArray { Count: > 0 } array) + { + foreach (JObject jObject in array) + { + yield return jObject; + } + } + } + + /// + /// Terminate specified Iterator session. + /// This RPC call does not affect the blockchain in any way. + /// + public async Task TerminateSessionAsync(string sessionId) + { + var result = await RpcSendAsync(GetRpcName(), sessionId).ConfigureAwait(false); + return result.GetBoolean(); + } + + #endregion SmartContract + + #region Utilities + + /// + /// Returns a list of plugins loaded by the node. + /// + public async Task ListPluginsAsync() + { + var result = await RpcSendAsync(GetRpcName()).ConfigureAwait(false); + return ((JArray)result).Select(p => RpcPlugin.FromJson((JObject)p)).ToArray(); + } + + /// + /// Verifies that the address is a correct NEO address. + /// + public async Task ValidateAddressAsync(string address) + { + var result = await RpcSendAsync(GetRpcName(), address).ConfigureAwait(false); + return RpcValidateAddressResult.FromJson((JObject)result); + } + + #endregion Utilities + + #region Wallet + + /// + /// Close the wallet opened by RPC. + /// + public async Task CloseWalletAsync() + { + var result = await RpcSendAsync(GetRpcName()).ConfigureAwait(false); + return result.AsBoolean(); + } + + /// + /// Exports the private key of the specified address. + /// + public async Task DumpPrivKeyAsync(string address) + { + var result = await RpcSendAsync(GetRpcName(), address).ConfigureAwait(false); + return result.AsString(); + } + + /// + /// Creates a new account in the wallet opened by RPC. + /// + public async Task GetNewAddressAsync() + { + var result = await RpcSendAsync(GetRpcName()).ConfigureAwait(false); + return result.AsString(); + } + + /// + /// Returns the balance of the corresponding asset in the wallet, based on the specified asset Id. + /// This method applies to assets that conform to NEP-17 standards. + /// + /// new address as string + public async Task GetWalletBalanceAsync(string assetId) + { + var result = await RpcSendAsync(GetRpcName(), assetId).ConfigureAwait(false); + BigInteger balance = BigInteger.Parse(result["balance"].AsString()); + byte decimals = await new Nep17API(this).DecimalsAsync(UInt160.Parse(assetId.AsScriptHash())).ConfigureAwait(false); + return new BigDecimal(balance, decimals); + } + + /// + /// Gets the amount of unclaimed GAS in the wallet. + /// + public async Task GetWalletUnclaimedGasAsync() + { + var result = await RpcSendAsync(GetRpcName()).ConfigureAwait(false); + return BigDecimal.Parse(result.AsString(), SmartContract.Native.NativeContract.GAS.Decimals); + } + + /// + /// Imports the private key to the wallet. + /// + public async Task ImportPrivKeyAsync(string wif) + { + var result = await RpcSendAsync(GetRpcName(), wif).ConfigureAwait(false); + return RpcAccount.FromJson((JObject)result); + } + + /// + /// Lists all the accounts in the current wallet. + /// + public async Task> ListAddressAsync() + { + var result = await RpcSendAsync(GetRpcName()).ConfigureAwait(false); + return ((JArray)result).Select(p => RpcAccount.FromJson((JObject)p)).ToList(); + } + + /// + /// Open wallet file in the provider's machine. + /// By default, this method is disabled by RpcServer config.json. + /// + public async Task OpenWalletAsync(string path, string password) + { + var result = await RpcSendAsync(GetRpcName(), path, password).ConfigureAwait(false); + return result.AsBoolean(); + } + + /// + /// Transfer from the specified address to the destination address. + /// + /// This function returns Signed Transaction JSON if successful, ContractParametersContext JSON if signing failed. + public async Task SendFromAsync(string assetId, string fromAddress, string toAddress, string amount) + { + return (JObject)await RpcSendAsync(GetRpcName(), assetId.AsScriptHash(), fromAddress.AsScriptHash(), + toAddress.AsScriptHash(), amount).ConfigureAwait(false); + } + + /// + /// Bulk transfer order, and you can specify a sender address. + /// + /// This function returns Signed Transaction JSON if successful, ContractParametersContext JSON if signing failed. + public async Task SendManyAsync(string fromAddress, IEnumerable outputs) + { + var parameters = new List(); + if (!string.IsNullOrEmpty(fromAddress)) + { + parameters.Add(fromAddress.AsScriptHash()); + } + parameters.Add(outputs.Select(p => p.ToJson(protocolSettings)).ToArray()); + + return (JObject)await RpcSendAsync(GetRpcName(), paraArgs: parameters.ToArray()).ConfigureAwait(false); + } + + /// + /// Transfer asset from the wallet to the destination address. + /// + /// This function returns Signed Transaction JSON if successful, ContractParametersContext JSON if signing failed. + public async Task SendToAddressAsync(string assetId, string address, string amount) + { + return (JObject)await RpcSendAsync(GetRpcName(), assetId.AsScriptHash(), address.AsScriptHash(), amount) + .ConfigureAwait(false); + } + + /// + /// Cancel Tx. + /// + /// This function returns Signed Transaction JSON if successful, ContractParametersContext JSON if signing failed. + public async Task CancelTransactionAsync(UInt256 txId, string[] signers, string extraFee) + { + JToken[] parameters = signers.Select(s => (JString)s.AsScriptHash()).ToArray(); + return (JObject)await RpcSendAsync(GetRpcName(), txId.ToString(), new JArray(parameters), extraFee).ConfigureAwait(false); + } + + #endregion Wallet + + #region Plugins + + /// + /// Returns the contract log based on the specified txHash. The complete contract logs are stored under the ApplicationLogs directory. + /// This method is provided by the plugin ApplicationLogs. + /// + public async Task GetApplicationLogAsync(string txHash) + { + var result = await RpcSendAsync(GetRpcName(), txHash).ConfigureAwait(false); + return RpcApplicationLog.FromJson((JObject)result, protocolSettings); + } + + /// + /// Returns the contract log based on the specified txHash. The complete contract logs are stored under the ApplicationLogs directory. + /// This method is provided by the plugin ApplicationLogs. + /// + public async Task GetApplicationLogAsync(string txHash, TriggerType triggerType) + { + var result = await RpcSendAsync(GetRpcName(), txHash, triggerType).ConfigureAwait(false); + return RpcApplicationLog.FromJson((JObject)result, protocolSettings); + } + + /// + /// Returns all the NEP-17 transaction information occurred in the specified address. + /// This method is provided by the plugin RpcNep17Tracker. + /// + /// The address to query the transaction information. + /// The start block Timestamp, default to seven days before UtcNow + /// The end block Timestamp, default to UtcNow + public async Task GetNep17TransfersAsync(string address, ulong? startTimestamp = default, ulong? endTimestamp = default) + { + startTimestamp ??= 0; + endTimestamp ??= DateTime.UtcNow.ToTimestampMS(); + var result = await RpcSendAsync(GetRpcName(), address.AsScriptHash(), startTimestamp, endTimestamp) + .ConfigureAwait(false); + return RpcNep17Transfers.FromJson((JObject)result, protocolSettings); + } + + /// + /// Returns the balance of all NEP-17 assets in the specified address. + /// This method is provided by the plugin RpcNep17Tracker. + /// + public async Task GetNep17BalancesAsync(string address) + { + var result = await RpcSendAsync(GetRpcName(), address.AsScriptHash()) + .ConfigureAwait(false); + return RpcNep17Balances.FromJson((JObject)result, protocolSettings); + } + + #endregion Plugins + } +} diff --git a/src/Plugins/RpcClient/RpcClient.csproj b/src/Plugins/RpcClient/RpcClient.csproj new file mode 100644 index 0000000000..cc634337d5 --- /dev/null +++ b/src/Plugins/RpcClient/RpcClient.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + Neo.Network.RPC.RpcClient + Neo.Network.RPC + + + + + + + diff --git a/src/Plugins/RpcClient/RpcException.cs b/src/Plugins/RpcClient/RpcException.cs new file mode 100644 index 0000000000..d0f2e5e64b --- /dev/null +++ b/src/Plugins/RpcClient/RpcException.cs @@ -0,0 +1,23 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcException.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.Network.RPC +{ + public class RpcException : Exception + { + public RpcException(int code, string message) : base(message) + { + HResult = code; + } + } +} diff --git a/src/Plugins/RpcClient/StateAPI.cs b/src/Plugins/RpcClient/StateAPI.cs new file mode 100644 index 0000000000..de8baa8077 --- /dev/null +++ b/src/Plugins/RpcClient/StateAPI.cs @@ -0,0 +1,88 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// StateAPI.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.Json; +using Neo.Network.RPC.Models; +using System; +using System.Threading.Tasks; + +namespace Neo.Network.RPC +{ + public class StateAPI + { + private readonly RpcClient rpcClient; + + public StateAPI(RpcClient rpc) + { + rpcClient = rpc; + } + + public async Task GetStateRootAsync(uint index) + { + var result = await rpcClient.RpcSendAsync(RpcClient.GetRpcName(), index).ConfigureAwait(false); + return RpcStateRoot.FromJson((JObject)result); + } + + public async Task GetProofAsync(UInt256 rootHash, UInt160 scriptHash, byte[] key) + { + var result = await rpcClient.RpcSendAsync(RpcClient.GetRpcName(), + rootHash.ToString(), scriptHash.ToString(), Convert.ToBase64String(key)).ConfigureAwait(false); + return Convert.FromBase64String(result.AsString()); + } + + public async Task VerifyProofAsync(UInt256 rootHash, byte[] proofBytes) + { + var result = await rpcClient.RpcSendAsync(RpcClient.GetRpcName(), + rootHash.ToString(), Convert.ToBase64String(proofBytes)).ConfigureAwait(false); + + return Convert.FromBase64String(result.AsString()); + } + + public async Task<(uint? localRootIndex, uint? validatedRootIndex)> GetStateHeightAsync() + { + var result = await rpcClient.RpcSendAsync(RpcClient.GetRpcName()).ConfigureAwait(false); + var localRootIndex = ToNullableUint(result["localrootindex"]); + var validatedRootIndex = ToNullableUint(result["validatedrootindex"]); + return (localRootIndex, validatedRootIndex); + } + + static uint? ToNullableUint(JToken json) => (json == null) ? null : (uint?)json.AsNumber(); + + public static JToken[] MakeFindStatesParams(UInt256 rootHash, UInt160 scriptHash, ReadOnlySpan prefix, ReadOnlySpan from = default, int? count = null) + { + var @params = new JToken[count.HasValue ? 5 : 4]; + @params[0] = rootHash.ToString(); + @params[1] = scriptHash.ToString(); + @params[2] = Convert.ToBase64String(prefix); + @params[3] = Convert.ToBase64String(from); + if (count.HasValue) + { + @params[4] = count.Value; + } + return @params; + } + + public async Task FindStatesAsync(UInt256 rootHash, UInt160 scriptHash, ReadOnlyMemory prefix, ReadOnlyMemory from = default, int? count = null) + { + var @params = MakeFindStatesParams(rootHash, scriptHash, prefix.Span, from.Span, count); + var result = await rpcClient.RpcSendAsync(RpcClient.GetRpcName(), @params).ConfigureAwait(false); + + return RpcFoundStates.FromJson((JObject)result); + } + + public async Task GetStateAsync(UInt256 rootHash, UInt160 scriptHash, byte[] key) + { + var result = await rpcClient.RpcSendAsync(RpcClient.GetRpcName(), + rootHash.ToString(), scriptHash.ToString(), Convert.ToBase64String(key)).ConfigureAwait(false); + return Convert.FromBase64String(result.AsString()); + } + } +} diff --git a/src/Plugins/RpcClient/TransactionManager.cs b/src/Plugins/RpcClient/TransactionManager.cs new file mode 100644 index 0000000000..ac20eed450 --- /dev/null +++ b/src/Plugins/RpcClient/TransactionManager.cs @@ -0,0 +1,215 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// TransactionManager.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Cryptography.ECC; +using Neo.IO; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.Wallets; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Neo.Network.RPC +{ + /// + /// This class helps to create transaction with RPC API. + /// + public class TransactionManager + { + private class SignItem { public Contract Contract; public HashSet KeyPairs; } + + private readonly RpcClient rpcClient; + + /// + /// The Transaction context to manage the witnesses + /// + private readonly ContractParametersContext context; + + /// + /// This container stores the keys for sign the transaction + /// + private readonly List signStore = new List(); + + /// + /// The Transaction managed by this instance + /// + private readonly Transaction tx; + + public Transaction Tx => tx; + + /// + /// TransactionManager Constructor + /// + /// the transaction to manage. Typically buildt + /// the RPC client to call NEO RPC API + public TransactionManager(Transaction tx, RpcClient rpcClient) + { + this.tx = tx; + context = new ContractParametersContext(null, tx, rpcClient.protocolSettings.Network); + this.rpcClient = rpcClient; + } + + /// + /// Helper function for one-off TransactionManager creation + /// + public static Task MakeTransactionAsync(RpcClient rpcClient, ReadOnlyMemory script, Signer[] signers = null, TransactionAttribute[] attributes = null) + { + var factory = new TransactionManagerFactory(rpcClient); + return factory.MakeTransactionAsync(script, signers, attributes); + } + + /// + /// Helper function for one-off TransactionManager creation + /// + public static Task MakeTransactionAsync(RpcClient rpcClient, ReadOnlyMemory script, long systemFee, Signer[] signers = null, TransactionAttribute[] attributes = null) + { + var factory = new TransactionManagerFactory(rpcClient); + return factory.MakeTransactionAsync(script, systemFee, signers, attributes); + } + + /// + /// Add Signature + /// + /// The KeyPair to sign transction + /// + public TransactionManager AddSignature(KeyPair key) + { + var contract = Contract.CreateSignatureContract(key.PublicKey); + AddSignItem(contract, key); + return this; + } + + /// + /// Add Multi-Signature + /// + /// The KeyPair to sign transction + /// The least count of signatures needed for multiple signature contract + /// The Public Keys construct the multiple signature contract + public TransactionManager AddMultiSig(KeyPair key, int m, params ECPoint[] publicKeys) + { + Contract contract = Contract.CreateMultiSigContract(m, publicKeys); + AddSignItem(contract, key); + return this; + } + + /// + /// Add Multi-Signature + /// + /// The KeyPairs to sign transction + /// The least count of signatures needed for multiple signature contract + /// The Public Keys construct the multiple signature contract + public TransactionManager AddMultiSig(KeyPair[] keys, int m, params ECPoint[] publicKeys) + { + Contract contract = Contract.CreateMultiSigContract(m, publicKeys); + for (int i = 0; i < keys.Length; i++) + { + AddSignItem(contract, keys[i]); + } + return this; + } + + private void AddSignItem(Contract contract, KeyPair key) + { + if (!Tx.GetScriptHashesForVerifying(null).Contains(contract.ScriptHash)) + { + throw new Exception($"Add SignItem error: Mismatch ScriptHash ({contract.ScriptHash})"); + } + + SignItem item = signStore.FirstOrDefault(p => p.Contract.ScriptHash == contract.ScriptHash); + if (item is null) + { + signStore.Add(new SignItem { Contract = contract, KeyPairs = new HashSet { key } }); + } + else if (!item.KeyPairs.Contains(key)) + { + item.KeyPairs.Add(key); + } + } + + /// + /// Add Witness with contract + /// + /// The witness verification contract + /// The witness invocation parameters + public TransactionManager AddWitness(Contract contract, params object[] parameters) + { + if (!context.Add(contract, parameters)) + { + throw new Exception("AddWitness failed!"); + }; + return this; + } + + /// + /// Add Witness with scriptHash + /// + /// The witness verification contract hash + /// The witness invocation parameters + public TransactionManager AddWitness(UInt160 scriptHash, params object[] parameters) + { + var contract = Contract.Create(scriptHash); + return AddWitness(contract, parameters); + } + + /// + /// Verify Witness count and add witnesses + /// + public async Task SignAsync() + { + // Calculate NetworkFee + Tx.Witnesses = Tx.GetScriptHashesForVerifying(null).Select(u => new Witness() + { + InvocationScript = Array.Empty(), + VerificationScript = GetVerificationScript(u) + }).ToArray(); + Tx.NetworkFee = await rpcClient.CalculateNetworkFeeAsync(Tx).ConfigureAwait(false); + Tx.Witnesses = null; + + var gasBalance = await new Nep17API(rpcClient).BalanceOfAsync(NativeContract.GAS.Hash, Tx.Sender).ConfigureAwait(false); + if (gasBalance < Tx.SystemFee + Tx.NetworkFee) + throw new InvalidOperationException($"Insufficient GAS in address: {Tx.Sender.ToAddress(rpcClient.protocolSettings.AddressVersion)}"); + + // Sign with signStore + for (int i = 0; i < signStore.Count; i++) + { + foreach (var key in signStore[i].KeyPairs) + { + byte[] signature = Tx.Sign(key, rpcClient.protocolSettings.Network); + if (!context.AddSignature(signStore[i].Contract, key.PublicKey, signature)) + { + throw new Exception("AddSignature failed!"); + } + } + } + + // Verify witness count + if (!context.Completed) + { + throw new Exception($"Please add signature or witness first!"); + } + Tx.Witnesses = context.GetWitnesses(); + return Tx; + } + + private byte[] GetVerificationScript(UInt160 hash) + { + foreach (var item in signStore) + { + if (item.Contract.ScriptHash == hash) return item.Contract.Script; + } + + return Array.Empty(); + } + } +} diff --git a/src/Plugins/RpcClient/TransactionManagerFactory.cs b/src/Plugins/RpcClient/TransactionManagerFactory.cs new file mode 100644 index 0000000000..3fe9e3ded5 --- /dev/null +++ b/src/Plugins/RpcClient/TransactionManagerFactory.cs @@ -0,0 +1,71 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// TransactionManagerFactory.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.Network.RPC.Models; +using System; +using System.Threading.Tasks; + +namespace Neo.Network.RPC +{ + public class TransactionManagerFactory + { + private readonly RpcClient rpcClient; + + /// + /// TransactionManagerFactory Constructor + /// + /// the RPC client to call NEO RPC API + public TransactionManagerFactory(RpcClient rpcClient) + { + this.rpcClient = rpcClient; + } + + /// + /// Create an unsigned Transaction object with given parameters. + /// + /// Transaction Script + /// Transaction Signers + /// Transaction Attributes + /// + public async Task MakeTransactionAsync(ReadOnlyMemory script, Signer[] signers = null, TransactionAttribute[] attributes = null) + { + RpcInvokeResult invokeResult = await rpcClient.InvokeScriptAsync(script, signers).ConfigureAwait(false); + return await MakeTransactionAsync(script, invokeResult.GasConsumed, signers, attributes).ConfigureAwait(false); + } + + /// + /// Create an unsigned Transaction object with given parameters. + /// + /// Transaction Script + /// Transaction System Fee + /// Transaction Signers + /// Transaction Attributes + /// + public async Task MakeTransactionAsync(ReadOnlyMemory script, long systemFee, Signer[] signers = null, TransactionAttribute[] attributes = null) + { + uint blockCount = await rpcClient.GetBlockCountAsync().ConfigureAwait(false) - 1; + + var tx = new Transaction + { + Version = 0, + Nonce = (uint)new Random().Next(), + Script = script, + Signers = signers ?? Array.Empty(), + ValidUntilBlock = blockCount - 1 + rpcClient.protocolSettings.MaxValidUntilBlockIncrement, + SystemFee = systemFee, + Attributes = attributes ?? Array.Empty(), + }; + + return new TransactionManager(tx, rpcClient); + } + } +} diff --git a/src/Plugins/RpcClient/Utility.cs b/src/Plugins/RpcClient/Utility.cs new file mode 100644 index 0000000000..659942f8f8 --- /dev/null +++ b/src/Plugins/RpcClient/Utility.cs @@ -0,0 +1,298 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Utility.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Cryptography.ECC; +using Neo.Json; +using Neo.Network.P2P.Payloads; +using Neo.Network.P2P.Payloads.Conditions; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM.Types; +using Neo.Wallets; +using System; +using System.Linq; +using System.Numerics; +using Array = Neo.VM.Types.Array; +using Buffer = Neo.VM.Types.Buffer; + +namespace Neo.Network.RPC +{ + public static class Utility + { + private static (BigInteger numerator, BigInteger denominator) Fraction(decimal d) + { + int[] bits = decimal.GetBits(d); + BigInteger numerator = (1 - ((bits[3] >> 30) & 2)) * + unchecked(((BigInteger)(uint)bits[2] << 64) | + ((BigInteger)(uint)bits[1] << 32) | + (uint)bits[0]); + BigInteger denominator = BigInteger.Pow(10, (bits[3] >> 16) & 0xff); + return (numerator, denominator); + } + + public static UInt160 ToScriptHash(this JToken value, ProtocolSettings protocolSettings) + { + var addressOrScriptHash = value.AsString(); + + return addressOrScriptHash.Length < 40 ? + addressOrScriptHash.ToScriptHash(protocolSettings.AddressVersion) : UInt160.Parse(addressOrScriptHash); + } + + public static string AsScriptHash(this string addressOrScriptHash) + { + foreach (var native in NativeContract.Contracts) + { + if (addressOrScriptHash.Equals(native.Name, StringComparison.InvariantCultureIgnoreCase) || + addressOrScriptHash == native.Id.ToString()) + return native.Hash.ToString(); + } + + return addressOrScriptHash.Length < 40 ? + addressOrScriptHash : UInt160.Parse(addressOrScriptHash).ToString(); + } + + /// + /// Parse WIF or private key hex string to KeyPair + /// + /// WIF or private key hex string + /// Example: WIF ("KyXwTh1hB76RRMquSvnxZrJzQx7h9nQP2PCRL38v6VDb5ip3nf1p"), PrivateKey ("450d6c2a04b5b470339a745427bae6828400cf048400837d73c415063835e005") + /// + public static KeyPair GetKeyPair(string key) + { + if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } + if (key.StartsWith("0x")) { key = key[2..]; } + + return key.Length switch + { + 52 => new KeyPair(Wallet.GetPrivateKeyFromWIF(key)), + 64 => new KeyPair(key.HexToBytes()), + _ => throw new FormatException() + }; + } + + /// + /// Parse address, scripthash or public key string to UInt160 + /// + /// account address, scripthash or public key string + /// Example: address ("Ncm9TEzrp8SSer6Wa3UCSLTRnqzwVhCfuE"), scripthash ("0xb0a31817c80ad5f87b6ed390ecb3f9d312f7ceb8"), public key ("02f9ec1fd0a98796cf75b586772a4ddd41a0af07a1dbdf86a7238f74fb72503575") + /// The protocol settings + /// + public static UInt160 GetScriptHash(string account, ProtocolSettings protocolSettings) + { + if (string.IsNullOrEmpty(account)) { throw new ArgumentNullException(nameof(account)); } + if (account.StartsWith("0x")) { account = account[2..]; } + + return account.Length switch + { + 34 => account.ToScriptHash(protocolSettings.AddressVersion), + 40 => UInt160.Parse(account), + 66 => Contract.CreateSignatureRedeemScript(ECPoint.Parse(account, ECCurve.Secp256r1)).ToScriptHash(), + _ => throw new FormatException(), + }; + } + + /// + /// Convert decimal amount to BigInteger: amount * 10 ^ decimals + /// + /// float value + /// token decimals + /// + public static BigInteger ToBigInteger(this decimal amount, uint decimals) + { + BigInteger factor = BigInteger.Pow(10, (int)decimals); + var (numerator, denominator) = Fraction(amount); + if (factor < denominator) + { + throw new ArgumentException("The decimal places is too long."); + } + + BigInteger res = factor * numerator / denominator; + return res; + } + + public static Block BlockFromJson(JObject json, ProtocolSettings protocolSettings) + { + return new Block() + { + Header = HeaderFromJson(json, protocolSettings), + Transactions = ((JArray)json["tx"]).Select(p => TransactionFromJson((JObject)p, protocolSettings)).ToArray() + }; + } + + public static JObject BlockToJson(Block block, ProtocolSettings protocolSettings) + { + JObject json = block.ToJson(protocolSettings); + json["tx"] = block.Transactions.Select(p => TransactionToJson(p, protocolSettings)).ToArray(); + return json; + } + + public static Header HeaderFromJson(JObject json, ProtocolSettings protocolSettings) + { + return new Header + { + Version = (uint)json["version"].AsNumber(), + PrevHash = UInt256.Parse(json["previousblockhash"].AsString()), + MerkleRoot = UInt256.Parse(json["merkleroot"].AsString()), + Timestamp = (ulong)json["time"].AsNumber(), + Nonce = Convert.ToUInt64(json["nonce"].AsString(), 16), + Index = (uint)json["index"].AsNumber(), + PrimaryIndex = (byte)json["primary"].AsNumber(), + NextConsensus = json["nextconsensus"].ToScriptHash(protocolSettings), + Witness = ((JArray)json["witnesses"]).Select(p => WitnessFromJson((JObject)p)).FirstOrDefault() + }; + } + + public static Transaction TransactionFromJson(JObject json, ProtocolSettings protocolSettings) + { + return new Transaction + { + Version = byte.Parse(json["version"].AsString()), + Nonce = uint.Parse(json["nonce"].AsString()), + Signers = ((JArray)json["signers"]).Select(p => SignerFromJson((JObject)p, protocolSettings)).ToArray(), + SystemFee = long.Parse(json["sysfee"].AsString()), + NetworkFee = long.Parse(json["netfee"].AsString()), + ValidUntilBlock = uint.Parse(json["validuntilblock"].AsString()), + Attributes = ((JArray)json["attributes"]).Select(p => TransactionAttributeFromJson((JObject)p)).ToArray(), + Script = Convert.FromBase64String(json["script"].AsString()), + Witnesses = ((JArray)json["witnesses"]).Select(p => WitnessFromJson((JObject)p)).ToArray() + }; + } + + public static JObject TransactionToJson(Transaction tx, ProtocolSettings protocolSettings) + { + JObject json = tx.ToJson(protocolSettings); + json["sysfee"] = tx.SystemFee.ToString(); + json["netfee"] = tx.NetworkFee.ToString(); + return json; + } + + public static Signer SignerFromJson(JObject json, ProtocolSettings protocolSettings) + { + return new Signer + { + Account = json["account"].ToScriptHash(protocolSettings), + Rules = ((JArray)json["rules"])?.Select(p => RuleFromJson((JObject)p, protocolSettings)).ToArray(), + Scopes = (WitnessScope)Enum.Parse(typeof(WitnessScope), json["scopes"].AsString()), + AllowedContracts = ((JArray)json["allowedcontracts"])?.Select(p => p.ToScriptHash(protocolSettings)).ToArray(), + AllowedGroups = ((JArray)json["allowedgroups"])?.Select(p => ECPoint.Parse(p.AsString(), ECCurve.Secp256r1)).ToArray() + }; + } + + public static TransactionAttribute TransactionAttributeFromJson(JObject json) + { + TransactionAttributeType usage = Enum.Parse(json["type"].AsString()); + return usage switch + { + TransactionAttributeType.HighPriority => new HighPriorityAttribute(), + TransactionAttributeType.OracleResponse => new OracleResponse() + { + Id = (ulong)json["id"].AsNumber(), + Code = Enum.Parse(json["code"].AsString()), + Result = Convert.FromBase64String(json["result"].AsString()), + }, + TransactionAttributeType.NotValidBefore => new NotValidBefore() + { + Height = (uint)json["height"].AsNumber(), + }, + TransactionAttributeType.Conflicts => new Conflicts() + { + Hash = UInt256.Parse(json["hash"].AsString()) + }, + _ => throw new FormatException(), + }; + } + + public static Witness WitnessFromJson(JObject json) + { + return new Witness + { + InvocationScript = Convert.FromBase64String(json["invocation"].AsString()), + VerificationScript = Convert.FromBase64String(json["verification"].AsString()) + }; + } + + public static WitnessRule RuleFromJson(JObject json, ProtocolSettings protocolSettings) + { + return new WitnessRule() + { + Action = Enum.Parse(json["action"].AsString()), + Condition = RuleExpressionFromJson((JObject)json["condition"], protocolSettings) + }; + } + + public static WitnessCondition RuleExpressionFromJson(JObject json, ProtocolSettings protocolSettings) + { + return json["type"].AsString() switch + { + "Or" => new OrCondition { Expressions = ((JArray)json["expressions"])?.Select(p => RuleExpressionFromJson((JObject)p, protocolSettings)).ToArray() }, + "And" => new AndCondition { Expressions = ((JArray)json["expressions"])?.Select(p => RuleExpressionFromJson((JObject)p, protocolSettings)).ToArray() }, + "Boolean" => new BooleanCondition { Expression = json["expression"].AsBoolean() }, + "Not" => new NotCondition { Expression = RuleExpressionFromJson((JObject)json["expression"], protocolSettings) }, + "Group" => new GroupCondition { Group = ECPoint.Parse(json["group"].AsString(), ECCurve.Secp256r1) }, + "CalledByContract" => new CalledByContractCondition { Hash = json["hash"].ToScriptHash(protocolSettings) }, + "ScriptHash" => new ScriptHashCondition { Hash = json["hash"].ToScriptHash(protocolSettings) }, + "CalledByEntry" => new CalledByEntryCondition(), + "CalledByGroup" => new CalledByGroupCondition { Group = ECPoint.Parse(json["group"].AsString(), ECCurve.Secp256r1) }, + _ => throw new FormatException("Wrong rule's condition type"), + }; + } + + public static StackItem StackItemFromJson(JObject json) + { + StackItemType type = json["type"].GetEnum(); + switch (type) + { + case StackItemType.Boolean: + return json["value"].GetBoolean() ? StackItem.True : StackItem.False; + case StackItemType.Buffer: + return new Buffer(Convert.FromBase64String(json["value"].AsString())); + case StackItemType.ByteString: + return new ByteString(Convert.FromBase64String(json["value"].AsString())); + case StackItemType.Integer: + return BigInteger.Parse(json["value"].AsString()); + case StackItemType.Array: + Array array = new(); + foreach (JObject item in (JArray)json["value"]) + array.Add(StackItemFromJson(item)); + return array; + case StackItemType.Struct: + Struct @struct = new(); + foreach (JObject item in (JArray)json["value"]) + @struct.Add(StackItemFromJson(item)); + return @struct; + case StackItemType.Map: + Map map = new(); + foreach (var item in (JArray)json["value"]) + { + PrimitiveType key = (PrimitiveType)StackItemFromJson((JObject)item["key"]); + map[key] = StackItemFromJson((JObject)item["value"]); + } + return map; + case StackItemType.Pointer: + return new Pointer(null, (int)json["value"].AsNumber()); + case StackItemType.InteropInterface: + return new InteropInterface(json); + default: + return json["value"]?.AsString() ?? StackItem.Null; + } + } + + public static string GetIteratorId(this StackItem item) + { + if (item is InteropInterface iop) + { + var json = iop.GetInterface(); + return json["id"]?.GetString(); + } + return null; + } + } +} diff --git a/src/Plugins/RpcClient/WalletAPI.cs b/src/Plugins/RpcClient/WalletAPI.cs new file mode 100644 index 0000000000..bbf684f758 --- /dev/null +++ b/src/Plugins/RpcClient/WalletAPI.cs @@ -0,0 +1,225 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// WalletAPI.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Cryptography.ECC; +using Neo.Network.P2P.Payloads; +using Neo.Network.RPC.Models; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.Wallets; +using System; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; + +namespace Neo.Network.RPC +{ + /// + /// Wallet Common APIs + /// + public class WalletAPI + { + private readonly RpcClient rpcClient; + private readonly Nep17API nep17API; + + /// + /// WalletAPI Constructor + /// + /// the RPC client to call NEO RPC methods + public WalletAPI(RpcClient rpc) + { + rpcClient = rpc; + nep17API = new Nep17API(rpc); + } + + /// + /// Get unclaimed gas with address, scripthash or public key string + /// + /// address, scripthash or public key string + /// Example: address ("Ncm9TEzrp8SSer6Wa3UCSLTRnqzwVhCfuE"), scripthash ("0xb0a31817c80ad5f87b6ed390ecb3f9d312f7ceb8"), public key ("02f9ec1fd0a98796cf75b586772a4ddd41a0af07a1dbdf86a7238f74fb72503575") + /// + public Task GetUnclaimedGasAsync(string account) + { + UInt160 accountHash = Utility.GetScriptHash(account, rpcClient.protocolSettings); + return GetUnclaimedGasAsync(accountHash); + } + + /// + /// Get unclaimed gas + /// + /// account scripthash + /// + public async Task GetUnclaimedGasAsync(UInt160 account) + { + UInt160 scriptHash = NativeContract.NEO.Hash; + var blockCount = await rpcClient.GetBlockCountAsync().ConfigureAwait(false); + var result = await nep17API.TestInvokeAsync(scriptHash, "unclaimedGas", account, blockCount - 1).ConfigureAwait(false); + BigInteger balance = result.Stack.Single().GetInteger(); + return ((decimal)balance) / (long)NativeContract.GAS.Factor; + } + + /// + /// Get Neo Balance + /// + /// address, scripthash or public key string + /// Example: address ("Ncm9TEzrp8SSer6Wa3UCSLTRnqzwVhCfuE"), scripthash ("0xb0a31817c80ad5f87b6ed390ecb3f9d312f7ceb8"), public key ("02f9ec1fd0a98796cf75b586772a4ddd41a0af07a1dbdf86a7238f74fb72503575") + /// + public async Task GetNeoBalanceAsync(string account) + { + BigInteger balance = await GetTokenBalanceAsync(NativeContract.NEO.Hash.ToString(), account).ConfigureAwait(false); + return (uint)balance; + } + + /// + /// Get Gas Balance + /// + /// address, scripthash or public key string + /// Example: address ("Ncm9TEzrp8SSer6Wa3UCSLTRnqzwVhCfuE"), scripthash ("0xb0a31817c80ad5f87b6ed390ecb3f9d312f7ceb8"), public key ("02f9ec1fd0a98796cf75b586772a4ddd41a0af07a1dbdf86a7238f74fb72503575") + /// + public async Task GetGasBalanceAsync(string account) + { + BigInteger balance = await GetTokenBalanceAsync(NativeContract.GAS.Hash.ToString(), account).ConfigureAwait(false); + return ((decimal)balance) / (long)NativeContract.GAS.Factor; + } + + /// + /// Get token balance with string parameters + /// + /// token script hash, Example: "0x43cf98eddbe047e198a3e5d57006311442a0ca15"(NEO) + /// address, scripthash or public key string + /// Example: address ("Ncm9TEzrp8SSer6Wa3UCSLTRnqzwVhCfuE"), scripthash ("0xb0a31817c80ad5f87b6ed390ecb3f9d312f7ceb8"), public key ("02f9ec1fd0a98796cf75b586772a4ddd41a0af07a1dbdf86a7238f74fb72503575") + /// + public Task GetTokenBalanceAsync(string tokenHash, string account) + { + UInt160 scriptHash = Utility.GetScriptHash(tokenHash, rpcClient.protocolSettings); + UInt160 accountHash = Utility.GetScriptHash(account, rpcClient.protocolSettings); + return nep17API.BalanceOfAsync(scriptHash, accountHash); + } + + /// + /// The GAS is claimed when doing NEO transfer + /// This function will transfer NEO balance from account to itself + /// + /// wif or private key + /// Example: WIF ("KyXwTh1hB76RRMquSvnxZrJzQx7h9nQP2PCRL38v6VDb5ip3nf1p"), PrivateKey ("450d6c2a04b5b470339a745427bae6828400cf048400837d73c415063835e005") + /// Add assert at the end of the script + /// The transaction sended + public Task ClaimGasAsync(string key, bool addAssert = true) + { + KeyPair keyPair = Utility.GetKeyPair(key); + return ClaimGasAsync(keyPair, addAssert); + } + + /// + /// The GAS is claimed when doing NEO transfer + /// This function will transfer NEO balance from account to itself + /// + /// keyPair + /// Add assert at the end of the script + /// The transaction sended + public async Task ClaimGasAsync(KeyPair keyPair, bool addAssert = true) + { + UInt160 toHash = Contract.CreateSignatureRedeemScript(keyPair.PublicKey).ToScriptHash(); + BigInteger balance = await nep17API.BalanceOfAsync(NativeContract.NEO.Hash, toHash).ConfigureAwait(false); + Transaction transaction = await nep17API.CreateTransferTxAsync(NativeContract.NEO.Hash, keyPair, toHash, balance, null, addAssert).ConfigureAwait(false); + await rpcClient.SendRawTransactionAsync(transaction).ConfigureAwait(false); + return transaction; + } + + /// + /// Transfer NEP17 token balance, with common data types + /// + /// nep17 token script hash, Example: scripthash ("0xb0a31817c80ad5f87b6ed390ecb3f9d312f7ceb8") + /// wif or private key + /// Example: WIF ("KyXwTh1hB76RRMquSvnxZrJzQx7h9nQP2PCRL38v6VDb5ip3nf1p"), PrivateKey ("450d6c2a04b5b470339a745427bae6828400cf048400837d73c415063835e005") + /// address or account script hash + /// token amount + /// onPayment data + /// Add assert at the end of the script + /// + public async Task TransferAsync(string tokenHash, string fromKey, string toAddress, decimal amount, object data = null, bool addAssert = true) + { + UInt160 scriptHash = Utility.GetScriptHash(tokenHash, rpcClient.protocolSettings); + var decimals = await nep17API.DecimalsAsync(scriptHash).ConfigureAwait(false); + + KeyPair from = Utility.GetKeyPair(fromKey); + UInt160 to = Utility.GetScriptHash(toAddress, rpcClient.protocolSettings); + BigInteger amountInteger = amount.ToBigInteger(decimals); + return await TransferAsync(scriptHash, from, to, amountInteger, data, addAssert).ConfigureAwait(false); + } + + /// + /// Transfer NEP17 token from single-sig account + /// + /// contract script hash + /// from KeyPair + /// to account script hash + /// transfer amount + /// onPayment data + /// Add assert at the end of the script + /// + public async Task TransferAsync(UInt160 scriptHash, KeyPair from, UInt160 to, BigInteger amountInteger, object data = null, bool addAssert = true) + { + Transaction transaction = await nep17API.CreateTransferTxAsync(scriptHash, from, to, amountInteger, data, addAssert).ConfigureAwait(false); + await rpcClient.SendRawTransactionAsync(transaction).ConfigureAwait(false); + return transaction; + } + + /// + /// Transfer NEP17 token from multi-sig account + /// + /// contract script hash + /// multi-sig min signature count + /// multi-sig pubKeys + /// sign keys + /// to account + /// transfer amount + /// onPayment data + /// Add assert at the end of the script + /// + public async Task TransferAsync(UInt160 scriptHash, int m, ECPoint[] pubKeys, KeyPair[] keys, UInt160 to, BigInteger amountInteger, object data = null, bool addAssert = true) + { + Transaction transaction = await nep17API.CreateTransferTxAsync(scriptHash, m, pubKeys, keys, to, amountInteger, data, addAssert).ConfigureAwait(false); + await rpcClient.SendRawTransactionAsync(transaction).ConfigureAwait(false); + return transaction; + } + + /// + /// Wait until the transaction is observable block chain + /// + /// the transaction to observe + /// TimeoutException throws after "timeout" seconds + /// the Transaction state, including vmState and blockhash + public async Task WaitTransactionAsync(Transaction transaction, int timeout = 60) + { + DateTime deadline = DateTime.UtcNow.AddSeconds(timeout); + RpcTransaction rpcTx = null; + while (rpcTx == null || rpcTx.Confirmations == null) + { + if (deadline < DateTime.UtcNow) + { + throw new TimeoutException(); + } + + try + { + rpcTx = await rpcClient.GetRawTransactionAsync(transaction.Hash.ToString()).ConfigureAwait(false); + if (rpcTx == null || rpcTx.Confirmations == null) + { + await Task.Delay((int)rpcClient.protocolSettings.MillisecondsPerBlock / 2); + } + } + catch (Exception) { } + } + return rpcTx; + } + } +} diff --git a/src/Plugins/RpcServer/Diagnostic.cs b/src/Plugins/RpcServer/Diagnostic.cs new file mode 100644 index 0000000000..a8cba4af9a --- /dev/null +++ b/src/Plugins/RpcServer/Diagnostic.cs @@ -0,0 +1,53 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Diagnostic.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.SmartContract; +using Neo.VM; + +namespace Neo.Plugins +{ + class Diagnostic : IDiagnostic + { + public Tree InvocationTree { get; } = new(); + + private TreeNode currentNodeOfInvocationTree = null; + + public void Initialized(ApplicationEngine engine) + { + } + + public void Disposed() + { + } + + public void ContextLoaded(ExecutionContext context) + { + var state = context.GetState(); + if (currentNodeOfInvocationTree is null) + currentNodeOfInvocationTree = InvocationTree.AddRoot(state.ScriptHash); + else + currentNodeOfInvocationTree = currentNodeOfInvocationTree.AddChild(state.ScriptHash); + } + + public void ContextUnloaded(ExecutionContext context) + { + currentNodeOfInvocationTree = currentNodeOfInvocationTree.Parent; + } + + public void PreExecuteInstruction(Instruction instruction) + { + } + + public void PostExecuteInstruction(Instruction instruction) + { + } + } +} diff --git a/src/Plugins/RpcServer/Result.cs b/src/Plugins/RpcServer/Result.cs new file mode 100644 index 0000000000..0ead2482cd --- /dev/null +++ b/src/Plugins/RpcServer/Result.cs @@ -0,0 +1,116 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Result.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.Plugins +{ + public static class Result + { + /// + /// Checks the execution result of a function and throws an exception if it is null or throw an exception. + /// + /// The function to execute + /// The rpc error + /// Append extra base exception message + /// The return type + /// The execution result + /// The Rpc exception + public static T Ok_Or(this Func function, RpcError err, bool withData = false) + { + try + { + var result = function(); + if (result == null) throw new RpcException(err); + return result; + } + catch (Exception ex) + { + if (withData) + throw new RpcException(err.WithData(ex.GetBaseException().Message)); + throw new RpcException(err); + } + } + + /// + /// Checks the execution result and throws an exception if it is null. + /// + /// The execution result + /// The rpc error + /// The return type + /// The execution result + /// The Rpc exception + public static T NotNull_Or(this T result, RpcError err) + { + if (result == null) throw new RpcException(err); + return result; + } + + /// + /// The execution result is true or throws an exception or null. + /// + /// The function to execute + /// the rpc exception code + /// the execution result + /// The rpc exception + public static bool True_Or(Func function, RpcError err) + { + try + { + var result = function(); + if (!result.Equals(true)) throw new RpcException(err); + return result; + } + catch + { + throw new RpcException(err); + } + } + + /// + /// Checks if the execution result is true or throws an exception. + /// + /// the execution result + /// the rpc exception code + /// the execution result + /// The rpc exception + public static bool True_Or(this bool result, RpcError err) + { + if (!result.Equals(true)) throw new RpcException(err); + return result; + } + + /// + /// Checks if the execution result is false or throws an exception. + /// + /// the execution result + /// the rpc exception code + /// the execution result + /// The rpc exception + public static bool False_Or(this bool result, RpcError err) + { + if (!result.Equals(false)) throw new RpcException(err); + return result; + } + + /// + /// Check if the execution result is null or throws an exception. + /// + /// The execution result + /// the rpc error + /// The execution result type + /// The execution result + /// the rpc exception + public static void Null_Or(this T result, RpcError err) + { + if (result != null) throw new RpcException(err); + } + } +} diff --git a/src/Plugins/RpcServer/RpcError.cs b/src/Plugins/RpcServer/RpcError.cs new file mode 100644 index 0000000000..7130540183 --- /dev/null +++ b/src/Plugins/RpcServer/RpcError.cs @@ -0,0 +1,103 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcError.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.Json; + +namespace Neo.Plugins +{ + public class RpcError + { + #region Default Values + + // https://www.jsonrpc.org/specification + // | code | message | meaning | + // |--------------------|-----------------|-----------------------------------------------------------------------------------| + // | -32700 | Parse error | Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text. | + // | -32600 | Invalid request | The JSON sent is not a valid Request object. | + // | -32601 | Method not found| The method does not exist / is not available. | + // | -32602 | Invalid params | Invalid method parameter(s). | + // | -32603 | Internal error | Internal JSON-RPC error. | + // | -32000 to -32099 | Server error | Reserved for implementation-defined server-errors. | + public static readonly RpcError InvalidRequest = new(-32600, "Invalid request"); + public static readonly RpcError MethodNotFound = new(-32601, "Method not found"); + public static readonly RpcError InvalidParams = new(-32602, "Invalid params"); + public static readonly RpcError InternalServerError = new(-32603, "Internal server RpcError"); + public static readonly RpcError BadRequest = new(-32700, "Bad request"); + + // https://github.com/neo-project/proposals/pull/156/files + public static readonly RpcError UnknownBlock = new(-101, "Unknown block"); + public static readonly RpcError UnknownContract = new(-102, "Unknown contract"); + public static readonly RpcError UnknownTransaction = new(-103, "Unknown transaction"); + public static readonly RpcError UnknownStorageItem = new(-104, "Unknown storage item"); + public static readonly RpcError UnknownScriptContainer = new(-105, "Unknown script container"); + public static readonly RpcError UnknownStateRoot = new(-106, "Unknown state root"); + public static readonly RpcError UnknownSession = new(-107, "Unknown session"); + public static readonly RpcError UnknownIterator = new(-108, "Unknown iterator"); + public static readonly RpcError UnknownHeight = new(-109, "Unknown height"); + + public static readonly RpcError InsufficientFundsWallet = new(-300, "Insufficient funds in wallet"); + public static readonly RpcError WalletFeeLimit = new(-301, "Wallet fee limit exceeded", "The necessary fee is more than the Max_fee, this transaction is failed. Please increase your Max_fee value."); + public static readonly RpcError NoOpenedWallet = new(-302, "No opened wallet"); + public static readonly RpcError WalletNotFound = new(-303, "Wallet not found"); + public static readonly RpcError WalletNotSupported = new(-304, "Wallet not supported"); + + public static readonly RpcError VerificationFailed = new(-500, "Inventory verification failed"); + public static readonly RpcError AlreadyExists = new(-501, "Inventory already exists"); + public static readonly RpcError MempoolCapReached = new(-502, "Memory pool capacity reached"); + public static readonly RpcError AlreadyInPool = new(-503, "Already in pool"); + public static readonly RpcError InsufficientNetworkFee = new(-504, "Insufficient network fee"); + public static readonly RpcError PolicyFailed = new(-505, "Policy check failed"); + public static readonly RpcError InvalidScript = new(-509, "Invalid transaction script"); + public static readonly RpcError InvalidAttribute = new(-507, "Invalid transaction attribute"); + public static readonly RpcError InvalidSignature = new(-508, "Invalid signature"); + public static readonly RpcError InvalidSize = new(-509, "Invalid inventory size"); + public static readonly RpcError ExpiredTransaction = new(-510, "Expired transaction"); + public static readonly RpcError InsufficientFunds = new(-511, "Insufficient funds for fee"); + public static readonly RpcError InvalidContractVerification = new(-512, "Invalid contract verification function"); + + public static readonly RpcError AccessDenied = new(-600, "Access denied"); + public static readonly RpcError SessionsDisabled = new(-601, "State iterator sessions disabled"); + public static readonly RpcError OracleDisabled = new(-602, "Oracle service disabled"); + public static readonly RpcError OracleRequestFinished = new(-603, "Oracle request already finished"); + public static readonly RpcError OracleRequestNotFound = new(-604, "Oracle request not found"); + public static readonly RpcError OracleNotDesignatedNode = new(-605, "Not a designated oracle node"); + public static readonly RpcError UnsupportedState = new(-606, "Old state not supported"); + public static readonly RpcError InvalidProof = new(-607, "Invalid state proof"); + public static readonly RpcError ExecutionFailed = new(-608, "Contract execution failed"); + + #endregion + + public int Code { get; set; } + public string Message { get; set; } + public string Data { get; set; } + + public RpcError(int code, string message, string data = null) + { + Code = code; + Message = message; + Data = data; + } + + public override string ToString() => string.IsNullOrEmpty(Data) ? $"{Message} ({Code})" : $"{Message} ({Code}) - {Data}"; + + public JToken ToJson() + { + JObject json = new(); + json["code"] = Code; + json["message"] = ErrorMessage; + if (!string.IsNullOrEmpty(Data)) + json["data"] = Data; + return json; + } + + public string ErrorMessage => string.IsNullOrEmpty(Data) ? $"{Message}" : $"{Message} - {Data}"; + } +} diff --git a/src/Plugins/RpcServer/RpcErrorFactory.cs b/src/Plugins/RpcServer/RpcErrorFactory.cs new file mode 100644 index 0000000000..328ba02f61 --- /dev/null +++ b/src/Plugins/RpcServer/RpcErrorFactory.cs @@ -0,0 +1,43 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcErrorFactory.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Cryptography.ECC; + +namespace Neo.Plugins +{ + public static class RpcErrorFactory + { + public static RpcError WithData(this RpcError error, string data = null) + { + return new RpcError(error.Code, error.Message, data); + } + + public static RpcError NewCustomError(int code, string message, string data = null) + { + return new RpcError(code, message, data); + } + + #region Require data + + public static RpcError MethodNotFound(string method) => RpcError.MethodNotFound.WithData($"The method '{method}' doesn't exists."); + public static RpcError AlreadyExists(string data) => RpcError.AlreadyExists.WithData(data); + public static RpcError InvalidParams(string data) => RpcError.InvalidParams.WithData(data); + public static RpcError BadRequest(string data) => RpcError.BadRequest.WithData(data); + public static RpcError InsufficientFundsWallet(string data) => RpcError.InsufficientFundsWallet.WithData(data); + public static RpcError VerificationFailed(string data) => RpcError.VerificationFailed.WithData(data); + public static RpcError InvalidContractVerification(UInt160 contractHash) => RpcError.InvalidContractVerification.WithData($"The smart contract {contractHash} haven't got verify method."); + public static RpcError InvalidContractVerification(string data) => RpcError.InvalidContractVerification.WithData(data); + public static RpcError InvalidSignature(string data) => RpcError.InvalidSignature.WithData(data); + public static RpcError OracleNotDesignatedNode(ECPoint oraclePub) => RpcError.OracleNotDesignatedNode.WithData($"{oraclePub} isn't an oracle node."); + + #endregion + } +} diff --git a/src/Plugins/RpcServer/RpcException.cs b/src/Plugins/RpcServer/RpcException.cs new file mode 100644 index 0000000000..5c7b5675a1 --- /dev/null +++ b/src/Plugins/RpcServer/RpcException.cs @@ -0,0 +1,23 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcException.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.Plugins +{ + public class RpcException : Exception + { + public RpcException(RpcError error) : base(error.ErrorMessage) + { + HResult = error.Code; + } + } +} diff --git a/src/Plugins/RpcServer/RpcMethodAttribute.cs b/src/Plugins/RpcServer/RpcMethodAttribute.cs new file mode 100644 index 0000000000..89edccdd5a --- /dev/null +++ b/src/Plugins/RpcServer/RpcMethodAttribute.cs @@ -0,0 +1,21 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcMethodAttribute.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.Plugins +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class RpcMethodAttribute : Attribute + { + public string Name { get; set; } + } +} diff --git a/src/Plugins/RpcServer/RpcServer.Blockchain.cs b/src/Plugins/RpcServer/RpcServer.Blockchain.cs new file mode 100644 index 0000000000..a4fda6e738 --- /dev/null +++ b/src/Plugins/RpcServer/RpcServer.Blockchain.cs @@ -0,0 +1,339 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcServer.Blockchain.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.Json; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Neo.Plugins +{ + partial class RpcServer + { + [RpcMethod] + protected virtual JToken GetBestBlockHash(JArray _params) + { + return NativeContract.Ledger.CurrentHash(system.StoreView).ToString(); + } + + [RpcMethod] + protected virtual JToken GetBlock(JArray _params) + { + JToken key = Result.Ok_Or(() => _params[0], RpcError.InvalidParams.WithData($"Invalid Block Hash or Index: {_params[0]}")); + bool verbose = _params.Count >= 2 && _params[1].AsBoolean(); + using var snapshot = system.GetSnapshot(); + Block block; + if (key is JNumber) + { + uint index = uint.Parse(key.AsString()); + block = NativeContract.Ledger.GetBlock(snapshot, index); + } + else + { + UInt256 hash = UInt256.Parse(key.AsString()); + block = NativeContract.Ledger.GetBlock(snapshot, hash); + } + block.NotNull_Or(RpcError.UnknownBlock); + if (verbose) + { + JObject json = Utility.BlockToJson(block, system.Settings); + json["confirmations"] = NativeContract.Ledger.CurrentIndex(snapshot) - block.Index + 1; + UInt256 hash = NativeContract.Ledger.GetBlockHash(snapshot, block.Index + 1); + if (hash != null) + json["nextblockhash"] = hash.ToString(); + return json; + } + return Convert.ToBase64String(block.ToArray()); + } + + [RpcMethod] + protected virtual JToken GetBlockHeaderCount(JArray _params) + { + return (system.HeaderCache.Last?.Index ?? NativeContract.Ledger.CurrentIndex(system.StoreView)) + 1; + } + + [RpcMethod] + protected virtual JToken GetBlockCount(JArray _params) + { + return NativeContract.Ledger.CurrentIndex(system.StoreView) + 1; + } + + [RpcMethod] + protected virtual JToken GetBlockHash(JArray _params) + { + uint height = Result.Ok_Or(() => uint.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid Height: {_params[0]}")); + var snapshot = system.StoreView; + if (height <= NativeContract.Ledger.CurrentIndex(snapshot)) + { + return NativeContract.Ledger.GetBlockHash(snapshot, height).ToString(); + } + throw new RpcException(RpcError.UnknownHeight); + } + + [RpcMethod] + protected virtual JToken GetBlockHeader(JArray _params) + { + JToken key = _params[0]; + bool verbose = _params.Count >= 2 && _params[1].AsBoolean(); + var snapshot = system.StoreView; + Header header; + if (key is JNumber) + { + uint height = uint.Parse(key.AsString()); + header = NativeContract.Ledger.GetHeader(snapshot, height).NotNull_Or(RpcError.UnknownBlock); + } + else + { + UInt256 hash = UInt256.Parse(key.AsString()); + header = NativeContract.Ledger.GetHeader(snapshot, hash).NotNull_Or(RpcError.UnknownBlock); + } + if (verbose) + { + JObject json = header.ToJson(system.Settings); + json["confirmations"] = NativeContract.Ledger.CurrentIndex(snapshot) - header.Index + 1; + UInt256 hash = NativeContract.Ledger.GetBlockHash(snapshot, header.Index + 1); + if (hash != null) + json["nextblockhash"] = hash.ToString(); + return json; + } + + return Convert.ToBase64String(header.ToArray()); + } + + [RpcMethod] + protected virtual JToken GetContractState(JArray _params) + { + if (int.TryParse(_params[0].AsString(), out int contractId)) + { + var contracts = NativeContract.ContractManagement.GetContractById(system.StoreView, contractId); + return contracts?.ToJson().NotNull_Or(RpcError.UnknownContract); + } + else + { + UInt160 script_hash = ToScriptHash(_params[0].AsString()); + ContractState contract = NativeContract.ContractManagement.GetContract(system.StoreView, script_hash); + return contract?.ToJson().NotNull_Or(RpcError.UnknownContract); + } + } + + private static UInt160 ToScriptHash(string keyword) + { + foreach (var native in NativeContract.Contracts) + { + if (keyword.Equals(native.Name, StringComparison.InvariantCultureIgnoreCase) || keyword == native.Id.ToString()) + return native.Hash; + } + + return UInt160.Parse(keyword); + } + + [RpcMethod] + protected virtual JToken GetRawMemPool(JArray _params) + { + bool shouldGetUnverified = _params.Count >= 1 && _params[0].AsBoolean(); + if (!shouldGetUnverified) + return new JArray(system.MemPool.GetVerifiedTransactions().Select(p => (JToken)p.Hash.ToString())); + + JObject json = new(); + json["height"] = NativeContract.Ledger.CurrentIndex(system.StoreView); + system.MemPool.GetVerifiedAndUnverifiedTransactions( + out IEnumerable verifiedTransactions, + out IEnumerable unverifiedTransactions); + json["verified"] = new JArray(verifiedTransactions.Select(p => (JToken)p.Hash.ToString())); + json["unverified"] = new JArray(unverifiedTransactions.Select(p => (JToken)p.Hash.ToString())); + return json; + } + + [RpcMethod] + protected virtual JToken GetRawTransaction(JArray _params) + { + UInt256 hash = Result.Ok_Or(() => UInt256.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid Transaction Hash: {_params[0]}")); + bool verbose = _params.Count >= 2 && _params[1].AsBoolean(); + if (system.MemPool.TryGetValue(hash, out Transaction tx) && !verbose) + return Convert.ToBase64String(tx.ToArray()); + var snapshot = system.StoreView; + TransactionState state = NativeContract.Ledger.GetTransactionState(snapshot, hash); + tx ??= state?.Transaction; + tx.NotNull_Or(RpcError.UnknownTransaction); + if (!verbose) return Convert.ToBase64String(tx.ToArray()); + JObject json = Utility.TransactionToJson(tx, system.Settings); + if (state is not null) + { + TrimmedBlock block = NativeContract.Ledger.GetTrimmedBlock(snapshot, NativeContract.Ledger.GetBlockHash(snapshot, state.BlockIndex)); + json["blockhash"] = block.Hash.ToString(); + json["confirmations"] = NativeContract.Ledger.CurrentIndex(snapshot) - block.Index + 1; + json["blocktime"] = block.Header.Timestamp; + } + return json; + } + + [RpcMethod] + protected virtual JToken GetStorage(JArray _params) + { + using var snapshot = system.GetSnapshot(); + if (!int.TryParse(_params[0].AsString(), out int id)) + { + UInt160 hash = UInt160.Parse(_params[0].AsString()); + ContractState contract = NativeContract.ContractManagement.GetContract(snapshot, hash).NotNull_Or(RpcError.UnknownContract); + id = contract.Id; + } + byte[] key = Convert.FromBase64String(_params[1].AsString()); + StorageItem item = snapshot.TryGet(new StorageKey + { + Id = id, + Key = key + }).NotNull_Or(RpcError.UnknownStorageItem); + return Convert.ToBase64String(item.Value.Span); + } + + [RpcMethod] + protected virtual JToken FindStorage(JArray _params) + { + using var snapshot = system.GetSnapshot(); + if (!int.TryParse(_params[0].AsString(), out int id)) + { + UInt160 hash = UInt160.Parse(_params[0].AsString()); + ContractState contract = NativeContract.ContractManagement.GetContract(snapshot, hash).NotNull_Or(RpcError.UnknownContract); + id = contract.Id; + } + + byte[] prefix = Convert.FromBase64String(_params[1].AsString()); + byte[] prefix_key = StorageKey.CreateSearchPrefix(id, prefix); + + if (!int.TryParse(_params[2].AsString(), out int start)) + { + start = 0; + } + + JObject json = new(); + JArray jarr = new(); + int pageSize = settings.FindStoragePageSize; + int i = 0; + + using (var iter = snapshot.Find(prefix_key).Skip(count: start).GetEnumerator()) + { + var hasMore = false; + while (iter.MoveNext()) + { + if (i == pageSize) + { + hasMore = true; + break; + } + + JObject j = new(); + j["key"] = Convert.ToBase64String(iter.Current.Key.Key.Span); + j["value"] = Convert.ToBase64String(iter.Current.Value.Value.Span); + jarr.Add(j); + i++; + } + json["truncated"] = hasMore; + } + + json["next"] = start + i; + json["results"] = jarr; + return json; + } + + [RpcMethod] + protected virtual JToken GetTransactionHeight(JArray _params) + { + UInt256 hash = Result.Ok_Or(() => UInt256.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid Transaction Hash: {_params[0]}")); + uint? height = NativeContract.Ledger.GetTransactionState(system.StoreView, hash)?.BlockIndex; + if (height.HasValue) return height.Value; + throw new RpcException(RpcError.UnknownTransaction); + } + + [RpcMethod] + protected virtual JToken GetNextBlockValidators(JArray _params) + { + using var snapshot = system.GetSnapshot(); + var validators = NativeContract.NEO.GetNextBlockValidators(snapshot, system.Settings.ValidatorsCount); + return validators.Select(p => + { + JObject validator = new(); + validator["publickey"] = p.ToString(); + validator["votes"] = (int)NativeContract.NEO.GetCandidateVote(snapshot, p); + return validator; + }).ToArray(); + } + + [RpcMethod] + protected virtual JToken GetCandidates(JArray _params) + { + using var snapshot = system.GetSnapshot(); + byte[] script; + using (ScriptBuilder sb = new()) + { + script = sb.EmitDynamicCall(NativeContract.NEO.Hash, "getCandidates", null).ToArray(); + } + StackItem[] resultstack; + try + { + using ApplicationEngine engine = ApplicationEngine.Run(script, snapshot, settings: system.Settings, gas: settings.MaxGasInvoke); + resultstack = engine.ResultStack.ToArray(); + } + catch + { + throw new RpcException(RpcError.InternalServerError.WithData("Can't get candidates.")); + } + + JObject json = new(); + try + { + if (resultstack.Length > 0) + { + JArray jArray = new(); + var validators = NativeContract.NEO.GetNextBlockValidators(snapshot, system.Settings.ValidatorsCount) ?? throw new RpcException(RpcError.InternalServerError.WithData("Can't get next block validators.")); + + foreach (var item in resultstack) + { + var value = (VM.Types.Array)item; + foreach (Struct ele in value) + { + var publickey = ele[0].GetSpan().ToHexString(); + json["publickey"] = publickey; + json["votes"] = ele[1].GetInteger().ToString(); + json["active"] = validators.ToByteArray().ToHexString().Contains(publickey); + jArray.Add(json); + json = new(); + } + return jArray; + } + } + } + catch + { + throw new RpcException(RpcError.InternalServerError.WithData("Can't get next block validators")); + } + + return json; + } + + [RpcMethod] + protected virtual JToken GetCommittee(JArray _params) + { + return new JArray(NativeContract.NEO.GetCommittee(system.StoreView).Select(p => (JToken)p.ToString())); + } + + [RpcMethod] + protected virtual JToken GetNativeContracts(JArray _params) + { + return new JArray(NativeContract.Contracts.Select(p => NativeContract.ContractManagement.GetContract(system.StoreView, p.Hash).ToJson())); + } + } +} diff --git a/src/Plugins/RpcServer/RpcServer.Node.cs b/src/Plugins/RpcServer/RpcServer.Node.cs new file mode 100644 index 0000000000..c455100802 --- /dev/null +++ b/src/Plugins/RpcServer/RpcServer.Node.cs @@ -0,0 +1,168 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcServer.Node.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Akka.Actor; +using Neo.IO; +using Neo.Json; +using Neo.Ledger; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using System; +using System.Linq; +using static Neo.Ledger.Blockchain; + +namespace Neo.Plugins +{ + partial class RpcServer + { + [RpcMethod] + protected virtual JToken GetConnectionCount(JArray _params) + { + return localNode.ConnectedCount; + } + + [RpcMethod] + protected virtual JToken GetPeers(JArray _params) + { + JObject json = new(); + json["unconnected"] = new JArray(localNode.GetUnconnectedPeers().Select(p => + { + JObject peerJson = new(); + peerJson["address"] = p.Address.ToString(); + peerJson["port"] = p.Port; + return peerJson; + })); + json["bad"] = new JArray(); //badpeers has been removed + json["connected"] = new JArray(localNode.GetRemoteNodes().Select(p => + { + JObject peerJson = new(); + peerJson["address"] = p.Remote.Address.ToString(); + peerJson["port"] = p.ListenerTcpPort; + return peerJson; + })); + return json; + } + + private static JObject GetRelayResult(VerifyResult reason, UInt256 hash) + { + + switch (reason) + { + case VerifyResult.Succeed: + { + var ret = new JObject(); + ret["hash"] = hash.ToString(); + return ret; + } + case VerifyResult.AlreadyExists: + { + throw new RpcException(RpcError.AlreadyExists.WithData(reason.ToString())); + } + case VerifyResult.AlreadyInPool: + { + throw new RpcException(RpcError.AlreadyInPool.WithData(reason.ToString())); + } + case VerifyResult.OutOfMemory: + { + throw new RpcException(RpcError.MempoolCapReached.WithData(reason.ToString())); + } + case VerifyResult.InvalidScript: + { + throw new RpcException(RpcError.InvalidScript.WithData(reason.ToString())); + } + case VerifyResult.InvalidAttribute: + { + throw new RpcException(RpcError.InvalidAttribute.WithData(reason.ToString())); + } + case VerifyResult.InvalidSignature: + { + throw new RpcException(RpcError.InvalidSignature.WithData(reason.ToString())); + } + case VerifyResult.OverSize: + { + throw new RpcException(RpcError.InvalidSize.WithData(reason.ToString())); + } + case VerifyResult.Expired: + { + throw new RpcException(RpcError.ExpiredTransaction.WithData(reason.ToString())); + } + case VerifyResult.InsufficientFunds: + { + throw new RpcException(RpcError.InsufficientFunds.WithData(reason.ToString())); + } + case VerifyResult.PolicyFail: + { + throw new RpcException(RpcError.PolicyFailed.WithData(reason.ToString())); + } + default: + { + throw new RpcException(RpcError.VerificationFailed.WithData(reason.ToString())); + } + } + } + + [RpcMethod] + protected virtual JToken GetVersion(JArray _params) + { + JObject json = new(); + json["tcpport"] = localNode.ListenerTcpPort; + json["nonce"] = LocalNode.Nonce; + json["useragent"] = LocalNode.UserAgent; + // rpc settings + JObject rpc = new(); + rpc["maxiteratorresultitems"] = settings.MaxIteratorResultItems; + rpc["sessionenabled"] = settings.SessionEnabled; + // protocol settings + JObject protocol = new(); + protocol["addressversion"] = system.Settings.AddressVersion; + protocol["network"] = system.Settings.Network; + protocol["validatorscount"] = system.Settings.ValidatorsCount; + protocol["msperblock"] = system.Settings.MillisecondsPerBlock; + protocol["maxtraceableblocks"] = system.Settings.MaxTraceableBlocks; + protocol["maxvaliduntilblockincrement"] = system.Settings.MaxValidUntilBlockIncrement; + protocol["maxtransactionsperblock"] = system.Settings.MaxTransactionsPerBlock; + protocol["memorypoolmaxtransactions"] = system.Settings.MemoryPoolMaxTransactions; + protocol["initialgasdistribution"] = system.Settings.InitialGasDistribution; + protocol["hardforks"] = new JArray(system.Settings.Hardforks.Select(hf => + { + JObject forkJson = new(); + // Strip "HF_" prefix. + forkJson["name"] = StripPrefix(hf.Key.ToString(), "HF_"); + forkJson["blockheight"] = hf.Value; + return forkJson; + })); + json["rpc"] = rpc; + json["protocol"] = protocol; + return json; + } + + private static string StripPrefix(string s, string prefix) + { + return s.StartsWith(prefix) ? s.Substring(prefix.Length) : s; + } + + [RpcMethod] + protected virtual JToken SendRawTransaction(JArray _params) + { + Transaction tx = Result.Ok_Or(() => Convert.FromBase64String(_params[0].AsString()).AsSerializable(), RpcError.InvalidParams.WithData($"Invalid Transaction Format: {_params[0]}")); + RelayResult reason = system.Blockchain.Ask(tx).Result; + return GetRelayResult(reason.Result, tx.Hash); + } + + [RpcMethod] + protected virtual JToken SubmitBlock(JArray _params) + { + Block block = Result.Ok_Or(() => Convert.FromBase64String(_params[0].AsString()).AsSerializable(), RpcError.InvalidParams.WithData($"Invalid Block Format: {_params[0]}")); + RelayResult reason = system.Blockchain.Ask(block).Result; + return GetRelayResult(reason.Result, block.Hash); + } + } +} diff --git a/src/Plugins/RpcServer/RpcServer.SmartContract.cs b/src/Plugins/RpcServer/RpcServer.SmartContract.cs new file mode 100644 index 0000000000..92ea7ff62c --- /dev/null +++ b/src/Plugins/RpcServer/RpcServer.SmartContract.cs @@ -0,0 +1,297 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcServer.SmartContract.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Cryptography.ECC; +using Neo.IO; +using Neo.Json; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Iterators; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using Neo.Wallets; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Array = System.Array; + +namespace Neo.Plugins +{ + partial class RpcServer + { + private readonly Dictionary sessions = new(); + private Timer timer; + + private void Initialize_SmartContract() + { + if (settings.SessionEnabled) + timer = new(OnTimer, null, settings.SessionExpirationTime, settings.SessionExpirationTime); + } + + private void Dispose_SmartContract() + { + timer?.Dispose(); + Session[] toBeDestroyed; + lock (sessions) + { + toBeDestroyed = sessions.Values.ToArray(); + sessions.Clear(); + } + foreach (Session session in toBeDestroyed) + session.Dispose(); + } + + private void OnTimer(object state) + { + List<(Guid Id, Session Session)> toBeDestroyed = new(); + lock (sessions) + { + foreach (var (id, session) in sessions) + if (DateTime.UtcNow >= session.StartTime + settings.SessionExpirationTime) + toBeDestroyed.Add((id, session)); + foreach (var (id, _) in toBeDestroyed) + sessions.Remove(id); + } + foreach (var (_, session) in toBeDestroyed) + session.Dispose(); + } + + private JObject GetInvokeResult(byte[] script, Signer[] signers = null, Witness[] witnesses = null, bool useDiagnostic = false) + { + JObject json = new(); + Session session = new(system, script, signers, witnesses, settings.MaxGasInvoke, useDiagnostic ? new Diagnostic() : null); + try + { + json["script"] = Convert.ToBase64String(script); + json["state"] = session.Engine.State; + json["gasconsumed"] = session.Engine.GasConsumed.ToString(); + json["exception"] = GetExceptionMessage(session.Engine.FaultException); + json["notifications"] = new JArray(session.Engine.Notifications.Select(n => + { + var obj = new JObject(); + obj["eventname"] = n.EventName; + obj["contract"] = n.ScriptHash.ToString(); + obj["state"] = ToJson(n.State, session); + return obj; + })); + if (useDiagnostic) + { + Diagnostic diagnostic = (Diagnostic)session.Engine.Diagnostic; + json["diagnostics"] = new JObject() + { + ["invokedcontracts"] = ToJson(diagnostic.InvocationTree.Root), + ["storagechanges"] = ToJson(session.Engine.Snapshot.GetChangeSet()) + }; + } + var stack = new JArray(); + foreach (var item in session.Engine.ResultStack) + { + try + { + stack.Add(ToJson(item, session)); + } + catch (Exception ex) + { + stack.Add("error: " + ex.Message); + } + } + json["stack"] = stack; + if (session.Engine.State != VMState.FAULT) + { + ProcessInvokeWithWallet(json, signers); + } + } + catch + { + session.Dispose(); + throw; + } + if (session.Iterators.Count == 0 || !settings.SessionEnabled) + { + session.Dispose(); + } + else + { + Guid id = Guid.NewGuid(); + json["session"] = id.ToString(); + lock (sessions) + sessions.Add(id, session); + } + return json; + } + + private static JObject ToJson(TreeNode node) + { + JObject json = new(); + json["hash"] = node.Item.ToString(); + if (node.Children.Any()) + { + json["call"] = new JArray(node.Children.Select(ToJson)); + } + return json; + } + + private static JArray ToJson(IEnumerable changes) + { + JArray array = new(); + foreach (var entry in changes) + { + array.Add(new JObject + { + ["state"] = entry.State.ToString(), + ["key"] = Convert.ToBase64String(entry.Key.ToArray()), + ["value"] = Convert.ToBase64String(entry.Item.Value.ToArray()) + }); + } + return array; + } + + private static JObject ToJson(StackItem item, Session session) + { + JObject json = item.ToJson(); + if (item is InteropInterface interopInterface && interopInterface.GetInterface() is IIterator iterator) + { + Guid id = Guid.NewGuid(); + session.Iterators.Add(id, iterator); + json["interface"] = nameof(IIterator); + json["id"] = id.ToString(); + } + return json; + } + + private static Signer[] SignersFromJson(JArray _params, ProtocolSettings settings) + { + if (_params.Count > Transaction.MaxTransactionAttributes) + { + throw new RpcException(RpcError.InvalidParams.WithData("Max allowed witness exceeded.")); + } + + var ret = _params.Select(u => new Signer + { + Account = AddressToScriptHash(u["account"].AsString(), settings.AddressVersion), + Scopes = (WitnessScope)Enum.Parse(typeof(WitnessScope), u["scopes"]?.AsString()), + AllowedContracts = ((JArray)u["allowedcontracts"])?.Select(p => UInt160.Parse(p.AsString())).ToArray() ?? Array.Empty(), + AllowedGroups = ((JArray)u["allowedgroups"])?.Select(p => ECPoint.Parse(p.AsString(), ECCurve.Secp256r1)).ToArray() ?? Array.Empty(), + Rules = ((JArray)u["rules"])?.Select(r => WitnessRule.FromJson((JObject)r)).ToArray() ?? Array.Empty(), + }).ToArray(); + + // Validate format + + _ = IO.Helper.ToByteArray(ret).AsSerializableArray(); + + return ret; + } + + private static Witness[] WitnessesFromJson(JArray _params) + { + if (_params.Count > Transaction.MaxTransactionAttributes) + { + throw new RpcException(RpcError.InvalidParams.WithData("Max allowed witness exceeded.")); + } + + return _params.Select(u => new + { + Invocation = u["invocation"]?.AsString(), + Verification = u["verification"]?.AsString() + }).Where(x => x.Invocation != null || x.Verification != null).Select(x => new Witness() + { + InvocationScript = Convert.FromBase64String(x.Invocation ?? string.Empty), + VerificationScript = Convert.FromBase64String(x.Verification ?? string.Empty) + }).ToArray(); + } + + [RpcMethod] + protected virtual JToken InvokeFunction(JArray _params) + { + UInt160 script_hash = Result.Ok_Or(() => UInt160.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid script hash {nameof(script_hash)}")); + string operation = Result.Ok_Or(() => _params[1].AsString(), RpcError.InvalidParams); + ContractParameter[] args = _params.Count >= 3 ? ((JArray)_params[2]).Select(p => ContractParameter.FromJson((JObject)p)).ToArray() : System.Array.Empty(); + Signer[] signers = _params.Count >= 4 ? SignersFromJson((JArray)_params[3], system.Settings) : null; + Witness[] witnesses = _params.Count >= 4 ? WitnessesFromJson((JArray)_params[3]) : null; + bool useDiagnostic = _params.Count >= 5 && _params[4].GetBoolean(); + + byte[] script; + using (ScriptBuilder sb = new()) + { + script = sb.EmitDynamicCall(script_hash, operation, args).ToArray(); + } + return GetInvokeResult(script, signers, witnesses, useDiagnostic); + } + + [RpcMethod] + protected virtual JToken InvokeScript(JArray _params) + { + byte[] script = Result.Ok_Or(() => Convert.FromBase64String(_params[0].AsString()), RpcError.InvalidParams); + Signer[] signers = _params.Count >= 2 ? SignersFromJson((JArray)_params[1], system.Settings) : null; + Witness[] witnesses = _params.Count >= 2 ? WitnessesFromJson((JArray)_params[1]) : null; + bool useDiagnostic = _params.Count >= 3 && _params[2].GetBoolean(); + return GetInvokeResult(script, signers, witnesses, useDiagnostic); + } + + [RpcMethod] + protected virtual JToken TraverseIterator(JArray _params) + { + settings.SessionEnabled.True_Or(RpcError.SessionsDisabled); + Guid sid = Result.Ok_Or(() => Guid.Parse(_params[0].GetString()), RpcError.InvalidParams.WithData($"Invalid session id {nameof(sid)}")); + Guid iid = Result.Ok_Or(() => Guid.Parse(_params[1].GetString()), RpcError.InvalidParams.WithData($"Invliad iterator id {nameof(iid)}")); + int count = _params[2].GetInt32(); + Result.True_Or(() => count <= settings.MaxIteratorResultItems, RpcError.InvalidParams.WithData($"Invalid iterator items count {nameof(count)}")); + Session session; + lock (sessions) + { + session = Result.Ok_Or(() => sessions[sid], RpcError.UnknownSession); + session.ResetExpiration(); + } + IIterator iterator = Result.Ok_Or(() => session.Iterators[iid], RpcError.UnknownIterator); + JArray json = new(); + while (count-- > 0 && iterator.Next()) + json.Add(iterator.Value(null).ToJson()); + return json; + } + + [RpcMethod] + protected virtual JToken TerminateSession(JArray _params) + { + settings.SessionEnabled.True_Or(RpcError.SessionsDisabled); + Guid sid = Result.Ok_Or(() => Guid.Parse(_params[0].GetString()), RpcError.InvalidParams.WithData("Invalid session id")); + + Session session = null; + bool result; + lock (sessions) + { + result = Result.Ok_Or(() => sessions.Remove(sid, out session), RpcError.UnknownSession); + } + if (result) session.Dispose(); + return result; + } + + [RpcMethod] + protected virtual JToken GetUnclaimedGas(JArray _params) + { + string address = Result.Ok_Or(() => _params[0].AsString(), RpcError.InvalidParams.WithData($"Invalid address {nameof(address)}")); + JObject json = new(); + UInt160 script_hash = Result.Ok_Or(() => AddressToScriptHash(address, system.Settings.AddressVersion), RpcError.InvalidParams); + + var snapshot = system.StoreView; + json["unclaimed"] = NativeContract.NEO.UnclaimedGas(snapshot, script_hash, NativeContract.Ledger.CurrentIndex(snapshot) + 1).ToString(); + json["address"] = script_hash.ToAddress(system.Settings.AddressVersion); + return json; + } + + static string GetExceptionMessage(Exception exception) + { + return exception?.GetBaseException().Message; + } + } +} diff --git a/src/Plugins/RpcServer/RpcServer.Utilities.cs b/src/Plugins/RpcServer/RpcServer.Utilities.cs new file mode 100644 index 0000000000..f410e01b73 --- /dev/null +++ b/src/Plugins/RpcServer/RpcServer.Utilities.cs @@ -0,0 +1,55 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcServer.Utilities.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.Json; +using Neo.Wallets; +using System.Linq; + +namespace Neo.Plugins +{ + partial class RpcServer + { + [RpcMethod] + protected virtual JToken ListPlugins(JArray _params) + { + return new JArray(Plugin.Plugins + .OrderBy(u => u.Name) + .Select(u => new JObject + { + ["name"] = u.Name, + ["version"] = u.Version.ToString(), + ["interfaces"] = new JArray(u.GetType().GetInterfaces() + .Select(p => p.Name) + .Where(p => p.EndsWith("Plugin")) + .Select(p => (JToken)p)) + })); + } + + [RpcMethod] + protected virtual JToken ValidateAddress(JArray _params) + { + string address = Result.Ok_Or(() => _params[0].AsString(), RpcError.InvalidParams.WithData($"Invlid address format: {_params[0]}")); + JObject json = new(); + UInt160 scriptHash; + try + { + scriptHash = address.ToScriptHash(system.Settings.AddressVersion); + } + catch + { + scriptHash = null; + } + json["address"] = address; + json["isvalid"] = scriptHash != null; + return json; + } + } +} diff --git a/src/Plugins/RpcServer/RpcServer.Wallet.cs b/src/Plugins/RpcServer/RpcServer.Wallet.cs new file mode 100644 index 0000000000..36825b3f82 --- /dev/null +++ b/src/Plugins/RpcServer/RpcServer.Wallet.cs @@ -0,0 +1,423 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcServer.Wallet.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.Json; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.Wallets; +using Neo.Wallets.NEP6; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; + +namespace Neo.Plugins +{ + partial class RpcServer + { + private class DummyWallet : Wallet + { + public DummyWallet(ProtocolSettings settings) : base(null, settings) { } + public override string Name => ""; + public override Version Version => new(); + + public override bool ChangePassword(string oldPassword, string newPassword) => false; + public override bool Contains(UInt160 scriptHash) => false; + public override WalletAccount CreateAccount(byte[] privateKey) => null; + public override WalletAccount CreateAccount(Contract contract, KeyPair key = null) => null; + public override WalletAccount CreateAccount(UInt160 scriptHash) => null; + public override void Delete() { } + public override bool DeleteAccount(UInt160 scriptHash) => false; + public override WalletAccount GetAccount(UInt160 scriptHash) => null; + public override IEnumerable GetAccounts() => Array.Empty(); + public override bool VerifyPassword(string password) => false; + public override void Save() { } + } + + protected Wallet wallet; + + private void CheckWallet() + { + wallet.NotNull_Or(RpcError.NoOpenedWallet); + } + + [RpcMethod] + protected virtual JToken CloseWallet(JArray _params) + { + wallet = null; + return true; + } + + [RpcMethod] + protected virtual JToken DumpPrivKey(JArray _params) + { + CheckWallet(); + UInt160 scriptHash = AddressToScriptHash(_params[0].AsString(), system.Settings.AddressVersion); + WalletAccount account = wallet.GetAccount(scriptHash); + return account.GetKey().Export(); + } + + [RpcMethod] + protected virtual JToken GetNewAddress(JArray _params) + { + CheckWallet(); + WalletAccount account = wallet.CreateAccount(); + if (wallet is NEP6Wallet nep6) + nep6.Save(); + return account.Address; + } + + [RpcMethod] + protected virtual JToken GetWalletBalance(JArray _params) + { + CheckWallet(); + UInt160 asset_id = UInt160.Parse(_params[0].AsString()); + JObject json = new(); + json["balance"] = wallet.GetAvailable(system.StoreView, asset_id).Value.ToString(); + return json; + } + + [RpcMethod] + protected virtual JToken GetWalletUnclaimedGas(JArray _params) + { + CheckWallet(); + BigInteger gas = BigInteger.Zero; + using (var snapshot = system.GetSnapshot()) + { + uint height = NativeContract.Ledger.CurrentIndex(snapshot) + 1; + foreach (UInt160 account in wallet.GetAccounts().Select(p => p.ScriptHash)) + gas += NativeContract.NEO.UnclaimedGas(snapshot, account, height); + } + return gas.ToString(); + } + + [RpcMethod] + protected virtual JToken ImportPrivKey(JArray _params) + { + CheckWallet(); + string privkey = _params[0].AsString(); + WalletAccount account = wallet.Import(privkey); + if (wallet is NEP6Wallet nep6wallet) + nep6wallet.Save(); + return new JObject + { + ["address"] = account.Address, + ["haskey"] = account.HasKey, + ["label"] = account.Label, + ["watchonly"] = account.WatchOnly + }; + } + + [RpcMethod] + protected virtual JToken CalculateNetworkFee(JArray _params) + { + var tx = Convert.FromBase64String(_params[0].AsString()); + + JObject account = new(); + var networkfee = Wallets.Helper.CalculateNetworkFee( + tx.AsSerializable(), system.StoreView, system.Settings, + wallet is not null ? a => wallet.GetAccount(a).Contract.Script : _ => null); + account["networkfee"] = networkfee.ToString(); + return account; + } + + [RpcMethod] + protected virtual JToken ListAddress(JArray _params) + { + CheckWallet(); + return wallet.GetAccounts().Select(p => + { + JObject account = new(); + account["address"] = p.Address; + account["haskey"] = p.HasKey; + account["label"] = p.Label; + account["watchonly"] = p.WatchOnly; + return account; + }).ToArray(); + } + + [RpcMethod] + protected virtual JToken OpenWallet(JArray _params) + { + string path = _params[0].AsString(); + string password = _params[1].AsString(); + File.Exists(path).True_Or(RpcError.WalletNotFound); + wallet = Wallet.Open(path, password, system.Settings).NotNull_Or(RpcError.WalletNotSupported); + return true; + } + + private void ProcessInvokeWithWallet(JObject result, Signer[] signers = null) + { + if (wallet == null || signers == null || signers.Length == 0) return; + + UInt160 sender = signers[0].Account; + Transaction tx; + try + { + tx = wallet.MakeTransaction(system.StoreView, Convert.FromBase64String(result["script"].AsString()), sender, signers, maxGas: settings.MaxGasInvoke); + } + catch (Exception e) + { + result["exception"] = GetExceptionMessage(e); + return; + } + ContractParametersContext context = new(system.StoreView, tx, settings.Network); + wallet.Sign(context); + if (context.Completed) + { + tx.Witnesses = context.GetWitnesses(); + result["tx"] = Convert.ToBase64String(tx.ToArray()); + } + else + { + result["pendingsignature"] = context.ToJson(); + } + } + + [RpcMethod] + protected virtual JToken SendFrom(JArray _params) + { + CheckWallet(); + UInt160 assetId = UInt160.Parse(_params[0].AsString()); + UInt160 from = AddressToScriptHash(_params[1].AsString(), system.Settings.AddressVersion); + UInt160 to = AddressToScriptHash(_params[2].AsString(), system.Settings.AddressVersion); + using var snapshot = system.GetSnapshot(); + AssetDescriptor descriptor = new(snapshot, system.Settings, assetId); + BigDecimal amount = new(BigInteger.Parse(_params[3].AsString()), descriptor.Decimals); + (amount.Sign > 0).True_Or(RpcErrorFactory.InvalidParams("Amount can't be negative.")); + Signer[] signers = _params.Count >= 5 ? ((JArray)_params[4]).Select(p => new Signer() { Account = AddressToScriptHash(p.AsString(), system.Settings.AddressVersion), Scopes = WitnessScope.CalledByEntry }).ToArray() : null; + + Transaction tx = wallet.MakeTransaction(snapshot, new[] + { + new TransferOutput + { + AssetId = assetId, + Value = amount, + ScriptHash = to + } + }, from, signers).NotNull_Or(RpcError.InsufficientFunds); + + ContractParametersContext transContext = new(snapshot, tx, settings.Network); + wallet.Sign(transContext); + if (!transContext.Completed) + return transContext.ToJson(); + tx.Witnesses = transContext.GetWitnesses(); + if (tx.Size > 1024) + { + long calFee = tx.Size * NativeContract.Policy.GetFeePerByte(snapshot) + 100000; + if (tx.NetworkFee < calFee) + tx.NetworkFee = calFee; + } + (tx.NetworkFee <= settings.MaxFee).True_Or(RpcError.WalletFeeLimit); + return SignAndRelay(snapshot, tx); + } + + [RpcMethod] + protected virtual JToken SendMany(JArray _params) + { + CheckWallet(); + int to_start = 0; + UInt160 from = null; + if (_params[0] is JString) + { + from = AddressToScriptHash(_params[0].AsString(), system.Settings.AddressVersion); + to_start = 1; + } + JArray to = Result.Ok_Or(() => (JArray)_params[to_start], RpcError.InvalidParams.WithData($"Invalid 'to' parameter: {_params[to_start]}")); + (to.Count != 0).True_Or(RpcErrorFactory.InvalidParams("Argument 'to' can't be empty.")); + Signer[] signers = _params.Count >= to_start + 2 ? ((JArray)_params[to_start + 1]).Select(p => new Signer() { Account = AddressToScriptHash(p.AsString(), system.Settings.AddressVersion), Scopes = WitnessScope.CalledByEntry }).ToArray() : null; + + TransferOutput[] outputs = new TransferOutput[to.Count]; + using var snapshot = system.GetSnapshot(); + for (int i = 0; i < to.Count; i++) + { + UInt160 asset_id = UInt160.Parse(to[i]["asset"].AsString()); + AssetDescriptor descriptor = new(snapshot, system.Settings, asset_id); + outputs[i] = new TransferOutput + { + AssetId = asset_id, + Value = new BigDecimal(BigInteger.Parse(to[i]["value"].AsString()), descriptor.Decimals), + ScriptHash = AddressToScriptHash(to[i]["address"].AsString(), system.Settings.AddressVersion) + }; + (outputs[i].Value.Sign > 0).True_Or(RpcErrorFactory.InvalidParams($"Amount of '{asset_id}' can't be negative.")); + } + Transaction tx = wallet.MakeTransaction(snapshot, outputs, from, signers).NotNull_Or(RpcError.InsufficientFunds); + + ContractParametersContext transContext = new(snapshot, tx, settings.Network); + wallet.Sign(transContext); + if (!transContext.Completed) + return transContext.ToJson(); + tx.Witnesses = transContext.GetWitnesses(); + if (tx.Size > 1024) + { + long calFee = tx.Size * NativeContract.Policy.GetFeePerByte(snapshot) + 100000; + if (tx.NetworkFee < calFee) + tx.NetworkFee = calFee; + } + (tx.NetworkFee <= settings.MaxFee).True_Or(RpcError.WalletFeeLimit); + return SignAndRelay(snapshot, tx); + } + + [RpcMethod] + protected virtual JToken SendToAddress(JArray _params) + { + CheckWallet(); + UInt160 assetId = Result.Ok_Or(() => UInt160.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid asset hash: {_params[0]}")); + UInt160 to = AddressToScriptHash(_params[1].AsString(), system.Settings.AddressVersion); + using var snapshot = system.GetSnapshot(); + AssetDescriptor descriptor = new(snapshot, system.Settings, assetId); + BigDecimal amount = new(BigInteger.Parse(_params[2].AsString()), descriptor.Decimals); + (amount.Sign > 0).True_Or(RpcError.InvalidParams); + Transaction tx = wallet.MakeTransaction(snapshot, new[] + { + new TransferOutput + { + AssetId = assetId, + Value = amount, + ScriptHash = to + } + }).NotNull_Or(RpcError.InsufficientFunds); + + ContractParametersContext transContext = new(snapshot, tx, settings.Network); + wallet.Sign(transContext); + if (!transContext.Completed) + return transContext.ToJson(); + tx.Witnesses = transContext.GetWitnesses(); + if (tx.Size > 1024) + { + long calFee = tx.Size * NativeContract.Policy.GetFeePerByte(snapshot) + 100000; + if (tx.NetworkFee < calFee) + tx.NetworkFee = calFee; + } + (tx.NetworkFee <= settings.MaxFee).True_Or(RpcError.WalletFeeLimit); + return SignAndRelay(snapshot, tx); + } + + [RpcMethod] + protected virtual JToken CancelTransaction(JArray _params) + { + CheckWallet(); + var txid = Result.Ok_Or(() => UInt256.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid txid: {_params[0]}")); + NativeContract.Ledger.GetTransactionState(system.StoreView, txid).Null_Or(RpcErrorFactory.AlreadyExists("This tx is already confirmed, can't be cancelled.")); + + var conflict = new TransactionAttribute[] { new Conflicts() { Hash = txid } }; + Signer[] signers = _params.Count >= 2 ? ((JArray)_params[1]).Select(j => new Signer() { Account = AddressToScriptHash(j.AsString(), system.Settings.AddressVersion), Scopes = WitnessScope.None }).ToArray() : Array.Empty(); + signers.Any().True_Or(RpcErrorFactory.BadRequest("No signer.")); + Transaction tx = new Transaction + { + Signers = signers, + Attributes = conflict, + Witnesses = Array.Empty(), + }; + + tx = Result.Ok_Or(() => wallet.MakeTransaction(system.StoreView, new[] { (byte)OpCode.RET }, signers[0].Account, signers, conflict), RpcError.InsufficientFunds, true); + + if (system.MemPool.TryGetValue(txid, out Transaction conflictTx)) + { + tx.NetworkFee = Math.Max(tx.NetworkFee, conflictTx.NetworkFee) + 1; + } + else if (_params.Count >= 3) + { + var extraFee = _params[2].AsString(); + AssetDescriptor descriptor = new(system.StoreView, system.Settings, NativeContract.GAS.Hash); + (BigDecimal.TryParse(extraFee, descriptor.Decimals, out BigDecimal decimalExtraFee) && decimalExtraFee.Sign > 0).True_Or(RpcErrorFactory.InvalidParams("Incorrect amount format.")); + + tx.NetworkFee += (long)decimalExtraFee.Value; + }; + return SignAndRelay(system.StoreView, tx); + } + + [RpcMethod] + protected virtual JToken InvokeContractVerify(JArray _params) + { + UInt160 script_hash = Result.Ok_Or(() => UInt160.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid script hash: {_params[0]}")); + ContractParameter[] args = _params.Count >= 2 ? ((JArray)_params[1]).Select(p => ContractParameter.FromJson((JObject)p)).ToArray() : Array.Empty(); + Signer[] signers = _params.Count >= 3 ? SignersFromJson((JArray)_params[2], system.Settings) : null; + Witness[] witnesses = _params.Count >= 3 ? WitnessesFromJson((JArray)_params[2]) : null; + return GetVerificationResult(script_hash, args, signers, witnesses); + } + + private JObject GetVerificationResult(UInt160 scriptHash, ContractParameter[] args, Signer[] signers = null, Witness[] witnesses = null) + { + using var snapshot = system.GetSnapshot(); + var contract = NativeContract.ContractManagement.GetContract(snapshot, scriptHash).NotNull_Or(RpcError.UnknownContract); + var md = contract.Manifest.Abi.GetMethod("verify", -1).NotNull_Or(RpcErrorFactory.InvalidContractVerification(contract.Hash)); + (md.ReturnType == ContractParameterType.Boolean).True_Or(RpcErrorFactory.InvalidContractVerification("The verify method doesn't return boolean value.")); + Transaction tx = new() + { + Signers = signers ?? new Signer[] { new() { Account = scriptHash } }, + Attributes = Array.Empty(), + Witnesses = witnesses, + Script = new[] { (byte)OpCode.RET } + }; + using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshot.CreateSnapshot(), settings: system.Settings); + engine.LoadContract(contract, md, CallFlags.ReadOnly); + + var invocationScript = Array.Empty(); + if (args.Length > 0) + { + using ScriptBuilder sb = new(); + for (int i = args.Length - 1; i >= 0; i--) + sb.EmitPush(args[i]); + + invocationScript = sb.ToArray(); + tx.Witnesses ??= new Witness[] { new() { InvocationScript = invocationScript } }; + engine.LoadScript(new Script(invocationScript), configureState: p => p.CallFlags = CallFlags.None); + } + JObject json = new(); + json["script"] = Convert.ToBase64String(invocationScript); + json["state"] = engine.Execute(); + json["gasconsumed"] = engine.GasConsumed.ToString(); + json["exception"] = GetExceptionMessage(engine.FaultException); + try + { + json["stack"] = new JArray(engine.ResultStack.Select(p => p.ToJson(settings.MaxStackSize))); + } + catch (Exception ex) + { + json["exception"] = ex.Message; + } + return json; + } + + private JObject SignAndRelay(DataCache snapshot, Transaction tx) + { + ContractParametersContext context = new(snapshot, tx, settings.Network); + wallet.Sign(context); + if (context.Completed) + { + tx.Witnesses = context.GetWitnesses(); + system.Blockchain.Tell(tx); + return Utility.TransactionToJson(tx, system.Settings); + } + else + { + return context.ToJson(); + } + } + + internal static UInt160 AddressToScriptHash(string address, byte version) + { + if (UInt160.TryParse(address, out var scriptHash)) + { + return scriptHash; + } + + return address.ToScriptHash(version); + } + } +} diff --git a/src/Plugins/RpcServer/RpcServer.cs b/src/Plugins/RpcServer/RpcServer.cs new file mode 100644 index 0000000000..f77c2d1e33 --- /dev/null +++ b/src/Plugins/RpcServer/RpcServer.cs @@ -0,0 +1,312 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcServer.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 Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.ResponseCompression; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.Extensions.DependencyInjection; +using Neo.Json; +using Neo.Network.P2P; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Security; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; + +namespace Neo.Plugins +{ + public partial class RpcServer : IDisposable + { + private const int MaxParamsDepth = 32; + + private readonly Dictionary> methods = new(); + + private IWebHost host; + private RpcServerSettings settings; + private readonly NeoSystem system; + private readonly LocalNode localNode; + + public RpcServer(NeoSystem system, RpcServerSettings settings) + { + this.system = system; + this.settings = settings; + localNode = system.LocalNode.Ask(new LocalNode.GetInstance()).Result; + RegisterMethods(this); + Initialize_SmartContract(); + } + + private bool CheckAuth(HttpContext context) + { + if (string.IsNullOrEmpty(settings.RpcUser)) return true; + + string reqauth = context.Request.Headers["Authorization"]; + if (string.IsNullOrEmpty(reqauth)) + { + context.Response.Headers["WWW-Authenticate"] = "Basic realm=\"Restricted\""; + context.Response.StatusCode = 401; + return false; + } + + string authstring; + try + { + authstring = Encoding.UTF8.GetString(Convert.FromBase64String(reqauth.Replace("Basic ", "").Trim())); + } + catch + { + return false; + } + + string[] authvalues = authstring.Split(new string[] { ":" }, StringSplitOptions.RemoveEmptyEntries); + if (authvalues.Length < 2) + return false; + + return authvalues[0] == settings.RpcUser && authvalues[1] == settings.RpcPass; + } + + private static JObject CreateErrorResponse(JToken id, RpcError rpcError) + { + JObject response = CreateResponse(id); + response["error"] = rpcError.ToJson(); + return response; + } + + private static JObject CreateResponse(JToken id) + { + JObject response = new(); + response["jsonrpc"] = "2.0"; + response["id"] = id; + return response; + } + + public void Dispose() + { + Dispose_SmartContract(); + if (host != null) + { + host.Dispose(); + host = null; + } + } + + public void StartRpcServer() + { + host = new WebHostBuilder().UseKestrel(options => options.Listen(settings.BindAddress, settings.Port, listenOptions => + { + // Default value is 5Mb + options.Limits.MaxRequestBodySize = settings.MaxRequestBodySize; + options.Limits.MaxRequestLineSize = Math.Min(settings.MaxRequestBodySize, options.Limits.MaxRequestLineSize); + // Default value is 40 + options.Limits.MaxConcurrentConnections = settings.MaxConcurrentConnections; + + // Default value is 1 minutes + options.Limits.KeepAliveTimeout = settings.KeepAliveTimeout == -1 ? + TimeSpan.MaxValue : + TimeSpan.FromSeconds(settings.KeepAliveTimeout); + + // Default value is 15 seconds + options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(settings.RequestHeadersTimeout); + + if (string.IsNullOrEmpty(settings.SslCert)) return; + listenOptions.UseHttps(settings.SslCert, settings.SslCertPassword, httpsConnectionAdapterOptions => + { + if (settings.TrustedAuthorities is null || settings.TrustedAuthorities.Length == 0) + return; + httpsConnectionAdapterOptions.ClientCertificateMode = ClientCertificateMode.RequireCertificate; + httpsConnectionAdapterOptions.ClientCertificateValidation = (cert, chain, err) => + { + if (err != SslPolicyErrors.None) + return false; + X509Certificate2 authority = chain.ChainElements[^1].Certificate; + return settings.TrustedAuthorities.Contains(authority.Thumbprint); + }; + }); + })) + .Configure(app => + { + if (settings.EnableCors) + app.UseCors("All"); + + app.UseResponseCompression(); + app.Run(ProcessAsync); + }) + .ConfigureServices(services => + { + if (settings.EnableCors) + { + if (settings.AllowOrigins.Length == 0) + services.AddCors(options => + { + options.AddPolicy("All", policy => + { + policy.AllowAnyOrigin() + .WithHeaders("Content-Type") + .WithMethods("GET", "POST"); + // The CORS specification states that setting origins to "*" (all origins) + // is invalid if the Access-Control-Allow-Credentials header is present. + }); + }); + else + services.AddCors(options => + { + options.AddPolicy("All", policy => + { + policy.WithOrigins(settings.AllowOrigins) + .WithHeaders("Content-Type") + .AllowCredentials() + .WithMethods("GET", "POST"); + }); + }); + } + + services.AddResponseCompression(options => + { + // options.EnableForHttps = false; + options.Providers.Add(); + options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Append("application/json"); + }); + + services.Configure(options => + { + options.Level = CompressionLevel.Fastest; + }); + }) + .Build(); + + host.Start(); + } + + internal void UpdateSettings(RpcServerSettings settings) + { + this.settings = settings; + } + + public async Task ProcessAsync(HttpContext context) + { + if (context.Request.Method != "GET" && context.Request.Method != "POST") return; + JToken request = null; + if (context.Request.Method == "GET") + { + string jsonrpc = context.Request.Query["jsonrpc"]; + string id = context.Request.Query["id"]; + string method = context.Request.Query["method"]; + string _params = context.Request.Query["params"]; + if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(method) && !string.IsNullOrEmpty(_params)) + { + try + { + _params = Encoding.UTF8.GetString(Convert.FromBase64String(_params)); + } + catch (FormatException) { } + request = new JObject(); + if (!string.IsNullOrEmpty(jsonrpc)) + request["jsonrpc"] = jsonrpc; + request["id"] = id; + request["method"] = method; + request["params"] = JToken.Parse(_params, MaxParamsDepth); + } + } + else if (context.Request.Method == "POST") + { + using StreamReader reader = new(context.Request.Body); + try + { + request = JToken.Parse(await reader.ReadToEndAsync(), MaxParamsDepth); + } + catch (FormatException) { } + } + JToken response; + if (request == null) + { + response = CreateErrorResponse(null, RpcError.BadRequest); + } + else if (request is JArray array) + { + if (array.Count == 0) + { + response = CreateErrorResponse(request["id"], RpcError.InvalidRequest); + } + else + { + var tasks = array.Select(p => ProcessRequestAsync(context, (JObject)p)); + var results = await Task.WhenAll(tasks); + response = results.Where(p => p != null).ToArray(); + } + } + else + { + response = await ProcessRequestAsync(context, (JObject)request); + } + if (response == null || (response as JArray)?.Count == 0) return; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(response.ToString(), Encoding.UTF8); + } + + private async Task ProcessRequestAsync(HttpContext context, JObject request) + { + if (!request.ContainsProperty("id")) return null; + JToken @params = request["params"] ?? new JArray(); + if (!request.ContainsProperty("method") || @params is not JArray) + { + return CreateErrorResponse(request["id"], RpcError.InvalidRequest); + } + JObject response = CreateResponse(request["id"]); + try + { + string method = request["method"].AsString(); + (CheckAuth(context) && !settings.DisabledMethods.Contains(method)).True_Or(RpcError.AccessDenied); + methods.TryGetValue(method, out var func).True_Or(RpcErrorFactory.MethodNotFound(method)); + response["result"] = func((JArray)@params) switch + { + JToken result => result, + Task task => await task, + _ => throw new NotSupportedException() + }; + return response; + } + catch (FormatException ex) + { + return CreateErrorResponse(request["id"], RpcError.InvalidParams.WithData(ex.Message)); + } + catch (IndexOutOfRangeException ex) + { + return CreateErrorResponse(request["id"], RpcError.InvalidParams.WithData(ex.Message)); + } + catch (Exception ex) + { +#if DEBUG + return CreateErrorResponse(request["id"], RpcErrorFactory.NewCustomError(ex.HResult, ex.Message, ex.StackTrace)); +#else + return CreateErrorResponse(request["id"], RpcErrorFactory.NewCustomError(ex.HResult, ex.Message)); +#endif + } + } + + public void RegisterMethods(object handler) + { + foreach (MethodInfo method in handler.GetType().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + RpcMethodAttribute attribute = method.GetCustomAttribute(); + if (attribute is null) continue; + string name = string.IsNullOrEmpty(attribute.Name) ? method.Name.ToLowerInvariant() : attribute.Name; + methods[name] = method.CreateDelegate>(handler); + } + } + } +} diff --git a/src/Plugins/RpcServer/RpcServer.csproj b/src/Plugins/RpcServer/RpcServer.csproj new file mode 100644 index 0000000000..ab2d7e6783 --- /dev/null +++ b/src/Plugins/RpcServer/RpcServer.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + Neo.Plugins.RpcServer + + + + + + + + + PreserveNewest + + + + diff --git a/src/Plugins/RpcServer/RpcServer.json b/src/Plugins/RpcServer/RpcServer.json new file mode 100644 index 0000000000..8f6905dead --- /dev/null +++ b/src/Plugins/RpcServer/RpcServer.json @@ -0,0 +1,29 @@ +{ + "PluginConfiguration": { + "Servers": [ + { + "Network": 860833102, + "BindAddress": "127.0.0.1", + "Port": 10332, + "SslCert": "", + "SslCertPassword": "", + "TrustedAuthorities": [], + "RpcUser": "", + "RpcPass": "", + "EnableCors": true, + "AllowOrigins": [], + "KeepAliveTimeout": 60, + "RequestHeadersTimeout": 15, + "MaxGasInvoke": 20, + "MaxFee": 0.1, + "MaxConcurrentConnections": 40, + "MaxIteratorResultItems": 100, + "MaxStackSize": 65535, + "DisabledMethods": [ "openwallet" ], + "SessionEnabled": false, + "SessionExpirationTime": 60, + "FindStoragePageSize": 50 + } + ] + } +} diff --git a/src/Plugins/RpcServer/RpcServerPlugin.cs b/src/Plugins/RpcServer/RpcServerPlugin.cs new file mode 100644 index 0000000000..61d9da4397 --- /dev/null +++ b/src/Plugins/RpcServer/RpcServerPlugin.cs @@ -0,0 +1,87 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcServerPlugin.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Collections.Generic; +using System.Linq; + +namespace Neo.Plugins +{ + public class RpcServerPlugin : Plugin + { + public override string Name => "RpcServer"; + public override string Description => "Enables RPC for the node"; + + private Settings settings; + private static readonly Dictionary servers = new(); + private static readonly Dictionary> handlers = new(); + + public override string ConfigFile => System.IO.Path.Combine(RootPath, "RpcServer.json"); + + protected override void Configure() + { + settings = new Settings(GetConfiguration()); + foreach (RpcServerSettings s in settings.Servers) + if (servers.TryGetValue(s.Network, out RpcServer server)) + server.UpdateSettings(s); + } + + public override void Dispose() + { + foreach (var (_, server) in servers) + server.Dispose(); + base.Dispose(); + } + + protected override void OnSystemLoaded(NeoSystem system) + { + RpcServerSettings s = settings.Servers.FirstOrDefault(p => p.Network == system.Settings.Network); + if (s is null) return; + + if (s.EnableCors && string.IsNullOrEmpty(s.RpcUser) == false && s.AllowOrigins.Length == 0) + { + Log("RcpServer: CORS is misconfigured!", LogLevel.Warning); + Log($"You have {nameof(s.EnableCors)} and Basic Authentication enabled but " + + $"{nameof(s.AllowOrigins)} is empty in config.json for RcpServer. " + + "You must add url origins to the list to have CORS work from " + + $"browser with basic authentication enabled. " + + $"Example: \"AllowOrigins\": [\"http://{s.BindAddress}:{s.Port}\"]", LogLevel.Info); + } + + RpcServer server = new(system, s); + + if (handlers.Remove(s.Network, out var list)) + { + foreach (var handler in list) + { + server.RegisterMethods(handler); + } + } + + server.StartRpcServer(); + servers.TryAdd(s.Network, server); + } + + public static void RegisterMethods(object handler, uint network) + { + if (servers.TryGetValue(network, out RpcServer server)) + { + server.RegisterMethods(handler); + return; + } + if (!handlers.TryGetValue(network, out var list)) + { + list = new List(); + handlers.Add(network, list); + } + list.Add(handler); + } + } +} diff --git a/src/Plugins/RpcServer/Session.cs b/src/Plugins/RpcServer/Session.cs new file mode 100644 index 0000000000..2b4213c28e --- /dev/null +++ b/src/Plugins/RpcServer/Session.cs @@ -0,0 +1,58 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Session.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.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Iterators; +using Neo.SmartContract.Native; +using System; +using System.Collections.Generic; + +namespace Neo.Plugins +{ + class Session : IDisposable + { + public readonly SnapshotCache Snapshot; + public readonly ApplicationEngine Engine; + public readonly Dictionary Iterators = new(); + public DateTime StartTime; + + public Session(NeoSystem system, byte[] script, Signer[] signers, Witness[] witnesses, long gas, Diagnostic diagnostic) + { + Random random = new(); + Snapshot = system.GetSnapshot(); + Transaction tx = signers == null ? null : new Transaction + { + Version = 0, + Nonce = (uint)random.Next(), + ValidUntilBlock = NativeContract.Ledger.CurrentIndex(Snapshot) + system.Settings.MaxValidUntilBlockIncrement, + Signers = signers, + Attributes = Array.Empty(), + Script = script, + Witnesses = witnesses + }; + Engine = ApplicationEngine.Run(script, Snapshot, container: tx, settings: system.Settings, gas: gas, diagnostic: diagnostic); + ResetExpiration(); + } + + public void ResetExpiration() + { + StartTime = DateTime.UtcNow; + } + + public void Dispose() + { + Engine.Dispose(); + Snapshot.Dispose(); + } + } +} diff --git a/src/Plugins/RpcServer/Settings.cs b/src/Plugins/RpcServer/Settings.cs new file mode 100644 index 0000000000..f4aef116c6 --- /dev/null +++ b/src/Plugins/RpcServer/Settings.cs @@ -0,0 +1,105 @@ +// 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; +using Neo.SmartContract.Native; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; + +namespace Neo.Plugins +{ + class Settings + { + public IReadOnlyList Servers { get; init; } + + public Settings(IConfigurationSection section) + { + Servers = section.GetSection(nameof(Servers)).GetChildren().Select(p => RpcServerSettings.Load(p)).ToArray(); + } + } + + public record RpcServerSettings + { + public uint Network { get; init; } + public IPAddress BindAddress { get; init; } + public ushort Port { get; init; } + public string SslCert { get; init; } + public string SslCertPassword { get; init; } + public string[] TrustedAuthorities { get; init; } + public int MaxConcurrentConnections { get; init; } + public int MaxRequestBodySize { get; init; } + public string RpcUser { get; init; } + public string RpcPass { get; init; } + public bool EnableCors { get; init; } + public string[] AllowOrigins { get; init; } + public int KeepAliveTimeout { get; init; } + public uint RequestHeadersTimeout { get; init; } + public long MaxGasInvoke { get; init; } + public long MaxFee { get; init; } + public int MaxIteratorResultItems { get; init; } + public int MaxStackSize { get; init; } + public string[] DisabledMethods { get; init; } + public bool SessionEnabled { get; init; } + public TimeSpan SessionExpirationTime { get; init; } + public int FindStoragePageSize { get; init; } + + public static RpcServerSettings Default { get; } = new RpcServerSettings + { + Network = 5195086u, + BindAddress = IPAddress.None, + SslCert = string.Empty, + SslCertPassword = string.Empty, + MaxGasInvoke = (long)new BigDecimal(10M, NativeContract.GAS.Decimals).Value, + MaxFee = (long)new BigDecimal(0.1M, NativeContract.GAS.Decimals).Value, + TrustedAuthorities = Array.Empty(), + EnableCors = true, + AllowOrigins = Array.Empty(), + KeepAliveTimeout = 60, + RequestHeadersTimeout = 15, + MaxIteratorResultItems = 100, + MaxStackSize = ushort.MaxValue, + DisabledMethods = Array.Empty(), + MaxConcurrentConnections = 40, + MaxRequestBodySize = 5 * 1024 * 1024, + SessionEnabled = false, + SessionExpirationTime = TimeSpan.FromSeconds(60), + FindStoragePageSize = 50 + }; + + public static RpcServerSettings Load(IConfigurationSection section) => new() + { + Network = section.GetValue("Network", Default.Network), + BindAddress = IPAddress.Parse(section.GetSection("BindAddress").Value), + Port = ushort.Parse(section.GetSection("Port").Value), + SslCert = section.GetSection("SslCert").Value, + SslCertPassword = section.GetSection("SslCertPassword").Value, + TrustedAuthorities = section.GetSection("TrustedAuthorities").GetChildren().Select(p => p.Get()).ToArray(), + RpcUser = section.GetSection("RpcUser").Value, + RpcPass = section.GetSection("RpcPass").Value, + EnableCors = section.GetValue(nameof(EnableCors), Default.EnableCors), + AllowOrigins = section.GetSection(nameof(AllowOrigins)).GetChildren().Select(p => p.Get()).ToArray(), + KeepAliveTimeout = section.GetValue(nameof(KeepAliveTimeout), Default.KeepAliveTimeout), + RequestHeadersTimeout = section.GetValue(nameof(RequestHeadersTimeout), Default.RequestHeadersTimeout), + MaxGasInvoke = (long)new BigDecimal(section.GetValue("MaxGasInvoke", Default.MaxGasInvoke), NativeContract.GAS.Decimals).Value, + MaxFee = (long)new BigDecimal(section.GetValue("MaxFee", Default.MaxFee), NativeContract.GAS.Decimals).Value, + MaxIteratorResultItems = section.GetValue("MaxIteratorResultItems", Default.MaxIteratorResultItems), + MaxStackSize = section.GetValue("MaxStackSize", Default.MaxStackSize), + DisabledMethods = section.GetSection("DisabledMethods").GetChildren().Select(p => p.Get()).ToArray(), + MaxConcurrentConnections = section.GetValue("MaxConcurrentConnections", Default.MaxConcurrentConnections), + MaxRequestBodySize = section.GetValue("MaxRequestBodySize", Default.MaxRequestBodySize), + SessionEnabled = section.GetValue("SessionEnabled", Default.SessionEnabled), + SessionExpirationTime = TimeSpan.FromSeconds(section.GetValue("SessionExpirationTime", (int)Default.SessionExpirationTime.TotalSeconds)), + FindStoragePageSize = section.GetValue("FindStoragePageSize", Default.FindStoragePageSize) + }; + } +} diff --git a/src/Plugins/RpcServer/Tree.cs b/src/Plugins/RpcServer/Tree.cs new file mode 100644 index 0000000000..77ca1fc2a5 --- /dev/null +++ b/src/Plugins/RpcServer/Tree.cs @@ -0,0 +1,36 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Tree.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.Collections.Generic; + +namespace Neo.Plugins +{ + class Tree + { + public TreeNode Root { get; private set; } + + public TreeNode AddRoot(T item) + { + if (Root is not null) + throw new InvalidOperationException(); + Root = new TreeNode(item, null); + return Root; + } + + public IEnumerable GetItems() + { + if (Root is null) yield break; + foreach (T item in Root.GetItems()) + yield return item; + } + } +} diff --git a/src/Plugins/RpcServer/TreeNode.cs b/src/Plugins/RpcServer/TreeNode.cs new file mode 100644 index 0000000000..82785277b6 --- /dev/null +++ b/src/Plugins/RpcServer/TreeNode.cs @@ -0,0 +1,45 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// TreeNode.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Collections.Generic; + +namespace Neo.Plugins +{ + class TreeNode + { + private readonly List> children = new(); + + public T Item { get; } + public TreeNode Parent { get; } + public IReadOnlyList> Children => children; + + internal TreeNode(T item, TreeNode parent) + { + Item = item; + Parent = parent; + } + + public TreeNode AddChild(T item) + { + TreeNode child = new(item, this); + children.Add(child); + return child; + } + + internal IEnumerable GetItems() + { + yield return Item; + foreach (var child in children) + foreach (T item in child.GetItems()) + yield return item; + } + } +} diff --git a/src/Plugins/RpcServer/Utility.cs b/src/Plugins/RpcServer/Utility.cs new file mode 100644 index 0000000000..24ccc9248d --- /dev/null +++ b/src/Plugins/RpcServer/Utility.cs @@ -0,0 +1,36 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Utility.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.Json; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract.Native; +using System.Linq; + +namespace Neo.Plugins +{ + static class Utility + { + public static JObject BlockToJson(Block block, ProtocolSettings settings) + { + JObject json = block.ToJson(settings); + json["tx"] = block.Transactions.Select(p => TransactionToJson(p, settings)).ToArray(); + return json; + } + + public static JObject TransactionToJson(Transaction tx, ProtocolSettings settings) + { + JObject json = tx.ToJson(settings); + json["sysfee"] = tx.SystemFee.ToString(); + json["netfee"] = tx.NetworkFee.ToString(); + return json; + } + } +} diff --git a/src/Plugins/SQLiteWallet/Account.cs b/src/Plugins/SQLiteWallet/Account.cs new file mode 100644 index 0000000000..e7ae09af90 --- /dev/null +++ b/src/Plugins/SQLiteWallet/Account.cs @@ -0,0 +1,18 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Account.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.Wallets.SQLite; + +class Account +{ + public byte[] PublicKeyHash { get; set; } + public string Nep2key { get; set; } +} diff --git a/src/Plugins/SQLiteWallet/Address.cs b/src/Plugins/SQLiteWallet/Address.cs new file mode 100644 index 0000000000..6f2b73e427 --- /dev/null +++ b/src/Plugins/SQLiteWallet/Address.cs @@ -0,0 +1,17 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Address.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.Wallets.SQLite; + +class Address +{ + public byte[] ScriptHash { get; set; } +} diff --git a/src/Plugins/SQLiteWallet/Contract.cs b/src/Plugins/SQLiteWallet/Contract.cs new file mode 100644 index 0000000000..2da432a63b --- /dev/null +++ b/src/Plugins/SQLiteWallet/Contract.cs @@ -0,0 +1,21 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Contract.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.Wallets.SQLite; + +class Contract +{ + public byte[] RawData { get; set; } + public byte[] ScriptHash { get; set; } + public byte[] PublicKeyHash { get; set; } + public Account Account { get; set; } + public Address Address { get; set; } +} diff --git a/src/Plugins/SQLiteWallet/Key.cs b/src/Plugins/SQLiteWallet/Key.cs new file mode 100644 index 0000000000..81a8a6daf4 --- /dev/null +++ b/src/Plugins/SQLiteWallet/Key.cs @@ -0,0 +1,18 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Key.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.Wallets.SQLite; + +class Key +{ + public string Name { get; set; } + public byte[] Value { get; set; } +} diff --git a/src/Plugins/SQLiteWallet/SQLiteWallet.cs b/src/Plugins/SQLiteWallet/SQLiteWallet.cs new file mode 100644 index 0000000000..a4004dcff0 --- /dev/null +++ b/src/Plugins/SQLiteWallet/SQLiteWallet.cs @@ -0,0 +1,416 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// SQLiteWallet.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.EntityFrameworkCore; +using Neo.Cryptography; +using Neo.IO; +using Neo.SmartContract; +using Neo.Wallets.NEP6; +using System.Buffers.Binary; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using static System.IO.Path; + +namespace Neo.Wallets.SQLite; + +/// +/// A wallet implementation that uses SQLite as the underlying storage. +/// +class SQLiteWallet : Wallet +{ + private readonly object db_lock = new(); + private readonly byte[] iv; + private readonly byte[] salt; + private readonly byte[] masterKey; + private readonly ScryptParameters scrypt; + private readonly Dictionary accounts; + + public override string Name => GetFileNameWithoutExtension(Path); + + public override Version Version + { + get + { + byte[] buffer = LoadStoredData("Version"); + if (buffer == null || buffer.Length < 16) return new Version(0, 0); + int major = BinaryPrimitives.ReadInt32LittleEndian(buffer); + int minor = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(4)); + int build = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(8)); + int revision = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(12)); + return new Version(major, minor, build, revision); + } + } + + private SQLiteWallet(string path, byte[] passwordKey, ProtocolSettings settings) : base(path, settings) + { + salt = LoadStoredData("Salt"); + byte[] passwordHash = LoadStoredData("PasswordHash"); + if (passwordHash != null && !passwordHash.SequenceEqual(passwordKey.Concat(salt).ToArray().Sha256())) + throw new CryptographicException(); + iv = LoadStoredData("IV"); + masterKey = Decrypt(LoadStoredData("MasterKey"), passwordKey, iv); + scrypt = new ScryptParameters + ( + BinaryPrimitives.ReadInt32LittleEndian(LoadStoredData("ScryptN")), + BinaryPrimitives.ReadInt32LittleEndian(LoadStoredData("ScryptR")), + BinaryPrimitives.ReadInt32LittleEndian(LoadStoredData("ScryptP")) + ); + accounts = LoadAccounts(); + } + + private SQLiteWallet(string path, byte[] passwordKey, ProtocolSettings settings, ScryptParameters scrypt) : base(path, settings) + { + iv = new byte[16]; + salt = new byte[20]; + masterKey = new byte[32]; + this.scrypt = scrypt; + accounts = new Dictionary(); + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(iv); + rng.GetBytes(salt); + rng.GetBytes(masterKey); + } + Version version = Assembly.GetExecutingAssembly().GetName().Version; + byte[] versionBuffer = new byte[sizeof(int) * 4]; + BinaryPrimitives.WriteInt32LittleEndian(versionBuffer, version.Major); + BinaryPrimitives.WriteInt32LittleEndian(versionBuffer.AsSpan(4), version.Minor); + BinaryPrimitives.WriteInt32LittleEndian(versionBuffer.AsSpan(8), version.Build); + BinaryPrimitives.WriteInt32LittleEndian(versionBuffer.AsSpan(12), version.Revision); + BuildDatabase(); + SaveStoredData("IV", iv); + SaveStoredData("Salt", salt); + SaveStoredData("PasswordHash", passwordKey.Concat(salt).ToArray().Sha256()); + SaveStoredData("MasterKey", Encrypt(masterKey, passwordKey, iv)); + SaveStoredData("Version", versionBuffer); + SaveStoredData("ScryptN", this.scrypt.N); + SaveStoredData("ScryptR", this.scrypt.R); + SaveStoredData("ScryptP", this.scrypt.P); + } + + private void AddAccount(SQLiteWalletAccount account) + { + lock (accounts) + { + if (accounts.TryGetValue(account.ScriptHash, out SQLiteWalletAccount account_old)) + { + if (account.Contract == null) + { + account.Contract = account_old.Contract; + } + } + accounts[account.ScriptHash] = account; + } + lock (db_lock) + { + using WalletDataContext ctx = new(Path); + if (account.HasKey) + { + Account db_account = ctx.Accounts.FirstOrDefault(p => p.PublicKeyHash == account.Key.PublicKeyHash.ToArray()); + if (db_account == null) + { + db_account = ctx.Accounts.Add(new Account + { + Nep2key = account.Key.Export(masterKey, ProtocolSettings.AddressVersion, scrypt.N, scrypt.R, scrypt.P), + PublicKeyHash = account.Key.PublicKeyHash.ToArray() + }).Entity; + } + else + { + db_account.Nep2key = account.Key.Export(masterKey, ProtocolSettings.AddressVersion, scrypt.N, scrypt.R, scrypt.P); + } + } + if (account.Contract != null) + { + Contract db_contract = ctx.Contracts.FirstOrDefault(p => p.ScriptHash == account.Contract.ScriptHash.ToArray()); + if (db_contract != null) + { + db_contract.PublicKeyHash = account.Key.PublicKeyHash.ToArray(); + } + else + { + ctx.Contracts.Add(new Contract + { + RawData = ((VerificationContract)account.Contract).ToArray(), + ScriptHash = account.Contract.ScriptHash.ToArray(), + PublicKeyHash = account.Key.PublicKeyHash.ToArray() + }); + } + } + //add address + { + Address db_address = ctx.Addresses.FirstOrDefault(p => p.ScriptHash == account.ScriptHash.ToArray()); + if (db_address == null) + { + ctx.Addresses.Add(new Address + { + ScriptHash = account.ScriptHash.ToArray() + }); + } + } + ctx.SaveChanges(); + } + } + + private void BuildDatabase() + { + using WalletDataContext ctx = new(Path); + ctx.Database.EnsureDeleted(); + ctx.Database.EnsureCreated(); + } + + public override bool ChangePassword(string oldPassword, string newPassword) + { + if (!VerifyPassword(oldPassword)) return false; + byte[] passwordKey = ToAesKey(newPassword); + try + { + SaveStoredData("PasswordHash", passwordKey.Concat(salt).ToArray().Sha256()); + SaveStoredData("MasterKey", Encrypt(masterKey, passwordKey, iv)); + return true; + } + finally + { + Array.Clear(passwordKey, 0, passwordKey.Length); + } + } + + public override bool Contains(UInt160 scriptHash) + { + lock (accounts) + { + return accounts.ContainsKey(scriptHash); + } + } + + /// + /// Creates a new wallet at the specified path. + /// + /// The path of the wallet. + /// The password of the wallet. + /// The to be used by the wallet. + /// The parameters of the SCrypt algorithm used for encrypting and decrypting the private keys in the wallet. + /// The created wallet. + public static SQLiteWallet Create(string path, string password, ProtocolSettings settings, ScryptParameters scrypt = null) + { + return new SQLiteWallet(path, ToAesKey(password), settings, scrypt ?? ScryptParameters.Default); + } + + public override WalletAccount CreateAccount(byte[] privateKey) + { + KeyPair key = new(privateKey); + VerificationContract contract = new() + { + Script = SmartContract.Contract.CreateSignatureRedeemScript(key.PublicKey), + ParameterList = new[] { ContractParameterType.Signature } + }; + SQLiteWalletAccount account = new(contract.ScriptHash, ProtocolSettings) + { + Key = key, + Contract = contract + }; + AddAccount(account); + return account; + } + + public override WalletAccount CreateAccount(SmartContract.Contract contract, KeyPair key = null) + { + if (contract is not VerificationContract verification_contract) + { + verification_contract = new VerificationContract + { + Script = contract.Script, + ParameterList = contract.ParameterList + }; + } + SQLiteWalletAccount account = new(verification_contract.ScriptHash, ProtocolSettings) + { + Key = key, + Contract = verification_contract + }; + AddAccount(account); + return account; + } + + public override WalletAccount CreateAccount(UInt160 scriptHash) + { + SQLiteWalletAccount account = new(scriptHash, ProtocolSettings); + AddAccount(account); + return account; + } + + public override void Delete() + { + using WalletDataContext ctx = new(Path); + ctx.Database.EnsureDeleted(); + } + + public override bool DeleteAccount(UInt160 scriptHash) + { + SQLiteWalletAccount account; + lock (accounts) + { + if (accounts.TryGetValue(scriptHash, out account)) + accounts.Remove(scriptHash); + } + if (account != null) + { + lock (db_lock) + { + using WalletDataContext ctx = new(Path); + if (account.HasKey) + { + Account db_account = ctx.Accounts.First(p => p.PublicKeyHash == account.Key.PublicKeyHash.ToArray()); + ctx.Accounts.Remove(db_account); + } + if (account.Contract != null) + { + Contract db_contract = ctx.Contracts.First(p => p.ScriptHash == scriptHash.ToArray()); + ctx.Contracts.Remove(db_contract); + } + //delete address + { + Address db_address = ctx.Addresses.First(p => p.ScriptHash == scriptHash.ToArray()); + ctx.Addresses.Remove(db_address); + } + ctx.SaveChanges(); + } + return true; + } + return false; + } + + public override WalletAccount GetAccount(UInt160 scriptHash) + { + lock (accounts) + { + accounts.TryGetValue(scriptHash, out SQLiteWalletAccount account); + return account; + } + } + + public override IEnumerable GetAccounts() + { + lock (accounts) + { + foreach (SQLiteWalletAccount account in accounts.Values) + yield return account; + } + } + + private Dictionary LoadAccounts() + { + using WalletDataContext ctx = new(Path); + Dictionary accounts = ctx.Addresses.Select(p => p.ScriptHash).AsEnumerable().Select(p => new SQLiteWalletAccount(new UInt160(p), ProtocolSettings)).ToDictionary(p => p.ScriptHash); + foreach (Contract db_contract in ctx.Contracts.Include(p => p.Account)) + { + VerificationContract contract = db_contract.RawData.AsSerializable(); + SQLiteWalletAccount account = accounts[contract.ScriptHash]; + account.Contract = contract; + account.Key = new KeyPair(GetPrivateKeyFromNEP2(db_contract.Account.Nep2key, masterKey, ProtocolSettings.AddressVersion, scrypt.N, scrypt.R, scrypt.P)); + } + return accounts; + } + + private byte[] LoadStoredData(string name) + { + using WalletDataContext ctx = new(Path); + return ctx.Keys.FirstOrDefault(p => p.Name == name)?.Value; + } + + /// + /// Opens a wallet at the specified path. + /// + /// The path of the wallet. + /// The password of the wallet. + /// The to be used by the wallet. + /// The opened wallet. + public static new SQLiteWallet Open(string path, string password, ProtocolSettings settings) + { + return new SQLiteWallet(path, ToAesKey(password), settings); + } + + public override void Save() + { + // Do nothing + } + + private void SaveStoredData(string name, int value) + { + byte[] data = new byte[sizeof(int)]; + BinaryPrimitives.WriteInt32LittleEndian(data, value); + SaveStoredData(name, data); + } + + private void SaveStoredData(string name, byte[] value) + { + lock (db_lock) + { + using WalletDataContext ctx = new(Path); + SaveStoredData(ctx, name, value); + ctx.SaveChanges(); + } + } + + private static void SaveStoredData(WalletDataContext ctx, string name, byte[] value) + { + Key key = ctx.Keys.FirstOrDefault(p => p.Name == name); + if (key == null) + { + ctx.Keys.Add(new Key + { + Name = name, + Value = value + }); + } + else + { + key.Value = value; + } + } + + public override bool VerifyPassword(string password) + { + return ToAesKey(password).Concat(salt).ToArray().Sha256().SequenceEqual(LoadStoredData("PasswordHash")); + } + + private static byte[] Encrypt(byte[] data, byte[] key, byte[] iv) + { + if (data == null || key == null || iv == null) throw new ArgumentNullException(); + if (data.Length % 16 != 0 || key.Length != 32 || iv.Length != 16) throw new ArgumentException(); + using Aes aes = Aes.Create(); + aes.Padding = PaddingMode.None; + using ICryptoTransform encryptor = aes.CreateEncryptor(key, iv); + return encryptor.TransformFinalBlock(data, 0, data.Length); + } + + private static byte[] Decrypt(byte[] data, byte[] key, byte[] iv) + { + if (data == null || key == null || iv == null) throw new ArgumentNullException(); + if (data.Length % 16 != 0 || key.Length != 32 || iv.Length != 16) throw new ArgumentException(); + using Aes aes = Aes.Create(); + aes.Padding = PaddingMode.None; + using ICryptoTransform decryptor = aes.CreateDecryptor(key, iv); + return decryptor.TransformFinalBlock(data, 0, data.Length); + } + + private static byte[] ToAesKey(string password) + { + using SHA256 sha256 = SHA256.Create(); + byte[] passwordBytes = Encoding.UTF8.GetBytes(password); + byte[] passwordHash = sha256.ComputeHash(passwordBytes); + byte[] passwordHash2 = sha256.ComputeHash(passwordHash); + Array.Clear(passwordBytes, 0, passwordBytes.Length); + Array.Clear(passwordHash, 0, passwordHash.Length); + return passwordHash2; + } +} diff --git a/src/Plugins/SQLiteWallet/SQLiteWallet.csproj b/src/Plugins/SQLiteWallet/SQLiteWallet.csproj new file mode 100644 index 0000000000..fb2d3e71b8 --- /dev/null +++ b/src/Plugins/SQLiteWallet/SQLiteWallet.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + Neo.Wallets.SQLite + Neo.Wallets.SQLite + enable + + + + + + + diff --git a/src/Plugins/SQLiteWallet/SQLiteWalletAccount.cs b/src/Plugins/SQLiteWallet/SQLiteWalletAccount.cs new file mode 100644 index 0000000000..88526f7e52 --- /dev/null +++ b/src/Plugins/SQLiteWallet/SQLiteWalletAccount.cs @@ -0,0 +1,29 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// SQLiteWalletAccount.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.Wallets.SQLite; + +sealed class SQLiteWalletAccount : WalletAccount +{ + public KeyPair Key; + + public override bool HasKey => Key != null; + + public SQLiteWalletAccount(UInt160 scriptHash, ProtocolSettings settings) + : base(scriptHash, settings) + { + } + + public override KeyPair GetKey() + { + return Key; + } +} diff --git a/src/Plugins/SQLiteWallet/SQLiteWalletFactory.cs b/src/Plugins/SQLiteWallet/SQLiteWalletFactory.cs new file mode 100644 index 0000000000..d952fd0fc3 --- /dev/null +++ b/src/Plugins/SQLiteWallet/SQLiteWalletFactory.cs @@ -0,0 +1,41 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// SQLiteWalletFactory.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; +using static System.IO.Path; + +namespace Neo.Wallets.SQLite; + +public class SQLiteWalletFactory : Plugin, IWalletFactory +{ + public override string Name => "SQLiteWallet"; + public override string Description => "A SQLite-based wallet provider that supports wallet files with .db3 suffix."; + + public SQLiteWalletFactory() + { + Wallet.RegisterFactory(this); + } + + public bool Handle(string path) + { + return GetExtension(path).ToLowerInvariant() == ".db3"; + } + + public Wallet CreateWallet(string name, string path, string password, ProtocolSettings settings) + { + return SQLiteWallet.Create(path, password, settings); + } + + public Wallet OpenWallet(string path, string password, ProtocolSettings settings) + { + return SQLiteWallet.Open(path, password, settings); + } +} diff --git a/src/Plugins/SQLiteWallet/VerificationContract.cs b/src/Plugins/SQLiteWallet/VerificationContract.cs new file mode 100644 index 0000000000..e128045adb --- /dev/null +++ b/src/Plugins/SQLiteWallet/VerificationContract.cs @@ -0,0 +1,56 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// VerificationContract.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.SmartContract; + +namespace Neo.Wallets.SQLite; + +class VerificationContract : SmartContract.Contract, IEquatable, ISerializable +{ + public int Size => ParameterList.GetVarSize() + Script.GetVarSize(); + + public void Deserialize(ref MemoryReader reader) + { + ReadOnlySpan span = reader.ReadVarMemory().Span; + ParameterList = new ContractParameterType[span.Length]; + for (int i = 0; i < span.Length; i++) + { + ParameterList[i] = (ContractParameterType)span[i]; + if (!Enum.IsDefined(typeof(ContractParameterType), ParameterList[i])) + throw new FormatException(); + } + Script = reader.ReadVarMemory().ToArray(); + } + + public bool Equals(VerificationContract other) + { + if (ReferenceEquals(this, other)) return true; + if (other is null) return false; + return ScriptHash.Equals(other.ScriptHash); + } + + public override bool Equals(object obj) + { + return Equals(obj as VerificationContract); + } + + public override int GetHashCode() + { + return ScriptHash.GetHashCode(); + } + + public void Serialize(BinaryWriter writer) + { + writer.WriteVarBytes(ParameterList.Select(p => (byte)p).ToArray()); + writer.WriteVarBytes(Script); + } +} diff --git a/src/Plugins/SQLiteWallet/WalletDataContext.cs b/src/Plugins/SQLiteWallet/WalletDataContext.cs new file mode 100644 index 0000000000..79ca538cd1 --- /dev/null +++ b/src/Plugins/SQLiteWallet/WalletDataContext.cs @@ -0,0 +1,64 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// WalletDataContext.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.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Neo.Wallets.SQLite; + +class WalletDataContext : DbContext +{ + public DbSet Accounts { get; set; } + public DbSet
Addresses { get; set; } + public DbSet Contracts { get; set; } + public DbSet Keys { get; set; } + + private readonly string filename; + + public WalletDataContext(string filename) + { + this.filename = filename; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + SqliteConnectionStringBuilder sb = new() + { + DataSource = filename + }; + optionsBuilder.UseSqlite(sb.ToString()); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().ToTable(nameof(Account)); + modelBuilder.Entity().HasKey(p => p.PublicKeyHash); + modelBuilder.Entity().Property(p => p.Nep2key).HasColumnType("VarChar").HasMaxLength(byte.MaxValue).IsRequired(); + modelBuilder.Entity().Property(p => p.PublicKeyHash).HasColumnType("Binary").HasMaxLength(20).IsRequired(); + modelBuilder.Entity
().ToTable(nameof(Address)); + modelBuilder.Entity
().HasKey(p => p.ScriptHash); + modelBuilder.Entity
().Property(p => p.ScriptHash).HasColumnType("Binary").HasMaxLength(20).IsRequired(); + modelBuilder.Entity().ToTable(nameof(Contract)); + modelBuilder.Entity().HasKey(p => p.ScriptHash); + modelBuilder.Entity().HasIndex(p => p.PublicKeyHash); + modelBuilder.Entity().HasOne(p => p.Account).WithMany().HasForeignKey(p => p.PublicKeyHash).OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity().HasOne(p => p.Address).WithMany().HasForeignKey(p => p.ScriptHash).OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity().Property(p => p.RawData).HasColumnType("VarBinary").IsRequired(); + modelBuilder.Entity().Property(p => p.ScriptHash).HasColumnType("Binary").HasMaxLength(20).IsRequired(); + modelBuilder.Entity().Property(p => p.PublicKeyHash).HasColumnType("Binary").HasMaxLength(20).IsRequired(); + modelBuilder.Entity().ToTable(nameof(Key)); + modelBuilder.Entity().HasKey(p => p.Name); + modelBuilder.Entity().Property(p => p.Name).HasColumnType("VarChar").HasMaxLength(20).IsRequired(); + modelBuilder.Entity().Property(p => p.Value).HasColumnType("VarBinary").IsRequired(); + } +} diff --git a/src/Plugins/StateService/Network/MessageType.cs b/src/Plugins/StateService/Network/MessageType.cs new file mode 100644 index 0000000000..88ed5b2e9f --- /dev/null +++ b/src/Plugins/StateService/Network/MessageType.cs @@ -0,0 +1,19 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// MessageType.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.Plugins.StateService.Network +{ + enum MessageType : byte + { + Vote, + StateRoot, + } +} diff --git a/src/Plugins/StateService/Network/StateRoot.cs b/src/Plugins/StateService/Network/StateRoot.cs new file mode 100644 index 0000000000..5b4e8610f2 --- /dev/null +++ b/src/Plugins/StateService/Network/StateRoot.cs @@ -0,0 +1,123 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// StateRoot.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Cryptography.ECC; +using Neo.IO; +using Neo.Json; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using System; +using System.IO; + +namespace Neo.Plugins.StateService.Network +{ + class StateRoot : IVerifiable + { + public const byte CurrentVersion = 0x00; + + public byte Version; + public uint Index; + public UInt256 RootHash; + public Witness Witness; + + private UInt256 _hash = null; + public UInt256 Hash + { + get + { + if (_hash is null) + { + _hash = this.CalculateHash(); + } + return _hash; + } + } + + Witness[] IVerifiable.Witnesses + { + get + { + return new[] { Witness }; + } + set + { + if (value.Length != 1) throw new ArgumentException(null, nameof(value)); + Witness = value[0]; + } + } + + int ISerializable.Size => + sizeof(byte) + //Version + sizeof(uint) + //Index + UInt256.Length + //RootHash + (Witness is null ? 1 : 1 + Witness.Size); //Witness + + void ISerializable.Deserialize(ref MemoryReader reader) + { + DeserializeUnsigned(ref reader); + Witness[] witnesses = reader.ReadSerializableArray(1); + Witness = witnesses.Length switch + { + 0 => null, + 1 => witnesses[0], + _ => throw new FormatException(), + }; + } + + public void DeserializeUnsigned(ref MemoryReader reader) + { + Version = reader.ReadByte(); + Index = reader.ReadUInt32(); + RootHash = reader.ReadSerializable(); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + SerializeUnsigned(writer); + if (Witness is null) + writer.WriteVarInt(0); + else + writer.Write(new[] { Witness }); + } + + public void SerializeUnsigned(BinaryWriter writer) + { + writer.Write(Version); + writer.Write(Index); + writer.Write(RootHash); + } + + public bool Verify(ProtocolSettings settings, DataCache snapshot) + { + return this.VerifyWitnesses(settings, snapshot, 2_00000000L); + } + + public UInt160[] GetScriptHashesForVerifying(DataCache snapshot) + { + ECPoint[] validators = NativeContract.RoleManagement.GetDesignatedByRole(snapshot, Role.StateValidator, Index); + if (validators.Length < 1) throw new InvalidOperationException("No script hash for state root verifying"); + return new UInt160[] { Contract.GetBFTAddress(validators) }; + } + + public JObject ToJson() + { + var json = new JObject(); + json["version"] = Version; + json["index"] = Index; + json["roothash"] = RootHash.ToString(); + json["witnesses"] = Witness is null ? new JArray() : new JArray(Witness.ToJson()); + return json; + } + } +} diff --git a/src/Plugins/StateService/Network/Vote.cs b/src/Plugins/StateService/Network/Vote.cs new file mode 100644 index 0000000000..e6840c7a9f --- /dev/null +++ b/src/Plugins/StateService/Network/Vote.cs @@ -0,0 +1,40 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Vote.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.Plugins.StateService.Network +{ + class Vote : ISerializable + { + public int ValidatorIndex; + public uint RootIndex; + public ReadOnlyMemory Signature; + + int ISerializable.Size => sizeof(int) + sizeof(uint) + Signature.GetVarSize(); + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write(ValidatorIndex); + writer.Write(RootIndex); + writer.WriteVarBytes(Signature.Span); + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + ValidatorIndex = reader.ReadInt32(); + RootIndex = reader.ReadUInt32(); + Signature = reader.ReadVarMemory(64); + } + } +} diff --git a/src/Plugins/StateService/README.md b/src/Plugins/StateService/README.md new file mode 100644 index 0000000000..c8e6a2ef0d --- /dev/null +++ b/src/Plugins/StateService/README.md @@ -0,0 +1,70 @@ +# StateService + +## RPC API + +### GetStateRoot +#### Params +|Name|Type|Summary|Required| +|-|-|-|-| +|Index|uint|index|true| +#### Result +StateRoot Object +|Name|Type|Summary| +|-|-|-| +|version|number|version| +|index|number|index| +|roothash|string|version| +|witness|Object|witness from validators| + +### GetProof +#### Params +|Name|Type|Summary|Required| +|-|-|-|-| +|RootHash|UInt256|state root|true| +|ScriptHash|UInt160|contract script hash|true| +|Key|base64 string|key|true| +#### Result +Proof in base64 string + +### VerifyProof +#### Params +|Name|Type|Summary| +|-|-|-| +|RootHash|UInt256|state root|true| +|Proof|base64 string|proof|true| +#### Result +Value in base64 string + +### GetStateheight +#### Result +|Name|Type|Summary| +|-|-|-| +|localrootindex|number|root hash index calculated locally| +|validatedrootindex|number|root hash index verified by validators| + +### GetState +#### Params +|Name|Type|Summary|Required| +|-|-|-|-| +|RootHash|UInt256|specify state|true| +|ScriptHash|UInt160|contract script hash|true| +|Key|base64 string|key|true| +#### Result +Value in base64 string or `null` + +### FindStates +#### Params +|Name|Type|Summary|Required| +|-|-|-|-| +|RootHash|UInt256|specify state|true| +|ScriptHash|UInt160|contract script hash|true| +|Prefix|base64 string|key prefix|true| +|From|base64 string|start key, default `Empty`|optional| +|Count|number|count of results in one request, default `MaxFindResultItems`|optional| +#### Result +|Name|Type|Summary| +|-|-|-| +|firstProof|string|proof of first value in results| +|lastProof|string|proof of last value in results| +|truncated|bool|whether the results is truncated because of limitation| +|results|array|key-values found| diff --git a/src/Plugins/StateService/Settings.cs b/src/Plugins/StateService/Settings.cs new file mode 100644 index 0000000000..8557866bc1 --- /dev/null +++ b/src/Plugins/StateService/Settings.cs @@ -0,0 +1,40 @@ +// 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.StateService +{ + internal class Settings + { + public string Path { get; } + public bool FullState { get; } + public uint Network { get; } + public bool AutoVerify { get; } + public int MaxFindResultItems { get; } + + public static Settings Default { get; private set; } + + private Settings(IConfigurationSection section) + { + Path = section.GetValue("Path", "Data_MPT_{0}"); + FullState = section.GetValue("FullState", false); + Network = section.GetValue("Network", 5195086u); + AutoVerify = section.GetValue("AutoVerify", false); + MaxFindResultItems = section.GetValue("MaxFindResultItems", 100); + } + + public static void Load(IConfigurationSection section) + { + Default = new Settings(section); + } + } +} diff --git a/src/Plugins/StateService/StatePlugin.cs b/src/Plugins/StateService/StatePlugin.cs new file mode 100644 index 0000000000..36d2c01050 --- /dev/null +++ b/src/Plugins/StateService/StatePlugin.cs @@ -0,0 +1,360 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// StatePlugin.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.Cryptography.MPTTrie; +using Neo.IO; +using Neo.Json; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.Plugins.StateService.Network; +using Neo.Plugins.StateService.Storage; +using Neo.Plugins.StateService.Verification; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.Wallets; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using static Neo.Ledger.Blockchain; + +namespace Neo.Plugins.StateService +{ + public class StatePlugin : Plugin + { + public const string StatePayloadCategory = "StateService"; + public override string Name => "StateService"; + public override string Description => "Enables MPT for the node"; + public override string ConfigFile => System.IO.Path.Combine(RootPath, "StateService.json"); + + internal IActorRef Store; + internal IActorRef Verifier; + + internal static NeoSystem _system; + private IWalletProvider walletProvider; + + public StatePlugin() + { + Blockchain.Committing += OnCommitting; + Blockchain.Committed += OnCommitted; + } + + protected override void Configure() + { + Settings.Load(GetConfiguration()); + } + + protected override void OnSystemLoaded(NeoSystem system) + { + if (system.Settings.Network != Settings.Default.Network) return; + _system = system; + Store = _system.ActorSystem.ActorOf(StateStore.Props(this, string.Format(Settings.Default.Path, system.Settings.Network.ToString("X8")))); + _system.ServiceAdded += NeoSystem_ServiceAdded; + RpcServerPlugin.RegisterMethods(this, Settings.Default.Network); + } + + private void NeoSystem_ServiceAdded(object sender, object service) + { + if (service is IWalletProvider) + { + walletProvider = service as IWalletProvider; + _system.ServiceAdded -= NeoSystem_ServiceAdded; + if (Settings.Default.AutoVerify) + { + walletProvider.WalletChanged += WalletProvider_WalletChanged; + } + } + } + + private void WalletProvider_WalletChanged(object sender, Wallet wallet) + { + walletProvider.WalletChanged -= WalletProvider_WalletChanged; + Start(wallet); + } + + public override void Dispose() + { + base.Dispose(); + Blockchain.Committing -= OnCommitting; + Blockchain.Committed -= OnCommitted; + if (Store is not null) _system.EnsureStopped(Store); + if (Verifier is not null) _system.EnsureStopped(Verifier); + } + + private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + if (system.Settings.Network != Settings.Default.Network) return; + StateStore.Singleton.UpdateLocalStateRootSnapshot(block.Index, snapshot.GetChangeSet().Where(p => p.State != TrackState.None).Where(p => p.Key.Id != NativeContract.Ledger.Id).ToList()); + } + + private void OnCommitted(NeoSystem system, Block block) + { + if (system.Settings.Network != Settings.Default.Network) return; + StateStore.Singleton.UpdateLocalStateRoot(block.Index); + } + + [ConsoleCommand("start states", Category = "StateService", Description = "Start as a state verifier if wallet is open")] + private void OnStartVerifyingState() + { + if (_system is null || _system.Settings.Network != Settings.Default.Network) throw new InvalidOperationException("Network doesn't match"); + Start(walletProvider.GetWallet()); + } + + public void Start(Wallet wallet) + { + if (Verifier is not null) + { + ConsoleHelper.Warning("Already started!"); + return; + } + if (wallet is null) + { + ConsoleHelper.Warning("Please open wallet first!"); + return; + } + Verifier = _system.ActorSystem.ActorOf(VerificationService.Props(wallet)); + } + + [ConsoleCommand("state root", Category = "StateService", Description = "Get state root by index")] + private void OnGetStateRoot(uint index) + { + if (_system is null || _system.Settings.Network != Settings.Default.Network) throw new InvalidOperationException("Network doesn't match"); + using var snapshot = StateStore.Singleton.GetSnapshot(); + StateRoot state_root = snapshot.GetStateRoot(index); + if (state_root is null) + ConsoleHelper.Warning("Unknown state root"); + else + ConsoleHelper.Info(state_root.ToJson().ToString()); + } + + [ConsoleCommand("state height", Category = "StateService", Description = "Get current state root index")] + private void OnGetStateHeight() + { + if (_system is null || _system.Settings.Network != Settings.Default.Network) throw new InvalidOperationException("Network doesn't match"); + ConsoleHelper.Info("LocalRootIndex: ", + $"{StateStore.Singleton.LocalRootIndex}", + " ValidatedRootIndex: ", + $"{StateStore.Singleton.ValidatedRootIndex}"); + } + + [ConsoleCommand("get proof", Category = "StateService", Description = "Get proof of key and contract hash")] + private void OnGetProof(UInt256 root_hash, UInt160 script_hash, string key) + { + if (_system is null || _system.Settings.Network != Settings.Default.Network) throw new InvalidOperationException("Network doesn't match"); + try + { + ConsoleHelper.Info("Proof: ", GetProof(root_hash, script_hash, Convert.FromBase64String(key))); + } + catch (RpcException e) + { + ConsoleHelper.Error(e.Message); + } + } + + [ConsoleCommand("verify proof", Category = "StateService", Description = "Verify proof, return value if successed")] + private void OnVerifyProof(UInt256 root_hash, string proof) + { + try + { + ConsoleHelper.Info("Verify Result: ", + VerifyProof(root_hash, Convert.FromBase64String(proof))); + } + catch (RpcException e) + { + ConsoleHelper.Error(e.Message); + } + } + + [RpcMethod] + public JToken GetStateRoot(JArray _params) + { + uint index = Result.Ok_Or(() => uint.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid state root index: {_params[0]}")); + using var snapshot = StateStore.Singleton.GetSnapshot(); + StateRoot state_root = snapshot.GetStateRoot(index).NotNull_Or(RpcError.UnknownStateRoot); + return state_root.ToJson(); + } + + private string GetProof(Trie trie, int contract_id, byte[] key) + { + StorageKey skey = new() + { + Id = contract_id, + Key = key, + }; + return GetProof(trie, skey); + } + + private string GetProof(Trie trie, StorageKey skey) + { + trie.TryGetProof(skey.ToArray(), out var proof).True_Or(RpcError.UnknownStorageItem); + using MemoryStream ms = new(); + using BinaryWriter writer = new(ms, Utility.StrictUTF8); + + writer.WriteVarBytes(skey.ToArray()); + writer.WriteVarInt(proof.Count); + foreach (var item in proof) + { + writer.WriteVarBytes(item); + } + writer.Flush(); + + return Convert.ToBase64String(ms.ToArray()); + } + + private string GetProof(UInt256 root_hash, UInt160 script_hash, byte[] key) + { + (!Settings.Default.FullState && StateStore.Singleton.CurrentLocalRootHash != root_hash).False_Or(RpcError.UnsupportedState); + using var store = StateStore.Singleton.GetStoreSnapshot(); + var trie = new Trie(store, root_hash); + var contract = GetHistoricalContractState(trie, script_hash).NotNull_Or(RpcError.UnknownContract); + return GetProof(trie, contract.Id, key); + } + + [RpcMethod] + public JToken GetProof(JArray _params) + { + UInt256 root_hash = Result.Ok_Or(() => UInt256.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid root hash: {_params[0]}")); + UInt160 script_hash = Result.Ok_Or(() => UInt160.Parse(_params[1].AsString()), RpcError.InvalidParams.WithData($"Invalid script hash: {_params[1]}")); + byte[] key = Result.Ok_Or(() => Convert.FromBase64String(_params[2].AsString()), RpcError.InvalidParams.WithData($"Invalid key: {_params[2]}")); + return GetProof(root_hash, script_hash, key); + } + + private string VerifyProof(UInt256 root_hash, byte[] proof) + { + var proofs = new HashSet(); + + using MemoryStream ms = new(proof, false); + using BinaryReader reader = new(ms, Utility.StrictUTF8); + + var key = reader.ReadVarBytes(Node.MaxKeyLength); + var count = reader.ReadVarInt(); + for (ulong i = 0; i < count; i++) + { + proofs.Add(reader.ReadVarBytes()); + } + + var value = Trie.VerifyProof(root_hash, key, proofs).NotNull_Or(RpcError.InvalidProof); + return Convert.ToBase64String(value); + } + + [RpcMethod] + public JToken VerifyProof(JArray _params) + { + UInt256 root_hash = Result.Ok_Or(() => UInt256.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid root hash: {_params[0]}")); + byte[] proof_bytes = Result.Ok_Or(() => Convert.FromBase64String(_params[1].AsString()), RpcError.InvalidParams.WithData($"Invalid proof: {_params[1]}")); + return VerifyProof(root_hash, proof_bytes); + } + + [RpcMethod] + public JToken GetStateHeight(JArray _params) + { + var json = new JObject(); + json["localrootindex"] = StateStore.Singleton.LocalRootIndex; + json["validatedrootindex"] = StateStore.Singleton.ValidatedRootIndex; + return json; + } + + private ContractState GetHistoricalContractState(Trie trie, UInt160 script_hash) + { + const byte prefix = 8; + StorageKey skey = new KeyBuilder(NativeContract.ContractManagement.Id, prefix).Add(script_hash); + return trie.TryGetValue(skey.ToArray(), out var value) ? value.AsSerializable().GetInteroperable() : null; + } + + private StorageKey ParseStorageKey(byte[] data) + { + return new() + { + Id = BinaryPrimitives.ReadInt32LittleEndian(data), + Key = data.AsMemory(sizeof(int)), + }; + } + + [RpcMethod] + public JToken FindStates(JArray _params) + { + var root_hash = Result.Ok_Or(() => UInt256.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid root hash: {_params[0]}")); + (!Settings.Default.FullState && StateStore.Singleton.CurrentLocalRootHash != root_hash).False_Or(RpcError.UnsupportedState); + var script_hash = Result.Ok_Or(() => UInt160.Parse(_params[1].AsString()), RpcError.InvalidParams.WithData($"Invalid script hash: {_params[1]}")); + var prefix = Result.Ok_Or(() => Convert.FromBase64String(_params[2].AsString()), RpcError.InvalidParams.WithData($"Invalid prefix: {_params[2]}")); + byte[] key = Array.Empty(); + if (3 < _params.Count) + key = Result.Ok_Or(() => Convert.FromBase64String(_params[3].AsString()), RpcError.InvalidParams.WithData($"Invalid key: {_params[3]}")); + int count = Settings.Default.MaxFindResultItems; + if (4 < _params.Count) + count = Result.Ok_Or(() => int.Parse(_params[4].AsString()), RpcError.InvalidParams.WithData($"Invalid count: {_params[4]}")); + if (Settings.Default.MaxFindResultItems < count) + count = Settings.Default.MaxFindResultItems; + using var store = StateStore.Singleton.GetStoreSnapshot(); + var trie = new Trie(store, root_hash); + var contract = GetHistoricalContractState(trie, script_hash).NotNull_Or(RpcError.UnknownContract); + StorageKey pkey = new() + { + Id = contract.Id, + Key = prefix, + }; + StorageKey fkey = new() + { + Id = pkey.Id, + Key = key, + }; + JObject json = new(); + JArray jarr = new(); + int i = 0; + foreach (var (ikey, ivalue) in trie.Find(pkey.ToArray(), 0 < key.Length ? fkey.ToArray() : null)) + { + if (count < i) break; + if (i < count) + { + JObject j = new(); + j["key"] = Convert.ToBase64String(ParseStorageKey(ikey.ToArray()).Key.Span); + j["value"] = Convert.ToBase64String(ivalue.Span); + jarr.Add(j); + } + i++; + }; + if (0 < jarr.Count) + { + json["firstProof"] = GetProof(trie, contract.Id, Convert.FromBase64String(jarr.First()["key"].AsString())); + } + if (1 < jarr.Count) + { + json["lastProof"] = GetProof(trie, contract.Id, Convert.FromBase64String(jarr.Last()["key"].AsString())); + } + json["truncated"] = count < i; + json["results"] = jarr; + return json; + } + + [RpcMethod] + public JToken GetState(JArray _params) + { + var root_hash = Result.Ok_Or(() => UInt256.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid root hash: {_params[0]}")); + (!Settings.Default.FullState && StateStore.Singleton.CurrentLocalRootHash != root_hash).False_Or(RpcError.UnsupportedState); + var script_hash = Result.Ok_Or(() => UInt160.Parse(_params[1].AsString()), RpcError.InvalidParams.WithData($"Invalid script hash: {_params[1]}")); + var key = Result.Ok_Or(() => Convert.FromBase64String(_params[2].AsString()), RpcError.InvalidParams.WithData($"Invalid key: {_params[2]}")); + using var store = StateStore.Singleton.GetStoreSnapshot(); + var trie = new Trie(store, root_hash); + + var contract = GetHistoricalContractState(trie, script_hash).NotNull_Or(RpcError.UnknownContract); + StorageKey skey = new() + { + Id = contract.Id, + Key = key, + }; + return Convert.ToBase64String(trie[skey.ToArray()]); + } + } +} diff --git a/src/Plugins/StateService/StateService.csproj b/src/Plugins/StateService/StateService.csproj new file mode 100644 index 0000000000..b8e395c990 --- /dev/null +++ b/src/Plugins/StateService/StateService.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + Neo.Plugins.StateService + true + + + + + + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/src/Plugins/StateService/StateService.json b/src/Plugins/StateService/StateService.json new file mode 100644 index 0000000000..265436fc30 --- /dev/null +++ b/src/Plugins/StateService/StateService.json @@ -0,0 +1,12 @@ +{ + "PluginConfiguration": { + "Path": "Data_MPT_{0}", + "FullState": false, + "Network": 860833102, + "AutoVerify": false, + "MaxFindResultItems": 100 + }, + "Dependency": [ + "RpcServer" + ] +} diff --git a/src/Plugins/StateService/Storage/Keys.cs b/src/Plugins/StateService/Storage/Keys.cs new file mode 100644 index 0000000000..4ffecef48e --- /dev/null +++ b/src/Plugins/StateService/Storage/Keys.cs @@ -0,0 +1,30 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Keys.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.Buffers.Binary; + +namespace Neo.Plugins.StateService.Storage +{ + public static class Keys + { + public static byte[] StateRoot(uint index) + { + byte[] buffer = new byte[sizeof(uint) + 1]; + buffer[0] = 1; + BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(1), index); + return buffer; + } + + public static readonly byte[] CurrentLocalRootIndex = { 0x02 }; + public static readonly byte[] CurrentValidatedRootIndex = { 0x04 }; + } +} diff --git a/src/Plugins/StateService/Storage/StateSnapshot.cs b/src/Plugins/StateService/Storage/StateSnapshot.cs new file mode 100644 index 0000000000..70ec006227 --- /dev/null +++ b/src/Plugins/StateService/Storage/StateSnapshot.cs @@ -0,0 +1,92 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// StateSnapshot.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.MPTTrie; +using Neo.IO; +using Neo.Persistence; +using Neo.Plugins.StateService.Network; +using System; + +namespace Neo.Plugins.StateService.Storage +{ + class StateSnapshot : IDisposable + { + private readonly ISnapshot snapshot; + public Trie Trie; + + public StateSnapshot(IStore store) + { + snapshot = store.GetSnapshot(); + Trie = new Trie(snapshot, CurrentLocalRootHash(), Settings.Default.FullState); + } + + public StateRoot GetStateRoot(uint index) + { + return snapshot.TryGet(Keys.StateRoot(index))?.AsSerializable(); + } + + public void AddLocalStateRoot(StateRoot state_root) + { + snapshot.Put(Keys.StateRoot(state_root.Index), state_root.ToArray()); + snapshot.Put(Keys.CurrentLocalRootIndex, BitConverter.GetBytes(state_root.Index)); + } + + public uint? CurrentLocalRootIndex() + { + var bytes = snapshot.TryGet(Keys.CurrentLocalRootIndex); + if (bytes is null) return null; + return BitConverter.ToUInt32(bytes); + } + + public UInt256 CurrentLocalRootHash() + { + var index = CurrentLocalRootIndex(); + if (index is null) return null; + return GetStateRoot((uint)index)?.RootHash; + } + + public void AddValidatedStateRoot(StateRoot state_root) + { + if (state_root?.Witness is null) + throw new ArgumentException(nameof(state_root) + " missing witness in invalidated state root"); + snapshot.Put(Keys.StateRoot(state_root.Index), state_root.ToArray()); + snapshot.Put(Keys.CurrentValidatedRootIndex, BitConverter.GetBytes(state_root.Index)); + } + + public uint? CurrentValidatedRootIndex() + { + var bytes = snapshot.TryGet(Keys.CurrentValidatedRootIndex); + if (bytes is null) return null; + return BitConverter.ToUInt32(bytes); + } + + public UInt256 CurrentValidatedRootHash() + { + var index = CurrentLocalRootIndex(); + if (index is null) return null; + var state_root = GetStateRoot((uint)index); + if (state_root is null || state_root.Witness is null) + throw new InvalidOperationException(nameof(CurrentValidatedRootHash) + " could not get validated state root"); + return state_root.RootHash; + } + + public void Commit() + { + Trie.Commit(); + snapshot.Commit(); + } + + public void Dispose() + { + snapshot.Dispose(); + } + } +} diff --git a/src/Plugins/StateService/Storage/StateStore.cs b/src/Plugins/StateService/Storage/StateStore.cs new file mode 100644 index 0000000000..f2ca6d7d05 --- /dev/null +++ b/src/Plugins/StateService/Storage/StateStore.cs @@ -0,0 +1,188 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// StateStore.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.Payloads; +using Neo.Persistence; +using Neo.Plugins.StateService.Network; +using Neo.Plugins.StateService.Verification; +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Neo.Plugins.StateService.Storage +{ + class StateStore : UntypedActor + { + private readonly StatePlugin system; + private readonly IStore store; + private const int MaxCacheCount = 100; + private readonly Dictionary cache = new Dictionary(); + private StateSnapshot currentSnapshot; + private StateSnapshot _state_snapshot; + public UInt256 CurrentLocalRootHash => currentSnapshot.CurrentLocalRootHash(); + public uint? LocalRootIndex => currentSnapshot.CurrentLocalRootIndex(); + public uint? ValidatedRootIndex => currentSnapshot.CurrentValidatedRootIndex(); + + private static StateStore singleton; + public static StateStore Singleton + { + get + { + while (singleton is null) Thread.Sleep(10); + return singleton; + } + } + + public StateStore(StatePlugin system, string path) + { + if (singleton != null) throw new InvalidOperationException(nameof(StateStore)); + this.system = system; + store = StatePlugin._system.LoadStore(path); + singleton = this; + StatePlugin._system.ActorSystem.EventStream.Subscribe(Self, typeof(Blockchain.RelayResult)); + UpdateCurrentSnapshot(); + } + + public void Dispose() + { + store.Dispose(); + } + + public StateSnapshot GetSnapshot() + { + return new StateSnapshot(store); + } + + public ISnapshot GetStoreSnapshot() + { + return store.GetSnapshot(); + } + + protected override void OnReceive(object message) + { + switch (message) + { + case StateRoot state_root: + OnNewStateRoot(state_root); + break; + case Blockchain.RelayResult rr: + if (rr.Result == VerifyResult.Succeed && rr.Inventory is ExtensiblePayload payload && payload.Category == StatePlugin.StatePayloadCategory) + OnStatePayload(payload); + break; + default: + break; + } + } + + private void OnStatePayload(ExtensiblePayload payload) + { + if (payload.Data.Length == 0) return; + if ((MessageType)payload.Data.Span[0] != MessageType.StateRoot) return; + StateRoot message; + try + { + message = payload.Data[1..].AsSerializable(); + } + catch (FormatException) + { + return; + } + OnNewStateRoot(message); + } + + private bool OnNewStateRoot(StateRoot state_root) + { + if (state_root?.Witness is null) return false; + if (ValidatedRootIndex != null && state_root.Index <= ValidatedRootIndex) return false; + if (LocalRootIndex is null) throw new InvalidOperationException(nameof(StateStore) + " could not get local root index"); + if (LocalRootIndex < state_root.Index && state_root.Index < LocalRootIndex + MaxCacheCount) + { + cache.Add(state_root.Index, state_root); + return true; + } + using var state_snapshot = Singleton.GetSnapshot(); + StateRoot local_root = state_snapshot.GetStateRoot(state_root.Index); + if (local_root is null || local_root.Witness != null) return false; + if (!state_root.Verify(StatePlugin._system.Settings, StatePlugin._system.StoreView)) return false; + if (local_root.RootHash != state_root.RootHash) return false; + state_snapshot.AddValidatedStateRoot(state_root); + state_snapshot.Commit(); + UpdateCurrentSnapshot(); + system.Verifier?.Tell(new VerificationService.ValidatedRootPersisted { Index = state_root.Index }); + return true; + } + + public void UpdateLocalStateRootSnapshot(uint height, List change_set) + { + _state_snapshot = Singleton.GetSnapshot(); + foreach (var item in change_set) + { + switch (item.State) + { + case TrackState.Added: + _state_snapshot.Trie.Put(item.Key.ToArray(), item.Item.ToArray()); + break; + case TrackState.Changed: + _state_snapshot.Trie.Put(item.Key.ToArray(), item.Item.ToArray()); + break; + case TrackState.Deleted: + _state_snapshot.Trie.Delete(item.Key.ToArray()); + break; + } + } + UInt256 root_hash = _state_snapshot.Trie.Root.Hash; + StateRoot state_root = new StateRoot + { + Version = StateRoot.CurrentVersion, + Index = height, + RootHash = root_hash, + Witness = null, + }; + _state_snapshot.AddLocalStateRoot(state_root); + } + + public void UpdateLocalStateRoot(uint height) + { + _state_snapshot?.Commit(); + _state_snapshot = null; + UpdateCurrentSnapshot(); + system.Verifier?.Tell(new VerificationService.BlockPersisted { Index = height }); + CheckValidatedStateRoot(height); + } + + private void CheckValidatedStateRoot(uint index) + { + if (cache.TryGetValue(index, out StateRoot state_root)) + { + cache.Remove(index); + Self.Tell(state_root); + } + } + + private void UpdateCurrentSnapshot() + { + Interlocked.Exchange(ref currentSnapshot, GetSnapshot())?.Dispose(); + } + + protected override void PostStop() + { + base.PostStop(); + } + + public static Props Props(StatePlugin system, string path) + { + return Akka.Actor.Props.Create(() => new StateStore(system, path)); + } + } +} diff --git a/src/Plugins/StateService/Verification/VerificationContext.cs b/src/Plugins/StateService/Verification/VerificationContext.cs new file mode 100644 index 0000000000..feaf591795 --- /dev/null +++ b/src/Plugins/StateService/Verification/VerificationContext.cs @@ -0,0 +1,177 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// VerificationContext.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.Cryptography.ECC; +using Neo.IO; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using Neo.Plugins.StateService.Network; +using Neo.Plugins.StateService.Storage; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.Wallets; +using System.Collections.Concurrent; +using System.IO; + +namespace Neo.Plugins.StateService.Verification +{ + class VerificationContext + { + private const uint MaxValidUntilBlockIncrement = 100; + private StateRoot root; + private ExtensiblePayload rootPayload; + private ExtensiblePayload votePayload; + private readonly Wallet wallet; + private readonly KeyPair keyPair; + private readonly int myIndex; + private readonly uint rootIndex; + private readonly ECPoint[] verifiers; + private int M => verifiers.Length - (verifiers.Length - 1) / 3; + private readonly ConcurrentDictionary signatures = new ConcurrentDictionary(); + + public int Retries; + public bool IsValidator => myIndex >= 0; + public int MyIndex => myIndex; + public uint RootIndex => rootIndex; + public ECPoint[] Verifiers => verifiers; + public int Sender + { + get + { + int p = ((int)rootIndex - Retries) % verifiers.Length; + return p >= 0 ? p : p + verifiers.Length; + } + } + public bool IsSender => myIndex == Sender; + public ICancelable Timer; + public StateRoot StateRoot + { + get + { + if (root is null) + { + using var snapshot = StateStore.Singleton.GetSnapshot(); + root = snapshot.GetStateRoot(rootIndex); + } + return root; + } + } + public ExtensiblePayload StateRootMessage => rootPayload; + public ExtensiblePayload VoteMessage + { + get + { + if (votePayload is null) + votePayload = CreateVoteMessage(); + return votePayload; + } + } + + public VerificationContext(Wallet wallet, uint index) + { + this.wallet = wallet; + Retries = 0; + myIndex = -1; + rootIndex = index; + verifiers = NativeContract.RoleManagement.GetDesignatedByRole(StatePlugin._system.StoreView, Role.StateValidator, index); + if (wallet is null) return; + for (int i = 0; i < verifiers.Length; i++) + { + WalletAccount account = wallet.GetAccount(verifiers[i]); + if (account?.HasKey != true) continue; + myIndex = i; + keyPair = account.GetKey(); + break; + } + } + + private ExtensiblePayload CreateVoteMessage() + { + if (StateRoot is null) return null; + if (!signatures.TryGetValue(myIndex, out byte[] sig)) + { + sig = StateRoot.Sign(keyPair, StatePlugin._system.Settings.Network); + signatures[myIndex] = sig; + } + return CreatePayload(MessageType.Vote, new Vote + { + RootIndex = rootIndex, + ValidatorIndex = myIndex, + Signature = sig + }, VerificationService.MaxCachedVerificationProcessCount); + } + + public bool AddSignature(int index, byte[] sig) + { + if (M <= signatures.Count) return false; + if (index < 0 || verifiers.Length <= index) return false; + if (signatures.ContainsKey(index)) return false; + Utility.Log(nameof(VerificationContext), LogLevel.Info, $"vote received, height={rootIndex}, index={index}"); + ECPoint validator = verifiers[index]; + byte[] hash_data = StateRoot?.GetSignData(StatePlugin._system.Settings.Network); + if (hash_data is null || !Crypto.VerifySignature(hash_data, sig, validator)) + { + Utility.Log(nameof(VerificationContext), LogLevel.Info, "incorrect vote, invalid signature"); + return false; + } + return signatures.TryAdd(index, sig); + } + + public bool CheckSignatures() + { + if (StateRoot is null) return false; + if (signatures.Count < M) return false; + if (StateRoot.Witness is null) + { + Contract contract = Contract.CreateMultiSigContract(M, verifiers); + ContractParametersContext sc = new(StatePlugin._system.StoreView, StateRoot, StatePlugin._system.Settings.Network); + for (int i = 0, j = 0; i < verifiers.Length && j < M; i++) + { + if (!signatures.TryGetValue(i, out byte[] sig)) continue; + sc.AddSignature(contract, verifiers[i], sig); + j++; + } + if (!sc.Completed) return false; + StateRoot.Witness = sc.GetWitnesses()[0]; + } + if (IsSender) + rootPayload = CreatePayload(MessageType.StateRoot, StateRoot, MaxValidUntilBlockIncrement); + return true; + } + + private ExtensiblePayload CreatePayload(MessageType type, ISerializable payload, uint validBlockEndThreshold) + { + byte[] data; + using (MemoryStream ms = new MemoryStream()) + using (BinaryWriter writer = new BinaryWriter(ms)) + { + writer.Write((byte)type); + payload.Serialize(writer); + writer.Flush(); + data = ms.ToArray(); + } + ExtensiblePayload msg = new ExtensiblePayload + { + Category = StatePlugin.StatePayloadCategory, + ValidBlockStart = StateRoot.Index, + ValidBlockEnd = StateRoot.Index + validBlockEndThreshold, + Sender = Contract.CreateSignatureRedeemScript(verifiers[MyIndex]).ToScriptHash(), + Data = data, + }; + ContractParametersContext sc = new ContractParametersContext(StatePlugin._system.StoreView, msg, StatePlugin._system.Settings.Network); + wallet.Sign(sc); + msg.Witness = sc.GetWitnesses()[0]; + return msg; + } + } +} diff --git a/src/Plugins/StateService/Verification/VerificationService.cs b/src/Plugins/StateService/Verification/VerificationService.cs new file mode 100644 index 0000000000..a8eb9fd030 --- /dev/null +++ b/src/Plugins/StateService/Verification/VerificationService.cs @@ -0,0 +1,170 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// VerificationService.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Akka.Actor; +using Akka.Util.Internal; +using Neo.IO; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Plugins.StateService.Network; +using Neo.Wallets; +using System; +using System.Collections.Concurrent; +using System.Linq; + +namespace Neo.Plugins.StateService.Verification +{ + public class VerificationService : UntypedActor + { + public class ValidatedRootPersisted { public uint Index; } + public class BlockPersisted { public uint Index; } + public const int MaxCachedVerificationProcessCount = 10; + private class Timer { public uint Index; } + private static readonly uint TimeoutMilliseconds = StatePlugin._system.Settings.MillisecondsPerBlock; + private static readonly uint DelayMilliseconds = 3000; + private readonly Wallet wallet; + private readonly ConcurrentDictionary contexts = new ConcurrentDictionary(); + + public VerificationService(Wallet wallet) + { + this.wallet = wallet; + StatePlugin._system.ActorSystem.EventStream.Subscribe(Self, typeof(Blockchain.RelayResult)); + } + + private void SendVote(VerificationContext context) + { + if (context.VoteMessage is null) return; + Utility.Log(nameof(VerificationService), LogLevel.Info, $"relay vote, height={context.RootIndex}, retry={context.Retries}"); + StatePlugin._system.Blockchain.Tell(context.VoteMessage); + } + + private void OnStateRootVote(Vote vote) + { + if (contexts.TryGetValue(vote.RootIndex, out VerificationContext context) && context.AddSignature(vote.ValidatorIndex, vote.Signature.ToArray())) + { + CheckVotes(context); + } + } + + private void CheckVotes(VerificationContext context) + { + if (context.IsSender && context.CheckSignatures()) + { + if (context.StateRootMessage is null) return; + Utility.Log(nameof(VerificationService), LogLevel.Info, $"relay state root, height={context.StateRoot.Index}, root={context.StateRoot.RootHash}"); + StatePlugin._system.Blockchain.Tell(context.StateRootMessage); + } + } + + private void OnBlockPersisted(uint index) + { + if (MaxCachedVerificationProcessCount <= contexts.Count) + { + contexts.Keys.OrderBy(p => p).Take(contexts.Count - MaxCachedVerificationProcessCount + 1).ForEach(p => + { + if (contexts.TryRemove(p, out var value)) + { + value.Timer.CancelIfNotNull(); + } + }); + } + var p = new VerificationContext(wallet, index); + if (p.IsValidator && contexts.TryAdd(index, p)) + { + p.Timer = Context.System.Scheduler.ScheduleTellOnceCancelable(TimeSpan.FromMilliseconds(DelayMilliseconds), Self, new Timer + { + Index = index, + }, ActorRefs.NoSender); + Utility.Log(nameof(VerificationContext), LogLevel.Info, $"new validate process, height={index}, index={p.MyIndex}, ongoing={contexts.Count}"); + } + } + + private void OnValidatedRootPersisted(uint index) + { + Utility.Log(nameof(VerificationService), LogLevel.Info, $"persisted state root, height={index}"); + foreach (var i in contexts.Where(i => i.Key <= index)) + { + if (contexts.TryRemove(i.Key, out var value)) + { + value.Timer.CancelIfNotNull(); + } + } + } + + private void OnTimer(uint index) + { + if (contexts.TryGetValue(index, out VerificationContext context)) + { + SendVote(context); + CheckVotes(context); + context.Timer.CancelIfNotNull(); + context.Timer = Context.System.Scheduler.ScheduleTellOnceCancelable(TimeSpan.FromMilliseconds(TimeoutMilliseconds << context.Retries), Self, new Timer + { + Index = index, + }, ActorRefs.NoSender); + context.Retries++; + } + } + + private void OnVoteMessage(ExtensiblePayload payload) + { + if (payload.Data.Length == 0) return; + if ((MessageType)payload.Data.Span[0] != MessageType.Vote) return; + Vote message; + try + { + message = payload.Data[1..].AsSerializable(); + } + catch (FormatException) + { + return; + } + OnStateRootVote(message); + } + + protected override void OnReceive(object message) + { + switch (message) + { + case Vote v: + OnStateRootVote(v); + break; + case BlockPersisted bp: + OnBlockPersisted(bp.Index); + break; + case ValidatedRootPersisted root: + OnValidatedRootPersisted(root.Index); + break; + case Timer timer: + OnTimer(timer.Index); + break; + case Blockchain.RelayResult rr: + if (rr.Result == VerifyResult.Succeed && rr.Inventory is ExtensiblePayload payload && payload.Category == StatePlugin.StatePayloadCategory) + { + OnVoteMessage(payload); + } + break; + default: + break; + } + } + + protected override void PostStop() + { + base.PostStop(); + } + + public static Props Props(Wallet wallet) + { + return Akka.Actor.Props.Create(() => new VerificationService(wallet)); + } + } +} diff --git a/src/Plugins/StatesDumper/PersistActions.cs b/src/Plugins/StatesDumper/PersistActions.cs new file mode 100644 index 0000000000..9c1aa2210c --- /dev/null +++ b/src/Plugins/StatesDumper/PersistActions.cs @@ -0,0 +1,21 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PersistActions.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.Plugins +{ + [Flags] + internal enum PersistActions : byte + { + StorageChanges = 0b00000001 + } +} diff --git a/src/Plugins/StatesDumper/Settings.cs b/src/Plugins/StatesDumper/Settings.cs new file mode 100644 index 0000000000..8fd3b9e163 --- /dev/null +++ b/src/Plugins/StatesDumper/Settings.cs @@ -0,0 +1,58 @@ +// 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; +using Neo.SmartContract.Native; +using System.Collections.Generic; +using System.Linq; + +namespace Neo.Plugins +{ + internal class Settings + { + /// + /// Amount of storages states (heights) to be dump in a given json file + /// + public uint BlockCacheSize { get; } + /// + /// Height to begin storage dump + /// + public uint HeightToBegin { get; } + /// + /// Height to begin real-time syncing and dumping on, consequently, dumping every block into a single files + /// + public int HeightToStartRealTimeSyncing { get; } + /// + /// Persisting actions + /// + public PersistActions PersistAction { get; } + public IReadOnlyList Exclude { get; } + + public static Settings Default { get; private set; } + + private Settings(IConfigurationSection section) + { + /// Geting settings for storage changes state dumper + BlockCacheSize = section.GetValue("BlockCacheSize", 1000u); + HeightToBegin = section.GetValue("HeightToBegin", 0u); + HeightToStartRealTimeSyncing = section.GetValue("HeightToStartRealTimeSyncing", -1); + PersistAction = section.GetValue("PersistAction", PersistActions.StorageChanges); + Exclude = section.GetSection("Exclude").Exists() + ? section.GetSection("Exclude").GetChildren().Select(p => int.Parse(p.Value)).ToArray() + : new[] { NativeContract.Ledger.Id }; + } + + public static void Load(IConfigurationSection section) + { + Default = new Settings(section); + } + } +} diff --git a/src/Plugins/StatesDumper/StatesDumper.cs b/src/Plugins/StatesDumper/StatesDumper.cs new file mode 100644 index 0000000000..da1bbd3f69 --- /dev/null +++ b/src/Plugins/StatesDumper/StatesDumper.cs @@ -0,0 +1,169 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// StatesDumper.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.ConsoleService; +using Neo.IO; +using Neo.Json; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract.Native; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Neo.Plugins +{ + public class StatesDumper : Plugin + { + private readonly Dictionary bs_cache = new Dictionary(); + private readonly Dictionary systems = new Dictionary(); + + public override string Description => "Exports Neo-CLI status data"; + + public override string ConfigFile => System.IO.Path.Combine(RootPath, "StatesDumper.json"); + + public StatesDumper() + { + Blockchain.Committing += OnCommitting; + Blockchain.Committed += OnCommitted; + } + + public override void Dispose() + { + Blockchain.Committing -= OnCommitting; + Blockchain.Committed -= OnCommitted; + } + + protected override void Configure() + { + Settings.Load(GetConfiguration()); + } + + protected override void OnSystemLoaded(NeoSystem system) + { + systems.Add(system.Settings.Network, system); + } + + /// + /// Process "dump storage" command + /// + [ConsoleCommand("dump storage", Category = "Storage", Description = "You can specify the contract script hash or use null to get the corresponding information from the storage")] + private void OnDumpStorage(uint network, UInt160 contractHash = null) + { + if (!systems.ContainsKey(network)) throw new InvalidOperationException("invalid network"); + string path = $"dump_{network:x8}.json"; + byte[] prefix = null; + if (contractHash is not null) + { + var contract = NativeContract.ContractManagement.GetContract(systems[network].StoreView, contractHash); + if (contract is null) throw new InvalidOperationException("contract not found"); + prefix = BitConverter.GetBytes(contract.Id); + } + var states = systems[network].StoreView.Find(prefix); + JArray array = new JArray(states.Where(p => !Settings.Default.Exclude.Contains(p.Key.Id)).Select(p => new JObject + { + ["key"] = Convert.ToBase64String(p.Key.ToArray()), + ["value"] = Convert.ToBase64String(p.Value.ToArray()) + })); + File.WriteAllText(path, array.ToString()); + ConsoleHelper.Info("States", + $"({array.Count})", + " have been dumped into file ", + $"{path}"); + } + + private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + if (Settings.Default.PersistAction.HasFlag(PersistActions.StorageChanges)) + OnPersistStorage(system.Settings.Network, snapshot); + } + + private void OnPersistStorage(uint network, DataCache snapshot) + { + uint blockIndex = NativeContract.Ledger.CurrentIndex(snapshot); + if (blockIndex >= Settings.Default.HeightToBegin) + { + JArray array = new JArray(); + + foreach (var trackable in snapshot.GetChangeSet()) + { + if (Settings.Default.Exclude.Contains(trackable.Key.Id)) + continue; + JObject state = new JObject(); + switch (trackable.State) + { + case TrackState.Added: + state["state"] = "Added"; + state["key"] = Convert.ToBase64String(trackable.Key.ToArray()); + state["value"] = Convert.ToBase64String(trackable.Item.ToArray()); + // Here we have a new trackable.Key and trackable.Item + break; + case TrackState.Changed: + state["state"] = "Changed"; + state["key"] = Convert.ToBase64String(trackable.Key.ToArray()); + state["value"] = Convert.ToBase64String(trackable.Item.ToArray()); + break; + case TrackState.Deleted: + state["state"] = "Deleted"; + state["key"] = Convert.ToBase64String(trackable.Key.ToArray()); + break; + } + array.Add(state); + } + + JObject bs_item = new JObject(); + bs_item["block"] = blockIndex; + bs_item["size"] = array.Count; + bs_item["storage"] = array; + if (!bs_cache.TryGetValue(network, out JArray cache)) + { + cache = new JArray(); + } + cache.Add(bs_item); + bs_cache[network] = cache; + } + } + + private void OnCommitted(NeoSystem system, Block block) + { + if (Settings.Default.PersistAction.HasFlag(PersistActions.StorageChanges)) + OnCommitStorage(system.Settings.Network, system.StoreView); + } + + void OnCommitStorage(uint network, DataCache snapshot) + { + if (!bs_cache.TryGetValue(network, out JArray cache)) return; + if (cache.Count == 0) return; + uint blockIndex = NativeContract.Ledger.CurrentIndex(snapshot); + if ((blockIndex % Settings.Default.BlockCacheSize == 0) || (Settings.Default.HeightToStartRealTimeSyncing != -1 && blockIndex >= Settings.Default.HeightToStartRealTimeSyncing)) + { + string path = HandlePaths(network, blockIndex); + path = $"{path}/dump-block-{blockIndex}.json"; + File.WriteAllText(path, cache.ToString()); + cache.Clear(); + } + } + + private static string HandlePaths(uint network, uint blockIndex) + { + //Default Parameter + uint storagePerFolder = 100000; + uint folder = (((blockIndex - 1) / storagePerFolder) + 1) * storagePerFolder; + if (blockIndex == 0) + folder = 0; + string dirPathWithBlock = $"./Storage_{network:x8}/BlockStorage_{folder}"; + Directory.CreateDirectory(dirPathWithBlock); + return dirPathWithBlock; + } + } +} diff --git a/src/Plugins/StatesDumper/StatesDumper.csproj b/src/Plugins/StatesDumper/StatesDumper.csproj new file mode 100644 index 0000000000..4ebf25f2c1 --- /dev/null +++ b/src/Plugins/StatesDumper/StatesDumper.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + Neo.Plugins.StatesDumper + + + + + + + + + PreserveNewest + + + + diff --git a/src/Plugins/StatesDumper/StatesDumper.json b/src/Plugins/StatesDumper/StatesDumper.json new file mode 100644 index 0000000000..c3b73767dd --- /dev/null +++ b/src/Plugins/StatesDumper/StatesDumper.json @@ -0,0 +1,9 @@ +{ + "PluginConfiguration": { + "PersistAction": "StorageChanges", + "BlockCacheSize": 1000, + "HeightToBegin": 0, + "HeightToStartRealTimeSyncing": -1, + "Exclude": [ -4 ] + } +} diff --git a/src/Plugins/StorageDumper/Settings.cs b/src/Plugins/StorageDumper/Settings.cs new file mode 100644 index 0000000000..84fe2b2231 --- /dev/null +++ b/src/Plugins/StorageDumper/Settings.cs @@ -0,0 +1,47 @@ +// 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; +using Neo.SmartContract.Native; + +namespace Neo.Plugins +{ + internal class Settings + { + /// + /// Amount of storages states (heights) to be dump in a given json file + /// + public uint BlockCacheSize { get; } + /// + /// Height to begin storage dump + /// + public uint HeightToBegin { get; } + + public IReadOnlyList Exclude { get; } + + public static Settings? Default { get; private set; } + + private Settings(IConfigurationSection section) + { + /// Geting settings for storage changes state dumper + BlockCacheSize = section.GetValue("BlockCacheSize", 1000u); + HeightToBegin = section.GetValue("HeightToBegin", 0u); + Exclude = section.GetSection("Exclude").Exists() + ? section.GetSection("Exclude").GetChildren().Select(p => int.Parse(p.Value)).ToArray() + : new[] { NativeContract.Ledger.Id }; + } + + public static void Load(IConfigurationSection section) + { + Default = new Settings(section); + } + } +} diff --git a/src/Plugins/StorageDumper/StorageDumper.cs b/src/Plugins/StorageDumper/StorageDumper.cs new file mode 100644 index 0000000000..696f5014ac --- /dev/null +++ b/src/Plugins/StorageDumper/StorageDumper.cs @@ -0,0 +1,184 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// StorageDumper.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.ConsoleService; +using Neo.IO; +using Neo.Json; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract.Native; + +namespace Neo.Plugins +{ + public class StorageDumper : Plugin + { + private readonly Dictionary systems = new Dictionary(); + + private StreamWriter _writer; + private JObject _currentBlock; + private string _lastCreateDirectory; + + + public override string Description => "Exports Neo-CLI status data"; + + public override string ConfigFile => System.IO.Path.Combine(RootPath, "StorageDumper.json"); + + public StorageDumper() + { + Blockchain.Committing += OnCommitting; + Blockchain.Committed += OnCommitted; + } + + public override void Dispose() + { + Blockchain.Committing -= OnCommitting; + Blockchain.Committed -= OnCommitted; + } + + protected override void Configure() + { + Settings.Load(GetConfiguration()); + } + + protected override void OnSystemLoaded(NeoSystem system) + { + systems.Add(system.Settings.Network, system); + } + + /// + /// Process "dump contract-storage" command + /// + [ConsoleCommand("dump contract-storage", Category = "Storage", Description = "You can specify the contract script hash or use null to get the corresponding information from the storage")] + private void OnDumpStorage(uint network, UInt160? contractHash = null) + { + if (!systems.ContainsKey(network)) throw new InvalidOperationException("invalid network"); + string path = $"dump_{network}.json"; + byte[]? prefix = null; + if (contractHash is not null) + { + var contract = NativeContract.ContractManagement.GetContract(systems[network].StoreView, contractHash); + if (contract is null) throw new InvalidOperationException("contract not found"); + prefix = BitConverter.GetBytes(contract.Id); + } + var states = systems[network].StoreView.Find(prefix); + JArray array = new JArray(states.Where(p => !Settings.Default.Exclude.Contains(p.Key.Id)).Select(p => new JObject + { + ["key"] = Convert.ToBase64String(p.Key.ToArray()), + ["value"] = Convert.ToBase64String(p.Value.ToArray()) + })); + File.WriteAllText(path, array.ToString()); + ConsoleHelper.Info("States", + $"({array.Count})", + " have been dumped into file ", + $"{path}"); + } + + private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + InitFileWriter(system.Settings.Network, snapshot); + OnPersistStorage(system.Settings.Network, snapshot); + } + + private void OnPersistStorage(uint network, DataCache snapshot) + { + uint blockIndex = NativeContract.Ledger.CurrentIndex(snapshot); + if (blockIndex >= Settings.Default.HeightToBegin) + { + JArray array = new JArray(); + + foreach (var trackable in snapshot.GetChangeSet()) + { + if (Settings.Default.Exclude.Contains(trackable.Key.Id)) + continue; + JObject state = new JObject(); + switch (trackable.State) + { + case TrackState.Added: + state["state"] = "Added"; + state["key"] = Convert.ToBase64String(trackable.Key.ToArray()); + state["value"] = Convert.ToBase64String(trackable.Item.ToArray()); + // Here we have a new trackable.Key and trackable.Item + break; + case TrackState.Changed: + state["state"] = "Changed"; + state["key"] = Convert.ToBase64String(trackable.Key.ToArray()); + state["value"] = Convert.ToBase64String(trackable.Item.ToArray()); + break; + case TrackState.Deleted: + state["state"] = "Deleted"; + state["key"] = Convert.ToBase64String(trackable.Key.ToArray()); + break; + } + array.Add(state); + } + + JObject bs_item = new JObject(); + bs_item["block"] = blockIndex; + bs_item["size"] = array.Count; + bs_item["storage"] = array; + _currentBlock = bs_item; + } + } + + + private void OnCommitted(NeoSystem system, Block block) + { + OnCommitStorage(system.Settings.Network, system.StoreView); + } + + void OnCommitStorage(uint network, DataCache snapshot) + { + if (_currentBlock != null) + { + _writer.WriteLine(_currentBlock.ToString()); + _writer.Flush(); + } + } + + private void InitFileWriter(uint network, DataCache snapshot) + { + uint blockIndex = NativeContract.Ledger.CurrentIndex(snapshot); + if (_writer == null + || blockIndex % Settings.Default.BlockCacheSize == 0) + { + string path = GetOrCreateDirectory(network, blockIndex); + var filepart = (blockIndex / Settings.Default.BlockCacheSize) * Settings.Default.BlockCacheSize; + path = $"{path}/dump-block-{filepart}.dump"; + if (_writer != null) + { + _writer.Dispose(); + } + _writer = new StreamWriter(new FileStream(path, FileMode.Append)); + } + } + + private string GetOrCreateDirectory(uint network, uint blockIndex) + { + string dirPathWithBlock = GetDirectoryPath(network, blockIndex); + if (_lastCreateDirectory != dirPathWithBlock) + { + Directory.CreateDirectory(dirPathWithBlock); + _lastCreateDirectory = dirPathWithBlock; + } + return dirPathWithBlock; + } + + private string GetDirectoryPath(uint network, uint blockIndex) + { + //Default Parameter + uint storagePerFolder = 100000; + uint folder = (blockIndex / storagePerFolder) * storagePerFolder; + return $"./StorageDumper_{network}/BlockStorage_{folder}"; + } + + } +} diff --git a/src/Plugins/StorageDumper/StorageDumper.csproj b/src/Plugins/StorageDumper/StorageDumper.csproj new file mode 100644 index 0000000000..49d5362538 --- /dev/null +++ b/src/Plugins/StorageDumper/StorageDumper.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + Neo.Plugins.StorageDumper + enable + enable + + + + + + + + + PreserveNewest + + + + diff --git a/src/Plugins/StorageDumper/StorageDumper.json b/src/Plugins/StorageDumper/StorageDumper.json new file mode 100644 index 0000000000..3f5c0537f0 --- /dev/null +++ b/src/Plugins/StorageDumper/StorageDumper.json @@ -0,0 +1,7 @@ +{ + "PluginConfiguration": { + "BlockCacheSize": 1000, + "HeightToBegin": 0, + "Exclude": [ -4 ] + } +} diff --git a/src/Plugins/TokensTracker/Extensions.cs b/src/Plugins/TokensTracker/Extensions.cs new file mode 100644 index 0000000000..7805056280 --- /dev/null +++ b/src/Plugins/TokensTracker/Extensions.cs @@ -0,0 +1,67 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Extensions.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.Persistence; +using Neo.VM.Types; +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace Neo.Plugins +{ + public static class Extensions + { + public static bool NotNull(this StackItem item) + { + return !item.IsNull; + } + + public static string ToBase64(this ReadOnlySpan item) + { + return item == null ? String.Empty : Convert.ToBase64String(item); + } + + public static int GetVarSize(this ByteString item) + { + var length = item.GetSpan().Length; + return IO.Helper.GetVarSize(length) + length; + } + + public static int GetVarSize(this BigInteger item) + { + var length = item.GetByteCount(); + return IO.Helper.GetVarSize(length) + length; + } + + public static IEnumerable<(TKey, TValue)> FindPrefix(this IStore db, byte[] prefix) + where TKey : ISerializable, new() + where TValue : class, ISerializable, new() + { + foreach (var (key, value) in db.Seek(prefix, SeekDirection.Forward)) + { + if (!key.AsSpan().StartsWith(prefix)) break; + yield return (key.AsSerializable(1), value.AsSerializable()); + } + } + + public static IEnumerable<(TKey, TValue)> FindRange(this IStore db, byte[] startKey, byte[] endKey) + where TKey : ISerializable, new() + where TValue : class, ISerializable, new() + { + foreach (var (key, value) in db.Seek(startKey, SeekDirection.Forward)) + { + if (key.AsSpan().SequenceCompareTo(endKey) > 0) break; + yield return (key.AsSerializable(1), value.AsSerializable()); + } + } + } +} diff --git a/src/Plugins/TokensTracker/TokensTracker.cs b/src/Plugins/TokensTracker/TokensTracker.cs new file mode 100644 index 0000000000..0d8863a767 --- /dev/null +++ b/src/Plugins/TokensTracker/TokensTracker.cs @@ -0,0 +1,104 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// TokensTracker.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; +using Neo.IO; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.Plugins.Trackers; +using System; +using System.Collections.Generic; +using System.Linq; +using static System.IO.Path; + +namespace Neo.Plugins +{ + public class TokensTracker : Plugin + { + private string _dbPath; + private bool _shouldTrackHistory; + private uint _maxResults; + private uint _network; + private string[] _enabledTrackers; + private IStore _db; + private NeoSystem neoSystem; + private readonly List trackers = new(); + + public override string Description => "Enquiries balances and transaction history of accounts through RPC"; + + public override string ConfigFile => System.IO.Path.Combine(RootPath, "TokensTracker.json"); + + public TokensTracker() + { + Blockchain.Committing += OnCommitting; + Blockchain.Committed += OnCommitted; + } + + public override void Dispose() + { + Blockchain.Committing -= OnCommitting; + Blockchain.Committed -= OnCommitted; + } + + protected override void Configure() + { + IConfigurationSection config = GetConfiguration(); + _dbPath = config.GetValue("DBPath", "TokensBalanceData"); + _shouldTrackHistory = config.GetValue("TrackHistory", true); + _maxResults = config.GetValue("MaxResults", 1000u); + _network = config.GetValue("Network", 860833102u); + _enabledTrackers = config.GetSection("EnabledTrackers").GetChildren().Select(p => p.Value).ToArray(); + } + + protected override void OnSystemLoaded(NeoSystem system) + { + if (system.Settings.Network != _network) return; + neoSystem = system; + string path = string.Format(_dbPath, neoSystem.Settings.Network.ToString("X8")); + _db = neoSystem.LoadStore(GetFullPath(path)); + if (_enabledTrackers.Contains("NEP-11")) + trackers.Add(new Trackers.NEP_11.Nep11Tracker(_db, _maxResults, _shouldTrackHistory, neoSystem)); + if (_enabledTrackers.Contains("NEP-17")) + trackers.Add(new Trackers.NEP_17.Nep17Tracker(_db, _maxResults, _shouldTrackHistory, neoSystem)); + foreach (TrackerBase tracker in trackers) + RpcServerPlugin.RegisterMethods(tracker, _network); + } + + private void ResetBatch() + { + foreach (var tracker in trackers) + { + tracker.ResetBatch(); + } + } + + private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + if (system.Settings.Network != _network) return; + // Start freshly with a new DBCache for each block. + ResetBatch(); + foreach (var tracker in trackers) + { + tracker.OnPersist(system, block, snapshot, applicationExecutedList); + } + } + + private void OnCommitted(NeoSystem system, Block block) + { + if (system.Settings.Network != _network) return; + foreach (var tracker in trackers) + { + tracker.Commit(); + } + } + } +} diff --git a/src/Plugins/TokensTracker/TokensTracker.csproj b/src/Plugins/TokensTracker/TokensTracker.csproj new file mode 100644 index 0000000000..7c3df5c0c6 --- /dev/null +++ b/src/Plugins/TokensTracker/TokensTracker.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + Neo.Plugins.TokensTracker + + + + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/src/Plugins/TokensTracker/TokensTracker.json b/src/Plugins/TokensTracker/TokensTracker.json new file mode 100644 index 0000000000..ca63183b68 --- /dev/null +++ b/src/Plugins/TokensTracker/TokensTracker.json @@ -0,0 +1,12 @@ +{ + "PluginConfiguration": { + "DBPath": "TokenBalanceData", + "TrackHistory": true, + "MaxResults": 1000, + "Network": 860833102, + "EnabledTrackers": [ "NEP-11", "NEP-17" ] + }, + "Dependency": [ + "RpcServer" + ] +} diff --git a/src/Plugins/TokensTracker/Trackers/NEP-11/Nep11BalanceKey.cs b/src/Plugins/TokensTracker/Trackers/NEP-11/Nep11BalanceKey.cs new file mode 100644 index 0000000000..1f375f33d9 --- /dev/null +++ b/src/Plugins/TokensTracker/Trackers/NEP-11/Nep11BalanceKey.cs @@ -0,0 +1,81 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Nep11BalanceKey.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.VM.Types; +using System; +using System.IO; + +namespace Neo.Plugins.Trackers.NEP_11 +{ + public class Nep11BalanceKey : IComparable, IEquatable, ISerializable + { + public readonly UInt160 UserScriptHash; + public readonly UInt160 AssetScriptHash; + public ByteString Token; + public int Size => UInt160.Length + UInt160.Length + Token.GetVarSize(); + + public Nep11BalanceKey() : this(new UInt160(), new UInt160(), ByteString.Empty) + { + } + + public Nep11BalanceKey(UInt160 userScriptHash, UInt160 assetScriptHash, ByteString tokenId) + { + if (userScriptHash == null || assetScriptHash == null || tokenId == null) + throw new ArgumentNullException(); + UserScriptHash = userScriptHash; + AssetScriptHash = assetScriptHash; + Token = tokenId; + } + + public int CompareTo(Nep11BalanceKey other) + { + if (other is null) return 1; + if (ReferenceEquals(this, other)) return 0; + int result = UserScriptHash.CompareTo(other.UserScriptHash); + if (result != 0) return result; + result = AssetScriptHash.CompareTo(other.AssetScriptHash); + if (result != 0) return result; + return (Token.GetInteger() - other.Token.GetInteger()).Sign; + } + + public bool Equals(Nep11BalanceKey other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return UserScriptHash.Equals(other.UserScriptHash) && AssetScriptHash.Equals(AssetScriptHash) && Token.Equals(other.Token); + } + + public override bool Equals(Object other) + { + return other is Nep11BalanceKey otherKey && Equals(otherKey); + } + + public override int GetHashCode() + { + return HashCode.Combine(UserScriptHash.GetHashCode(), AssetScriptHash.GetHashCode(), Token.GetHashCode()); + } + + public void Serialize(BinaryWriter writer) + { + writer.Write(UserScriptHash); + writer.Write(AssetScriptHash); + writer.WriteVarBytes(Token.GetSpan()); + } + + public void Deserialize(ref MemoryReader reader) + { + ((ISerializable)UserScriptHash).Deserialize(ref reader); + ((ISerializable)AssetScriptHash).Deserialize(ref reader); + Token = reader.ReadVarMemory(); + } + } +} diff --git a/src/Plugins/TokensTracker/Trackers/NEP-11/Nep11Tracker.cs b/src/Plugins/TokensTracker/Trackers/NEP-11/Nep11Tracker.cs new file mode 100644 index 0000000000..35a579eb77 --- /dev/null +++ b/src/Plugins/TokensTracker/Trackers/NEP-11/Nep11Tracker.cs @@ -0,0 +1,321 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Nep11Tracker.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.Json; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using Neo.Wallets; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Array = Neo.VM.Types.Array; + +namespace Neo.Plugins.Trackers.NEP_11 +{ + class Nep11Tracker : TrackerBase + { + private const byte Nep11BalancePrefix = 0xf8; + private const byte Nep11TransferSentPrefix = 0xf9; + private const byte Nep11TransferReceivedPrefix = 0xfa; + private uint _currentHeight; + private Block _currentBlock; + private readonly HashSet _properties = new() + { + "name", + "description", + "image", + "tokenURI" + }; + + public override string TrackName => nameof(Nep11Tracker); + + public Nep11Tracker(IStore db, uint maxResult, bool shouldRecordHistory, NeoSystem system) : base(db, maxResult, shouldRecordHistory, system) + { + } + + public override void OnPersist(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + _currentBlock = block; + _currentHeight = block.Index; + uint nep11TransferIndex = 0; + List transfers = new(); + foreach (Blockchain.ApplicationExecuted appExecuted in applicationExecutedList) + { + // Executions that fault won't modify storage, so we can skip them. + if (appExecuted.VMState.HasFlag(VMState.FAULT)) continue; + foreach (var notifyEventArgs in appExecuted.Notifications) + { + if (notifyEventArgs.EventName != "Transfer" || notifyEventArgs?.State is not Array stateItems || + stateItems.Count == 0) + continue; + var contract = NativeContract.ContractManagement.GetContract(snapshot, notifyEventArgs.ScriptHash); + if (contract?.Manifest.SupportedStandards.Contains("NEP-11") == true) + { + try + { + HandleNotificationNep11(notifyEventArgs.ScriptContainer, notifyEventArgs.ScriptHash, stateItems, transfers, ref nep11TransferIndex); + } + catch (Exception e) + { + Log(e.ToString(), LogLevel.Error); + throw; + } + } + + } + } + + // update nep11 balance + var contracts = new Dictionary(); + foreach (var transferRecord in transfers) + { + if (!contracts.ContainsKey(transferRecord.asset)) + { + var state = NativeContract.ContractManagement.GetContract(snapshot, transferRecord.asset); + var balanceMethod = state.Manifest.Abi.GetMethod("balanceOf", 1); + var balanceMethod2 = state.Manifest.Abi.GetMethod("balanceOf", 2); + if (balanceMethod == null && balanceMethod2 == null) + { + Log($"{state.Hash} is not nft!", LogLevel.Warning); + continue; + } + + var isDivisible = balanceMethod2 != null; + contracts[transferRecord.asset] = (isDivisible, state); + } + + var asset = contracts[transferRecord.asset]; + if (asset.isDivisible) + { + SaveDivisibleNFTBalance(transferRecord, snapshot); + } + else + { + SaveNFTBalance(transferRecord); + } + } + } + + private void SaveDivisibleNFTBalance(TransferRecord record, DataCache snapshot) + { + using ScriptBuilder sb = new(); + sb.EmitDynamicCall(record.asset, "balanceOf", record.from, record.tokenId); + sb.EmitDynamicCall(record.asset, "balanceOf", record.to, record.tokenId); + using ApplicationEngine engine = ApplicationEngine.Run(sb.ToArray(), snapshot, settings: _neoSystem.Settings, gas: 3400_0000); + if (engine.State.HasFlag(VMState.FAULT) || engine.ResultStack.Count != 2) + { + Log($"Fault: from[{record.from}] to[{record.to}] get {record.asset} token [{record.tokenId.ToHexString()}] balance fault", LogLevel.Warning); + return; + } + var toBalance = engine.ResultStack.Pop(); + var fromBalance = engine.ResultStack.Pop(); + if (toBalance is not Integer || fromBalance is not Integer) + { + Log($"Fault: from[{record.from}] to[{record.to}] get {record.asset} token [{record.tokenId.ToHexString()}] balance not number", LogLevel.Warning); + return; + } + Put(Nep11BalancePrefix, new Nep11BalanceKey(record.to, record.asset, record.tokenId), new TokenBalance { Balance = toBalance.GetInteger(), LastUpdatedBlock = _currentHeight }); + Put(Nep11BalancePrefix, new Nep11BalanceKey(record.from, record.asset, record.tokenId), new TokenBalance { Balance = fromBalance.GetInteger(), LastUpdatedBlock = _currentHeight }); + } + + private void SaveNFTBalance(TransferRecord record) + { + if (record.from != UInt160.Zero) + { + Delete(Nep11BalancePrefix, new Nep11BalanceKey(record.from, record.asset, record.tokenId)); + } + + if (record.to != UInt160.Zero) + { + Put(Nep11BalancePrefix, new Nep11BalanceKey(record.to, record.asset, record.tokenId), new TokenBalance { Balance = 1, LastUpdatedBlock = _currentHeight }); + } + } + + + private void HandleNotificationNep11(IVerifiable scriptContainer, UInt160 asset, Array stateItems, List transfers, ref uint transferIndex) + { + if (stateItems.Count != 4) return; + var transferRecord = GetTransferRecord(asset, stateItems); + if (transferRecord == null) return; + + transfers.Add(transferRecord); + if (scriptContainer is Transaction transaction) + { + RecordTransferHistoryNep11(asset, transferRecord.from, transferRecord.to, transferRecord.tokenId, transferRecord.amount, transaction.Hash, ref transferIndex); + } + } + + + private void RecordTransferHistoryNep11(UInt160 contractHash, UInt160 from, UInt160 to, ByteString tokenId, BigInteger amount, UInt256 txHash, ref uint transferIndex) + { + if (!_shouldTrackHistory) return; + if (from != UInt160.Zero) + { + Put(Nep11TransferSentPrefix, + new Nep11TransferKey(from, _currentBlock.Header.Timestamp, contractHash, tokenId, transferIndex), + new TokenTransfer + { + Amount = amount, + UserScriptHash = to, + BlockIndex = _currentHeight, + TxHash = txHash + }); + } + + if (to != UInt160.Zero) + { + Put(Nep11TransferReceivedPrefix, + new Nep11TransferKey(to, _currentBlock.Header.Timestamp, contractHash, tokenId, transferIndex), + new TokenTransfer + { + Amount = amount, + UserScriptHash = from, + BlockIndex = _currentHeight, + TxHash = txHash + }); + } + transferIndex++; + } + + + [RpcMethod] + public JToken GetNep11Transfers(JArray _params) + { + _shouldTrackHistory.True_Or(RpcError.MethodNotFound); + UInt160 userScriptHash = GetScriptHashFromParam(_params[0].AsString()); + // If start time not present, default to 1 week of history. + ulong startTime = _params.Count > 1 ? (ulong)_params[1].AsNumber() : + (DateTime.UtcNow - TimeSpan.FromDays(7)).ToTimestampMS(); + ulong endTime = _params.Count > 2 ? (ulong)_params[2].AsNumber() : DateTime.UtcNow.ToTimestampMS(); + (endTime >= startTime).True_Or(RpcError.InvalidParams); + + JObject json = new(); + json["address"] = userScriptHash.ToAddress(_neoSystem.Settings.AddressVersion); + JArray transfersSent = new(); + json["sent"] = transfersSent; + JArray transfersReceived = new(); + json["received"] = transfersReceived; + AddNep11Transfers(Nep11TransferSentPrefix, userScriptHash, startTime, endTime, transfersSent); + AddNep11Transfers(Nep11TransferReceivedPrefix, userScriptHash, startTime, endTime, transfersReceived); + return json; + } + + [RpcMethod] + public JToken GetNep11Balances(JArray _params) + { + UInt160 userScriptHash = GetScriptHashFromParam(_params[0].AsString()); + + JObject json = new(); + JArray balances = new(); + json["address"] = userScriptHash.ToAddress(_neoSystem.Settings.AddressVersion); + json["balance"] = balances; + + var map = new Dictionary>(); + int count = 0; + byte[] prefix = Key(Nep11BalancePrefix, userScriptHash); + foreach (var (key, value) in _db.FindPrefix(prefix)) + { + if (NativeContract.ContractManagement.GetContract(_neoSystem.StoreView, key.AssetScriptHash) is null) + continue; + if (!map.TryGetValue(key.AssetScriptHash, out var list)) + { + map[key.AssetScriptHash] = list = new List<(string, BigInteger, uint)>(); + } + list.Add((key.Token.GetSpan().ToHexString(), value.Balance, value.LastUpdatedBlock)); + count++; + if (count >= _maxResults) + { + break; + } + } + foreach (var key in map.Keys) + { + try + { + using var script = new ScriptBuilder(); + script.EmitDynamicCall(key, "decimals"); + script.EmitDynamicCall(key, "symbol"); + + var engine = ApplicationEngine.Run(script.ToArray(), _neoSystem.StoreView, settings: _neoSystem.Settings); + var symbol = engine.ResultStack.Pop().GetString(); + var decimals = engine.ResultStack.Pop().GetInteger(); + var name = NativeContract.ContractManagement.GetContract(_neoSystem.StoreView, key).Manifest.Name; + + balances.Add(new JObject + { + ["assethash"] = key.ToString(), + ["name"] = name, + ["symbol"] = symbol, + ["decimals"] = decimals.ToString(), + ["tokens"] = new JArray(map[key].Select(v => new JObject + { + ["tokenid"] = v.tokenid, + ["amount"] = v.amount.ToString(), + ["lastupdatedblock"] = v.height + })), + }); + } + catch { } + } + return json; + } + + [RpcMethod] + public JToken GetNep11Properties(JArray _params) + { + UInt160 nep11Hash = GetScriptHashFromParam(_params[0].AsString()); + var tokenId = _params[1].AsString().HexToBytes(); + + using ScriptBuilder sb = new(); + sb.EmitDynamicCall(nep11Hash, "properties", CallFlags.ReadOnly, tokenId); + using var snapshot = _neoSystem.GetSnapshot(); + + using var engine = ApplicationEngine.Run(sb.ToArray(), snapshot, settings: _neoSystem.Settings); + JObject json = new(); + + if (engine.State == VMState.HALT) + { + var map = engine.ResultStack.Pop(); + foreach (var keyValue in map) + { + if (keyValue.Value is CompoundType) continue; + var key = keyValue.Key.GetString(); + if (_properties.Contains(key)) + { + json[key] = keyValue.Value.GetString(); + } + else + { + json[key] = keyValue.Value.IsNull ? null : keyValue.Value.GetSpan().ToBase64(); + } + } + } + return json; + } + + private void AddNep11Transfers(byte dbPrefix, UInt160 userScriptHash, ulong startTime, ulong endTime, JArray parentJArray) + { + var transferPairs = QueryTransfers(dbPrefix, userScriptHash, startTime, endTime).Take((int)_maxResults).ToList(); + foreach (var (key, value) in transferPairs.OrderByDescending(l => l.key.TimestampMS)) + { + JObject transfer = ToJson(key, value); + transfer["tokenid"] = key.Token.GetSpan().ToHexString(); + parentJArray.Add(transfer); + } + } + } +} diff --git a/src/Plugins/TokensTracker/Trackers/NEP-11/Nep11TransferKey.cs b/src/Plugins/TokensTracker/Trackers/NEP-11/Nep11TransferKey.cs new file mode 100644 index 0000000000..5c999e8e00 --- /dev/null +++ b/src/Plugins/TokensTracker/Trackers/NEP-11/Nep11TransferKey.cs @@ -0,0 +1,80 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Nep11TransferKey.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.VM.Types; +using System; +using System.IO; + +namespace Neo.Plugins.Trackers.NEP_11 +{ + public class Nep11TransferKey : TokenTransferKey, IComparable, IEquatable + { + public ByteString Token; + public override int Size => base.Size + Token.GetVarSize(); + + public Nep11TransferKey() : this(new UInt160(), 0, new UInt160(), ByteString.Empty, 0) + { + } + + public Nep11TransferKey(UInt160 userScriptHash, ulong timestamp, UInt160 assetScriptHash, ByteString tokenId, uint xferIndex) : base(userScriptHash, timestamp, assetScriptHash, xferIndex) + { + Token = tokenId; + } + + public int CompareTo(Nep11TransferKey other) + { + if (other is null) return 1; + if (ReferenceEquals(this, other)) return 0; + int result = UserScriptHash.CompareTo(other.UserScriptHash); + if (result != 0) return result; + int result2 = TimestampMS.CompareTo(other.TimestampMS); + if (result2 != 0) return result2; + int result3 = AssetScriptHash.CompareTo(other.AssetScriptHash); + if (result3 != 0) return result3; + var result4 = BlockXferNotificationIndex.CompareTo(other.BlockXferNotificationIndex); + if (result4 != 0) return result4; + return (Token.GetInteger() - other.Token.GetInteger()).Sign; + } + + public bool Equals(Nep11TransferKey other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return UserScriptHash.Equals(other.UserScriptHash) + && TimestampMS.Equals(other.TimestampMS) && AssetScriptHash.Equals(other.AssetScriptHash) + && Token.Equals(other.Token) + && BlockXferNotificationIndex.Equals(other.BlockXferNotificationIndex); + } + + public override bool Equals(Object other) + { + return other is Nep11TransferKey otherKey && Equals(otherKey); + } + + public override int GetHashCode() + { + return HashCode.Combine(UserScriptHash.GetHashCode(), TimestampMS.GetHashCode(), AssetScriptHash.GetHashCode(), BlockXferNotificationIndex.GetHashCode(), Token.GetHashCode()); + } + + public override void Serialize(BinaryWriter writer) + { + base.Serialize(writer); + writer.WriteVarBytes(Token.GetSpan()); + } + + public override void Deserialize(ref MemoryReader reader) + { + base.Deserialize(ref reader); + Token = reader.ReadVarMemory(); + } + } +} diff --git a/src/Plugins/TokensTracker/Trackers/NEP-17/Nep17BalanceKey.cs b/src/Plugins/TokensTracker/Trackers/NEP-17/Nep17BalanceKey.cs new file mode 100644 index 0000000000..bbceabec2b --- /dev/null +++ b/src/Plugins/TokensTracker/Trackers/NEP-17/Nep17BalanceKey.cs @@ -0,0 +1,75 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Nep17BalanceKey.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.Plugins.Trackers.NEP_17 +{ + public class Nep17BalanceKey : IComparable, IEquatable, ISerializable + { + public readonly UInt160 UserScriptHash; + public readonly UInt160 AssetScriptHash; + + public int Size => UInt160.Length + UInt160.Length; + + public Nep17BalanceKey() : this(new UInt160(), new UInt160()) + { + } + + public Nep17BalanceKey(UInt160 userScriptHash, UInt160 assetScriptHash) + { + if (userScriptHash == null || assetScriptHash == null) + throw new ArgumentNullException(); + UserScriptHash = userScriptHash; + AssetScriptHash = assetScriptHash; + } + + public int CompareTo(Nep17BalanceKey other) + { + if (other is null) return 1; + if (ReferenceEquals(this, other)) return 0; + int result = UserScriptHash.CompareTo(other.UserScriptHash); + if (result != 0) return result; + return AssetScriptHash.CompareTo(other.AssetScriptHash); + } + + public bool Equals(Nep17BalanceKey other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return UserScriptHash.Equals(other.UserScriptHash) && AssetScriptHash.Equals(AssetScriptHash); + } + + public override bool Equals(Object other) + { + return other is Nep17BalanceKey otherKey && Equals(otherKey); + } + + public override int GetHashCode() + { + return HashCode.Combine(UserScriptHash.GetHashCode(), AssetScriptHash.GetHashCode()); + } + + public void Serialize(BinaryWriter writer) + { + writer.Write(UserScriptHash); + writer.Write(AssetScriptHash); + } + + public void Deserialize(ref MemoryReader reader) + { + ((ISerializable)UserScriptHash).Deserialize(ref reader); + ((ISerializable)AssetScriptHash).Deserialize(ref reader); + } + } +} diff --git a/src/Plugins/TokensTracker/Trackers/NEP-17/Nep17Tracker.cs b/src/Plugins/TokensTracker/Trackers/NEP-17/Nep17Tracker.cs new file mode 100644 index 0000000000..8ea2efa6a6 --- /dev/null +++ b/src/Plugins/TokensTracker/Trackers/NEP-17/Nep17Tracker.cs @@ -0,0 +1,256 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Nep17Tracker.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.Json; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using Neo.Wallets; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Array = Neo.VM.Types.Array; + +namespace Neo.Plugins.Trackers.NEP_17 +{ + record BalanceChangeRecord(UInt160 User, UInt160 Asset); + + class Nep17Tracker : TrackerBase + { + private const byte Nep17BalancePrefix = 0xe8; + private const byte Nep17TransferSentPrefix = 0xe9; + private const byte Nep17TransferReceivedPrefix = 0xea; + private uint _currentHeight; + private Block _currentBlock; + + public override string TrackName => nameof(Nep17Tracker); + + public Nep17Tracker(IStore db, uint maxResult, bool shouldRecordHistory, NeoSystem system) : base(db, maxResult, shouldRecordHistory, system) + { + } + + public override void OnPersist(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + _currentBlock = block; + _currentHeight = block.Index; + uint nep17TransferIndex = 0; + var balanceChangeRecords = new HashSet(); + + foreach (Blockchain.ApplicationExecuted appExecuted in applicationExecutedList) + { + // Executions that fault won't modify storage, so we can skip them. + if (appExecuted.VMState.HasFlag(VMState.FAULT)) continue; + foreach (var notifyEventArgs in appExecuted.Notifications) + { + if (notifyEventArgs.EventName != "Transfer" || notifyEventArgs?.State is not Array stateItems || stateItems.Count == 0) + continue; + var contract = NativeContract.ContractManagement.GetContract(snapshot, notifyEventArgs.ScriptHash); + if (contract?.Manifest.SupportedStandards.Contains("NEP-17") == true) + { + try + { + HandleNotificationNep17(notifyEventArgs.ScriptContainer, notifyEventArgs.ScriptHash, stateItems, balanceChangeRecords, ref nep17TransferIndex); + } + catch (Exception e) + { + Log(e.ToString(), LogLevel.Error); + throw; + } + } + } + } + + //update nep17 balance + foreach (var balanceChangeRecord in balanceChangeRecords) + { + try + { + SaveNep17Balance(balanceChangeRecord, snapshot); + } + catch (Exception e) + { + Log(e.ToString(), LogLevel.Error); + throw; + } + } + } + + + private void HandleNotificationNep17(IVerifiable scriptContainer, UInt160 asset, Array stateItems, HashSet balanceChangeRecords, ref uint transferIndex) + { + if (stateItems.Count != 3) return; + var transferRecord = GetTransferRecord(asset, stateItems); + if (transferRecord == null) return; + if (transferRecord.from != UInt160.Zero) + { + balanceChangeRecords.Add(new BalanceChangeRecord(transferRecord.from, asset)); + } + if (transferRecord.to != UInt160.Zero) + { + balanceChangeRecords.Add(new BalanceChangeRecord(transferRecord.to, asset)); + } + if (scriptContainer is Transaction transaction) + { + RecordTransferHistoryNep17(asset, transferRecord.from, transferRecord.to, transferRecord.amount, transaction.Hash, ref transferIndex); + } + } + + + private void SaveNep17Balance(BalanceChangeRecord balanceChanged, DataCache snapshot) + { + var key = new Nep17BalanceKey(balanceChanged.User, balanceChanged.Asset); + using ScriptBuilder sb = new(); + sb.EmitDynamicCall(balanceChanged.Asset, "balanceOf", balanceChanged.User); + using ApplicationEngine engine = ApplicationEngine.Run(sb.ToArray(), snapshot, settings: _neoSystem.Settings, gas: 1700_0000); + + if (engine.State.HasFlag(VMState.FAULT) || engine.ResultStack.Count == 0) + { + Log($"Fault:{balanceChanged.User} get {balanceChanged.Asset} balance fault", LogLevel.Warning); + return; + } + + var balanceItem = engine.ResultStack.Pop(); + if (balanceItem is not Integer) + { + Log($"Fault:{balanceChanged.User} get {balanceChanged.Asset} balance not number", LogLevel.Warning); + return; + } + + var balance = balanceItem.GetInteger(); + + if (balance.IsZero) + { + Delete(Nep17BalancePrefix, key); + return; + } + + Put(Nep17BalancePrefix, key, new TokenBalance { Balance = balance, LastUpdatedBlock = _currentHeight }); + } + + + [RpcMethod] + public JToken GetNep17Transfers(JArray _params) + { + _shouldTrackHistory.True_Or(RpcError.MethodNotFound); + UInt160 userScriptHash = GetScriptHashFromParam(_params[0].AsString()); + // If start time not present, default to 1 week of history. + ulong startTime = _params.Count > 1 ? (ulong)_params[1].AsNumber() : + (DateTime.UtcNow - TimeSpan.FromDays(7)).ToTimestampMS(); + ulong endTime = _params.Count > 2 ? (ulong)_params[2].AsNumber() : DateTime.UtcNow.ToTimestampMS(); + + (endTime >= startTime).True_Or(RpcError.InvalidParams); + + JObject json = new(); + json["address"] = userScriptHash.ToAddress(_neoSystem.Settings.AddressVersion); + JArray transfersSent = new(); + json["sent"] = transfersSent; + JArray transfersReceived = new(); + json["received"] = transfersReceived; + AddNep17Transfers(Nep17TransferSentPrefix, userScriptHash, startTime, endTime, transfersSent); + AddNep17Transfers(Nep17TransferReceivedPrefix, userScriptHash, startTime, endTime, transfersReceived); + return json; + } + + [RpcMethod] + public JToken GetNep17Balances(JArray _params) + { + UInt160 userScriptHash = GetScriptHashFromParam(_params[0].AsString()); + + JObject json = new(); + JArray balances = new(); + json["address"] = userScriptHash.ToAddress(_neoSystem.Settings.AddressVersion); + json["balance"] = balances; + + int count = 0; + byte[] prefix = Key(Nep17BalancePrefix, userScriptHash); + foreach (var (key, value) in _db.FindPrefix(prefix)) + { + if (NativeContract.ContractManagement.GetContract(_neoSystem.StoreView, key.AssetScriptHash) is null) + continue; + + try + { + using var script = new ScriptBuilder(); + script.EmitDynamicCall(key.AssetScriptHash, "decimals"); + script.EmitDynamicCall(key.AssetScriptHash, "symbol"); + + var engine = ApplicationEngine.Run(script.ToArray(), _neoSystem.StoreView, settings: _neoSystem.Settings); + var symbol = engine.ResultStack.Pop().GetString(); + var decimals = engine.ResultStack.Pop().GetInteger(); + var name = NativeContract.ContractManagement.GetContract(_neoSystem.StoreView, key.AssetScriptHash).Manifest.Name; + + balances.Add(new JObject + { + ["assethash"] = key.AssetScriptHash.ToString(), + ["name"] = name, + ["symbol"] = symbol, + ["decimals"] = decimals.ToString(), + ["amount"] = value.Balance.ToString(), + ["lastupdatedblock"] = value.LastUpdatedBlock + }); + count++; + if (count >= _maxResults) + { + break; + } + } + catch { } + } + return json; + } + + private void AddNep17Transfers(byte dbPrefix, UInt160 userScriptHash, ulong startTime, ulong endTime, JArray parentJArray) + { + var transferPairs = QueryTransfers(dbPrefix, userScriptHash, startTime, endTime).Take((int)_maxResults).ToList(); + foreach (var (key, value) in transferPairs.OrderByDescending(l => l.key.TimestampMS)) + { + parentJArray.Add(ToJson(key, value)); + } + } + + + private void RecordTransferHistoryNep17(UInt160 scriptHash, UInt160 from, UInt160 to, BigInteger amount, UInt256 txHash, ref uint transferIndex) + { + if (!_shouldTrackHistory) return; + if (from != UInt160.Zero) + { + Put(Nep17TransferSentPrefix, + new Nep17TransferKey(from, _currentBlock.Header.Timestamp, scriptHash, transferIndex), + new TokenTransfer + { + Amount = amount, + UserScriptHash = to, + BlockIndex = _currentHeight, + TxHash = txHash + }); + } + + if (to != UInt160.Zero) + { + Put(Nep17TransferReceivedPrefix, + new Nep17TransferKey(to, _currentBlock.Header.Timestamp, scriptHash, transferIndex), + new TokenTransfer + { + Amount = amount, + UserScriptHash = from, + BlockIndex = _currentHeight, + TxHash = txHash + }); + } + transferIndex++; + } + } +} diff --git a/src/Plugins/TokensTracker/Trackers/NEP-17/Nep17TransferKey.cs b/src/Plugins/TokensTracker/Trackers/NEP-17/Nep17TransferKey.cs new file mode 100644 index 0000000000..8f48195fc9 --- /dev/null +++ b/src/Plugins/TokensTracker/Trackers/NEP-17/Nep17TransferKey.cs @@ -0,0 +1,59 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Nep17TransferKey.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; + +namespace Neo.Plugins.Trackers.NEP_17 +{ + public class Nep17TransferKey : TokenTransferKey, IComparable, IEquatable, ISerializable + { + public Nep17TransferKey() : base(new UInt160(), 0, new UInt160(), 0) + { + } + + public Nep17TransferKey(UInt160 userScriptHash, ulong timestamp, UInt160 assetScriptHash, uint xferIndex) : base(userScriptHash, timestamp, assetScriptHash, xferIndex) + { + } + + public int CompareTo(Nep17TransferKey other) + { + if (other is null) return 1; + if (ReferenceEquals(this, other)) return 0; + int result = UserScriptHash.CompareTo(other.UserScriptHash); + if (result != 0) return result; + int result2 = TimestampMS.CompareTo(other.TimestampMS); + if (result2 != 0) return result2; + int result3 = AssetScriptHash.CompareTo(other.AssetScriptHash); + if (result3 != 0) return result3; + return BlockXferNotificationIndex.CompareTo(other.BlockXferNotificationIndex); + } + + public bool Equals(Nep17TransferKey other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return UserScriptHash.Equals(other.UserScriptHash) + && TimestampMS.Equals(other.TimestampMS) && AssetScriptHash.Equals(other.AssetScriptHash) + && BlockXferNotificationIndex.Equals(other.BlockXferNotificationIndex); + } + + public override bool Equals(Object other) + { + return other is Nep17TransferKey otherKey && Equals(otherKey); + } + + public override int GetHashCode() + { + return HashCode.Combine(UserScriptHash.GetHashCode(), TimestampMS.GetHashCode(), AssetScriptHash.GetHashCode(), BlockXferNotificationIndex.GetHashCode()); + } + } +} diff --git a/src/Plugins/TokensTracker/Trackers/TokenBalance.cs b/src/Plugins/TokensTracker/Trackers/TokenBalance.cs new file mode 100644 index 0000000000..f54a7c9856 --- /dev/null +++ b/src/Plugins/TokensTracker/Trackers/TokenBalance.cs @@ -0,0 +1,39 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// TokenBalance.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; +using System.Numerics; + +namespace Neo.Plugins.Trackers +{ + public class TokenBalance : ISerializable + { + public BigInteger Balance; + public uint LastUpdatedBlock; + + int ISerializable.Size => + Balance.GetVarSize() + // Balance + sizeof(uint); // LastUpdatedBlock + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.WriteVarBytes(Balance.ToByteArray()); + writer.Write(LastUpdatedBlock); + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + Balance = new BigInteger(reader.ReadVarMemory(32).Span); + LastUpdatedBlock = reader.ReadUInt32(); + } + } +} diff --git a/src/Plugins/TokensTracker/Trackers/TokenTransfer.cs b/src/Plugins/TokensTracker/Trackers/TokenTransfer.cs new file mode 100644 index 0000000000..0a221850fc --- /dev/null +++ b/src/Plugins/TokensTracker/Trackers/TokenTransfer.cs @@ -0,0 +1,47 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// TokenTransfer.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; +using System.Numerics; + +namespace Neo.Plugins.Trackers +{ + public class TokenTransfer : ISerializable + { + public UInt160 UserScriptHash; + public uint BlockIndex; + public UInt256 TxHash; + public BigInteger Amount; + + int ISerializable.Size => + UInt160.Length + // UserScriptHash + sizeof(uint) + // BlockIndex + UInt256.Length + // TxHash + Amount.GetVarSize(); // Amount + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write(UserScriptHash); + writer.Write(BlockIndex); + writer.Write(TxHash); + writer.WriteVarBytes(Amount.ToByteArray()); + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + UserScriptHash = reader.ReadSerializable(); + BlockIndex = reader.ReadUInt32(); + TxHash = reader.ReadSerializable(); + Amount = new BigInteger(reader.ReadVarMemory(32).Span); + } + } +} diff --git a/src/Plugins/TokensTracker/Trackers/TokenTransferKey.cs b/src/Plugins/TokensTracker/Trackers/TokenTransferKey.cs new file mode 100644 index 0000000000..252eb20af0 --- /dev/null +++ b/src/Plugins/TokensTracker/Trackers/TokenTransferKey.cs @@ -0,0 +1,57 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// TokenTransferKey.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.Buffers.Binary; +using System.IO; + +namespace Neo.Plugins.Trackers +{ + public class TokenTransferKey : ISerializable + { + public UInt160 UserScriptHash { get; protected set; } + public ulong TimestampMS { get; protected set; } + public UInt160 AssetScriptHash { get; protected set; } + public uint BlockXferNotificationIndex { get; protected set; } + + public TokenTransferKey(UInt160 userScriptHash, ulong timestamp, UInt160 assetScriptHash, uint xferIndex) + { + if (userScriptHash is null || assetScriptHash is null) + throw new ArgumentNullException(); + UserScriptHash = userScriptHash; + TimestampMS = timestamp; + AssetScriptHash = assetScriptHash; + BlockXferNotificationIndex = xferIndex; + } + public virtual void Serialize(BinaryWriter writer) + { + writer.Write(UserScriptHash); + writer.Write(BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(TimestampMS) : TimestampMS); + writer.Write(AssetScriptHash); + writer.Write(BlockXferNotificationIndex); + } + + public virtual void Deserialize(ref MemoryReader reader) + { + UserScriptHash.Deserialize(ref reader); + TimestampMS = BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(reader.ReadUInt64()) : reader.ReadUInt64(); + AssetScriptHash.Deserialize(ref reader); + BlockXferNotificationIndex = reader.ReadUInt32(); + } + + public virtual int Size => + UInt160.Length + //UserScriptHash + sizeof(ulong) + //TimestampMS + UInt160.Length + //AssetScriptHash + sizeof(uint); //BlockXferNotificationIndex + } +} diff --git a/src/Plugins/TokensTracker/Trackers/TrackerBase.cs b/src/Plugins/TokensTracker/Trackers/TrackerBase.cs new file mode 100644 index 0000000000..471362b019 --- /dev/null +++ b/src/Plugins/TokensTracker/Trackers/TrackerBase.cs @@ -0,0 +1,162 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// TrackerBase.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.Json; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.VM.Types; +using Neo.Wallets; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using Array = Neo.VM.Types.Array; + +namespace Neo.Plugins.Trackers +{ + record TransferRecord(UInt160 asset, UInt160 from, UInt160 to, byte[] tokenId, BigInteger amount); + + abstract class TrackerBase + { + protected bool _shouldTrackHistory; + protected uint _maxResults; + protected IStore _db; + private ISnapshot _levelDbSnapshot; + protected NeoSystem _neoSystem; + public abstract string TrackName { get; } + + protected TrackerBase(IStore db, uint maxResult, bool shouldTrackHistory, NeoSystem neoSystem) + { + _db = db; + _maxResults = maxResult; + _shouldTrackHistory = shouldTrackHistory; + _neoSystem = neoSystem; + } + + public abstract void OnPersist(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList); + + public void ResetBatch() + { + _levelDbSnapshot?.Dispose(); + _levelDbSnapshot = _db.GetSnapshot(); + } + + public void Commit() + { + _levelDbSnapshot?.Commit(); + } + + public IEnumerable<(TKey key, TValue val)> QueryTransfers(byte dbPrefix, UInt160 userScriptHash, ulong startTime, ulong endTime) + where TKey : ISerializable, new() + where TValue : class, ISerializable, new() + { + var prefix = new[] { dbPrefix }.Concat(userScriptHash.ToArray()).ToArray(); + byte[] startTimeBytes, endTimeBytes; + if (BitConverter.IsLittleEndian) + { + startTimeBytes = BitConverter.GetBytes(BinaryPrimitives.ReverseEndianness(startTime)); + endTimeBytes = BitConverter.GetBytes(BinaryPrimitives.ReverseEndianness(endTime)); + } + else + { + startTimeBytes = BitConverter.GetBytes(startTime); + endTimeBytes = BitConverter.GetBytes(endTime); + } + var transferPairs = _db.FindRange(prefix.Concat(startTimeBytes).ToArray(), prefix.Concat(endTimeBytes).ToArray()); + return transferPairs; + } + + protected static byte[] Key(byte prefix, ISerializable key) + { + byte[] buffer = new byte[key.Size + 1]; + using (MemoryStream ms = new(buffer, true)) + using (BinaryWriter writer = new(ms)) + { + writer.Write(prefix); + key.Serialize(writer); + } + return buffer; + } + + protected void Put(byte prefix, ISerializable key, ISerializable value) + { + _levelDbSnapshot.Put(Key(prefix, key), value.ToArray()); + } + + protected void Delete(byte prefix, ISerializable key) + { + _levelDbSnapshot.Delete(Key(prefix, key)); + } + + protected TransferRecord GetTransferRecord(UInt160 asset, Array stateItems) + { + if (stateItems.Count < 3) + { + return null; + } + var fromItem = stateItems[0]; + var toItem = stateItems[1]; + var amountItem = stateItems[2]; + if (fromItem.NotNull() && fromItem is not ByteString) + return null; + if (toItem.NotNull() && toItem is not ByteString) + return null; + if (amountItem is not ByteString && amountItem is not Integer) + return null; + + byte[] fromBytes = fromItem.IsNull ? null : fromItem.GetSpan().ToArray(); + if (fromBytes != null && fromBytes.Length != UInt160.Length) + return null; + byte[] toBytes = toItem.IsNull ? null : toItem.GetSpan().ToArray(); + if (toBytes != null && toBytes.Length != UInt160.Length) + return null; + if (fromBytes == null && toBytes == null) + return null; + + var from = fromBytes == null ? UInt160.Zero : new UInt160(fromBytes); + var to = toBytes == null ? UInt160.Zero : new UInt160(toBytes); + return stateItems.Count switch + { + 3 => new TransferRecord(asset, @from, to, null, amountItem.GetInteger()), + 4 when (stateItems[3] is ByteString tokenId) => new TransferRecord(asset, @from, to, tokenId.Memory.ToArray(), amountItem.GetInteger()), + _ => null + }; + } + + protected JObject ToJson(TokenTransferKey key, TokenTransfer value) + { + JObject transfer = new(); + transfer["timestamp"] = key.TimestampMS; + transfer["assethash"] = key.AssetScriptHash.ToString(); + transfer["transferaddress"] = value.UserScriptHash == UInt160.Zero ? null : value.UserScriptHash.ToAddress(_neoSystem.Settings.AddressVersion); + transfer["amount"] = value.Amount.ToString(); + transfer["blockindex"] = value.BlockIndex; + transfer["transfernotifyindex"] = key.BlockXferNotificationIndex; + transfer["txhash"] = value.TxHash.ToString(); + return transfer; + } + + public UInt160 GetScriptHashFromParam(string addressOrScriptHash) + { + return addressOrScriptHash.Length < 40 ? + addressOrScriptHash.ToScriptHash(_neoSystem.Settings.AddressVersion) : UInt160.Parse(addressOrScriptHash); + } + + public void Log(string message, LogLevel level = LogLevel.Info) + { + Utility.Log(TrackName, level, message); + } + } +} diff --git a/tests/Neo.ConsoleService.Tests/Neo.ConsoleService.Tests.csproj b/tests/Neo.ConsoleService.Tests/Neo.ConsoleService.Tests.csproj index 03e67d6f78..6562480859 100644 --- a/tests/Neo.ConsoleService.Tests/Neo.ConsoleService.Tests.csproj +++ b/tests/Neo.ConsoleService.Tests/Neo.ConsoleService.Tests.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/tests/Neo.Cryptography.BLS12_381.Tests/Neo.Cryptography.BLS12_381.Tests.csproj b/tests/Neo.Cryptography.BLS12_381.Tests/Neo.Cryptography.BLS12_381.Tests.csproj index 5c4a8f5951..999bc1c5f4 100644 --- a/tests/Neo.Cryptography.BLS12_381.Tests/Neo.Cryptography.BLS12_381.Tests.csproj +++ b/tests/Neo.Cryptography.BLS12_381.Tests/Neo.Cryptography.BLS12_381.Tests.csproj @@ -12,8 +12,8 @@ - - + + diff --git a/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/Helper.cs b/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/Helper.cs new file mode 100644 index 0000000000..15f796046b --- /dev/null +++ b/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/Helper.cs @@ -0,0 +1,32 @@ +// 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 System.IO; + +namespace Neo.Cryptography.MPTTrie.Tests +{ + public static class Helper + { + private static readonly byte Prefix = 0xf0; + + public static byte[] ToKey(this UInt256 hash) + { + byte[] buffer = new byte[UInt256.Length + 1]; + using (MemoryStream ms = new MemoryStream(buffer, true)) + using (BinaryWriter writer = new BinaryWriter(ms)) + { + writer.Write(Prefix); + hash.Serialize(writer); + } + return buffer; + } + } +} diff --git a/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Cache.cs b/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Cache.cs new file mode 100644 index 0000000000..3a34962059 --- /dev/null +++ b/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Cache.cs @@ -0,0 +1,235 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_Cache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.IO; +using Neo.Persistence; +using System.Text; + +namespace Neo.Cryptography.MPTTrie.Tests +{ + + [TestClass] + public class UT_Cache + { + private readonly byte Prefix = 0xf0; + + [TestMethod] + public void TestResolveLeaf() + { + var n = Node.NewLeaf(Encoding.ASCII.GetBytes("leaf")); + var store = new MemoryStore(); + store.Put(n.Hash.ToKey(), n.ToArray()); + var snapshot = store.GetSnapshot(); + var cache = new Cache(snapshot, Prefix); + var resolved = cache.Resolve(n.Hash); + Assert.AreEqual(n.Hash, resolved.Hash); + Assert.AreEqual(n.Value.Span.ToHexString(), resolved.Value.Span.ToHexString()); + } + + [TestMethod] + public void TestResolveBranch() + { + var l = Node.NewLeaf(Encoding.ASCII.GetBytes("leaf")); + var b = Node.NewBranch(); + b.Children[1] = l; + var store = new MemoryStore(); + store.Put(b.Hash.ToKey(), b.ToArray()); + store.Put(l.Hash.ToKey(), l.ToArray()); + var snapshot = store.GetSnapshot(); + var cache = new Cache(snapshot, Prefix); + var resolved_b = cache.Resolve(b.Hash); + Assert.AreEqual(b.Hash, resolved_b.Hash); + Assert.AreEqual(l.Hash, resolved_b.Children[1].Hash); + var resolved_l = cache.Resolve(l.Hash); + Assert.AreEqual(l.Value.Span.ToHexString(), resolved_l.Value.Span.ToHexString()); + } + + [TestMethod] + public void TestResolveExtension() + { + var e = Node.NewExtension(new byte[] { 0x01 }, new Node()); + var store = new MemoryStore(); + store.Put(e.Hash.ToKey(), e.ToArray()); + var snapshot = store.GetSnapshot(); + var cache = new Cache(snapshot, Prefix); + var re = cache.Resolve(e.Hash); + Assert.AreEqual(e.Hash, re.Hash); + Assert.AreEqual(e.Key.Span.ToHexString(), re.Key.Span.ToHexString()); + Assert.IsTrue(re.Next.IsEmpty); + } + + [TestMethod] + public void TestGetAndChangedBranch() + { + var l = Node.NewLeaf(Encoding.ASCII.GetBytes("leaf")); + var b = Node.NewBranch(); + var store = new MemoryStore(); + store.Put(b.Hash.ToKey(), b.ToArray()); + var snapshot = store.GetSnapshot(); + var cache = new Cache(snapshot, Prefix); + var resolved_b = cache.Resolve(b.Hash); + Assert.AreEqual(resolved_b.Hash, b.Hash); + foreach (var n in resolved_b.Children) + { + Assert.IsTrue(n.IsEmpty); + } + resolved_b.Children[1] = l; + resolved_b.SetDirty(); + var resovled_b1 = cache.Resolve(b.Hash); + Assert.AreEqual(resovled_b1.Hash, b.Hash); + foreach (var n in resovled_b1.Children) + { + Assert.IsTrue(n.IsEmpty); + } + } + + [TestMethod] + public void TestGetAndChangedExtension() + { + var e = Node.NewExtension(new byte[] { 0x01 }, new Node()); + var store = new MemoryStore(); + store.Put(e.Hash.ToKey(), e.ToArray()); + var snapshot = store.GetSnapshot(); + var cache = new Cache(snapshot, Prefix); + var re = cache.Resolve(e.Hash); + Assert.AreEqual(e.Hash, re.Hash); + Assert.AreEqual(e.Key.Span.ToHexString(), re.Key.Span.ToHexString()); + Assert.IsTrue(re.Next.IsEmpty); + re.Key = new byte[] { 0x02 }; + re.SetDirty(); + var re1 = cache.Resolve(e.Hash); + Assert.AreEqual(e.Hash, re1.Hash); + Assert.AreEqual(e.Key.Span.ToHexString(), re1.Key.Span.ToHexString()); + Assert.IsTrue(re1.Next.IsEmpty); + } + + [TestMethod] + public void TestGetAndChangedLeaf() + { + var l = Node.NewLeaf(Encoding.ASCII.GetBytes("leaf")); + var store = new MemoryStore(); + store.Put(l.Hash.ToKey(), l.ToArray()); + var snapshot = store.GetSnapshot(); + var cache = new Cache(snapshot, Prefix); + var rl = cache.Resolve(l.Hash); + Assert.AreEqual(l.Hash, rl.Hash); + Assert.AreEqual("leaf", Encoding.ASCII.GetString(rl.Value.Span)); + rl.Value = new byte[] { 0x01 }; + rl.SetDirty(); + var rl1 = cache.Resolve(l.Hash); + Assert.AreEqual(l.Hash, rl1.Hash); + Assert.AreEqual("leaf", Encoding.ASCII.GetString(rl1.Value.Span)); + } + + [TestMethod] + public void TestPutAndChangedBranch() + { + var l = Node.NewLeaf(Encoding.ASCII.GetBytes("leaf")); + var b = Node.NewBranch(); + var h = b.Hash; + var store = new MemoryStore(); + var snapshot = store.GetSnapshot(); + var cache = new Cache(snapshot, Prefix); + cache.PutNode(b); + var rb = cache.Resolve(h); + Assert.AreEqual(h, rb.Hash); + foreach (var n in rb.Children) + { + Assert.IsTrue(n.IsEmpty); + } + rb.Children[1] = l; + rb.SetDirty(); + var rb1 = cache.Resolve(h); + Assert.AreEqual(h, rb1.Hash); + foreach (var n in rb1.Children) + { + Assert.IsTrue(n.IsEmpty); + } + } + + [TestMethod] + public void TestPutAndChangedExtension() + { + var e = Node.NewExtension(new byte[] { 0x01 }, new Node()); + var h = e.Hash; + var store = new MemoryStore(); + var snapshot = store.GetSnapshot(); + var cache = new Cache(snapshot, Prefix); + cache.PutNode(e); + var re = cache.Resolve(e.Hash); + Assert.AreEqual(e.Hash, re.Hash); + Assert.AreEqual(e.Key.Span.ToHexString(), re.Key.Span.ToHexString()); + Assert.IsTrue(re.Next.IsEmpty); + e.Key = new byte[] { 0x02 }; + e.Next = e; + e.SetDirty(); + var re1 = cache.Resolve(h); + Assert.AreEqual(h, re1.Hash); + Assert.AreEqual("01", re1.Key.Span.ToHexString()); + Assert.IsTrue(re1.Next.IsEmpty); + } + + [TestMethod] + public void TestPutAndChangedLeaf() + { + var l = Node.NewLeaf(Encoding.ASCII.GetBytes("leaf")); + var h = l.Hash; + var store = new MemoryStore(); + var snapshot = store.GetSnapshot(); + var cache = new Cache(snapshot, Prefix); + cache.PutNode(l); + var rl = cache.Resolve(l.Hash); + Assert.AreEqual(h, rl.Hash); + Assert.AreEqual("leaf", Encoding.ASCII.GetString(rl.Value.Span)); + l.Value = new byte[] { 0x01 }; + l.SetDirty(); + var rl1 = cache.Resolve(h); + Assert.AreEqual(h, rl1.Hash); + Assert.AreEqual("leaf", Encoding.ASCII.GetString(rl1.Value.Span)); + } + + [TestMethod] + public void TestReference1() + { + var l = Node.NewLeaf(Encoding.ASCII.GetBytes("leaf")); + var store = new MemoryStore(); + var snapshot = store.GetSnapshot(); + var cache = new Cache(snapshot, Prefix); + cache.PutNode(l); + cache.Commit(); + snapshot.Commit(); + var snapshot1 = store.GetSnapshot(); + var cache1 = new Cache(snapshot1, Prefix); + cache1.PutNode(l); + cache1.Commit(); + snapshot1.Commit(); + var snapshot2 = store.GetSnapshot(); + var cache2 = new Cache(snapshot2, Prefix); + var rl = cache2.Resolve(l.Hash); + Assert.AreEqual(2, rl.Reference); + } + + [TestMethod] + public void TestReference2() + { + var l = Node.NewLeaf(Encoding.ASCII.GetBytes("leaf")); + var store = new MemoryStore(); + var snapshot = store.GetSnapshot(); + var cache = new Cache(snapshot, Prefix); + cache.PutNode(l); + cache.PutNode(l); + cache.DeleteNode(l.Hash); + var rl = cache.Resolve(l.Hash); + Assert.AreEqual(1, rl.Reference); + } + } +} diff --git a/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Helper.cs b/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Helper.cs new file mode 100644 index 0000000000..980e7a429e --- /dev/null +++ b/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Helper.cs @@ -0,0 +1,37 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_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 Microsoft.VisualStudio.TestTools.UnitTesting; +using System; + +namespace Neo.Cryptography.MPTTrie.Tests +{ + [TestClass] + public class UT_Helper + { + [TestMethod] + public void TestCompareTo() + { + ReadOnlySpan arr1 = new byte[] { 0, 1, 2 }; + ReadOnlySpan arr2 = new byte[] { 0, 1, 2 }; + Assert.AreEqual(0, arr1.CompareTo(arr2)); + arr1 = new byte[] { 0, 1 }; + Assert.AreEqual(-1, arr1.CompareTo(arr2)); + arr2 = new byte[] { 0 }; + Assert.AreEqual(1, arr1.CompareTo(arr2)); + arr2 = new byte[] { 0, 2 }; + Assert.AreEqual(-1, arr1.CompareTo(arr2)); + arr1 = new byte[] { 0, 3, 1 }; + Assert.AreEqual(1, arr1.CompareTo(arr2)); + Assert.AreEqual(0, ReadOnlySpan.Empty.CompareTo(ReadOnlySpan.Empty)); + } + } +} diff --git a/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Node.cs b/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Node.cs new file mode 100644 index 0000000000..8f46fbec27 --- /dev/null +++ b/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Node.cs @@ -0,0 +1,216 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_Node.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.IO; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Neo.Cryptography.MPTTrie.Tests +{ + + [TestClass] + public class UT_Node + { + private byte[] NodeToArrayAsChild(Node n) + { + using var ms = new MemoryStream(); + using var writer = new BinaryWriter(ms, Neo.Utility.StrictUTF8, true); + + n.SerializeAsChild(writer); + writer.Flush(); + return ms.ToArray(); + } + + [TestMethod] + public void TestHashSerialize() + { + var n = Node.NewHash(UInt256.Zero); + var expect = "030000000000000000000000000000000000000000000000000000000000000000"; + Assert.AreEqual(expect, n.ToArray().ToHexString()); + Assert.AreEqual(expect, NodeToArrayAsChild(n).ToHexString()); + } + + [TestMethod] + public void TestEmptySerialize() + { + var n = new Node(); + var expect = "04"; + Assert.AreEqual(expect, n.ToArray().ToHexString()); + Assert.AreEqual(expect, NodeToArrayAsChild(n).ToHexString()); + } + + [TestMethod] + public void TestLeafSerialize() + { + var n = Node.NewLeaf(Encoding.ASCII.GetBytes("leaf")); + var expect = "02" + "04" + Encoding.ASCII.GetBytes("leaf").ToHexString(); + Assert.AreEqual(expect, n.ToArrayWithoutReference().ToHexString()); + expect += "01"; + Assert.AreEqual(expect, n.ToArray().ToHexString()); + Assert.AreEqual(7, n.Size); + } + + [TestMethod] + public void TestLeafSerializeAsChild() + { + var l = Node.NewLeaf(Encoding.ASCII.GetBytes("leaf")); + var expect = "03" + Crypto.Hash256(new byte[] { 0x02, 0x04 }.Concat(Encoding.ASCII.GetBytes("leaf")).ToArray()).ToHexString(); + Assert.AreEqual(expect, NodeToArrayAsChild(l).ToHexString()); + } + + [TestMethod] + public void TestExtensionSerialize() + { + var e = Node.NewExtension("010a".HexToBytes(), new Node()); + var expect = "01" + "02" + "010a" + "04"; + Assert.AreEqual(expect, e.ToArrayWithoutReference().ToHexString()); + expect += "01"; + Assert.AreEqual(expect, e.ToArray().ToHexString()); + Assert.AreEqual(6, e.Size); + } + + [TestMethod] + public void TestExtensionSerializeAsChild() + { + var e = Node.NewExtension("010a".HexToBytes(), new Node()); + var expect = "03" + Crypto.Hash256(new byte[] { 0x01, 0x02, 0x01, 0x0a, 0x04 + }).ToHexString(); + Assert.AreEqual(expect, NodeToArrayAsChild(e).ToHexString()); + } + + [TestMethod] + public void TestBranchSerialize() + { + var n = Node.NewBranch(); + n.Children[1] = Node.NewLeaf(Encoding.ASCII.GetBytes("leaf1")); + n.Children[10] = Node.NewLeaf(Encoding.ASCII.GetBytes("leafa")); + var expect = "00"; + for (int i = 0; i < Node.BranchChildCount; i++) + { + if (i == 1) + expect += "03" + Crypto.Hash256(new byte[] { 0x02, 0x05 }.Concat(Encoding.ASCII.GetBytes("leaf1")).ToArray()).ToHexString(); + else if (i == 10) + expect += "03" + Crypto.Hash256(new byte[] { 0x02, 0x05 }.Concat(Encoding.ASCII.GetBytes("leafa")).ToArray()).ToHexString(); + else + expect += "04"; + } + expect += "01"; + Assert.AreEqual(expect, n.ToArray().ToHexString()); + Assert.AreEqual(83, n.Size); + } + + [TestMethod] + public void TestBranchSerializeAsChild() + { + var n = Node.NewBranch(); + var data = new List(); + data.Add(0x00); + for (int i = 0; i < Node.BranchChildCount; i++) + { + data.Add(0x04); + } + var expect = "03" + Crypto.Hash256(data.ToArray()).ToHexString(); + Assert.AreEqual(expect, NodeToArrayAsChild(n).ToHexString()); + } + + [TestMethod] + public void TestCloneBranch() + { + var l = Node.NewLeaf(Encoding.ASCII.GetBytes("leaf")); + var n = Node.NewBranch(); + var n1 = n.Clone(); + n1.Children[0] = l; + Assert.IsTrue(n.Children[0].IsEmpty); + } + + [TestMethod] + public void TestCloneExtension() + { + var l = Node.NewLeaf(Encoding.ASCII.GetBytes("leaf")); + var n = Node.NewExtension(new byte[] { 0x01 }, new Node()); + var n1 = n.Clone(); + n1.Next = l; + Assert.IsTrue(n.Next.IsEmpty); + } + + [TestMethod] + public void TestCloneLeaf() + { + var l = Node.NewLeaf(Encoding.ASCII.GetBytes("leaf")); + var n = l.Clone(); + n.Value = Encoding.ASCII.GetBytes("value"); + Assert.AreEqual("leaf", Encoding.ASCII.GetString(l.Value.Span)); + } + + [TestMethod] + public void TestNewExtensionException() + { + Assert.ThrowsException(() => Node.NewExtension(null, new Node())); + Assert.ThrowsException(() => Node.NewExtension(new byte[] { 0x01 }, null)); + Assert.ThrowsException(() => Node.NewExtension(Array.Empty(), new Node())); + } + + [TestMethod] + public void TestNewHashException() + { + Assert.ThrowsException(() => Node.NewHash(null)); + } + + [TestMethod] + public void TestNewLeafException() + { + Assert.ThrowsException(() => Node.NewLeaf(null)); + } + + [TestMethod] + public void TestSize() + { + var n = new Node(); + Assert.AreEqual(1, n.Size); + n = Node.NewBranch(); + Assert.AreEqual(19, n.Size); + n = Node.NewExtension(new byte[] { 0x00 }, new Node()); + Assert.AreEqual(5, n.Size); + n = Node.NewLeaf(new byte[] { 0x00 }); + Assert.AreEqual(4, n.Size); + n = Node.NewHash(UInt256.Zero); + Assert.AreEqual(33, n.Size); + } + + [TestMethod] + public void TestFromReplica() + { + var l = Node.NewLeaf(new byte[] { 0x00 }); + var n = Node.NewBranch(); + n.Children[1] = l; + var r = new Node(); + r.FromReplica(n); + Assert.AreEqual(n.Hash, r.Hash); + Assert.AreEqual(NodeType.HashNode, r.Children[1].Type); + Assert.AreEqual(l.Hash, r.Children[1].Hash); + } + + [TestMethod] + public void TestEmptyLeaf() + { + var leaf = Node.NewLeaf(Array.Empty()); + var data = leaf.ToArray(); + Assert.AreEqual(3, data.Length); + var l = data.AsSerializable(); + Assert.AreEqual(NodeType.LeafNode, l.Type); + Assert.AreEqual(0, l.Value.Length); + } + } +} diff --git a/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Trie.cs b/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Trie.cs new file mode 100644 index 0000000000..372de6a738 --- /dev/null +++ b/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Trie.cs @@ -0,0 +1,573 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_Trie.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.IO; +using Neo.Persistence; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using static Neo.Helper; + +namespace Neo.Cryptography.MPTTrie.Tests +{ + class TestSnapshot : ISnapshot + { + public Dictionary store = new Dictionary(ByteArrayEqualityComparer.Default); + + private byte[] StoreKey(byte[] key) + { + return Concat(key); + } + + public void Put(byte[] key, byte[] value) + { + store[key] = value; + } + + public void Delete(byte[] key) + { + store.Remove(StoreKey(key)); + } + + public void Commit() { throw new NotImplementedException(); } + + public bool Contains(byte[] key) { throw new System.NotImplementedException(); } + + public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[] key, SeekDirection direction) { throw new System.NotImplementedException(); } + + public byte[] TryGet(byte[] key) + { + var result = store.TryGetValue(StoreKey(key), out byte[] value); + if (result) return value; + return null; + } + + public void Dispose() { throw new System.NotImplementedException(); } + + public int Size => store.Count; + } + + [TestClass] + public class UT_Trie + { + private Node root; + private IStore mptdb; + + private void PutToStore(IStore store, Node node) + { + store.Put(Concat(new byte[] { 0xf0 }, node.Hash.ToArray()), node.ToArray()); + } + + [TestInitialize] + public void TestInit() + { + var b = Node.NewBranch(); + var r = Node.NewExtension("0a0c".HexToBytes(), b); + var v1 = Node.NewLeaf("abcd".HexToBytes());//key=ac01 + var v2 = Node.NewLeaf("2222".HexToBytes());//key=ac + var v3 = Node.NewLeaf(Encoding.ASCII.GetBytes("existing"));//key=acae + var v4 = Node.NewLeaf(Encoding.ASCII.GetBytes("missing")); + var h3 = Node.NewHash(v3.Hash); + var e1 = Node.NewExtension(new byte[] { 0x01 }, v1); + var e3 = Node.NewExtension(new byte[] { 0x0e }, h3); + var e4 = Node.NewExtension(new byte[] { 0x01 }, v4); + b.Children[0] = e1; + b.Children[10] = e3; + b.Children[16] = v2; + b.Children[15] = Node.NewHash(e4.Hash); + this.root = r; + this.mptdb = new MemoryStore(); + PutToStore(mptdb, r); + PutToStore(mptdb, b); + PutToStore(mptdb, e1); + PutToStore(mptdb, e3); + PutToStore(mptdb, v1); + PutToStore(mptdb, v2); + PutToStore(mptdb, v3); + } + + [TestMethod] + public void TestTryGet() + { + var mpt = new Trie(mptdb.GetSnapshot(), root.Hash); + Assert.ThrowsException(() => mpt[Array.Empty()]); + Assert.AreEqual("abcd", mpt["ac01".HexToBytes()].ToHexString()); + Assert.AreEqual("2222", mpt["ac".HexToBytes()].ToHexString()); + Assert.ThrowsException(() => mpt["ab99".HexToBytes()]); + Assert.ThrowsException(() => mpt["ac39".HexToBytes()]); + Assert.ThrowsException(() => mpt["ac02".HexToBytes()]); + Assert.ThrowsException(() => mpt["ac0100".HexToBytes()]); + Assert.ThrowsException(() => mpt["ac9910".HexToBytes()]); + Assert.ThrowsException(() => mpt["acf1".HexToBytes()]); + } + + [TestMethod] + public void TestTryGetResolve() + { + var mpt = new Trie(mptdb.GetSnapshot(), root.Hash); + Assert.AreEqual(Encoding.ASCII.GetBytes("existing").ToHexString(), mpt["acae".HexToBytes()].ToHexString()); + } + + [TestMethod] + public void TestTryPut() + { + var store = new MemoryStore(); + var mpt = new Trie(store.GetSnapshot(), null); + mpt.Put("ac01".HexToBytes(), "abcd".HexToBytes()); + mpt.Put("ac".HexToBytes(), "2222".HexToBytes()); + mpt.Put("acae".HexToBytes(), Encoding.ASCII.GetBytes("existing")); + mpt.Put("acf1".HexToBytes(), Encoding.ASCII.GetBytes("missing")); + Assert.AreEqual(root.Hash.ToString(), mpt.Root.Hash.ToString()); + Assert.ThrowsException(() => mpt.Put(Array.Empty(), "01".HexToBytes())); + mpt.Put("01".HexToBytes(), Array.Empty()); + Assert.ThrowsException(() => mpt.Put(new byte[Node.MaxKeyLength / 2 + 1], Array.Empty())); + Assert.ThrowsException(() => mpt.Put("01".HexToBytes(), new byte[Node.MaxValueLength + 1])); + mpt.Put("ac01".HexToBytes(), "ab".HexToBytes()); + } + + [TestMethod] + public void TestPutCantResolve() + { + var mpt = new Trie(mptdb.GetSnapshot(), root.Hash); + Assert.ThrowsException(() => mpt.Put("acf111".HexToBytes(), new byte[] { 1 })); + } + + [TestMethod] + public void TestTryDelete() + { + var mpt = new Trie(mptdb.GetSnapshot(), root.Hash); + Assert.IsNotNull(mpt["ac".HexToBytes()]); + Assert.IsFalse(mpt.Delete("0c99".HexToBytes())); + Assert.ThrowsException(() => mpt.Delete(Array.Empty())); + Assert.IsFalse(mpt.Delete("ac20".HexToBytes())); + Assert.ThrowsException(() => mpt.Delete("acf1".HexToBytes())); + Assert.IsTrue(mpt.Delete("ac".HexToBytes())); + Assert.IsFalse(mpt.Delete("acae01".HexToBytes())); + Assert.IsTrue(mpt.Delete("acae".HexToBytes())); + Assert.AreEqual("0xcb06925428b7c727375c7fdd943a302fe2c818cf2e2eaf63a7932e3fd6cb3408", mpt.Root.Hash.ToString()); + } + + [TestMethod] + public void TestDeleteRemainCanResolve() + { + var store = new MemoryStore(); + var snapshot = store.GetSnapshot(); + var mpt1 = new Trie(snapshot, null); + mpt1.Put("ac00".HexToBytes(), "abcd".HexToBytes()); + mpt1.Put("ac10".HexToBytes(), "abcd".HexToBytes()); + mpt1.Commit(); + snapshot.Commit(); + var snapshot2 = store.GetSnapshot(); + var mpt2 = new Trie(snapshot2, mpt1.Root.Hash); + Assert.IsTrue(mpt2.Delete("ac00".HexToBytes())); + mpt2.Commit(); + snapshot2.Commit(); + Assert.IsTrue(mpt2.Delete("ac10".HexToBytes())); + } + + [TestMethod] + public void TestDeleteRemainCantResolve() + { + var b = Node.NewBranch(); + var r = Node.NewExtension("0a0c".HexToBytes(), b); + var v1 = Node.NewLeaf("abcd".HexToBytes());//key=ac01 + var v4 = Node.NewLeaf(Encoding.ASCII.GetBytes("missing")); + var e1 = Node.NewExtension(new byte[] { 0x01 }, v1); + var e4 = Node.NewExtension(new byte[] { 0x01 }, v4); + b.Children[0] = e1; + b.Children[15] = Node.NewHash(e4.Hash); + var store = new MemoryStore(); + PutToStore(store, r); + PutToStore(store, b); + PutToStore(store, e1); + PutToStore(store, v1); + + var snapshot = store.GetSnapshot(); + var mpt = new Trie(snapshot, r.Hash); + Assert.ThrowsException(() => mpt.Delete("ac01".HexToBytes())); + } + + + [TestMethod] + public void TestDeleteSameValue() + { + var store = new MemoryStore(); + var snapshot = store.GetSnapshot(); + var mpt = new Trie(snapshot, null); + mpt.Put("ac01".HexToBytes(), "abcd".HexToBytes()); + mpt.Put("ac02".HexToBytes(), "abcd".HexToBytes()); + Assert.IsNotNull(mpt["ac01".HexToBytes()]); + Assert.IsNotNull(mpt["ac02".HexToBytes()]); + mpt.Delete("ac01".HexToBytes()); + Assert.IsNotNull(mpt["ac02".HexToBytes()]); + mpt.Commit(); + snapshot.Commit(); + var mpt0 = new Trie(store.GetSnapshot(), mpt.Root.Hash); + Assert.IsNotNull(mpt0["ac02".HexToBytes()]); + } + + [TestMethod] + public void TestBranchNodeRemainValue() + { + var snapshot = new TestSnapshot(); + var mpt = new Trie(snapshot, null); + mpt.Put("ac11".HexToBytes(), "ac11".HexToBytes()); + mpt.Put("ac22".HexToBytes(), "ac22".HexToBytes()); + mpt.Put("ac".HexToBytes(), "ac".HexToBytes()); + mpt.Commit(); + Assert.AreEqual(7, snapshot.Size); + Assert.IsTrue(mpt.Delete("ac11".HexToBytes())); + mpt.Commit(); + Assert.AreEqual(5, snapshot.Size); + Assert.IsTrue(mpt.Delete("ac22".HexToBytes())); + Assert.IsNotNull(mpt["ac".HexToBytes()]); + mpt.Commit(); + Assert.AreEqual(2, snapshot.Size); + } + + [TestMethod] + public void TestGetProof() + { + var b = Node.NewBranch(); + var r = Node.NewExtension("0a0c".HexToBytes(), b); + var v1 = Node.NewLeaf("abcd".HexToBytes());//key=ac01 + var v2 = Node.NewLeaf("2222".HexToBytes());//key=ac + var v3 = Node.NewLeaf(Encoding.ASCII.GetBytes("existing"));//key=acae + var v4 = Node.NewLeaf(Encoding.ASCII.GetBytes("missing")); + var h3 = Node.NewHash(v3.Hash); + var e1 = Node.NewExtension(new byte[] { 0x01 }, v1); + var e3 = Node.NewExtension(new byte[] { 0x0e }, h3); + var e4 = Node.NewExtension(new byte[] { 0x01 }, v4); + b.Children[0] = e1; + b.Children[10] = e3; + b.Children[16] = v2; + b.Children[15] = Node.NewHash(e4.Hash); + + var mpt = new Trie(mptdb.GetSnapshot(), r.Hash); + Assert.AreEqual(r.Hash.ToString(), mpt.Root.Hash.ToString()); + var result = mpt.TryGetProof("ac01".HexToBytes(), out var proof); + Assert.IsTrue(result); + Assert.AreEqual(4, proof.Count); + Assert.IsTrue(proof.Contains(b.ToArrayWithoutReference())); + Assert.IsTrue(proof.Contains(r.ToArrayWithoutReference())); + Assert.IsTrue(proof.Contains(e1.ToArrayWithoutReference())); + Assert.IsTrue(proof.Contains(v1.ToArrayWithoutReference())); + + result = mpt.TryGetProof("ac".HexToBytes(), out proof); + Assert.AreEqual(3, proof.Count); + + result = mpt.TryGetProof("ac10".HexToBytes(), out proof); + Assert.IsFalse(result); + + result = mpt.TryGetProof("acae".HexToBytes(), out proof); + Assert.AreEqual(4, proof.Count); + + Assert.ThrowsException(() => mpt.TryGetProof(Array.Empty(), out proof)); + + result = mpt.TryGetProof("ac0100".HexToBytes(), out proof); + Assert.IsFalse(result); + + Assert.ThrowsException(() => mpt.TryGetProof("acf1".HexToBytes(), out var proof)); + } + + [TestMethod] + public void TestVerifyProof() + { + var mpt = new Trie(mptdb.GetSnapshot(), root.Hash); + var result = mpt.TryGetProof("ac01".HexToBytes(), out var proof); + Assert.IsTrue(result); + var value = Trie.VerifyProof(root.Hash, "ac01".HexToBytes(), proof); + Assert.IsNotNull(value); + Assert.AreEqual(value.ToHexString(), "abcd"); + } + + [TestMethod] + public void TestAddLongerKey() + { + var store = new MemoryStore(); + var snapshot = store.GetSnapshot(); + var mpt = new Trie(snapshot, null); + mpt.Put(new byte[] { 0xab }, new byte[] { 0x01 }); + mpt.Put(new byte[] { 0xab, 0xcd }, new byte[] { 0x02 }); + Assert.AreEqual("01", mpt[new byte[] { 0xab }].ToHexString()); + } + + [TestMethod] + public void TestSplitKey() + { + var store = new MemoryStore(); + var snapshot = store.GetSnapshot(); + var mpt1 = new Trie(snapshot, null); + mpt1.Put(new byte[] { 0xab, 0xcd }, new byte[] { 0x01 }); + mpt1.Put(new byte[] { 0xab }, new byte[] { 0x02 }); + var r = mpt1.TryGetProof(new byte[] { 0xab, 0xcd }, out var set1); + Assert.IsTrue(r); + Assert.AreEqual(4, set1.Count); + var mpt2 = new Trie(snapshot, null); + mpt2.Put(new byte[] { 0xab }, new byte[] { 0x02 }); + mpt2.Put(new byte[] { 0xab, 0xcd }, new byte[] { 0x01 }); + r = mpt2.TryGetProof(new byte[] { 0xab, 0xcd }, out var set2); + Assert.IsTrue(r); + Assert.AreEqual(4, set2.Count); + Assert.AreEqual(mpt1.Root.Hash, mpt2.Root.Hash); + } + + [TestMethod] + public void TestFind() + { + var store = new MemoryStore(); + var snapshot = store.GetSnapshot(); + var mpt1 = new Trie(snapshot, null); + var results = mpt1.Find(ReadOnlySpan.Empty).ToArray(); + Assert.AreEqual(0, results.Length); + var mpt2 = new Trie(snapshot, null); + mpt2.Put(new byte[] { 0xab, 0xcd, 0xef }, new byte[] { 0x01 }); + mpt2.Put(new byte[] { 0xab, 0xcd, 0xe1 }, new byte[] { 0x02 }); + mpt2.Put(new byte[] { 0xab }, new byte[] { 0x03 }); + results = mpt2.Find(ReadOnlySpan.Empty).ToArray(); + Assert.AreEqual(3, results.Length); + results = mpt2.Find(new byte[] { 0xab }).ToArray(); + Assert.AreEqual(3, results.Length); + results = mpt2.Find(new byte[] { 0xab, 0xcd }).ToArray(); + Assert.AreEqual(2, results.Length); + results = mpt2.Find(new byte[] { 0xac }).ToArray(); + Assert.AreEqual(0, results.Length); + results = mpt2.Find(new byte[] { 0xab, 0xcd, 0xef, 0x00 }).ToArray(); + Assert.AreEqual(0, results.Length); + } + + [TestMethod] + public void TestFindCantResolve() + { + var b = Node.NewBranch(); + var r = Node.NewExtension("0a0c".HexToBytes(), b); + var v1 = Node.NewLeaf("abcd".HexToBytes());//key=ac01 + var v4 = Node.NewLeaf(Encoding.ASCII.GetBytes("missing")); + var e1 = Node.NewExtension(new byte[] { 0x01 }, v1); + var e4 = Node.NewExtension(new byte[] { 0x01 }, v4); + b.Children[0] = e1; + b.Children[15] = Node.NewHash(e4.Hash); + var store = new MemoryStore(); + PutToStore(store, r); + PutToStore(store, b); + PutToStore(store, e1); + PutToStore(store, v1); + + var snapshot = store.GetSnapshot(); + var mpt = new Trie(snapshot, r.Hash); + Assert.ThrowsException(() => mpt.Find("ac".HexToBytes()).Count()); + } + + [TestMethod] + public void TestFindLeadNode() + { + // r.Key = 0x0a0c + // b.Key = 0x00 + // l1.Key = 0x01 + var mpt = new Trie(mptdb.GetSnapshot(), root.Hash); + var prefix = new byte[] { 0xac, 0x01 }; // = FromNibbles(path = { 0x0a, 0x0c, 0x00, 0x01 }); + var results = mpt.Find(prefix).ToArray(); + Assert.AreEqual(1, results.Count()); + + prefix = new byte[] { 0xac }; // = FromNibbles(path = { 0x0a, 0x0c }); + Assert.ThrowsException(() => mpt.Find(prefix).ToArray()); + } + + [TestMethod] + public void TestFromNibblesException() + { + var b = Node.NewBranch(); + var r = Node.NewExtension("0c".HexToBytes(), b); + var v1 = Node.NewLeaf("abcd".HexToBytes());//key=ac01 + var v2 = Node.NewLeaf("2222".HexToBytes());//key=ac + var e1 = Node.NewExtension(new byte[] { 0x01 }, v1); + b.Children[0] = e1; + b.Children[16] = v2; + var store = new MemoryStore(); + PutToStore(store, r); + PutToStore(store, b); + PutToStore(store, e1); + PutToStore(store, v1); + PutToStore(store, v2); + + var snapshot = store.GetSnapshot(); + var mpt = new Trie(snapshot, r.Hash); + Assert.ThrowsException(() => mpt.Find(Array.Empty()).Count()); + } + + [TestMethod] + public void TestReference1() + { + var store = new MemoryStore(); + var snapshot = store.GetSnapshot(); + var mpt = new Trie(snapshot, null); + mpt.Put("a101".HexToBytes(), "01".HexToBytes()); + mpt.Put("a201".HexToBytes(), "01".HexToBytes()); + mpt.Put("a301".HexToBytes(), "01".HexToBytes()); + mpt.Commit(); + snapshot.Commit(); + var snapshot1 = store.GetSnapshot(); + var mpt1 = new Trie(snapshot1, mpt.Root.Hash); + mpt1.Delete("a301".HexToBytes()); + mpt1.Commit(); + snapshot1.Commit(); + var snapshot2 = store.GetSnapshot(); + var mpt2 = new Trie(snapshot2, mpt1.Root.Hash); + mpt2.Delete("a201".HexToBytes()); + Assert.AreEqual("01", mpt2["a101".HexToBytes()].ToHexString()); + } + + [TestMethod] + public void TestReference2() + { + var snapshot = new TestSnapshot(); + var mpt = new Trie(snapshot, null); + mpt.Put("a101".HexToBytes(), "01".HexToBytes()); + mpt.Put("a201".HexToBytes(), "01".HexToBytes()); + mpt.Put("a301".HexToBytes(), "01".HexToBytes()); + mpt.Commit(); + Assert.AreEqual(4, snapshot.Size); + mpt.Delete("a301".HexToBytes()); + mpt.Commit(); + Assert.AreEqual(4, snapshot.Size); + mpt.Delete("a201".HexToBytes()); + mpt.Commit(); + Assert.AreEqual(2, snapshot.Size); + Assert.AreEqual("01", mpt["a101".HexToBytes()].ToHexString()); + } + + + [TestMethod] + public void TestExtensionDeleteDirty() + { + var snapshot = new TestSnapshot(); + var mpt = new Trie(snapshot, null); + mpt.Put("a1".HexToBytes(), "01".HexToBytes()); + mpt.Put("a2".HexToBytes(), "02".HexToBytes()); + mpt.Commit(); + Assert.AreEqual(4, snapshot.Size); + var mpt1 = new Trie(snapshot, mpt.Root.Hash); + mpt1.Delete("a1".HexToBytes()); + mpt1.Commit(); + Assert.AreEqual(2, snapshot.Size); + var mpt2 = new Trie(snapshot, mpt1.Root.Hash); + mpt2.Delete("a2".HexToBytes()); + mpt2.Commit(); + Assert.AreEqual(0, snapshot.Size); + } + + [TestMethod] + public void TestBranchDeleteDirty() + { + var snapshot = new TestSnapshot(); + var mpt = new Trie(snapshot, null); + mpt.Put("10".HexToBytes(), "01".HexToBytes()); + mpt.Put("20".HexToBytes(), "02".HexToBytes()); + mpt.Put("30".HexToBytes(), "03".HexToBytes()); + mpt.Commit(); + Assert.AreEqual(7, snapshot.Size); + var mpt1 = new Trie(snapshot, mpt.Root.Hash); + mpt1.Delete("10".HexToBytes()); + mpt1.Commit(); + Assert.AreEqual(5, snapshot.Size); + var mpt2 = new Trie(snapshot, mpt1.Root.Hash); + mpt2.Delete("20".HexToBytes()); + mpt2.Commit(); + Assert.AreEqual(2, snapshot.Size); + var mpt3 = new Trie(snapshot, mpt2.Root.Hash); + mpt3.Delete("30".HexToBytes()); + mpt3.Commit(); + Assert.AreEqual(0, snapshot.Size); + } + + [TestMethod] + public void TestExtensionPutDirty() + { + var snapshot = new TestSnapshot(); + var mpt = new Trie(snapshot, null); + mpt.Put("a1".HexToBytes(), "01".HexToBytes()); + mpt.Put("a2".HexToBytes(), "02".HexToBytes()); + mpt.Commit(); + Assert.AreEqual(4, snapshot.Size); + var mpt1 = new Trie(snapshot, mpt.Root.Hash); + mpt1.Put("a3".HexToBytes(), "03".HexToBytes()); + mpt1.Commit(); + Assert.AreEqual(5, snapshot.Size); + } + + [TestMethod] + public void TestBranchPutDirty() + { + var snapshot = new TestSnapshot(); + var mpt = new Trie(snapshot, null); + mpt.Put("10".HexToBytes(), "01".HexToBytes()); + mpt.Put("20".HexToBytes(), "02".HexToBytes()); + mpt.Commit(); + Assert.AreEqual(5, snapshot.Size); + var mpt1 = new Trie(snapshot, mpt.Root.Hash); + mpt1.Put("30".HexToBytes(), "03".HexToBytes()); + mpt1.Commit(); + Assert.AreEqual(7, snapshot.Size); + } + + [TestMethod] + public void TestEmptyValueIssue633() + { + var key = "01".HexToBytes(); + var snapshot = new TestSnapshot(); + var mpt = new Trie(snapshot, null); + mpt.Put(key, Array.Empty()); + var val = mpt[key]; + Assert.IsNotNull(val); + Assert.AreEqual(0, val.Length); + var r = mpt.TryGetProof(key, out var proof); + Assert.IsTrue(r); + val = Trie.VerifyProof(mpt.Root.Hash, key, proof); + Assert.IsNotNull(val); + Assert.AreEqual(0, val.Length); + } + + [TestMethod] + public void TestFindWithFrom() + { + var snapshot = new TestSnapshot(); + var mpt = new Trie(snapshot, null); + mpt.Put("aa".HexToBytes(), "02".HexToBytes()); + mpt.Put("aa10".HexToBytes(), "03".HexToBytes()); + mpt.Put("aa50".HexToBytes(), "04".HexToBytes()); + var r = mpt.Find("aa".HexToBytes()).ToList(); + Assert.AreEqual(3, r.Count); + r = mpt.Find("aa".HexToBytes(), "aa30".HexToBytes()).ToList(); + Assert.AreEqual(1, r.Count); + r = mpt.Find("aa".HexToBytes(), "aa60".HexToBytes()).ToList(); + Assert.AreEqual(0, r.Count); + r = mpt.Find("aa".HexToBytes(), "aa10".HexToBytes()).ToList(); + Assert.AreEqual(1, r.Count); + } + + [TestMethod] + public void TestFindStatesIssue652() + { + var snapshot = new TestSnapshot(); + var mpt = new Trie(snapshot, null); + mpt.Put("abc1".HexToBytes(), "01".HexToBytes()); + mpt.Put("abc3".HexToBytes(), "02".HexToBytes()); + var r = mpt.Find("ab".HexToBytes(), "abd2".HexToBytes()).ToList(); + Assert.AreEqual(0, r.Count); + r = mpt.Find("ab".HexToBytes(), "abb2".HexToBytes()).ToList(); + Assert.AreEqual(2, r.Count); + r = mpt.Find("ab".HexToBytes(), "abc2".HexToBytes()).ToList(); + Assert.AreEqual(1, r.Count); + } + } +} diff --git a/tests/Neo.Cryptography.MPTTrie.Tests/Neo.Cryptography.MPTTrie.Tests.csproj b/tests/Neo.Cryptography.MPTTrie.Tests/Neo.Cryptography.MPTTrie.Tests.csproj new file mode 100644 index 0000000000..3691fba90a --- /dev/null +++ b/tests/Neo.Cryptography.MPTTrie.Tests/Neo.Cryptography.MPTTrie.Tests.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + Neo.Cryptography.MPT.Tests + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Neo.Json.UnitTests/Neo.Json.UnitTests.csproj b/tests/Neo.Json.UnitTests/Neo.Json.UnitTests.csproj index 1e66e0ce65..81e70ebbe1 100644 --- a/tests/Neo.Json.UnitTests/Neo.Json.UnitTests.csproj +++ b/tests/Neo.Json.UnitTests/Neo.Json.UnitTests.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/tests/Neo.Network.RPC.Tests/Neo.Network.RPC.Tests.csproj b/tests/Neo.Network.RPC.Tests/Neo.Network.RPC.Tests.csproj new file mode 100644 index 0000000000..13a13f5c98 --- /dev/null +++ b/tests/Neo.Network.RPC.Tests/Neo.Network.RPC.Tests.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + Neo.Network.RPC.Tests + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/tests/Neo.Network.RPC.Tests/RpcTestCases.json b/tests/Neo.Network.RPC.Tests/RpcTestCases.json new file mode 100644 index 0000000000..cfbffb3ede --- /dev/null +++ b/tests/Neo.Network.RPC.Tests/RpcTestCases.json @@ -0,0 +1,3340 @@ +[ + { + "Name": "sendrawtransactionasyncerror", + "Request": { + "jsonrpc": "2.0", + "method": "sendrawtransaction", + "params": [ "ANIHn05ujtUAAAAAACYcEwAAAAAAQkEAAAEKo4e1Ppa3mJpjFDGgVt0fQKBC9gEAXQMAyBeoBAAAAAwUzViuz9M1vh6z0xHh3IAJY9/XLZ8MFAqjh7U+lreYmmMUMaBW3R9AoEL2E8AMCHRyYW5zZmVyDBSlB7dGdv/td+dUuG7NmQnwus08ukFifVtSOAFCDEDh8zgTrGUXyzVX60wBCMyajNRfzFRiEPAe8CgGQ10bA2C3fnVz68Gw+Amgn5gmvuNfYKgWQ/W68Km1bYUPlnEYKQwhA86j4vgfGvk1ItKe3k8kofC+3q1ykzkdM4gPVHXZeHjJC0GVRA14" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -500, + "message": "InsufficientFunds", + "data": " at Neo.Plugins.RpcServer.GetRelayResult(RelayResultReason reason, UInt256 hash)\r\n at Neo.Network.RPC.Models.RpcServer.SendRawTransaction(JArray _params)\r\n at Neo.Network.RPC.Models.RpcServer.ProcessRequest(HttpContext context, JObject request)" + } + } + }, + { + "Name": "getbestblockhashasync", + "Request": { + "jsonrpc": "2.0", + "method": "getbestblockhash", + "params": [], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": "0x530de76326a8662d1b730ba4fbdf011051eabd142015587e846da42376adf35f" + } + }, + { + "Name": "getblockhexasync", + "Request": { + "jsonrpc": "2.0", + "method": "getblock", + "params": [ 0 ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": "0000000000000000000000000000000000000000000000000000000000000000000000002bbb6298fc7039330cdfd2e4dfbe976ee72c4cba6c16d68f0b49ab1bca685b7388ea19ef55010000000000009903b0c3d292988febe5f306a02f654ea2eb16290100011102001dac2b7c000000000000000000ca61e52e881d41374e640f819cd118cc153b21a7000000000000000000000000000000000000000000000541123e7fe801000111" + } + }, + { + "Name": "getblockhexasync", + "Request": { + "jsonrpc": "2.0", + "method": "getblock", + "params": [ "0xe191fe1aea732c3e23f20af8a95e09f95891176f8064a2fce8571d51f80619a8" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": "0000000000000000000000000000000000000000000000000000000000000000000000002bbb6298fc7039330cdfd2e4dfbe976ee72c4cba6c16d68f0b49ab1bca685b7388ea19ef55010000000000009903b0c3d292988febe5f306a02f654ea2eb16290100011102001dac2b7c000000000000000000ca61e52e881d41374e640f819cd118cc153b21a7000000000000000000000000000000000000000000000541123e7fe801000111" + } + }, + { + "Name": "getblockasync", + "Request": { + "jsonrpc": "2.0", + "method": "getblock", + "params": [ 7, true ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "hash": "0x6d1556889c92249da88d2fb7729ae82fb2cc1b45dcd9030a40208b72a1d3cb83", + "size": 470, + "version": 0, + "previousblockhash": "0xaae8867e9086afaf06fd02cc538e88a69b801abd6f9d3ae39ae630e29d5b39e2", + "merkleroot": "0xe95761f21c733ad53066786af24ee5d613b32bd5aae538df2d611492ec0cae82", + "time": 1594867377561, + "nonce": "FFFFFFFFFFFFFFFF", + "index": 7, + "primary": 1, + "nextconsensus": "NikvsLcNP1jWhrFPrfS3n4spEASgdNYTG2", + "witnesses": [ + { + "invocation": "DEBs6hZDHUtL7KOJuF1m8/vITM8VeduwegKhBdbqcLKdBzXA1uZZiBl8jM/rhjXBaIGQSFIQuq8Er1Nb5y5/DWUx", + "verification": "EQwhAqnqaELMDLOw8jF7B8hQ3j0eKyQ6mO0tVqP/TKZqrzMLEQtBE43vrw==" + } + ], + "tx": [ + { + "hash": "0x83d44d71d59f854bc29f4e3932bf68703545807d05fb5429504d70cfc8d05071", + "size": 248, + "version": 0, + "nonce": 631973574, + "sender": "NikvsLcNP1jWhrFPrfS3n4spEASgdNYTG2", + "sysfee": "9007990", + "netfee": "1248450", + "validuntilblock": 2102405, + "signers": [ + { + "account": "0xe19de267a37a71734478f512b3e92c79fc3695fa", + "scopes": "CalledByEntry" + } + ], + "attributes": [], + "script": "AccyDBQcA1dGS3d\u002Bz2tfOsOJOs4fixYh9gwU\u002BpU2/Hks6bMS9XhEc3F6o2fineETwAwIdHJhbnNmZXIMFCUFnstIeNOodfkcUc7e0zDUV1/eQWJ9W1I4", + "witnesses": [ + { + "invocation": "DEDZxkskUb1aH1I4EX5ja02xrYX4fCubAmQzBuPpfY7pDEb1n4Dzx\u002BUB\u002BqSdC/CGskGf5BuzJ0MWJJipsHuivKmU", + "verification": "EQwhAqnqaELMDLOw8jF7B8hQ3j0eKyQ6mO0tVqP/TKZqrzMLEQtBE43vrw==" + } + ] + } + ], + "confirmations": 695, + "nextblockhash": "0xc4b986813396932a47d6823a9987ccee0148c6fca0150102f4b24ce05cfc9c6f" + } + } + }, + { + "Name": "getblockasync", + "Request": { + "jsonrpc": "2.0", + "method": "getblock", + "params": [ "0xb9579b028e4cf31a0c3bd9582f9f7fbd40b0e0495604406b8f530c7ebce5bcc8", true ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "hash": "0x6d1556889c92249da88d2fb7729ae82fb2cc1b45dcd9030a40208b72a1d3cb83", + "size": 470, + "version": 0, + "previousblockhash": "0xaae8867e9086afaf06fd02cc538e88a69b801abd6f9d3ae39ae630e29d5b39e2", + "merkleroot": "0xe95761f21c733ad53066786af24ee5d613b32bd5aae538df2d611492ec0cae82", + "time": 1594867377561, + "nonce": "FFFFFFFFFFFFFFFF", + "index": 7, + "primary": 1, + "nextconsensus": "NikvsLcNP1jWhrFPrfS3n4spEASgdNYTG2", + "witnesses": [ + { + "invocation": "DEBs6hZDHUtL7KOJuF1m8/vITM8VeduwegKhBdbqcLKdBzXA1uZZiBl8jM/rhjXBaIGQSFIQuq8Er1Nb5y5/DWUx", + "verification": "EQwhAqnqaELMDLOw8jF7B8hQ3j0eKyQ6mO0tVqP/TKZqrzMLEQtBE43vrw==" + } + ], + "tx": [ + { + "hash": "0x83d44d71d59f854bc29f4e3932bf68703545807d05fb5429504d70cfc8d05071", + "size": 248, + "version": 0, + "nonce": 631973574, + "sender": "NikvsLcNP1jWhrFPrfS3n4spEASgdNYTG2", + "sysfee": "9007990", + "netfee": "1248450", + "validuntilblock": 2102405, + "signers": [ + { + "account": "0xe19de267a37a71734478f512b3e92c79fc3695fa", + "scopes": "CalledByEntry" + } + ], + "attributes": [], + "script": "AccyDBQcA1dGS3d\u002Bz2tfOsOJOs4fixYh9gwU\u002BpU2/Hks6bMS9XhEc3F6o2fineETwAwIdHJhbnNmZXIMFCUFnstIeNOodfkcUc7e0zDUV1/eQWJ9W1I4", + "witnesses": [ + { + "invocation": "DEDZxkskUb1aH1I4EX5ja02xrYX4fCubAmQzBuPpfY7pDEb1n4Dzx\u002BUB\u002BqSdC/CGskGf5BuzJ0MWJJipsHuivKmU", + "verification": "EQwhAqnqaELMDLOw8jF7B8hQ3j0eKyQ6mO0tVqP/TKZqrzMLEQtBE43vrw==" + } + ] + } + ], + "confirmations": 695, + "nextblockhash": "0xc4b986813396932a47d6823a9987ccee0148c6fca0150102f4b24ce05cfc9c6f" + } + } + }, + { + "Name": "getblockheadercountasync", + "Request": { + "jsonrpc": "2.0", + "method": "getblockheadercount", + "params": [], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": 3825 + } + }, + { + "Name": "getblockcountasync", + "Request": { + "jsonrpc": "2.0", + "method": "getblockcount", + "params": [], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": 2691 + } + }, + { + "Name": "getblockhashasync", + "Request": { + "jsonrpc": "2.0", + "method": "getblockhash", + "params": [ 0 ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": "0xe191fe1aea732c3e23f20af8a95e09f95891176f8064a2fce8571d51f80619a8" + } + }, + { + "Name": "getblockheaderhexasync", + "Request": { + "jsonrpc": "2.0", + "method": "getblockheader", + "params": [ 0 ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": "0000000000000000000000000000000000000000000000000000000000000000000000002bbb6298fc7039330cdfd2e4dfbe976ee72c4cba6c16d68f0b49ab1bca685b7388ea19ef55010000000000009903b0c3d292988febe5f306a02f654ea2eb16290100011100" + } + }, + { + "Name": "getblockheaderhexasync", + "Request": { + "jsonrpc": "2.0", + "method": "getblockheader", + "params": [ "0xe191fe1aea732c3e23f20af8a95e09f95891176f8064a2fce8571d51f80619a8" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": "0000000000000000000000000000000000000000000000000000000000000000000000002bbb6298fc7039330cdfd2e4dfbe976ee72c4cba6c16d68f0b49ab1bca685b7388ea19ef55010000000000009903b0c3d292988febe5f306a02f654ea2eb16290100011100" + } + }, + { + "Name": "getblockheaderasync", + "Request": { + "jsonrpc": "2.0", + "method": "getblockheader", + "params": [ 0, true ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "hash": "0xbbf7e191d4947f8a4dc33477902dacd0b047e371a81c18a6df62fe0d541725f5", + "size": 113, + "version": 0, + "previousblockhash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "merkleroot": "0x735b68ca1bab490b8fd6166cba4c2ce76e97bedfe4d2df0c333970fc9862bb2b", + "time": 1468595301000, + "nonce": "FFFFFFFFFFFFFFFF", + "index": 0, + "primary": 1, + "nextconsensus": "NZs2zXSPuuv9ZF6TDGSWT1RBmE8rfGj7UW", + "witnesses": [ + { + "invocation": "", + "verification": "EQ==" + } + ], + "confirmations": 2700, + "nextblockhash": "0x423173109798b038019b35129417b55cc4b5976ac79978dfab8ea2512d155f69" + } + } + }, + { + "Name": "getblockheaderasync", + "Request": { + "jsonrpc": "2.0", + "method": "getblockheader", + "params": [ "0x656bcb02e4fe8a19dbb15149073a5ae0bd8adc2da8504b67b112b44f68b4c9d7", true ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "hash": "0xbbf7e191d4947f8a4dc33477902dacd0b047e371a81c18a6df62fe0d541725f5", + "size": 113, + "version": 0, + "previousblockhash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "merkleroot": "0x735b68ca1bab490b8fd6166cba4c2ce76e97bedfe4d2df0c333970fc9862bb2b", + "time": 1468595301000, + "nonce": "FFFFFFFFFFFFFFFF", + "index": 0, + "primary": 1, + "nextconsensus": "NZs2zXSPuuv9ZF6TDGSWT1RBmE8rfGj7UW", + "witnesses": [ + { + "invocation": "", + "verification": "EQ==" + } + ], + "confirmations": 2700, + "nextblockhash": "0x423173109798b038019b35129417b55cc4b5976ac79978dfab8ea2512d155f69" + } + } + }, + { + "Name": "getblocksysfeeasync", + "Request": { + "jsonrpc": "2.0", + "method": "getblocksysfee", + "params": [ 100 ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": "300000000" + } + }, + { + "Name": "getcommitteeasync", + "Request": { + "jsonrpc": "2.0", + "method": "getcommittee", + "params": [], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": [ + "02ced432397ddc44edba031c0bc3b933f28fdd9677792d7b20e6c036ddaaacf1e2" + ] + } + }, + { + "Name": "getcontractstateasync", + "Request": { + "jsonrpc": "2.0", + "method": "getcontractstate", + "params": [ "gastoken" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "id": -4, + "updatecounter": 0, + "hash": "0xd2a4cff31913016155e38e474a2c06d08be276cf", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "APxBGvd7Zw==", + "checksum": 3155977747 + }, + "manifest": { + "name": "GasToken", + "groups": [], + "features": {}, + "supportedstandards": [ + "NEP-17" + ], + "abi": { + "methods": [ + { + "name": "balanceOf", + "parameters": [ + { + "name": "account", + "type": "Hash160" + } + ], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "decimals", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "symbol", + "parameters": [], + "returntype": "String", + "offset": 0, + "safe": true + }, + { + "name": "totalSupply", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + }, + { + "name": "data", + "type": "Any" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + } + ], + "events": [ + { + "name": "Transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + } + ] + } + ] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": null + } + } + } + }, + { + "Name": "getcontractstateasync", + "Request": { + "jsonrpc": "2.0", + "method": "getcontractstate", + "params": [ "0xd2a4cff31913016155e38e474a2c06d08be276cf" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "id": -4, + "updatecounter": 0, + "hash": "0xd2a4cff31913016155e38e474a2c06d08be276cf", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "APxBGvd7Zw==", + "checksum": 3155977747 + }, + "manifest": { + "name": "GasToken", + "groups": [], + "features": {}, + "supportedstandards": [ + "NEP-17" + ], + "abi": { + "methods": [ + { + "name": "balanceOf", + "parameters": [ + { + "name": "account", + "type": "Hash160" + } + ], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "decimals", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "symbol", + "parameters": [], + "returntype": "String", + "offset": 0, + "safe": true + }, + { + "name": "totalSupply", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + }, + { + "name": "data", + "type": "Any" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + } + ], + "events": [ + { + "name": "Transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + } + ] + } + ] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": null + } + } + } + }, + { + "Name": "getcontractstateasync", + "Request": { + "jsonrpc": "2.0", + "id": 1, + "method": "getcontractstate", + "params": [ "neotoken" ] + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "id": -3, + "updatecounter": 0, + "hash": "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "AP1BGvd7Zw==", + "checksum": 3921333105 + }, + "manifest": { + "name": "NeoToken", + "groups": [], + "features": {}, + "supportedstandards": [ + "NEP-17" + ], + "abi": { + "methods": [ + { + "name": "balanceOf", + "parameters": [ + { + "name": "account", + "type": "Hash160" + } + ], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "decimals", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "getCandidates", + "parameters": [], + "returntype": "Array", + "offset": 0, + "safe": true + }, + { + "name": "getCommittee", + "parameters": [], + "returntype": "Array", + "offset": 0, + "safe": true + }, + { + "name": "getGasPerBlock", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "getNextBlockValidators", + "parameters": [], + "returntype": "Array", + "offset": 0, + "safe": true + }, + { + "name": "registerCandidate", + "parameters": [ + { + "name": "pubkey", + "type": "ByteArray" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "setGasPerBlock", + "parameters": [ + { + "name": "gasPerBlock", + "type": "Integer" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "symbol", + "parameters": [], + "returntype": "String", + "offset": 0, + "safe": true + }, + { + "name": "totalSupply", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + }, + { + "name": "data", + "type": "Any" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "unclaimedGas", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "end", + "type": "Integer" + } + ], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "unregisterCandidate", + "parameters": [ + { + "name": "pubkey", + "type": "ByteArray" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "vote", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "voteTo", + "type": "ByteArray" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + } + ], + "events": [ + { + "name": "Transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + } + ] + } + ] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": null + } + } + } + }, + { + "Name": "getcontractstateasync", + "Request": { + "jsonrpc": "2.0", + "id": 1, + "method": "getcontractstate", + "params": [ "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5" ] + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "id": -3, + "updatecounter": 0, + "hash": "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "AP1BGvd7Zw==", + "checksum": 3921333105 + }, + "manifest": { + "name": "NeoToken", + "groups": [], + "features": {}, + "supportedstandards": [ + "NEP-17" + ], + "abi": { + "methods": [ + { + "name": "balanceOf", + "parameters": [ + { + "name": "account", + "type": "Hash160" + } + ], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "decimals", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "getCandidates", + "parameters": [], + "returntype": "Array", + "offset": 0, + "safe": true + }, + { + "name": "getCommittee", + "parameters": [], + "returntype": "Array", + "offset": 0, + "safe": true + }, + { + "name": "getGasPerBlock", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "getNextBlockValidators", + "parameters": [], + "returntype": "Array", + "offset": 0, + "safe": true + }, + { + "name": "registerCandidate", + "parameters": [ + { + "name": "pubkey", + "type": "ByteArray" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "setGasPerBlock", + "parameters": [ + { + "name": "gasPerBlock", + "type": "Integer" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "symbol", + "parameters": [], + "returntype": "String", + "offset": 0, + "safe": true + }, + { + "name": "totalSupply", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + }, + { + "name": "data", + "type": "Any" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "unclaimedGas", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "end", + "type": "Integer" + } + ], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "unregisterCandidate", + "parameters": [ + { + "name": "pubkey", + "type": "ByteArray" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "vote", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "voteTo", + "type": "ByteArray" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + } + ], + "events": [ + { + "name": "Transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + } + ] + } + ] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": null + } + } + } + }, + { + "Name": "getnativecontractsasync", + "Request": { + "jsonrpc": "2.0", + "id": 1, + "method": "getnativecontracts", + "params": [] + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": [ + { + "id": -1, + "updatecounter": 0, + "hash": "0xa501d7d7d10983673b61b7a2d3a813b36f9f0e43", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "D0Ea93tn", + "checksum": 3516775561 + }, + "manifest": { + "name": "ContractManagement", + "groups": [], + "features": {}, + "supportedstandards": [], + "abi": { + "methods": [ + { + "name": "deploy", + "parameters": [ + { + "name": "nefFile", + "type": "ByteArray" + }, + { + "name": "manifest", + "type": "ByteArray" + } + ], + "returntype": "Array", + "offset": 0, + "safe": false + }, + { + "name": "deploy", + "parameters": [ + { + "name": "nefFile", + "type": "ByteArray" + }, + { + "name": "manifest", + "type": "ByteArray" + }, + { + "name": "data", + "type": "Any" + } + ], + "returntype": "Array", + "offset": 0, + "safe": false + }, + { + "name": "destroy", + "parameters": [], + "returntype": "Void", + "offset": 0, + "safe": false + }, + { + "name": "getContract", + "parameters": [ + { + "name": "hash", + "type": "Hash160" + } + ], + "returntype": "Array", + "offset": 0, + "safe": true + }, + { + "name": "getMinimumDeploymentFee", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "setMinimumDeploymentFee", + "parameters": [ + { + "name": "value", + "type": "Integer" + } + ], + "returntype": "Void", + "offset": 0, + "safe": false + }, + { + "name": "update", + "parameters": [ + { + "name": "nefFile", + "type": "ByteArray" + }, + { + "name": "manifest", + "type": "ByteArray" + } + ], + "returntype": "Void", + "offset": 0, + "safe": false + }, + { + "name": "update", + "parameters": [ + { + "name": "nefFile", + "type": "ByteArray" + }, + { + "name": "manifest", + "type": "ByteArray" + }, + { + "name": "data", + "type": "Any" + } + ], + "returntype": "Void", + "offset": 0, + "safe": false + } + ], + "events": [ + { + "name": "Deploy", + "parameters": [ + { + "name": "Hash", + "type": "Hash160" + } + ] + }, + { + "name": "Update", + "parameters": [ + { + "name": "Hash", + "type": "Hash160" + } + ] + }, + { + "name": "Destroy", + "parameters": [ + { + "name": "Hash", + "type": "Hash160" + } + ] + } + ] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": null + } + }, + { + "id": -2, + "updatecounter": 1, + "hash": "0x971d69c6dd10ce88e7dfffec1dc603c6125a8764", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "AP5BGvd7Zw==", + "checksum": 3395482975 + }, + "manifest": { + "name": "LedgerContract", + "groups": [], + "features": {}, + "supportedstandards": [], + "abi": { + "methods": [ + { + "name": "currentHash", + "parameters": [], + "returntype": "Hash256", + "offset": 0, + "safe": true + }, + { + "name": "currentIndex", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "getBlock", + "parameters": [ + { + "name": "indexOrHash", + "type": "ByteArray" + } + ], + "returntype": "Array", + "offset": 0, + "safe": true + }, + { + "name": "getTransaction", + "parameters": [ + { + "name": "hash", + "type": "Hash256" + } + ], + "returntype": "Array", + "offset": 0, + "safe": true + }, + { + "name": "getTransactionFromBlock", + "parameters": [ + { + "name": "blockIndexOrHash", + "type": "ByteArray" + }, + { + "name": "txIndex", + "type": "Integer" + } + ], + "returntype": "Array", + "offset": 0, + "safe": true + }, + { + "name": "getTransactionHeight", + "parameters": [ + { + "name": "hash", + "type": "Hash256" + } + ], + "returntype": "Integer", + "offset": 0, + "safe": true + } + ], + "events": [] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": null + } + }, + { + "id": -3, + "updatecounter": 0, + "hash": "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "AP1BGvd7Zw==", + "checksum": 3921333105 + }, + "manifest": { + "name": "NeoToken", + "groups": [], + "features": {}, + "supportedstandards": [ + "NEP-17" + ], + "abi": { + "methods": [ + { + "name": "balanceOf", + "parameters": [ + { + "name": "account", + "type": "Hash160" + } + ], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "decimals", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "getCandidates", + "parameters": [], + "returntype": "Array", + "offset": 0, + "safe": true + }, + { + "name": "getCommittee", + "parameters": [], + "returntype": "Array", + "offset": 0, + "safe": true + }, + { + "name": "getGasPerBlock", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "getNextBlockValidators", + "parameters": [], + "returntype": "Array", + "offset": 0, + "safe": true + }, + { + "name": "registerCandidate", + "parameters": [ + { + "name": "pubkey", + "type": "ByteArray" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "setGasPerBlock", + "parameters": [ + { + "name": "gasPerBlock", + "type": "Integer" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "symbol", + "parameters": [], + "returntype": "String", + "offset": 0, + "safe": true + }, + { + "name": "totalSupply", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + }, + { + "name": "data", + "type": "Any" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "unclaimedGas", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "end", + "type": "Integer" + } + ], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "unregisterCandidate", + "parameters": [ + { + "name": "pubkey", + "type": "ByteArray" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "vote", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "voteTo", + "type": "ByteArray" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + } + ], + "events": [ + { + "name": "Transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + } + ] + } + ] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": null + } + }, + { + "id": -4, + "updatecounter": 0, + "hash": "0xd2a4cff31913016155e38e474a2c06d08be276cf", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "APxBGvd7Zw==", + "checksum": 3155977747 + }, + "manifest": { + "name": "GasToken", + "groups": [], + "features": {}, + "supportedstandards": [ + "NEP-17" + ], + "abi": { + "methods": [ + { + "name": "balanceOf", + "parameters": [ + { + "name": "account", + "type": "Hash160" + } + ], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "decimals", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "symbol", + "parameters": [], + "returntype": "String", + "offset": 0, + "safe": true + }, + { + "name": "totalSupply", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + }, + { + "name": "data", + "type": "Any" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + } + ], + "events": [ + { + "name": "Transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + } + ] + } + ] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": null + } + }, + { + "id": -5, + "updatecounter": 0, + "hash": "0x79bcd398505eb779df6e67e4be6c14cded08e2f2", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "APtBGvd7Zw==", + "checksum": 1136340263 + }, + "manifest": { + "name": "PolicyContract", + "groups": [], + "features": {}, + "supportedstandards": [], + "abi": { + "methods": [ + { + "name": "blockAccount", + "parameters": [ + { + "name": "account", + "type": "Hash160" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "getExecFeeFactor", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "getFeePerByte", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "getMaxBlockSize", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "getMaxBlockSystemFee", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "getMaxTransactionsPerBlock", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "getStoragePrice", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "isBlocked", + "parameters": [ + { + "name": "account", + "type": "Hash160" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": true + }, + { + "name": "setExecFeeFactor", + "parameters": [ + { + "name": "value", + "type": "Integer" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "setFeePerByte", + "parameters": [ + { + "name": "value", + "type": "Integer" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "setMaxBlockSize", + "parameters": [ + { + "name": "value", + "type": "Integer" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "setMaxBlockSystemFee", + "parameters": [ + { + "name": "value", + "type": "Integer" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "setMaxTransactionsPerBlock", + "parameters": [ + { + "name": "value", + "type": "Integer" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "setStoragePrice", + "parameters": [ + { + "name": "value", + "type": "Integer" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "unblockAccount", + "parameters": [ + { + "name": "account", + "type": "Hash160" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + } + ], + "events": [] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": null + } + }, + { + "id": -6, + "updatecounter": 0, + "hash": "0x597b1471bbce497b7809e2c8f10db67050008b02", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "APpBGvd7Zw==", + "checksum": 3289425910 + }, + "manifest": { + "name": "RoleManagement", + "groups": [], + "features": {}, + "supportedstandards": [], + "abi": { + "methods": [ + { + "name": "designateAsRole", + "parameters": [ + { + "name": "role", + "type": "Integer" + }, + { + "name": "nodes", + "type": "Array" + } + ], + "returntype": "Void", + "offset": 0, + "safe": false + }, + { + "name": "getDesignatedByRole", + "parameters": [ + { + "name": "role", + "type": "Integer" + }, + { + "name": "index", + "type": "Integer" + } + ], + "returntype": "Array", + "offset": 0, + "safe": true + } + ], + "events": [] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": null + } + }, + { + "id": -7, + "updatecounter": 0, + "hash": "0x8dc0e742cbdfdeda51ff8a8b78d46829144c80ee", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "APlBGvd7Zw==", + "checksum": 3902663397 + }, + "manifest": { + "name": "OracleContract", + "groups": [], + "features": {}, + "supportedstandards": [], + "abi": { + "methods": [ + { + "name": "finish", + "parameters": [], + "returntype": "Void", + "offset": 0, + "safe": false + }, + { + "name": "request", + "parameters": [ + { + "name": "url", + "type": "String" + }, + { + "name": "filter", + "type": "String" + }, + { + "name": "callback", + "type": "String" + }, + { + "name": "userData", + "type": "Any" + }, + { + "name": "gasForResponse", + "type": "Integer" + } + ], + "returntype": "Void", + "offset": 0, + "safe": false + }, + { + "name": "verify", + "parameters": [], + "returntype": "Boolean", + "offset": 0, + "safe": true + } + ], + "events": [ + { + "name": "OracleRequest", + "parameters": [ + { + "name": "Id", + "type": "Integer" + }, + { + "name": "RequestContract", + "type": "Hash160" + }, + { + "name": "Url", + "type": "String" + }, + { + "name": "Filter", + "type": "String" + } + ] + }, + { + "name": "OracleResponse", + "parameters": [ + { + "name": "Id", + "type": "Integer" + }, + { + "name": "OriginalTx", + "type": "Hash256" + } + ] + } + ] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": null + } + }, + { + "id": -8, + "updatecounter": 0, + "hash": "0xa2b524b68dfe43a9d56af84f443c6b9843b8028c", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "APhBGvd7Zw==", + "checksum": 3740064217 + }, + "manifest": { + "name": "NameService", + "groups": [], + "features": {}, + "supportedstandards": [], + "abi": { + "methods": [ + { + "name": "addRoot", + "parameters": [ + { + "name": "root", + "type": "String" + } + ], + "returntype": "Void", + "offset": 0, + "safe": false + }, + { + "name": "balanceOf", + "parameters": [ + { + "name": "owner", + "type": "Hash160" + } + ], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "decimals", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "deleteRecord", + "parameters": [ + { + "name": "name", + "type": "String" + }, + { + "name": "type", + "type": "Integer" + } + ], + "returntype": "Void", + "offset": 0, + "safe": false + }, + { + "name": "getPrice", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "getRecord", + "parameters": [ + { + "name": "name", + "type": "String" + }, + { + "name": "type", + "type": "Integer" + } + ], + "returntype": "String", + "offset": 0, + "safe": true + }, + { + "name": "isAvailable", + "parameters": [ + { + "name": "name", + "type": "String" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": true + }, + { + "name": "ownerOf", + "parameters": [ + { + "name": "tokenId", + "type": "ByteArray" + } + ], + "returntype": "Hash160", + "offset": 0, + "safe": true + }, + { + "name": "properties", + "parameters": [ + { + "name": "tokenId", + "type": "ByteArray" + } + ], + "returntype": "Map", + "offset": 0, + "safe": true + }, + { + "name": "register", + "parameters": [ + { + "name": "name", + "type": "String" + }, + { + "name": "owner", + "type": "Hash160" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + }, + { + "name": "renew", + "parameters": [ + { + "name": "name", + "type": "String" + } + ], + "returntype": "Integer", + "offset": 0, + "safe": false + }, + { + "name": "resolve", + "parameters": [ + { + "name": "name", + "type": "String" + }, + { + "name": "type", + "type": "Integer" + } + ], + "returntype": "String", + "offset": 0, + "safe": true + }, + { + "name": "setAdmin", + "parameters": [ + { + "name": "name", + "type": "String" + }, + { + "name": "admin", + "type": "Hash160" + } + ], + "returntype": "Void", + "offset": 0, + "safe": false + }, + { + "name": "setPrice", + "parameters": [ + { + "name": "price", + "type": "Integer" + } + ], + "returntype": "Void", + "offset": 0, + "safe": false + }, + { + "name": "setRecord", + "parameters": [ + { + "name": "name", + "type": "String" + }, + { + "name": "type", + "type": "Integer" + }, + { + "name": "data", + "type": "String" + } + ], + "returntype": "Void", + "offset": 0, + "safe": false + }, + { + "name": "symbol", + "parameters": [], + "returntype": "String", + "offset": 0, + "safe": true + }, + { + "name": "tokensOf", + "parameters": [ + { + "name": "owner", + "type": "Hash160" + } + ], + "returntype": "Any", + "offset": 0, + "safe": true + }, + { + "name": "totalSupply", + "parameters": [], + "returntype": "Integer", + "offset": 0, + "safe": true + }, + { + "name": "transfer", + "parameters": [ + { + "name": "to", + "type": "Hash160" + }, + { + "name": "tokenId", + "type": "ByteArray" + } + ], + "returntype": "Boolean", + "offset": 0, + "safe": false + } + ], + "events": [ + { + "name": "Transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + }, + { + "name": "tokenId", + "type": "ByteArray" + } + ] + } + ] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": null + } + } + ] + } + }, + { + "Name": "getrawmempoolasync", + "Request": { + "jsonrpc": "2.0", + "method": "getrawmempool", + "params": [], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": [ "0x9786cce0dddb524c40ddbdd5e31a41ed1f6b5c8a683c122f627ca4a007a7cf4e", "0xb488ad25eb474f89d5ca3f985cc047ca96bc7373a6d3da8c0f192722896c1cd7" ] + } + }, + { + "Name": "getrawmempoolbothasync", + "Request": { + "jsonrpc": "2.0", + "method": "getrawmempool", + "params": [ true ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "height": 2846, + "verified": [ "0x9786cce0dddb524c40ddbdd5e31a41ed1f6b5c8a683c122f627ca4a007a7cf4e" ], + "unverified": [ "0xb488ad25eb474f89d5ca3f985cc047ca96bc7373a6d3da8c0f192722896c1cd7" ] + } + } + }, + { + "Name": "getrawtransactionhexasync", + "Request": { + "jsonrpc": "2.0", + "method": "getrawtransaction", + "params": [ "0x0cfd49c48306f9027dc71585589b6456bcc53567c359fb7858eabca482186b78" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": "004cdec1396925aa554712439a9c613ba114efa3fac23ddbca00e1f50500000000466a130000000000311d2000005d030010a5d4e80000000c149903b0c3d292988febe5f306a02f654ea2eb16290c146925aa554712439a9c613ba114efa3fac23ddbca13c00c087472616e736665720c143b7d3711c6f0ccf9b1dca903d1bfa1d896f1238c41627d5b523901420c401f85b40d7fa12164aa1d4d18b06ca470f2c89572dc5b901ab1667faebb587cf536454b98a09018adac72376c5e7c5d164535155b763564347aa47b69aa01b3cc290c2103aa052fbcb8e5b33a4eefd662536f8684641f04109f1d5e69cdda6f084890286a0b410a906ad4" + } + }, + { + "Name": "getrawtransactionasync", + "Request": { + "jsonrpc": "2.0", + "method": "getrawtransaction", + "params": [ "0xc97cc05c790a844f05f582d80952c4ced3894cbe6d96a74f3e5589d741372dd4", true ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "hash": "0x99eaba3e230702d428ce6bfb4a2dceba6d4cd441f9ca1b7bfe2a418926ae40ab", + "size": 252, + "version": 0, + "nonce": 969006668, + "sender": "NikvsLcNP1jWhrFPrfS3n4spEASgdNYTG2", + "sysfee": "100000000", + "netfee": "1272390", + "validuntilblock": 2104625, + "signers": [ + { + "account": "0xe19de267a37a71734478f512b3e92c79fc3695fa", + "scopes": "CalledByEntry" + } + ], + "attributes": [], + "script": "AwAQpdToAAAADBSZA7DD0pKYj\u002Bvl8wagL2VOousWKQwUaSWqVUcSQ5qcYTuhFO\u002Bj\u002BsI928oTwAwIdHJhbnNmZXIMFDt9NxHG8Mz5sdypA9G/odiW8SOMQWJ9W1I5", + "witnesses": [ + { + "invocation": "DEAfhbQNf6EhZKodTRiwbKRw8siVctxbkBqxZn\u002Buu1h89TZFS5igkBitrHI3bF58XRZFNRVbdjVkNHqke2mqAbPM", + "verification": "DCEDqgUvvLjlszpO79ZiU2\u002BGhGQfBBCfHV5pzdpvCEiQKGoLQQqQatQ=" + } + ], + "blockhash": "0xc1ed259e394c9cd93c1e0eb1e0f144c0d10da64861a24c0084f0d98270b698f1", + "confirmations": 643, + "blocktime": 1579417249620, + "vmstate": "HALT" + } + } + }, + { + "Name": "getstorageasync", + "Request": { + "jsonrpc": "2.0", + "method": "getstorage", + "params": [ "0x8c23f196d8a1bfd103a9dcb1f9ccf0c611377d3b", "146925aa554712439a9c613ba114efa3fac23ddbca" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": "410121064c5d11a2a700" + } + }, + { + "Name": "getstorageasync", + "Request": { + "jsonrpc": "2.0", + "method": "getstorage", + "params": [ -2, "146925aa554712439a9c613ba114efa3fac23ddbca" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": "410121064c5d11a2a700" + } + }, + { + "Name": "gettransactionheightasync", + "Request": { + "jsonrpc": "2.0", + "method": "gettransactionheight", + "params": [ "0x0cfd49c48306f9027dc71585589b6456bcc53567c359fb7858eabca482186b78" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": 2226 + } + }, + { + "Name": "getnextblockvalidatorsasync", + "Request": { + "jsonrpc": "2.0", + "method": "getnextblockvalidators", + "params": [], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": [ + { + "publickey": "03aa052fbcb8e5b33a4eefd662536f8684641f04109f1d5e69cdda6f084890286a", + "votes": "0" + } + ] + } + }, + + + { + "Name": "getconnectioncountasync", + "Request": { + "jsonrpc": "2.0", + "method": "getconnectioncount", + "params": [], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": 0 + } + }, + { + "Name": "getpeersasync", + "Request": { + "jsonrpc": "2.0", + "method": "getpeers", + "params": [], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "unconnected": [ + { + "address": "::ffff:70.73.16.236", + "port": 10333 + } + ], + "bad": [], + "connected": [ + { + "address": "::ffff:139.219.106.33", + "port": 10333 + }, + { + "address": "::ffff:47.88.53.224", + "port": 10333 + } + ] + } + } + }, + { + "Name": "getversionasync", + "Request": { + "jsonrpc": "2.0", + "method": "getversion", + "params": [], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "network": 0, + "tcpport": 20333, + "nonce": 592651621, + "useragent": "/Neo:3.0.0-rc1/", + "protocol": { + "network": 0, + "validatorscount": 0, + "msperblock": 15000, + "maxvaliduntilblockincrement": 1, + "maxtraceableblocks": 1, + "addressversion": 0, + "maxtransactionsperblock": 0, + "memorypoolmaxtransactions": 0, + "initialgasdistribution": 0, + "hardforks": [ + { + "name": "Aspidochelone", + "blockheight": 0 + } + ] + } + } + } + }, + { + "Name": "sendrawtransactionasync", + "Request": { + "jsonrpc": "2.0", + "method": "sendrawtransaction", + "params": [ "ANIHn05ujtUAAAAAACYcEwAAAAAAQkEAAAEKo4e1Ppa3mJpjFDGgVt0fQKBC9gEAXQMAyBeoBAAAAAwUzViuz9M1vh6z0xHh3IAJY9/XLZ8MFAqjh7U+lreYmmMUMaBW3R9AoEL2E8AMCHRyYW5zZmVyDBSlB7dGdv/td+dUuG7NmQnwus08ukFifVtSOAFCDEDh8zgTrGUXyzVX60wBCMyajNRfzFRiEPAe8CgGQ10bA2C3fnVz68Gw+Amgn5gmvuNfYKgWQ/W68Km1bYUPlnEYKQwhA86j4vgfGvk1ItKe3k8kofC+3q1ykzkdM4gPVHXZeHjJC0GVRA14" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "hash": "0x4d47255ff5564aaa73855068c3574f8f28e2bb18c7fb7256e58ae51fab44c9bc" + } + } + }, + { + "Name": "submitblockasync", + "Request": { + "jsonrpc": "2.0", + "method": "submitblock", + "params": [ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI+JfEVZZd6cjX2qJADFSuzRR40IzeV3K1zS9Q2wqetqI6hnvVQEAAAAAAAD6lrDvowCyjK9dBALCmE1fvMuahQEAARECAB2sK3wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHKYeUuiB1BN05kD4Gc0RjMFTshpwAABUESPn/oAQABEQ==" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "hash": "0xa11c9d14748f967178fe22fdcfb829354ae6ccb86824675e147cb128f16d8171" + } + } + }, + + + { + "Name": "invokefunctionasync", + "Request": { + "jsonrpc": "2.0", + "method": "invokefunction", + "params": [ + "0x8c23f196d8a1bfd103a9dcb1f9ccf0c611377d3b", + "balanceOf", + [ + { + "type": "Hash160", + "value": "91b83e96f2a7c4fdf0c1688441ec61986c7cae26" + } + ] + ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "script": "0c1426ae7c6c9861ec418468c1f0fdc4a7f2963eb89111c00c0962616c616e63654f660c143b7d3711c6f0ccf9b1dca903d1bfa1d896f1238c41627d5b52", + "state": "HALT", + "gasconsumed": "2007570", + "stack": [ + { + "type": "Integer", + "value": "0" + } + ], + "tx": "00d1eb88136925aa554712439a9c613ba114efa3fac23ddbca00e1f50500000000269f1200000000004520200000003e0c1426ae7c6c9861ec418468c1f0fdc4a7f2963eb89111c00c0962616c616e63654f660c143b7d3711c6f0ccf9b1dca903d1bfa1d896f1238c41627d5b5201420c40794c91299bba340ea2505c777d15ca898f75bcce686461066a2b8018cc1de114a122dcdbc77b447ac7db5fb1584f1533b164fbc8f30ddf5bd6acf016a125e983290c2103aa052fbcb8e5b33a4eefd662536f8684641f04109f1d5e69cdda6f084890286a0b410a906ad4" + } + } + }, + { + "Name": "invokescriptasync", + "Request": { + "jsonrpc": "2.0", + "method": "invokescript", + "params": [ "EMMMBG5hbWUMFDt9NxHG8Mz5sdypA9G/odiW8SOMQWJ9W1IQwwwGc3ltYm9sDBQ7fTcRxvDM+bHcqQPRv6HYlvEjjEFifVtSEMMMCGRlY2ltYWxzDBQ7fTcRxvDM+bHcqQPRv6HYlvEjjEFifVtSEMMMC3RvdGFsU3VwcGx5DBQ7fTcRxvDM+bHcqQPRv6HYlvEjjEFifVtS" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "script": "EMMMBG5hbWUMFDt9NxHG8Mz5sdypA9G/odiW8SOMQWJ9W1IQwwwGc3ltYm9sDBQ7fTcRxvDM+bHcqQPRv6HYlvEjjEFifVtSEMMMCGRlY2ltYWxzDBQ7fTcRxvDM+bHcqQPRv6HYlvEjjEFifVtSEMMMC3RvdGFsU3VwcGx5DBQ7fTcRxvDM+bHcqQPRv6HYlvEjjEFifVtS", + "state": "HALT", + "gasconsumed": "5061560", + "stack": [ + { + "type": "Array", + "value": [ + { + "type": "ByteString", + "value": "dGVzdA==" + }, + { + "type": "InteropInterface" + }, + { + "type": "Integer", + "value": "1" + }, + { + "type": "Buffer", + "value": "CAwiNQw=" + }, + { + "type": "Array", + "value": [ + { + "type": "ByteString", + "value": "YmI=" + }, + { + "type": "ByteString", + "value": "Y2Mw" + } + ] + }, + { + "type": "Map", + "value": [ + { + "key": { + "type": "Integer", + "value": "2" + }, + "value": { + "type": "Integer", + "value": "12" + } + }, + { + "key": { + "type": "Integer", + "value": "0" + }, + "value": { + "type": "Integer", + "value": "24" + } + } + ] + } + ] + } + ], + "tx": "00769d16556925aa554712439a9c613ba114efa3fac23ddbca00e1f505000000009e021400000000005620200000009910c30c046e616d650c143b7d3711c6f0ccf9b1dca903d1bfa1d896f1238c41627d5b5210c30c0673796d626f6c0c143b7d3711c6f0ccf9b1dca903d1bfa1d896f1238c41627d5b5210c30c08646563696d616c730c143b7d3711c6f0ccf9b1dca903d1bfa1d896f1238c41627d5b5210c30c0b746f74616c537570706c790c143b7d3711c6f0ccf9b1dca903d1bfa1d896f1238c41627d5b5201420c40c848d0fcbf5e6a820508242ea8b7ccbeed3caefeed5db570537279c2154f7cfd8b0d8f477f37f4e6ca912935b732684d57c455dff7aa525ad4ab000931f22208290c2103aa052fbcb8e5b33a4eefd662536f8684641f04109f1d5e69cdda6f084890286a0b410a906ad4" + } + } + }, + + { + "Name": "getunclaimedgasasync", + "Request": { + "jsonrpc": "2.0", + "method": "getunclaimedgas", + "params": [ "NPvKVTGZapmFWABLsyvfreuqn73jCjJtN1" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "unclaimed": "735870007400", + "address": "NPvKVTGZapmFWABLsyvfreuqn73jCjJtN1" + } + } + }, + + { + "Name": "listpluginsasync", + "Request": { + "jsonrpc": "2.0", + "method": "listplugins", + "params": [], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": [ + { + "name": "ApplicationLogs", + "version": "3.0.0.0", + "interfaces": [ + "IPersistencePlugin" + ] + }, + { + "name": "LevelDBStore", + "version": "3.0.0.0", + "interfaces": [ + "IStoragePlugin" + ] + }, + { + "name": "RpcNep17Tracker", + "version": "3.0.0.0", + "interfaces": [ + "IPersistencePlugin" + ] + }, + { + "name": "RpcServer", + "version": "3.0.0.0", + "interfaces": [] + } + ] + } + }, + { + "Name": "validateaddressasync", + "Request": { + "jsonrpc": "2.0", + "method": "validateaddress", + "params": [ "NZs2zXSPuuv9ZF6TDGSWT1RBmE8rfGj7UW" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "address": "NZs2zXSPuuv9ZF6TDGSWT1RBmE8rfGj7UW", + "isvalid": true + } + } + }, + + + { + "Name": "closewalletasync", + "Request": { + "jsonrpc": "2.0", + "method": "closewallet", + "params": [], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": true + } + }, + { + "Name": "dumpprivkeyasync", + "Request": { + "jsonrpc": "2.0", + "method": "dumpprivkey", + "params": [ "NVVwFw6XyhtRCFQ8SpUTMdPyYt4Vd9A1XQ" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": "KyoYyZpoccbR6KZ25eLzhMTUxREwCpJzDsnuodGTKXSG8fDW9t7x" + } + }, + { + "Name": "invokescriptasync", + "Request": { + "jsonrpc": "2.0", + "id": 1, + "method": "invokescript", + "params": [ + "EMAfDAhkZWNpbWFscwwU++3+LtIiZZK2SMTal7nJzV3BpqZBYn1bUg==" + ] + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "script": "HxDDDAhkZWNpbWFscwwU++3+LtIiZZK2SMTal7nJzV3BpqZB7vQM2w==", + "state": "HALT", + "gasconsumed": "999180", + "exception": null, + "stack": [ + { + "type": "Integer", + "value": "8" + } + ] + } + } + }, + { + "Name": "invokescriptasync", + "Request": { + "jsonrpc": "2.0", + "id": 1, + "method": "invokescript", + "params": [ + "wh8MCGRlY2ltYWxzDBTPduKL0AYsSkeO41VhARMZ88+k0kFifVtS" + ] + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "script": "EBEfDAhkZWNpbWFscwwU++3+LtIiZZK2SMTal7nJzV3BpqZBYn1bUg==", + "state": "HALT", + "gasconsumed": "999180", + "exception": null, + "stack": [ + { + "type": "Integer", + "value": "8" + } + ] + } + } + }, + { + "Name": "getnewaddressasync", + "Request": { + "jsonrpc": "2.0", + "method": "getnewaddress", + "params": [], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": "NXpCs9kcDkPvfyAobNYmFg8yfRZaDopDbf" + } + }, + { + "Name": "getwalletbalanceasync", + "Request": { + "jsonrpc": "2.0", + "method": "getwalletbalance", + "params": [ "0xd2a4cff31913016155e38e474a2c06d08be276cf" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "balance": "3001101329992600" + } + } + }, + { + "Name": "getwalletunclaimedgasasync", + "Request": { + "jsonrpc": "2.0", + "method": "getwalletunclaimedgas", + "params": [], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": "735870007400" + } + }, + { + "Name": "importprivkeyasync", + "Request": { + "jsonrpc": "2.0", + "method": "importprivkey", + "params": [ "KyoYyZpoccbR6KZ25eLzhMTUxREwCpJzDsnuodGTKXSG8fDW9t7x" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "address": "NVVwFw6XyhtRCFQ8SpUTMdPyYt4Vd9A1XQ", + "haskey": true, + "label": null, + "watchonly": false + } + } + }, + { + "Name": "listaddressasync", + "Request": { + "jsonrpc": "2.0", + "method": "listaddress", + "params": [], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": [ + { + "address": "NVVwFw6XyhtRCFQ8SpUTMdPyYt4Vd9A1XQ", + "haskey": true, + "label": null, + "watchonly": false + }, + { + "address": "NZs2zXSPuuv9ZF6TDGSWT1RBmE8rfGj7UW", + "haskey": true, + "label": null, + "watchonly": false + } + ] + } + }, + { + "Name": "openwalletasync", + "Request": { + "jsonrpc": "2.0", + "method": "openwallet", + "params": [ "D:\\temp\\3.json", "1111" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": true + } + }, + { + "Name": "sendfromasync", + "Request": { + "jsonrpc": "2.0", + "method": "sendfrom", + "params": [ "0x8c23f196d8a1bfd103a9dcb1f9ccf0c611377d3b", "NVVwFw6XyhtRCFQ8SpUTMdPyYt4Vd9A1XQ", "NZs2zXSPuuv9ZF6TDGSWT1RBmE8rfGj7UW", "100.123" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "hash": "0x035facc3be1fc57da1690e3d2f8214f449d368437d8557ffabb2d408caf9ad76", + "size": 272, + "version": 0, + "nonce": 1553700339, + "sender": "NVVwFw6XyhtRCFQ8SpUTMdPyYt4Vd9A1XQ", + "sysfee": "100000000", + "netfee": "1272390", + "validuntilblock": 2105487, + "attributes": [], + "cosigners": [ + { + "account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", + "scopes": "CalledByEntry" + } + ], + "script": "A+CSx1QCAAAADBSZA7DD0pKYj+vl8wagL2VOousWKQwUaSWqVUcSQ5qcYTuhFO+j+sI928oTwAwIdHJhbnNmZXIMFDt9NxHG8Mz5sdypA9G/odiW8SOMQWJ9W1I5", + "witnesses": [ + { + "invocation": "DEDOA/QF5jYT2TCl9T94fFwAncuBhVhciISaq4fZ3WqGarEoT/0iDo3RIwGjfRW0mm/SV3nAVGEQeZInLqKQ98HX", + "verification": "DCEDqgUvvLjlszpO79ZiU2+GhGQfBBCfHV5pzdpvCEiQKGoLQQqQatQ=" + } + ] + } + } + }, + { + "Name": "sendmanyasync", + "Request": { + "jsonrpc": "2.0", + "method": "sendmany", + "params": [ + "NVVwFw6XyhtRCFQ8SpUTMdPyYt4Vd9A1XQ", + [ + { + "asset": "0x9bde8f209c88dd0e7ca3bf0af0f476cdd8207789", + "value": "10", + "address": "NVVwFw6XyhtRCFQ8SpUTMdPyYt4Vd9A1XQ" + }, + { + "asset": "0x8c23f196d8a1bfd103a9dcb1f9ccf0c611377d3b", + "value": "1.2345", + "address": "NZs2zXSPuuv9ZF6TDGSWT1RBmE8rfGj7UW" + } + ] + ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "hash": "0x542e64a9048bbe1ee565b840c41ccf9b5a1ef11f52e5a6858a523938a20c53ec", + "size": 483, + "version": 0, + "nonce": 34429660, + "sender": "NUMK37TV9yYKbJr1Gufh74nZiM623eBLqX", + "sysfee": "100000000", + "netfee": "2483780", + "validuntilblock": 2105494, + "attributes": [], + "cosigners": [ + { + "account": "0x36d6200fb4c9737c7b552d2b5530ab43605c5869", + "scopes": "CalledByEntry" + }, + { + "account": "0x9a55ca1006e2c359bbc8b9b0de6458abdff98b5c", + "scopes": "CalledByEntry" + } + ], + "script": "GgwUaSWqVUcSQ5qcYTuhFO+j+sI928oMFGlYXGBDqzBVKy1Ve3xzybQPINY2E8AMCHRyYW5zZmVyDBSJdyDYzXb08Aq/o3wO3YicII/em0FifVtSOQKQslsHDBSZA7DD0pKYj+vl8wagL2VOousWKQwUXIv536tYZN6wuci7WcPiBhDKVZoTwAwIdHJhbnNmZXIMFDt9NxHG8Mz5sdypA9G/odiW8SOMQWJ9W1I5", + "witnesses": [ + { + "invocation": "DECOdTEWg1WkuHN0GNV67kwxeuKADyC6TO59vTaU5dK6K1BGt8+EM6L3TdMga4qB2J+Meez8eYwZkSSRubkuvfr9", + "verification": "DCECeiS9CyBqFJwNKzonOs/yzajOraFep4IqFJVxBe6TesULQQqQatQ=" + }, + { + "invocation": "DEB1Laj6lvjoBJLTgE/RdvbJiXOmaKp6eNWDJt+p8kxnW6jbeKoaBRZWfUflqrKV7mZEE2JHA5MxrL5TkRIvsL5K", + "verification": "DCECkXL4gxd936eGEDt3KWfIuAsBsQcfyyBUcS8ggF6lZnwLQQqQatQ=" + } + ] + } + } + }, + { + "Name": "sendtoaddressasync", + "Request": { + "jsonrpc": "2.0", + "method": "sendtoaddress", + "params": [ "0x8c23f196d8a1bfd103a9dcb1f9ccf0c611377d3b", "NVVwFw6XyhtRCFQ8SpUTMdPyYt4Vd9A1XQ", "100.123" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "hash": "0xee5fc3f57d9f9bc9695c88ecc504444aab622b1680b1cb0848d5b6e39e7fd118", + "size": 381, + "version": 0, + "nonce": 330056065, + "sender": "NUMK37TV9yYKbJr1Gufh74nZiM623eBLqX", + "sysfee": "100000000", + "netfee": "2381780", + "validuntilblock": 2105500, + "attributes": [], + "cosigners": [ + { + "account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", + "scopes": "CalledByEntry" + } + ], + "script": "A+CSx1QCAAAADBRpJapVRxJDmpxhO6EU76P6wj3bygwUaSWqVUcSQ5qcYTuhFO+j+sI928oTwAwIdHJhbnNmZXIMFDt9NxHG8Mz5sdypA9G/odiW8SOMQWJ9W1I5", + "witnesses": [ + { + "invocation": "DECruSKmQKs0Y2cxplKROjPx8HKiyiYrrPn7zaV9zwHPumLzFc8DvgIo2JxmTnJsORyygN/su8mTmSLLb3PesBvY", + "verification": "DCECkXL4gxd936eGEDt3KWfIuAsBsQcfyyBUcS8ggF6lZnwLQQqQatQ=" + }, + { + "invocation": "DECS5npCs5PwsPUAQ01KyHyCev27dt3kDdT1Vi0K8PwnEoSlxYTOGGQCAwaiNEXSyBdBmT6unhZydmFnkezD7qzW", + "verification": "DCEDqgUvvLjlszpO79ZiU2+GhGQfBBCfHV5pzdpvCEiQKGoLQQqQatQ=" + } + ] + } + } + }, + + + { + "Name": "getapplicationlogasync", + "Request": { + "jsonrpc": "2.0", + "method": "getapplicationlog", + "params": [ "0x6ea186fe714b8168ede3b78461db8945c06d867da649852352dbe7cbf1ba3724" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "blockhash": "0x6ea186fe714b8168ede3b78461db8945c06d867da649852352dbe7cbf1ba3724", + "executions": [ + { + "trigger": "OnPersist", + "vmstate": "HALT", + "gasconsumed": "2031260", + "exception": null, + "stack": [], + "notifications": [ + { + "contract": "0x668e0c1f9d7b70a99dd9e06eadd4c784d641afbc", + "eventname": "Transfer", + "state": { + "type": "Array", + "value": [ + { + "type": "ByteString", + "value": "CqOHtT6Wt5iaYxQxoFbdH0CgQvY=" + }, + { + "type": "Any" + }, + { + "type": "Integer", + "value": "18083410" + } + ] + } + }, + { + "contract": "0x668e0c1f9d7b70a99dd9e06eadd4c784d641afbc", + "eventname": "Transfer", + "state": { + "type": "Array", + "value": [ + { + "type": "Any" + }, + { + "type": "ByteString", + "value": "z6LDQN4w1uEMToIZiPSxToNRPog=" + }, + { + "type": "Integer", + "value": "1252390" + } + ] + } + } + ] + }, + { + "trigger": "PostPersist", + "vmstate": "HALT", + "gasconsumed": "2031260", + "exception": null, + "stack": [], + "notifications": [ + { + "contract": "0x668e0c1f9d7b70a99dd9e06eadd4c784d641afbc", + "eventname": "Transfer", + "state": { + "type": "Array", + "value": [ + { + "type": "Any" + }, + { + "type": "ByteString", + "value": "z6LDQN4w1uEMToIZiPSxToNRPog=" + }, + { + "type": "Integer", + "value": "50000000" + } + ] + } + } + ] + } + ] + } + } + }, + { + "Name": "getapplicationlogasync_triggertype", + "Request": { + "jsonrpc": "2.0", + "method": "getapplicationlog", + "params": [ "0x6ea186fe714b8168ede3b78461db8945c06d867da649852352dbe7cbf1ba3724", "OnPersist" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "blockhash": "0x6ea186fe714b8168ede3b78461db8945c06d867da649852352dbe7cbf1ba3724", + "executions": [ + { + "trigger": "OnPersist", + "vmstate": "HALT", + "gasconsumed": "2031260", + "exception": null, + "stack": [], + "notifications": [ + { + "contract": "0x668e0c1f9d7b70a99dd9e06eadd4c784d641afbc", + "eventname": "Transfer", + "state": { + "type": "Array", + "value": [ + { + "type": "ByteString", + "value": "CqOHtT6Wt5iaYxQxoFbdH0CgQvY=" + }, + { + "type": "Any" + }, + { + "type": "Integer", + "value": "18083410" + } + ] + } + }, + { + "contract": "0x668e0c1f9d7b70a99dd9e06eadd4c784d641afbc", + "eventname": "Transfer", + "state": { + "type": "Array", + "value": [ + { + "type": "Any" + }, + { + "type": "ByteString", + "value": "z6LDQN4w1uEMToIZiPSxToNRPog=" + }, + { + "type": "Integer", + "value": "1252390" + } + ] + } + } + ] + } + ] + } + } + }, + { + "Name": "getnep17transfersasync", + "Request": { + "jsonrpc": "2.0", + "method": "getnep17transfers", + "params": [ "NVVwFw6XyhtRCFQ8SpUTMdPyYt4Vd9A1XQ", 0, 1868595301000 ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "sent": [ + { + "timestamp": 1579250114541, + "assethash": "0x8c23f196d8a1bfd103a9dcb1f9ccf0c611377d3b", + "transferaddress": "NVVwFw6XyhtRCFQ8SpUTMdPyYt4Vd9A1XQ", + "amount": "1000000000", + "blockindex": 603, + "transfernotifyindex": 0, + "txhash": "0x5e177b8d1dc33e9103c0cfd42f6dbf4efbe43029e2d6a18ea5ba0cb8437056b3" + }, + { + "timestamp": 1579406581635, + "assethash": "0x8c23f196d8a1bfd103a9dcb1f9ccf0c611377d3b", + "transferaddress": "NUMK37TV9yYKbJr1Gufh74nZiM623eBLqX", + "amount": "1000000000", + "blockindex": 1525, + "transfernotifyindex": 0, + "txhash": "0xc9c618b48972b240e0058d97b8d79b807ad51015418c84012765298526aeb77d" + } + ], + "received": [ + { + "timestamp": 1579250114541, + "assethash": "0x8c23f196d8a1bfd103a9dcb1f9ccf0c611377d3b", + "transferaddress": "NVVwFw6XyhtRCFQ8SpUTMdPyYt4Vd9A1XQ", + "amount": "1000000000", + "blockindex": 603, + "transfernotifyindex": 0, + "txhash": "0x5e177b8d1dc33e9103c0cfd42f6dbf4efbe43029e2d6a18ea5ba0cb8437056b3" + } + ], + "address": "NVVwFw6XyhtRCFQ8SpUTMdPyYt4Vd9A1XQ" + } + } + }, + { + "Name": "getnep17transfersasync_with_null_transferaddress", + "Request": { + "jsonrpc": "2.0", + "method": "getnep17transfers", + "params": [ "Ncb7jVsYWBt1q5T5k3ZTP8bn5eK4DuanLd", 0, 1868595301000 ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "sent": [ + { + "timestamp": 1579250114541, + "assethash": "0x8c23f196d8a1bfd103a9dcb1f9ccf0c611377d3b", + "transferaddress": null, + "amount": "1000000000", + "blockindex": 603, + "transfernotifyindex": 0, + "txhash": "0x5e177b8d1dc33e9103c0cfd42f6dbf4efbe43029e2d6a18ea5ba0cb8437056b3" + }, + { + "timestamp": 1579406581635, + "assethash": "0x8c23f196d8a1bfd103a9dcb1f9ccf0c611377d3b", + "transferaddress": "Ncb7jVsYWBt1q5T5k3ZTP8bn5eK4DuanLd", + "amount": "1000000000", + "blockindex": 1525, + "transfernotifyindex": 0, + "txhash": "0xc9c618b48972b240e0058d97b8d79b807ad51015418c84012765298526aeb77d" + } + ], + "received": [ + { + "timestamp": 1579250114541, + "assethash": "0x8c23f196d8a1bfd103a9dcb1f9ccf0c611377d3b", + "transferaddress": null, + "amount": "1000000000", + "blockindex": 603, + "transfernotifyindex": 0, + "txhash": "0x5e177b8d1dc33e9103c0cfd42f6dbf4efbe43029e2d6a18ea5ba0cb8437056b3" + } + ], + "address": "Ncb7jVsYWBt1q5T5k3ZTP8bn5eK4DuanLd" + } + } + }, + { + "Name": "getnep17balancesasync", + "Request": { + "jsonrpc": "2.0", + "method": "getnep17balances", + "params": [ "NVVwFw6XyhtRCFQ8SpUTMdPyYt4Vd9A1XQ" ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "balance": [ + { + "assethash": "0x8c23f196d8a1bfd103a9dcb1f9ccf0c611377d3b", + "amount": "719978585420", + "lastupdatedblock": 3101 + }, + { + "assethash": "0x9bde8f209c88dd0e7ca3bf0af0f476cdd8207789", + "amount": "89999810", + "lastupdatedblock": 3096 + } + ], + "address": "NVVwFw6XyhtRCFQ8SpUTMdPyYt4Vd9A1XQ" + } + } + } +] diff --git a/tests/Neo.Network.RPC.Tests/TestUtils.cs b/tests/Neo.Network.RPC.Tests/TestUtils.cs new file mode 100644 index 0000000000..068de3388b --- /dev/null +++ b/tests/Neo.Network.RPC.Tests/TestUtils.cs @@ -0,0 +1,95 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// TestUtils.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.Json; +using Neo.Network.P2P.Payloads; +using Neo.Network.RPC.Models; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Neo.Network.RPC.Tests +{ + internal static class TestUtils + { + public readonly static List RpcTestCases = ((JArray)JToken.Parse(File.ReadAllText("RpcTestCases.json"))).Select(p => RpcTestCase.FromJson((JObject)p)).ToList(); + + public static Block GetBlock(int txCount) + { + return new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + Witness = new Witness + { + InvocationScript = new byte[0], + VerificationScript = new byte[0] + } + }, + Transactions = Enumerable.Range(0, txCount).Select(p => GetTransaction()).ToArray() + }; + } + + public static Header GetHeader() + { + return GetBlock(0).Header; + } + + public static Transaction GetTransaction() + { + return new Transaction + { + Script = new byte[1], + Signers = new Signer[] { new Signer { Account = UInt160.Zero } }, + Attributes = new TransactionAttribute[0], + Witnesses = new Witness[] + { + new Witness + { + InvocationScript = new byte[0], + VerificationScript = new byte[0] + } + } + }; + } + } + + internal class RpcTestCase + { + public string Name { get; set; } + public RpcRequest Request { get; set; } + public RpcResponse Response { get; set; } + + public JObject ToJson() + { + return new JObject + { + ["Name"] = Name, + ["Request"] = Request.ToJson(), + ["Response"] = Response.ToJson(), + }; + } + + public static RpcTestCase FromJson(JObject json) + { + return new RpcTestCase + { + Name = json["Name"].AsString(), + Request = RpcRequest.FromJson((JObject)json["Request"]), + Response = RpcResponse.FromJson((JObject)json["Response"]), + }; + } + + } +} diff --git a/tests/Neo.Network.RPC.Tests/UT_ContractClient.cs b/tests/Neo.Network.RPC.Tests/UT_ContractClient.cs new file mode 100644 index 0000000000..c3cf226a3e --- /dev/null +++ b/tests/Neo.Network.RPC.Tests/UT_ContractClient.cs @@ -0,0 +1,81 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_ContractClient.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.VisualStudio.TestTools.UnitTesting; +using Moq; +using Neo.SmartContract; +using Neo.SmartContract.Manifest; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.Wallets; +using System.Threading.Tasks; + +namespace Neo.Network.RPC.Tests +{ + [TestClass] + public class UT_ContractClient + { + Mock rpcClientMock; + KeyPair keyPair1; + UInt160 sender; + + [TestInitialize] + public void TestSetup() + { + keyPair1 = new KeyPair(Wallet.GetPrivateKeyFromWIF("KyXwTh1hB76RRMquSvnxZrJzQx7h9nQP2PCRL38v6VDb5ip3nf1p")); + sender = Contract.CreateSignatureRedeemScript(keyPair1.PublicKey).ToScriptHash(); + rpcClientMock = UT_TransactionManager.MockRpcClient(sender, new byte[0]); + } + + [TestMethod] + public async Task TestInvoke() + { + byte[] testScript = NativeContract.GAS.Hash.MakeScript("balanceOf", UInt160.Zero); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.ByteArray, Value = "00e057eb481b".HexToBytes() }); + + ContractClient contractClient = new ContractClient(rpcClientMock.Object); + var result = await contractClient.TestInvokeAsync(NativeContract.GAS.Hash, "balanceOf", UInt160.Zero); + + Assert.AreEqual(30000000000000L, (long)result.Stack[0].GetInteger()); + } + + [TestMethod] + public async Task TestDeployContract() + { + byte[] script; + var manifest = new ContractManifest() + { + Permissions = new[] { ContractPermission.DefaultPermission }, + Abi = new ContractAbi() + { + Events = new ContractEventDescriptor[0], + Methods = new ContractMethodDescriptor[0] + }, + Groups = new ContractGroup[0], + Trusts = WildcardContainer.Create(), + SupportedStandards = new string[] { "NEP-10" }, + Extra = null, + }; + using (ScriptBuilder sb = new ScriptBuilder()) + { + sb.EmitDynamicCall(NativeContract.ContractManagement.Hash, "deploy", new byte[1], manifest.ToJson().ToString()); + script = sb.ToArray(); + } + + UT_TransactionManager.MockInvokeScript(rpcClientMock, script, new ContractParameter()); + + ContractClient contractClient = new ContractClient(rpcClientMock.Object); + var result = await contractClient.CreateDeployContractTxAsync(new byte[1], manifest, keyPair1); + + Assert.IsNotNull(result); + } + } +} diff --git a/tests/Neo.Network.RPC.Tests/UT_Nep17API.cs b/tests/Neo.Network.RPC.Tests/UT_Nep17API.cs new file mode 100644 index 0000000000..3bfb4e87ff --- /dev/null +++ b/tests/Neo.Network.RPC.Tests/UT_Nep17API.cs @@ -0,0 +1,168 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_Nep17API.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.VisualStudio.TestTools.UnitTesting; +using Moq; +using Neo.Json; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.Wallets; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using static Neo.Helper; + +namespace Neo.Network.RPC.Tests +{ + [TestClass] + public class UT_Nep17API + { + Mock rpcClientMock; + KeyPair keyPair1; + UInt160 sender; + Nep17API nep17API; + + [TestInitialize] + public void TestSetup() + { + keyPair1 = new KeyPair(Wallet.GetPrivateKeyFromWIF("KyXwTh1hB76RRMquSvnxZrJzQx7h9nQP2PCRL38v6VDb5ip3nf1p")); + sender = Contract.CreateSignatureRedeemScript(keyPair1.PublicKey).ToScriptHash(); + rpcClientMock = UT_TransactionManager.MockRpcClient(sender, new byte[0]); + nep17API = new Nep17API(rpcClientMock.Object); + } + + [TestMethod] + public async Task TestBalanceOf() + { + byte[] testScript = NativeContract.GAS.Hash.MakeScript("balanceOf", UInt160.Zero); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(10000) }); + + var balance = await nep17API.BalanceOfAsync(NativeContract.GAS.Hash, UInt160.Zero); + Assert.AreEqual(10000, (int)balance); + } + + [TestMethod] + public async Task TestGetSymbol() + { + byte[] testScript = NativeContract.GAS.Hash.MakeScript("symbol"); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.String, Value = NativeContract.GAS.Symbol }); + + var result = await nep17API.SymbolAsync(NativeContract.GAS.Hash); + Assert.AreEqual(NativeContract.GAS.Symbol, result); + } + + [TestMethod] + public async Task TestGetDecimals() + { + byte[] testScript = NativeContract.GAS.Hash.MakeScript("decimals"); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(NativeContract.GAS.Decimals) }); + + var result = await nep17API.DecimalsAsync(NativeContract.GAS.Hash); + Assert.AreEqual(NativeContract.GAS.Decimals, result); + } + + [TestMethod] + public async Task TestGetTotalSupply() + { + byte[] testScript = NativeContract.GAS.Hash.MakeScript("totalSupply"); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(1_00000000) }); + + var result = await nep17API.TotalSupplyAsync(NativeContract.GAS.Hash); + Assert.AreEqual(1_00000000, (int)result); + } + + [TestMethod] + public async Task TestGetTokenInfo() + { + UInt160 scriptHash = NativeContract.GAS.Hash; + byte[] testScript = Concat( + scriptHash.MakeScript("symbol"), + scriptHash.MakeScript("decimals"), + scriptHash.MakeScript("totalSupply")); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, + new ContractParameter { Type = ContractParameterType.String, Value = NativeContract.GAS.Symbol }, + new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(NativeContract.GAS.Decimals) }, + new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(1_00000000) }); + + scriptHash = NativeContract.NEO.Hash; + testScript = Concat( + scriptHash.MakeScript("symbol"), + scriptHash.MakeScript("decimals"), + scriptHash.MakeScript("totalSupply")); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, + new ContractParameter { Type = ContractParameterType.String, Value = NativeContract.NEO.Symbol }, + new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(NativeContract.NEO.Decimals) }, + new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(1_00000000) }); + + var tests = TestUtils.RpcTestCases.Where(p => p.Name == "getcontractstateasync"); + var haveGasTokenUT = false; + var haveNeoTokenUT = false; + foreach (var test in tests) + { + rpcClientMock.Setup(p => p.RpcSendAsync("getcontractstate", It.Is(u => true))) + .ReturnsAsync(test.Response.Result) + .Verifiable(); + if (test.Request.Params[0].AsString() == NativeContract.GAS.Hash.ToString() || test.Request.Params[0].AsString().Equals(NativeContract.GAS.Name, System.StringComparison.OrdinalIgnoreCase)) + { + var result = await nep17API.GetTokenInfoAsync(NativeContract.GAS.Name.ToLower()); + Assert.AreEqual(NativeContract.GAS.Symbol, result.Symbol); + Assert.AreEqual(8, result.Decimals); + Assert.AreEqual(1_00000000, (int)result.TotalSupply); + Assert.AreEqual("GasToken", result.Name); + + result = await nep17API.GetTokenInfoAsync(NativeContract.GAS.Hash); + Assert.AreEqual(NativeContract.GAS.Symbol, result.Symbol); + Assert.AreEqual(8, result.Decimals); + Assert.AreEqual(1_00000000, (int)result.TotalSupply); + Assert.AreEqual("GasToken", result.Name); + haveGasTokenUT = true; + } + else if (test.Request.Params[0].AsString() == NativeContract.NEO.Hash.ToString() || test.Request.Params[0].AsString().Equals(NativeContract.NEO.Name, System.StringComparison.OrdinalIgnoreCase)) + { + var result = await nep17API.GetTokenInfoAsync(NativeContract.NEO.Name.ToLower()); + Assert.AreEqual(NativeContract.NEO.Symbol, result.Symbol); + Assert.AreEqual(0, result.Decimals); + Assert.AreEqual(1_00000000, (int)result.TotalSupply); + Assert.AreEqual("NeoToken", result.Name); + + result = await nep17API.GetTokenInfoAsync(NativeContract.NEO.Hash); + Assert.AreEqual(NativeContract.NEO.Symbol, result.Symbol); + Assert.AreEqual(0, result.Decimals); + Assert.AreEqual(1_00000000, (int)result.TotalSupply); + Assert.AreEqual("NeoToken", result.Name); + haveNeoTokenUT = true; + } + } + Assert.IsTrue(haveGasTokenUT && haveNeoTokenUT); //Update RpcTestCases.json + } + + [TestMethod] + public async Task TestTransfer() + { + byte[] testScript = NativeContract.GAS.Hash.MakeScript("transfer", sender, UInt160.Zero, new BigInteger(1_00000000), null) + .Concat(new[] { (byte)OpCode.ASSERT }) + .ToArray(); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter()); + + var client = rpcClientMock.Object; + var result = await nep17API.CreateTransferTxAsync(NativeContract.GAS.Hash, keyPair1, UInt160.Zero, new BigInteger(1_00000000), null, true); + + testScript = NativeContract.GAS.Hash.MakeScript("transfer", sender, UInt160.Zero, new BigInteger(1_00000000), string.Empty) + .Concat(new[] { (byte)OpCode.ASSERT }) + .ToArray(); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter()); + + result = await nep17API.CreateTransferTxAsync(NativeContract.GAS.Hash, keyPair1, UInt160.Zero, new BigInteger(1_00000000), string.Empty, true); + Assert.IsNotNull(result); + } + } +} diff --git a/tests/Neo.Network.RPC.Tests/UT_PolicyAPI.cs b/tests/Neo.Network.RPC.Tests/UT_PolicyAPI.cs new file mode 100644 index 0000000000..7defa163ad --- /dev/null +++ b/tests/Neo.Network.RPC.Tests/UT_PolicyAPI.cs @@ -0,0 +1,79 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_PolicyAPI.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.VisualStudio.TestTools.UnitTesting; +using Moq; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.Wallets; +using System.Numerics; +using System.Threading.Tasks; + +namespace Neo.Network.RPC.Tests +{ + [TestClass] + public class UT_PolicyAPI + { + Mock rpcClientMock; + KeyPair keyPair1; + UInt160 sender; + PolicyAPI policyAPI; + + [TestInitialize] + public void TestSetup() + { + keyPair1 = new KeyPair(Wallet.GetPrivateKeyFromWIF("KyXwTh1hB76RRMquSvnxZrJzQx7h9nQP2PCRL38v6VDb5ip3nf1p")); + sender = Contract.CreateSignatureRedeemScript(keyPair1.PublicKey).ToScriptHash(); + rpcClientMock = UT_TransactionManager.MockRpcClient(sender, new byte[0]); + policyAPI = new PolicyAPI(rpcClientMock.Object); + } + + [TestMethod] + public async Task TestGetExecFeeFactor() + { + byte[] testScript = NativeContract.Policy.Hash.MakeScript("getExecFeeFactor"); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(30) }); + + var result = await policyAPI.GetExecFeeFactorAsync(); + Assert.AreEqual(30u, result); + } + + [TestMethod] + public async Task TestGetStoragePrice() + { + byte[] testScript = NativeContract.Policy.Hash.MakeScript("getStoragePrice"); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(100000) }); + + var result = await policyAPI.GetStoragePriceAsync(); + Assert.AreEqual(100000u, result); + } + + [TestMethod] + public async Task TestGetFeePerByte() + { + byte[] testScript = NativeContract.Policy.Hash.MakeScript("getFeePerByte"); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(1000) }); + + var result = await policyAPI.GetFeePerByteAsync(); + Assert.AreEqual(1000L, result); + } + + [TestMethod] + public async Task TestIsBlocked() + { + byte[] testScript = NativeContract.Policy.Hash.MakeScript("isBlocked", UInt160.Zero); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Boolean, Value = true }); + var result = await policyAPI.IsBlockedAsync(UInt160.Zero); + Assert.AreEqual(true, result); + } + } +} diff --git a/tests/Neo.Network.RPC.Tests/UT_RpcClient.cs b/tests/Neo.Network.RPC.Tests/UT_RpcClient.cs new file mode 100644 index 0000000000..4af0f557e3 --- /dev/null +++ b/tests/Neo.Network.RPC.Tests/UT_RpcClient.cs @@ -0,0 +1,525 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_RpcClient.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 FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Moq.Protected; +using Neo.IO; +using Neo.Json; +using Neo.Network.P2P.Payloads; +using Neo.Network.RPC.Models; +using Neo.SmartContract; +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Neo.Network.RPC.Tests +{ + [TestClass] + public class UT_RpcClient + { + RpcClient rpc; + Mock handlerMock; + + [TestInitialize] + public void TestSetup() + { + handlerMock = new Mock(MockBehavior.Strict); + + // use real http client with mocked handler here + var httpClient = new HttpClient(handlerMock.Object); + rpc = new RpcClient(httpClient, new Uri("http://seed1.neo.org:10331"), null); + foreach (var test in TestUtils.RpcTestCases) + { + MockResponse(test.Request, test.Response); + } + } + + private void MockResponse(RpcRequest request, RpcResponse response) + { + handlerMock.Protected() + // Setup the PROTECTED method to mock + .Setup>( + "SendAsync", + ItExpr.Is(p => p.Content.ReadAsStringAsync().Result == request.ToJson().ToString()), + ItExpr.IsAny() + ) + // prepare the expected response of the mocked http call + .ReturnsAsync(new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(response.ToJson().ToString()), + }) + .Verifiable(); + } + + [TestMethod] + public async Task TestErrorResponse() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == (nameof(rpc.SendRawTransactionAsync) + "error").ToLower()); + try + { + var result = await rpc.SendRawTransactionAsync(Convert.FromBase64String(test.Request.Params[0].AsString()).AsSerializable()); + } + catch (RpcException ex) + { + Assert.AreEqual(-500, ex.HResult); + Assert.AreEqual("InsufficientFunds", ex.Message); + } + } + + [TestMethod] + public async Task TestNoThrowErrorResponse() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == (nameof(rpc.SendRawTransactionAsync) + "error").ToLower()); + handlerMock = new Mock(MockBehavior.Strict); + handlerMock.Protected() + // Setup the PROTECTED method to mock + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + // prepare the expected response of the mocked http call + .ReturnsAsync(new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(test.Response.ToJson().ToString()), + }) + .Verifiable(); + + var httpClient = new HttpClient(handlerMock.Object); + var client = new RpcClient(httpClient, new Uri("http://seed1.neo.org:10331"), null); + var response = await client.SendAsync(test.Request, false); + + Assert.IsNull(response.Result); + Assert.IsNotNull(response.Error); + Assert.AreEqual(-500, response.Error.Code); + Assert.AreEqual("InsufficientFunds", response.Error.Message); + } + + [TestMethod] + public void TestConstructorByUrlAndDispose() + { + //dummy url for test + var client = new RpcClient(new Uri("http://www.xxx.yyy")); + Action action = () => client.Dispose(); + action.Should().NotThrow(); + } + + [TestMethod] + public void TestConstructorWithBasicAuth() + { + var client = new RpcClient(new Uri("http://www.xxx.yyy"), "krain", "123456"); + client.Dispose(); + } + + #region Blockchain + + [TestMethod] + public async Task TestGetBestBlockHash() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetBestBlockHashAsync).ToLower()); + var result = await rpc.GetBestBlockHashAsync(); + Assert.AreEqual(test.Response.Result.AsString(), result); + } + + [TestMethod] + public async Task TestGetBlockHex() + { + var tests = TestUtils.RpcTestCases.Where(p => p.Name == nameof(rpc.GetBlockHexAsync).ToLower()); + foreach (var test in tests) + { + var result = await rpc.GetBlockHexAsync(test.Request.Params[0].AsString()); + Assert.AreEqual(test.Response.Result.AsString(), result); + } + } + + [TestMethod] + public async Task TestGetBlock() + { + var tests = TestUtils.RpcTestCases.Where(p => p.Name == nameof(rpc.GetBlockAsync).ToLower()); + foreach (var test in tests) + { + var result = await rpc.GetBlockAsync(test.Request.Params[0].AsString()); + Assert.AreEqual(test.Response.Result.AsString(), result.ToJson(rpc.protocolSettings).ToString()); + } + } + + [TestMethod] + public async Task TestGetBlockHeaderCount() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetBlockHeaderCountAsync).ToLower()); + var result = await rpc.GetBlockHeaderCountAsync(); + Assert.AreEqual(test.Response.Result.AsString(), result.ToString()); + } + + [TestMethod] + public async Task TestGetBlockCount() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetBlockCountAsync).ToLower()); + var result = await rpc.GetBlockCountAsync(); + Assert.AreEqual(test.Response.Result.AsString(), result.ToString()); + } + + [TestMethod] + public async Task TestGetBlockHash() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetBlockHashAsync).ToLower()); + var result = await rpc.GetBlockHashAsync((uint)test.Request.Params[0].AsNumber()); + Assert.AreEqual(test.Response.Result.AsString(), result.ToString()); + } + + [TestMethod] + public async Task TestGetBlockHeaderHex() + { + var tests = TestUtils.RpcTestCases.Where(p => p.Name == nameof(rpc.GetBlockHeaderHexAsync).ToLower()); + foreach (var test in tests) + { + var result = await rpc.GetBlockHeaderHexAsync(test.Request.Params[0].AsString()); + Assert.AreEqual(test.Response.Result.AsString(), result); + } + } + + [TestMethod] + public async Task TestGetBlockHeader() + { + var tests = TestUtils.RpcTestCases.Where(p => p.Name == nameof(rpc.GetBlockHeaderAsync).ToLower()); + foreach (var test in tests) + { + var result = await rpc.GetBlockHeaderAsync(test.Request.Params[0].AsString()); + Assert.AreEqual(test.Response.Result.ToString(), result.ToJson(rpc.protocolSettings).ToString()); + } + } + + [TestMethod] + public async Task TestGetCommittee() + { + var tests = TestUtils.RpcTestCases.Where(p => p.Name == nameof(rpc.GetCommitteeAsync).ToLower()); + foreach (var test in tests) + { + var result = await rpc.GetCommitteeAsync(); + Assert.AreEqual(test.Response.Result.ToString(), ((JArray)result.Select(p => (JToken)p).ToArray()).ToString()); + } + } + + [TestMethod] + public async Task TestGetContractState() + { + var tests = TestUtils.RpcTestCases.Where(p => p.Name == nameof(rpc.GetContractStateAsync).ToLower()); + foreach (var test in tests) + { + var result = await rpc.GetContractStateAsync(test.Request.Params[0].AsString()); + Assert.AreEqual(test.Response.Result.ToString(), result.ToJson().ToString()); + } + } + + [TestMethod] + public async Task TestGetNativeContracts() + { + var tests = TestUtils.RpcTestCases.Where(p => p.Name == nameof(rpc.GetNativeContractsAsync).ToLower()); + foreach (var test in tests) + { + var result = await rpc.GetNativeContractsAsync(); + Assert.AreEqual(test.Response.Result.ToString(), ((JArray)result.Select(p => p.ToJson()).ToArray()).ToString()); + } + } + + [TestMethod] + public async Task TestGetRawMempool() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetRawMempoolAsync).ToLower()); + var result = await rpc.GetRawMempoolAsync(); + Assert.AreEqual(test.Response.Result.ToString(), ((JArray)result.Select(p => (JToken)p).ToArray()).ToString()); + } + + [TestMethod] + public async Task TestGetRawMempoolBoth() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetRawMempoolBothAsync).ToLower()); + var result = await rpc.GetRawMempoolBothAsync(); + Assert.AreEqual(test.Response.Result.ToString(), result.ToJson().ToString()); + } + + [TestMethod] + public async Task TestGetRawTransactionHex() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetRawTransactionHexAsync).ToLower()); + var result = await rpc.GetRawTransactionHexAsync(test.Request.Params[0].AsString()); + Assert.AreEqual(test.Response.Result.AsString(), result); + } + + [TestMethod] + public async Task TestGetRawTransaction() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetRawTransactionAsync).ToLower()); + var result = await rpc.GetRawTransactionAsync(test.Request.Params[0].AsString()); + Assert.AreEqual(test.Response.Result.ToString(), result.ToJson(rpc.protocolSettings).ToString()); + } + + [TestMethod] + public async Task TestGetStorage() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetStorageAsync).ToLower()); + var result = await rpc.GetStorageAsync(test.Request.Params[0].AsString(), test.Request.Params[1].AsString()); + Assert.AreEqual(test.Response.Result.AsString(), result); + } + + [TestMethod] + public async Task TestGetTransactionHeight() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetTransactionHeightAsync).ToLower()); + var result = await rpc.GetTransactionHeightAsync(test.Request.Params[0].AsString()); + Assert.AreEqual(test.Response.Result.ToString(), result.ToString()); + } + + [TestMethod] + public async Task TestGetNextBlockValidators() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetNextBlockValidatorsAsync).ToLower()); + var result = await rpc.GetNextBlockValidatorsAsync(); + Assert.AreEqual(test.Response.Result.ToString(), ((JArray)result.Select(p => p.ToJson()).ToArray()).ToString()); + } + + #endregion Blockchain + + #region Node + + [TestMethod] + public async Task TestGetConnectionCount() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetConnectionCountAsync).ToLower()); + var result = await rpc.GetConnectionCountAsync(); + Assert.AreEqual(test.Response.Result.ToString(), result.ToString()); + } + + [TestMethod] + public async Task TestGetPeers() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetPeersAsync).ToLower()); + var result = await rpc.GetPeersAsync(); + Assert.AreEqual(test.Response.Result.ToString(), result.ToJson().ToString()); + } + + [TestMethod] + public async Task TestGetVersion() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetVersionAsync).ToLower()); + var result = await rpc.GetVersionAsync(); + Assert.AreEqual(test.Response.Result.ToString(), result.ToJson().ToString()); + } + + [TestMethod] + public async Task TestSendRawTransaction() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.SendRawTransactionAsync).ToLower()); + var result = await rpc.SendRawTransactionAsync(Convert.FromBase64String(test.Request.Params[0].AsString()).AsSerializable()); + Assert.AreEqual(test.Response.Result["hash"].AsString(), result.ToString()); + } + + [TestMethod] + public async Task TestSubmitBlock() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.SubmitBlockAsync).ToLower()); + var result = await rpc.SubmitBlockAsync(Convert.FromBase64String(test.Request.Params[0].AsString())); + Assert.AreEqual(test.Response.Result["hash"].AsString(), result.ToString()); + } + + #endregion Node + + #region SmartContract + + [TestMethod] + public async Task TestInvokeFunction() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.InvokeFunctionAsync).ToLower()); + var result = await rpc.InvokeFunctionAsync(test.Request.Params[0].AsString(), test.Request.Params[1].AsString(), + ((JArray)test.Request.Params[2]).Select(p => RpcStack.FromJson((JObject)p)).ToArray()); + Assert.AreEqual(test.Response.Result.ToString(), result.ToJson().ToString()); + + // TODO test verify method + } + + [TestMethod] + public async Task TestInvokeScript() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.InvokeScriptAsync).ToLower()); + var result = await rpc.InvokeScriptAsync(Convert.FromBase64String(test.Request.Params[0].AsString())); + Assert.AreEqual(test.Response.Result.ToString(), result.ToJson().ToString()); + } + + [TestMethod] + public async Task TestGetUnclaimedGas() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetUnclaimedGasAsync).ToLower()); + var result = await rpc.GetUnclaimedGasAsync(test.Request.Params[0].AsString()); + Assert.AreEqual(result.ToJson().AsString(), RpcUnclaimedGas.FromJson(result.ToJson()).ToJson().AsString()); + Assert.AreEqual(test.Response.Result["unclaimed"].AsString(), result.Unclaimed.ToString()); + } + + #endregion SmartContract + + #region Utilities + + [TestMethod] + public async Task TestListPlugins() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.ListPluginsAsync).ToLower()); + var result = await rpc.ListPluginsAsync(); + Assert.AreEqual(test.Response.Result.ToString(), ((JArray)result.Select(p => p.ToJson()).ToArray()).ToString()); + } + + [TestMethod] + public async Task TestValidateAddress() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.ValidateAddressAsync).ToLower()); + var result = await rpc.ValidateAddressAsync(test.Request.Params[0].AsString()); + Assert.AreEqual(test.Response.Result.ToString(), result.ToJson().ToString()); + } + + #endregion Utilities + + #region Wallet + + [TestMethod] + public async Task TestCloseWallet() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.CloseWalletAsync).ToLower()); + var result = await rpc.CloseWalletAsync(); + Assert.AreEqual(test.Response.Result.AsBoolean(), result); + } + + [TestMethod] + public async Task TestDumpPrivKey() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.DumpPrivKeyAsync).ToLower()); + var result = await rpc.DumpPrivKeyAsync(test.Request.Params[0].AsString()); + Assert.AreEqual(test.Response.Result.AsString(), result); + } + + [TestMethod] + public async Task TestGetNewAddress() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetNewAddressAsync).ToLower()); + var result = await rpc.GetNewAddressAsync(); + Assert.AreEqual(test.Response.Result.AsString(), result); + } + + [TestMethod] + public async Task TestGetWalletBalance() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetWalletBalanceAsync).ToLower()); + var result = await rpc.GetWalletBalanceAsync(test.Request.Params[0].AsString()); + Assert.AreEqual(test.Response.Result["balance"].AsString(), result.Value.ToString()); + } + + [TestMethod] + public async Task TestGetWalletUnclaimedGas() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetWalletUnclaimedGasAsync).ToLower()); + var result = await rpc.GetWalletUnclaimedGasAsync(); + Assert.AreEqual(test.Response.Result.AsString(), result.ToString()); + } + + [TestMethod] + public async Task TestImportPrivKey() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.ImportPrivKeyAsync).ToLower()); + var result = await rpc.ImportPrivKeyAsync(test.Request.Params[0].AsString()); + Assert.AreEqual(test.Response.Result.ToString(), result.ToJson().ToString()); + } + + [TestMethod] + public async Task TestListAddress() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.ListAddressAsync).ToLower()); + var result = await rpc.ListAddressAsync(); + Assert.AreEqual(test.Response.Result.ToString(), ((JArray)result.Select(p => p.ToJson()).ToArray()).ToString()); + } + + [TestMethod] + public async Task TestOpenWallet() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.OpenWalletAsync).ToLower()); + var result = await rpc.OpenWalletAsync(test.Request.Params[0].AsString(), test.Request.Params[1].AsString()); + Assert.AreEqual(test.Response.Result.AsBoolean(), result); + } + + [TestMethod] + public async Task TestSendFrom() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.SendFromAsync).ToLower()); + var result = await rpc.SendFromAsync(test.Request.Params[0].AsString(), test.Request.Params[1].AsString(), + test.Request.Params[2].AsString(), test.Request.Params[3].AsString()); + Assert.AreEqual(test.Response.Result.ToString(), result.ToString()); + } + + [TestMethod] + public async Task TestSendMany() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.SendManyAsync).ToLower()); + var result = await rpc.SendManyAsync(test.Request.Params[0].AsString(), ((JArray)test.Request.Params[1]).Select(p => RpcTransferOut.FromJson((JObject)p, rpc.protocolSettings))); + Assert.AreEqual(test.Response.Result.ToString(), result.ToString()); + } + + [TestMethod] + public async Task TestSendToAddress() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.SendToAddressAsync).ToLower()); + var result = await rpc.SendToAddressAsync(test.Request.Params[0].AsString(), test.Request.Params[1].AsString(), test.Request.Params[2].AsString()); + Assert.AreEqual(test.Response.Result.ToString(), result.ToString()); + } + + #endregion Wallet + + #region Plugins + + [TestMethod()] + public async Task GetApplicationLogTest() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetApplicationLogAsync).ToLower()); + var result = await rpc.GetApplicationLogAsync(test.Request.Params[0].AsString()); + Assert.AreEqual(test.Response.Result.ToString(), result.ToJson().ToString()); + } + + [TestMethod()] + public async Task GetApplicationLogTest_TriggerType() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == (nameof(rpc.GetApplicationLogAsync) + "_triggertype").ToLower()); + var result = await rpc.GetApplicationLogAsync(test.Request.Params[0].AsString(), TriggerType.OnPersist); + Assert.AreEqual(test.Response.Result.ToString(), result.ToJson().ToString()); + } + + [TestMethod()] + public async Task GetNep17TransfersTest() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetNep17TransfersAsync).ToLower()); + var result = await rpc.GetNep17TransfersAsync(test.Request.Params[0].AsString(), (ulong)test.Request.Params[1].AsNumber(), (ulong)test.Request.Params[2].AsNumber()); + Assert.AreEqual(test.Response.Result.ToString(), result.ToJson(rpc.protocolSettings).ToString()); + test = TestUtils.RpcTestCases.Find(p => p.Name == (nameof(rpc.GetNep17TransfersAsync).ToLower() + "_with_null_transferaddress")); + result = await rpc.GetNep17TransfersAsync(test.Request.Params[0].AsString(), (ulong)test.Request.Params[1].AsNumber(), (ulong)test.Request.Params[2].AsNumber()); + Assert.AreEqual(test.Response.Result.ToString(), result.ToJson(rpc.protocolSettings).ToString()); + } + + [TestMethod()] + public async Task GetNep17BalancesTest() + { + var test = TestUtils.RpcTestCases.Find(p => p.Name == nameof(rpc.GetNep17BalancesAsync).ToLower()); + var result = await rpc.GetNep17BalancesAsync(test.Request.Params[0].AsString()); + Assert.AreEqual(test.Response.Result.ToString(), result.ToJson(rpc.protocolSettings).ToString()); + } + + #endregion Plugins + } +} diff --git a/tests/Neo.Network.RPC.Tests/UT_RpcModels.cs b/tests/Neo.Network.RPC.Tests/UT_RpcModels.cs new file mode 100644 index 0000000000..43eb7cfe7b --- /dev/null +++ b/tests/Neo.Network.RPC.Tests/UT_RpcModels.cs @@ -0,0 +1,175 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_RpcModels.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.VisualStudio.TestTools.UnitTesting; +using Moq; +using Neo.Json; +using Neo.Network.RPC.Models; +using System; +using System.Linq; +using System.Net.Http; + +namespace Neo.Network.RPC.Tests +{ + [TestClass()] + public class UT_RpcModels + { + RpcClient rpc; + Mock handlerMock; + + [TestInitialize] + public void TestSetup() + { + handlerMock = new Mock(MockBehavior.Strict); + + // use real http client with mocked handler here + var httpClient = new HttpClient(handlerMock.Object); + rpc = new RpcClient(httpClient, new Uri("http://seed1.neo.org:10331"), null); + } + + [TestMethod()] + public void TestRpcAccount() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.ImportPrivKeyAsync).ToLower()).Response.Result; + var item = RpcAccount.FromJson((JObject)json); + Assert.AreEqual(json.ToString(), item.ToJson().ToString()); + } + + [TestMethod()] + public void TestRpcApplicationLog() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.GetApplicationLogAsync).ToLower()).Response.Result; + var item = RpcApplicationLog.FromJson((JObject)json, rpc.protocolSettings); + Assert.AreEqual(json.ToString(), item.ToJson().ToString()); + } + + [TestMethod()] + public void TestRpcBlock() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.GetBlockAsync).ToLower()).Response.Result; + var item = RpcBlock.FromJson((JObject)json, rpc.protocolSettings); + Assert.AreEqual(json.ToString(), item.ToJson(rpc.protocolSettings).ToString()); + } + + [TestMethod()] + public void TestRpcBlockHeader() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.GetBlockHeaderAsync).ToLower()).Response.Result; + var item = RpcBlockHeader.FromJson((JObject)json, rpc.protocolSettings); + Assert.AreEqual(json.ToString(), item.ToJson(rpc.protocolSettings).ToString()); + } + + [TestMethod()] + public void TestGetContractState() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.GetContractStateAsync).ToLower()).Response.Result; + var item = RpcContractState.FromJson((JObject)json); + Assert.AreEqual(json.ToString(), item.ToJson().ToString()); + + var nef = RpcNefFile.FromJson((JObject)json["nef"]); + Assert.AreEqual(json["nef"].ToString(), nef.ToJson().ToString()); + } + + [TestMethod()] + public void TestRpcInvokeResult() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.InvokeFunctionAsync).ToLower()).Response.Result; + var item = RpcInvokeResult.FromJson((JObject)json); + Assert.AreEqual(json.ToString(), item.ToJson().ToString()); + } + + [TestMethod()] + public void TestRpcMethodToken() + { + RpcMethodToken.FromJson((JObject)JToken.Parse("{\"hash\": \"0x0e1b9bfaa44e60311f6f3c96cfcd6d12c2fc3add\", \"method\":\"test\",\"paramcount\":\"1\",\"hasreturnvalue\":\"true\",\"callflags\":\"All\"}")); + } + + [TestMethod()] + public void TestRpcNep17Balances() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.GetNep17BalancesAsync).ToLower()).Response.Result; + var item = RpcNep17Balances.FromJson((JObject)json, rpc.protocolSettings); + Assert.AreEqual(json.ToString(), item.ToJson(rpc.protocolSettings).ToString()); + } + + [TestMethod()] + public void TestRpcNep17Transfers() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.GetNep17TransfersAsync).ToLower()).Response.Result; + var item = RpcNep17Transfers.FromJson((JObject)json, rpc.protocolSettings); + Assert.AreEqual(json.ToString(), item.ToJson(rpc.protocolSettings).ToString()); + } + + [TestMethod()] + public void TestRpcPeers() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.GetPeersAsync).ToLower()).Response.Result; + var item = RpcPeers.FromJson((JObject)json); + Assert.AreEqual(json.ToString(), item.ToJson().ToString()); + } + + [TestMethod()] + public void TestRpcPlugin() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.ListPluginsAsync).ToLower()).Response.Result; + var item = ((JArray)json).Select(p => RpcPlugin.FromJson((JObject)p)); + Assert.AreEqual(json.ToString(), ((JArray)item.Select(p => p.ToJson()).ToArray()).ToString()); + } + + [TestMethod()] + public void TestRpcRawMemPool() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.GetRawMempoolBothAsync).ToLower()).Response.Result; + var item = RpcRawMemPool.FromJson((JObject)json); + Assert.AreEqual(json.ToString(), item.ToJson().ToString()); + } + + [TestMethod()] + public void TestRpcTransaction() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.GetRawTransactionAsync).ToLower()).Response.Result; + var item = RpcTransaction.FromJson((JObject)json, rpc.protocolSettings); + Assert.AreEqual(json.ToString(), item.ToJson(rpc.protocolSettings).ToString()); + } + + [TestMethod()] + public void TestRpcTransferOut() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.SendManyAsync).ToLower()).Request.Params[1]; + var item = ((JArray)json).Select(p => RpcTransferOut.FromJson((JObject)p, rpc.protocolSettings)); + Assert.AreEqual(json.ToString(), ((JArray)item.Select(p => p.ToJson(rpc.protocolSettings)).ToArray()).ToString()); + } + + [TestMethod()] + public void TestRpcValidateAddressResult() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.ValidateAddressAsync).ToLower()).Response.Result; + var item = RpcValidateAddressResult.FromJson((JObject)json); + Assert.AreEqual(json.ToString(), item.ToJson().ToString()); + } + + [TestMethod()] + public void TestRpcValidator() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.GetNextBlockValidatorsAsync).ToLower()).Response.Result; + var item = ((JArray)json).Select(p => RpcValidator.FromJson((JObject)p)); + Assert.AreEqual(json.ToString(), ((JArray)item.Select(p => p.ToJson()).ToArray()).ToString()); + } + + [TestMethod()] + public void TestRpcVersion() + { + JToken json = TestUtils.RpcTestCases.Find(p => p.Name == nameof(RpcClient.GetVersionAsync).ToLower()).Response.Result; + var item = RpcVersion.FromJson((JObject)json); + Assert.AreEqual(json.ToString(), item.ToJson().ToString()); + } + } +} diff --git a/tests/Neo.Network.RPC.Tests/UT_TransactionManager.cs b/tests/Neo.Network.RPC.Tests/UT_TransactionManager.cs new file mode 100644 index 0000000000..ae025cf65a --- /dev/null +++ b/tests/Neo.Network.RPC.Tests/UT_TransactionManager.cs @@ -0,0 +1,267 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_TransactionManager.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.VisualStudio.TestTools.UnitTesting; +using Moq; +using Neo.Cryptography; +using Neo.Cryptography.ECC; +using Neo.IO; +using Neo.Json; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using Neo.Network.RPC.Models; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.Wallets; +using System; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; + +namespace Neo.Network.RPC.Tests +{ + [TestClass] + public class UT_TransactionManager + { + TransactionManager txManager; + Mock rpcClientMock; + Mock multiSigMock; + KeyPair keyPair1; + KeyPair keyPair2; + UInt160 sender; + UInt160 multiHash; + RpcClient client; + + [TestInitialize] + public void TestSetup() + { + keyPair1 = new KeyPair(Wallet.GetPrivateKeyFromWIF("KyXwTh1hB76RRMquSvnxZrJzQx7h9nQP2PCRL38v6VDb5ip3nf1p")); + keyPair2 = new KeyPair(Wallet.GetPrivateKeyFromWIF("L2LGkrwiNmUAnWYb1XGd5mv7v2eDf6P4F3gHyXSrNJJR4ArmBp7Q")); + sender = Contract.CreateSignatureRedeemScript(keyPair1.PublicKey).ToScriptHash(); + multiHash = Contract.CreateMultiSigContract(2, new ECPoint[] { keyPair1.PublicKey, keyPair2.PublicKey }).ScriptHash; + rpcClientMock = MockRpcClient(sender, new byte[1]); + client = rpcClientMock.Object; + multiSigMock = MockMultiSig(multiHash, new byte[1]); + } + + public static Mock MockRpcClient(UInt160 sender, byte[] script) + { + var mockRpc = new Mock(MockBehavior.Strict, new Uri("http://seed1.neo.org:10331"), null, null, null); + + // MockHeight + mockRpc.Setup(p => p.RpcSendAsync("getblockcount")).ReturnsAsync(100).Verifiable(); + + // calculatenetworkfee + var networkfee = new JObject(); + networkfee["networkfee"] = 100000000; + mockRpc.Setup(p => p.RpcSendAsync("calculatenetworkfee", It.Is(u => true))) + .ReturnsAsync(networkfee) + .Verifiable(); + + // MockGasBalance + byte[] balanceScript = NativeContract.GAS.Hash.MakeScript("balanceOf", sender); + var balanceResult = new ContractParameter() { Type = ContractParameterType.Integer, Value = BigInteger.Parse("10000000000000000") }; + + MockInvokeScript(mockRpc, balanceScript, balanceResult); + + // MockFeePerByte + byte[] policyScript = NativeContract.Policy.Hash.MakeScript("getFeePerByte"); + var policyResult = new ContractParameter() { Type = ContractParameterType.Integer, Value = BigInteger.Parse("1000") }; + + MockInvokeScript(mockRpc, policyScript, policyResult); + + // MockGasConsumed + var result = new ContractParameter(); + MockInvokeScript(mockRpc, script, result); + + return mockRpc; + } + + public static Mock MockMultiSig(UInt160 multiHash, byte[] script) + { + var mockRpc = new Mock(MockBehavior.Strict, new Uri("http://seed1.neo.org:10331"), null, null, null); + + // MockHeight + mockRpc.Setup(p => p.RpcSendAsync("getblockcount")).ReturnsAsync(100).Verifiable(); + + // calculatenetworkfee + var networkfee = new JObject(); + networkfee["networkfee"] = 100000000; + mockRpc.Setup(p => p.RpcSendAsync("calculatenetworkfee", It.Is(u => true))) + .ReturnsAsync(networkfee) + .Verifiable(); + + // MockGasBalance + byte[] balanceScript = NativeContract.GAS.Hash.MakeScript("balanceOf", multiHash); + var balanceResult = new ContractParameter() { Type = ContractParameterType.Integer, Value = BigInteger.Parse("10000000000000000") }; + + MockInvokeScript(mockRpc, balanceScript, balanceResult); + + // MockFeePerByte + byte[] policyScript = NativeContract.Policy.Hash.MakeScript("getFeePerByte"); + var policyResult = new ContractParameter() { Type = ContractParameterType.Integer, Value = BigInteger.Parse("1000") }; + + MockInvokeScript(mockRpc, policyScript, policyResult); + + // MockGasConsumed + var result = new ContractParameter(); + MockInvokeScript(mockRpc, script, result); + + return mockRpc; + } + + public static void MockInvokeScript(Mock mockClient, byte[] script, params ContractParameter[] parameters) + { + var result = new RpcInvokeResult() + { + Stack = parameters.Select(p => p.ToStackItem()).ToArray(), + GasConsumed = 100, + Script = Convert.ToBase64String(script), + State = VMState.HALT + }; + + mockClient.Setup(p => p.RpcSendAsync("invokescript", It.Is(j => + Convert.FromBase64String(j[0].AsString()).SequenceEqual(script)))) + .ReturnsAsync(result.ToJson()) + .Verifiable(); + } + + [TestMethod] + public async Task TestMakeTransaction() + { + Signer[] signers = new Signer[1] + { + new Signer + { + Account = sender, + Scopes= WitnessScope.Global + } + }; + + byte[] script = new byte[1]; + txManager = await TransactionManager.MakeTransactionAsync(rpcClientMock.Object, script, signers); + + var tx = txManager.Tx; + Assert.AreEqual(WitnessScope.Global, tx.Signers[0].Scopes); + } + + [TestMethod] + public async Task TestSign() + { + Signer[] signers = new Signer[1] + { + new Signer + { + Account = sender, + Scopes = WitnessScope.Global + } + }; + + byte[] script = new byte[1]; + txManager = await TransactionManager.MakeTransactionAsync(client, script, signers); + await txManager + .AddSignature(keyPair1) + .SignAsync(); + + // get signature from Witnesses + var tx = txManager.Tx; + ReadOnlyMemory signature = tx.Witnesses[0].InvocationScript[2..]; + + Assert.IsTrue(Crypto.VerifySignature(tx.GetSignData(client.protocolSettings.Network), signature.Span, keyPair1.PublicKey)); + // verify network fee and system fee + Assert.AreEqual(100000000/*Mock*/, tx.NetworkFee); + Assert.AreEqual(100, tx.SystemFee); + + // duplicate sign should not add new witness + await ThrowsAsync(async () => await txManager.AddSignature(keyPair1).SignAsync()); + Assert.AreEqual(null, txManager.Tx.Witnesses); + + // throw exception when the KeyPair is wrong + await ThrowsAsync(async () => await txManager.AddSignature(keyPair2).SignAsync()); + } + + // https://docs.microsoft.com/en-us/archive/msdn-magazine/2014/november/async-programming-unit-testing-asynchronous-code#testing-exceptions + static async Task ThrowsAsync(Func action, bool allowDerivedTypes = true) + where TException : Exception + { + try + { + await action(); + } + catch (Exception ex) + { + if (allowDerivedTypes && !(ex is TException)) + throw new Exception("Delegate threw exception of type " + + ex.GetType().Name + ", but " + typeof(TException).Name + + " or a derived type was expected.", ex); + if (!allowDerivedTypes && ex.GetType() != typeof(TException)) + throw new Exception("Delegate threw exception of type " + + ex.GetType().Name + ", but " + typeof(TException).Name + + " was expected.", ex); + return (TException)ex; + } + throw new Exception("Delegate did not throw expected exception " + + typeof(TException).Name + "."); + } + + [TestMethod] + public async Task TestSignMulti() + { + // Cosigner needs multi signature + Signer[] signers = new Signer[1] + { + new Signer + { + Account = multiHash, + Scopes = WitnessScope.Global + } + }; + + byte[] script = new byte[1]; + txManager = await TransactionManager.MakeTransactionAsync(multiSigMock.Object, script, signers); + await txManager + .AddMultiSig(keyPair1, 2, keyPair1.PublicKey, keyPair2.PublicKey) + .AddMultiSig(keyPair2, 2, keyPair1.PublicKey, keyPair2.PublicKey) + .SignAsync(); + } + + [TestMethod] + public async Task TestAddWitness() + { + // Cosigner as contract scripthash + Signer[] signers = new Signer[2] + { + new Signer + { + Account = sender, + Scopes = WitnessScope.Global + }, + new Signer + { + Account = UInt160.Zero, + Scopes = WitnessScope.Global + } + }; + + byte[] script = new byte[1]; + txManager = await TransactionManager.MakeTransactionAsync(rpcClientMock.Object, script, signers); + txManager.AddWitness(UInt160.Zero); + txManager.AddSignature(keyPair1); + await txManager.SignAsync(); + + var tx = txManager.Tx; + Assert.AreEqual(2, tx.Witnesses.Length); + Assert.AreEqual(40, tx.Witnesses[0].VerificationScript.Length); + Assert.AreEqual(66, tx.Witnesses[0].InvocationScript.Length); + } + } +} diff --git a/tests/Neo.Network.RPC.Tests/UT_Utility.cs b/tests/Neo.Network.RPC.Tests/UT_Utility.cs new file mode 100644 index 0000000000..fa410c5437 --- /dev/null +++ b/tests/Neo.Network.RPC.Tests/UT_Utility.cs @@ -0,0 +1,87 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_Utility.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.VisualStudio.TestTools.UnitTesting; +using Neo.SmartContract; +using Neo.Wallets; +using System; +using System.Numerics; + +namespace Neo.Network.RPC.Tests +{ + [TestClass] + public class UT_Utility + { + private KeyPair keyPair; + private UInt160 scriptHash; + private ProtocolSettings protocolSettings; + + [TestInitialize] + public void TestSetup() + { + keyPair = new KeyPair(Wallet.GetPrivateKeyFromWIF("KyXwTh1hB76RRMquSvnxZrJzQx7h9nQP2PCRL38v6VDb5ip3nf1p")); + scriptHash = Contract.CreateSignatureRedeemScript(keyPair.PublicKey).ToScriptHash(); + protocolSettings = ProtocolSettings.Load("protocol.json"); + } + + [TestMethod] + public void TestGetKeyPair() + { + string nul = null; + Assert.ThrowsException(() => Utility.GetKeyPair(nul)); + + string wif = "KyXwTh1hB76RRMquSvnxZrJzQx7h9nQP2PCRL38v6VDb5ip3nf1p"; + var result = Utility.GetKeyPair(wif); + Assert.AreEqual(keyPair, result); + + string privateKey = keyPair.PrivateKey.ToHexString(); + result = Utility.GetKeyPair(privateKey); + Assert.AreEqual(keyPair, result); + } + + [TestMethod] + public void TestGetScriptHash() + { + string nul = null; + Assert.ThrowsException(() => Utility.GetScriptHash(nul, protocolSettings)); + + string addr = scriptHash.ToAddress(protocolSettings.AddressVersion); + var result = Utility.GetScriptHash(addr, protocolSettings); + Assert.AreEqual(scriptHash, result); + + string hash = scriptHash.ToString(); + result = Utility.GetScriptHash(hash, protocolSettings); + Assert.AreEqual(scriptHash, result); + + string publicKey = keyPair.PublicKey.ToString(); + result = Utility.GetScriptHash(publicKey, protocolSettings); + Assert.AreEqual(scriptHash, result); + } + + [TestMethod] + public void TestToBigInteger() + { + decimal amount = 1.23456789m; + uint decimals = 9; + var result = amount.ToBigInteger(decimals); + Assert.AreEqual(1234567890, result); + + amount = 1.23456789m; + decimals = 18; + result = amount.ToBigInteger(decimals); + Assert.AreEqual(BigInteger.Parse("1234567890000000000"), result); + + amount = 1.23456789m; + decimals = 4; + Assert.ThrowsException(() => result = amount.ToBigInteger(decimals)); + } + } +} diff --git a/tests/Neo.Network.RPC.Tests/UT_WalletAPI.cs b/tests/Neo.Network.RPC.Tests/UT_WalletAPI.cs new file mode 100644 index 0000000000..10a35fd064 --- /dev/null +++ b/tests/Neo.Network.RPC.Tests/UT_WalletAPI.cs @@ -0,0 +1,181 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_WalletAPI.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.VisualStudio.TestTools.UnitTesting; +using Moq; +using Neo.Cryptography.ECC; +using Neo.Json; +using Neo.Network.P2P.Payloads; +using Neo.Network.RPC.Models; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.Wallets; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; + +namespace Neo.Network.RPC.Tests +{ + [TestClass] + public class UT_WalletAPI + { + Mock rpcClientMock; + KeyPair keyPair1; + string address1; + UInt160 sender; + WalletAPI walletAPI; + UInt160 multiSender; + RpcClient client; + + [TestInitialize] + public void TestSetup() + { + keyPair1 = new KeyPair(Wallet.GetPrivateKeyFromWIF("KyXwTh1hB76RRMquSvnxZrJzQx7h9nQP2PCRL38v6VDb5ip3nf1p")); + sender = Contract.CreateSignatureRedeemScript(keyPair1.PublicKey).ToScriptHash(); + multiSender = Contract.CreateMultiSigContract(1, new ECPoint[] { keyPair1.PublicKey }).ScriptHash; + rpcClientMock = UT_TransactionManager.MockRpcClient(sender, new byte[0]); + client = rpcClientMock.Object; + address1 = Wallets.Helper.ToAddress(sender, client.protocolSettings.AddressVersion); + walletAPI = new WalletAPI(rpcClientMock.Object); + } + + [TestMethod] + public async Task TestGetUnclaimedGas() + { + byte[] testScript = NativeContract.NEO.Hash.MakeScript("unclaimedGas", sender, 99); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(1_10000000) }); + + var balance = await walletAPI.GetUnclaimedGasAsync(address1); + Assert.AreEqual(1.1m, balance); + } + + [TestMethod] + public async Task TestGetNeoBalance() + { + byte[] testScript = NativeContract.NEO.Hash.MakeScript("balanceOf", sender); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(1_00000000) }); + + var balance = await walletAPI.GetNeoBalanceAsync(address1); + Assert.AreEqual(1_00000000u, balance); + } + + [TestMethod] + public async Task TestGetGasBalance() + { + byte[] testScript = NativeContract.GAS.Hash.MakeScript("balanceOf", sender); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(1_10000000) }); + + var balance = await walletAPI.GetGasBalanceAsync(address1); + Assert.AreEqual(1.1m, balance); + } + + [TestMethod] + public async Task TestGetTokenBalance() + { + byte[] testScript = UInt160.Zero.MakeScript("balanceOf", sender); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(1_10000000) }); + + var balance = await walletAPI.GetTokenBalanceAsync(UInt160.Zero.ToString(), address1); + Assert.AreEqual(1_10000000, balance); + } + + [TestMethod] + public async Task TestClaimGas() + { + byte[] balanceScript = NativeContract.NEO.Hash.MakeScript("balanceOf", sender); + UT_TransactionManager.MockInvokeScript(rpcClientMock, balanceScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(1_00000000) }); + + byte[] testScript = NativeContract.NEO.Hash.MakeScript("transfer", sender, sender, new BigInteger(1_00000000), null); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(1_10000000) }); + + var json = new JObject(); + json["hash"] = UInt256.Zero.ToString(); + rpcClientMock.Setup(p => p.RpcSendAsync("sendrawtransaction", It.IsAny())).ReturnsAsync(json); + + var tranaction = await walletAPI.ClaimGasAsync(keyPair1.Export(), false); + Assert.AreEqual(testScript.ToHexString(), tranaction.Script.Span.ToHexString()); + } + + [TestMethod] + public async Task TestTransfer() + { + byte[] decimalsScript = NativeContract.GAS.Hash.MakeScript("decimals"); + UT_TransactionManager.MockInvokeScript(rpcClientMock, decimalsScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(8) }); + + byte[] testScript = NativeContract.GAS.Hash.MakeScript("transfer", sender, UInt160.Zero, NativeContract.GAS.Factor * 100, null) + .Concat(new[] { (byte)OpCode.ASSERT }) + .ToArray(); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(1_10000000) }); + + var json = new JObject(); + json["hash"] = UInt256.Zero.ToString(); + rpcClientMock.Setup(p => p.RpcSendAsync("sendrawtransaction", It.IsAny())).ReturnsAsync(json); + + var tranaction = await walletAPI.TransferAsync(NativeContract.GAS.Hash.ToString(), keyPair1.Export(), UInt160.Zero.ToAddress(client.protocolSettings.AddressVersion), 100, null, true); + Assert.AreEqual(testScript.ToHexString(), tranaction.Script.Span.ToHexString()); + } + + [TestMethod] + public async Task TestTransferfromMultiSigAccount() + { + byte[] balanceScript = NativeContract.GAS.Hash.MakeScript("balanceOf", multiSender); + var balanceResult = new ContractParameter() { Type = ContractParameterType.Integer, Value = BigInteger.Parse("10000000000000000") }; + + UT_TransactionManager.MockInvokeScript(rpcClientMock, balanceScript, balanceResult); + + byte[] decimalsScript = NativeContract.GAS.Hash.MakeScript("decimals"); + UT_TransactionManager.MockInvokeScript(rpcClientMock, decimalsScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(8) }); + + byte[] testScript = NativeContract.GAS.Hash.MakeScript("transfer", multiSender, UInt160.Zero, NativeContract.GAS.Factor * 100, null) + .Concat(new[] { (byte)OpCode.ASSERT }) + .ToArray(); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(1_10000000) }); + + var json = new JObject(); + json["hash"] = UInt256.Zero.ToString(); + rpcClientMock.Setup(p => p.RpcSendAsync("sendrawtransaction", It.IsAny())).ReturnsAsync(json); + + var tranaction = await walletAPI.TransferAsync(NativeContract.GAS.Hash, 1, new[] { keyPair1.PublicKey }, new[] { keyPair1 }, UInt160.Zero, NativeContract.GAS.Factor * 100, null, true); + Assert.AreEqual(testScript.ToHexString(), tranaction.Script.Span.ToHexString()); + + try + { + tranaction = await walletAPI.TransferAsync(NativeContract.GAS.Hash, 2, new[] { keyPair1.PublicKey }, new[] { keyPair1 }, UInt160.Zero, NativeContract.GAS.Factor * 100, null, true); + Assert.Fail(); + } + catch (System.Exception e) + { + Assert.AreEqual(e.Message, $"Need at least 2 KeyPairs for signing!"); + } + + testScript = NativeContract.GAS.Hash.MakeScript("transfer", multiSender, UInt160.Zero, NativeContract.GAS.Factor * 100, string.Empty) + .Concat(new[] { (byte)OpCode.ASSERT }) + .ToArray(); + UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(1_10000000) }); + + tranaction = await walletAPI.TransferAsync(NativeContract.GAS.Hash, 1, new[] { keyPair1.PublicKey }, new[] { keyPair1 }, UInt160.Zero, NativeContract.GAS.Factor * 100, string.Empty, true); + Assert.AreEqual(testScript.ToHexString(), tranaction.Script.Span.ToHexString()); + } + + [TestMethod] + public async Task TestWaitTransaction() + { + Transaction transaction = TestUtils.GetTransaction(); + rpcClientMock.Setup(p => p.RpcSendAsync("getrawtransaction", It.Is(j => j[0].AsString() == transaction.Hash.ToString()))) + .ReturnsAsync(new RpcTransaction { Transaction = transaction, VMState = VMState.HALT, BlockHash = UInt256.Zero, BlockTime = 100, Confirmations = 1 }.ToJson(client.protocolSettings)); + + var tx = await walletAPI.WaitTransactionAsync(transaction); + Assert.AreEqual(VMState.HALT, tx.VMState); + Assert.AreEqual(UInt256.Zero, tx.BlockHash); + } + } +} diff --git a/tests/Neo.Plugins.OracleService.Tests/Neo.Plugins.OracleService.Tests.csproj b/tests/Neo.Plugins.OracleService.Tests/Neo.Plugins.OracleService.Tests.csproj new file mode 100644 index 0000000000..4dd4abbfa4 --- /dev/null +++ b/tests/Neo.Plugins.OracleService.Tests/Neo.Plugins.OracleService.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + OracleService.Tests + Neo.Plugins + + + + + + + + + + + + + + + + + PreserveNewest + PreserveNewest + + + + diff --git a/tests/Neo.Plugins.OracleService.Tests/TestBlockchain.cs b/tests/Neo.Plugins.OracleService.Tests/TestBlockchain.cs new file mode 100644 index 0000000000..88c08575fa --- /dev/null +++ b/tests/Neo.Plugins.OracleService.Tests/TestBlockchain.cs @@ -0,0 +1,36 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// TestBlockchain.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; + +namespace Neo.Plugins +{ + public static class TestBlockchain + { + public static readonly NeoSystem TheNeoSystem; + + static TestBlockchain() + { + Console.WriteLine("initialize NeoSystem"); + TheNeoSystem = new NeoSystem(ProtocolSettings.Load("config.json"), new MemoryStoreProvider()); + } + + public static void InitializeMockNeoSystem() + { + } + + internal static DataCache GetTestSnapshot() + { + return TheNeoSystem.GetSnapshot().CreateSnapshot(); + } + } +} diff --git a/tests/Neo.Plugins.OracleService.Tests/TestUtils.cs b/tests/Neo.Plugins.OracleService.Tests/TestUtils.cs new file mode 100644 index 0000000000..5700b83769 --- /dev/null +++ b/tests/Neo.Plugins.OracleService.Tests/TestUtils.cs @@ -0,0 +1,32 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// TestUtils.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.SmartContract; +using Neo.SmartContract.Native; + +namespace Neo.Plugins +{ + public static class TestUtils + { + public static StorageKey CreateStorageKey(this NativeContract contract, byte prefix, ISerializable key) + { + var k = new KeyBuilder(contract.Id, prefix); + if (key != null) k = k.Add(key); + return k; + } + + public static StorageKey CreateStorageKey(this NativeContract contract, byte prefix, uint value) + { + return new KeyBuilder(contract.Id, prefix).AddBigEndian(value); + } + } +} diff --git a/tests/Neo.Plugins.OracleService.Tests/UT_OracleService.cs b/tests/Neo.Plugins.OracleService.Tests/UT_OracleService.cs new file mode 100644 index 0000000000..08f96c95d7 --- /dev/null +++ b/tests/Neo.Plugins.OracleService.Tests/UT_OracleService.cs @@ -0,0 +1,118 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_OracleService.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Akka.TestKit.Xunit2; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Cryptography.ECC; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.SmartContract.Native; + +namespace Neo.Plugins.Tests +{ + [TestClass] + public class UT_OracleService : TestKit + { + [TestInitialize] + public void TestSetup() + { + TestBlockchain.InitializeMockNeoSystem(); + } + + [TestMethod] + public void TestFilter() + { + var json = @"{ + ""Stores"": [ + ""Lambton Quay"", + ""Willis Street"" + ], + ""Manufacturers"": [ + { + ""Name"": ""Acme Co"", + ""Products"": [ + { + ""Name"": ""Anvil"", + ""Price"": 50 + } + ] + }, + { + ""Name"": ""Contoso"", + ""Products"": [ + { + ""Name"": ""Elbow Grease"", + ""Price"": 99.95 + }, + { + ""Name"": ""Headlight Fluid"", + ""Price"": 4 + } + ] + } + ] +}"; + + Assert.AreEqual(@"[""Acme Co""]", Utility.StrictUTF8.GetString(OracleService.Filter(json, "$.Manufacturers[0].Name"))); + Assert.AreEqual("[50]", Utility.StrictUTF8.GetString(OracleService.Filter(json, "$.Manufacturers[0].Products[0].Price"))); + Assert.AreEqual(@"[""Elbow Grease""]", Utility.StrictUTF8.GetString(OracleService.Filter(json, "$.Manufacturers[1].Products[0].Name"))); + Assert.AreEqual(@"[{""Name"":""Elbow Grease"",""Price"":99.95}]", Utility.StrictUTF8.GetString(OracleService.Filter(json, "$.Manufacturers[1].Products[0]"))); + } + + [TestMethod] + public void TestCreateOracleResponseTx() + { + var snapshot = TestBlockchain.GetTestSnapshot(); + + var executionFactor = NativeContract.Policy.GetExecFeeFactor(snapshot); + Assert.AreEqual(executionFactor, (uint)30); + var feePerByte = NativeContract.Policy.GetFeePerByte(snapshot); + Assert.AreEqual(feePerByte, 1000); + + OracleRequest request = new OracleRequest + { + OriginalTxid = UInt256.Zero, + GasForResponse = 100000000 * 1, + Url = "https://127.0.0.1/test", + Filter = "", + CallbackContract = UInt160.Zero, + CallbackMethod = "callback", + UserData = System.Array.Empty() + }; + byte Prefix_Transaction = 11; + snapshot.Add(NativeContract.Ledger.CreateStorageKey(Prefix_Transaction, request.OriginalTxid), new StorageItem(new TransactionState() + { + BlockIndex = 1, + Transaction = new Transaction() + { + ValidUntilBlock = 1 + } + })); + OracleResponse response = new OracleResponse() { Id = 1, Code = OracleResponseCode.Success, Result = new byte[] { 0x00 } }; + ECPoint[] oracleNodes = new ECPoint[] { ECCurve.Secp256r1.G }; + var tx = OracleService.CreateResponseTx(snapshot, request, response, oracleNodes, ProtocolSettings.Default); + + Assert.AreEqual(166, tx.Size); + Assert.AreEqual(2198650, tx.NetworkFee); + Assert.AreEqual(97801350, tx.SystemFee); + + // case (2) The size of attribute exceed the maximum limit + + request.GasForResponse = 0_10000000; + response.Result = new byte[10250]; + tx = OracleService.CreateResponseTx(snapshot, request, response, oracleNodes, ProtocolSettings.Default); + Assert.AreEqual(165, tx.Size); + Assert.AreEqual(OracleResponseCode.InsufficientFunds, response.Code); + Assert.AreEqual(2197650, tx.NetworkFee); + Assert.AreEqual(7802350, tx.SystemFee); + } + } +} diff --git a/tests/Neo.Plugins.OracleService.Tests/config.json b/tests/Neo.Plugins.OracleService.Tests/config.json new file mode 100644 index 0000000000..b4800b80ea --- /dev/null +++ b/tests/Neo.Plugins.OracleService.Tests/config.json @@ -0,0 +1,74 @@ +{ + "ApplicationConfiguration": { + "Logger": { + "Path": "Logs", + "ConsoleOutput": false, + "Active": false + }, + "Storage": { + "Engine": "LevelDBStore", // Candidates [MemoryStore, LevelDBStore, RocksDBStore] + "Path": "Data_LevelDB_{0}" // {0} is a placeholder for the network id + }, + "P2P": { + "Port": 10333, + "MinDesiredConnections": 10, + "MaxConnections": 40, + "MaxConnectionsPerAddress": 3 + }, + "UnlockWallet": { + "Path": "", + "Password": "", + "IsActive": false + }, + "Contracts": { + "NeoNameService": "0x50ac1c37690cc2cfc594472833cf57505d5f46de" + }, + "Plugins": { + "DownloadUrl": "https://api.github.com/repos/neo-project/neo-modules/releases" + } + }, + "ProtocolConfiguration": { + "Network": 860833102, + "AddressVersion": 53, + "MillisecondsPerBlock": 15000, + "MaxTransactionsPerBlock": 512, + "MemoryPoolMaxTransactions": 50000, + "MaxTraceableBlocks": 2102400, + "Hardforks": { + "HF_Aspidochelone": 1730000, + "HF_Basilisk": 4120000 + }, + "InitialGasDistribution": 5200000000000000, + "ValidatorsCount": 7, + "StandbyCommittee": [ + "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", + "02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093", + "03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a", + "02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554", + "024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d", + "02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e", + "02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70", + "023a36c72844610b4d34d1968662424011bf783ca9d984efa19a20babf5582f3fe", + "03708b860c1de5d87f5b151a12c2a99feebd2e8b315ee8e7cf8aa19692a9e18379", + "03c6aa6e12638b36e88adc1ccdceac4db9929575c3e03576c617c49cce7114a050", + "03204223f8c86b8cd5c89ef12e4f0dbb314172e9241e30c9ef2293790793537cf0", + "02a62c915cf19c7f19a50ec217e79fac2439bbaad658493de0c7d8ffa92ab0aa62", + "03409f31f0d66bdc2f70a9730b66fe186658f84a8018204db01c106edc36553cd0", + "0288342b141c30dc8ffcde0204929bb46aed5756b41ef4a56778d15ada8f0c6654", + "020f2887f41474cfeb11fd262e982051c1541418137c02a0f4961af911045de639", + "0222038884bbd1d8ff109ed3bdef3542e768eef76c1247aea8bc8171f532928c30", + "03d281b42002647f0113f36c7b8efb30db66078dfaaa9ab3ff76d043a98d512fde", + "02504acbc1f4b3bdad1d86d6e1a08603771db135a73e61c9d565ae06a1938cd2ad", + "0226933336f1b75baa42d42b71d9091508b638046d19abd67f4e119bf64a7cfb4d", + "03cdcea66032b82f5c30450e381e5295cae85c5e6943af716cc6b646352a6067dc", + "02cd5a5547119e24feaa7c2a0f37b8c9366216bab7054de0065c9be42084003c8a" + ], + "SeedList": [ + "seed1.neo.org:10333", + "seed2.neo.org:10333", + "seed3.neo.org:10333", + "seed4.neo.org:10333", + "seed5.neo.org:10333" + ] + } +} diff --git a/tests/Neo.Plugins.RpcServer.Tests/Neo.Plugins.RpcServer.Tests.csproj b/tests/Neo.Plugins.RpcServer.Tests/Neo.Plugins.RpcServer.Tests.csproj new file mode 100644 index 0000000000..2459b1cb5f --- /dev/null +++ b/tests/Neo.Plugins.RpcServer.Tests/Neo.Plugins.RpcServer.Tests.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + Neo.Plugins.RpcServer.Tests + Neo.Plugins.RpcServer.Tests + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcError.cs b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcError.cs new file mode 100644 index 0000000000..d3c43a5458 --- /dev/null +++ b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcError.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_RpcError.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.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Neo.Plugins.RpcServer.Tests +{ + [TestClass] + public class UT_RpcError + { + [TestMethod] + public void AllDifferent() + { + HashSet codes = new(); + + foreach (RpcError error in typeof(RpcError) + .GetFields(BindingFlags.Static | BindingFlags.Public) + .Where(u => u.DeclaringType == typeof(RpcError)) + .Select(u => u.GetValue(null)) + .Cast()) + { + Assert.IsTrue(codes.Add(error.ToString())); + + if (error.Code == RpcError.WalletFeeLimit.Code) + Assert.IsNotNull(error.Data); + else + Assert.IsNull(error.Data); + } + } + + [TestMethod] + public void TestJson() + { + Assert.AreEqual("{\"code\":-600,\"message\":\"Access denied\"}", RpcError.AccessDenied.ToJson().ToString(false)); + } + } +} diff --git a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.cs b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.cs new file mode 100644 index 0000000000..803980eb8f --- /dev/null +++ b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.cs @@ -0,0 +1,20 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_RpcServer.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.VisualStudio.TestTools.UnitTesting; + +namespace Neo.Plugins.RpcServer.Tests +{ + [TestClass] + public class UT_RpcServer + { + } +} diff --git a/tests/Neo.Plugins.Storage.Tests/Neo.Plugins.Storage.Tests.csproj b/tests/Neo.Plugins.Storage.Tests/Neo.Plugins.Storage.Tests.csproj new file mode 100644 index 0000000000..3e06cf32ca --- /dev/null +++ b/tests/Neo.Plugins.Storage.Tests/Neo.Plugins.Storage.Tests.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + Neo.Plugins.Storage.Tests + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Neo.Plugins.Storage.Tests/StoreTest.cs b/tests/Neo.Plugins.Storage.Tests/StoreTest.cs new file mode 100644 index 0000000000..2773f92e80 --- /dev/null +++ b/tests/Neo.Plugins.Storage.Tests/StoreTest.cs @@ -0,0 +1,200 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// StoreTest.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.VisualStudio.TestTools.UnitTesting; +using Neo.Persistence; +using System.IO; +using System.Linq; + +namespace Neo.Plugins.Storage.Tests +{ + [TestClass] + public class StoreTest + { + private const string path_leveldb = "Data_LevelDB_UT"; + private const string path_rocksdb = "Data_RocksDB_UT"; + + [TestInitialize] + public void OnStart() + { + if (Directory.Exists(path_leveldb)) Directory.Delete(path_leveldb, true); + if (Directory.Exists(path_rocksdb)) Directory.Delete(path_rocksdb, true); + } + + [TestMethod] + public void TestMemory() + { + using var store = new MemoryStore(); + TestPersistenceDelete(store); + // Test all with the same store + + TestStorage(store); + + // Test with different storages + + TestPersistenceWrite(store); + TestPersistenceRead(store, true); + TestPersistenceDelete(store); + TestPersistenceRead(store, false); + } + + [TestMethod] + public void TestLevelDb() + { + using var plugin = new LevelDBStore(); + TestPersistenceDelete(plugin.GetStore(path_leveldb)); + // Test all with the same store + + TestStorage(plugin.GetStore(path_leveldb)); + + // Test with different storages + + TestPersistenceWrite(plugin.GetStore(path_leveldb)); + TestPersistenceRead(plugin.GetStore(path_leveldb), true); + TestPersistenceDelete(plugin.GetStore(path_leveldb)); + TestPersistenceRead(plugin.GetStore(path_leveldb), false); + } + + [TestMethod] + public void TestRocksDb() + { + using var plugin = new RocksDBStore(); + TestPersistenceDelete(plugin.GetStore(path_rocksdb)); + // Test all with the same store + + TestStorage(plugin.GetStore(path_rocksdb)); + + // Test with different storages + + TestPersistenceWrite(plugin.GetStore(path_rocksdb)); + TestPersistenceRead(plugin.GetStore(path_rocksdb), true); + TestPersistenceDelete(plugin.GetStore(path_rocksdb)); + TestPersistenceRead(plugin.GetStore(path_rocksdb), false); + } + + /// + /// Test Put/Delete/TryGet/Seek + /// + /// Store + private void TestStorage(IStore store) + { + using (store) + { + var key1 = new byte[] { 0x01, 0x02 }; + var value1 = new byte[] { 0x03, 0x04 }; + + store.Delete(key1); + var ret = store.TryGet(key1); + Assert.IsNull(ret); + + store.Put(key1, value1); + ret = store.TryGet(key1); + CollectionAssert.AreEqual(value1, ret); + Assert.IsTrue(store.Contains(key1)); + + ret = store.TryGet(value1); + Assert.IsNull(ret); + Assert.IsTrue(store.Contains(key1)); + + store.Delete(key1); + + ret = store.TryGet(key1); + Assert.IsNull(ret); + Assert.IsFalse(store.Contains(key1)); + + // Test seek in order + + store.Put(new byte[] { 0x00, 0x00, 0x04 }, new byte[] { 0x04 }); + store.Put(new byte[] { 0x00, 0x00, 0x00 }, new byte[] { 0x00 }); + store.Put(new byte[] { 0x00, 0x00, 0x01 }, new byte[] { 0x01 }); + store.Put(new byte[] { 0x00, 0x00, 0x02 }, new byte[] { 0x02 }); + store.Put(new byte[] { 0x00, 0x00, 0x03 }, new byte[] { 0x03 }); + + // Seek Forward + + var entries = store.Seek(new byte[] { 0x00, 0x00, 0x02 }, SeekDirection.Forward).ToArray(); + Assert.AreEqual(3, entries.Length); + CollectionAssert.AreEqual(new byte[] { 0x00, 0x00, 0x02 }, entries[0].Key); + CollectionAssert.AreEqual(new byte[] { 0x02 }, entries[0].Value); + CollectionAssert.AreEqual(new byte[] { 0x00, 0x00, 0x03 }, entries[1].Key); + CollectionAssert.AreEqual(new byte[] { 0x03 }, entries[1].Value); + CollectionAssert.AreEqual(new byte[] { 0x00, 0x00, 0x04 }, entries[2].Key); + CollectionAssert.AreEqual(new byte[] { 0x04 }, entries[2].Value); + + // Seek Backward + + entries = store.Seek(new byte[] { 0x00, 0x00, 0x02 }, SeekDirection.Backward).ToArray(); + Assert.AreEqual(3, entries.Length); + CollectionAssert.AreEqual(new byte[] { 0x00, 0x00, 0x02 }, entries[0].Key); + CollectionAssert.AreEqual(new byte[] { 0x02 }, entries[0].Value); + CollectionAssert.AreEqual(new byte[] { 0x00, 0x00, 0x01 }, entries[1].Key); + CollectionAssert.AreEqual(new byte[] { 0x01 }, entries[1].Value); + + // Seek Backward + store.Delete(new byte[] { 0x00, 0x00, 0x00 }); + store.Delete(new byte[] { 0x00, 0x00, 0x01 }); + store.Delete(new byte[] { 0x00, 0x00, 0x02 }); + store.Delete(new byte[] { 0x00, 0x00, 0x03 }); + store.Delete(new byte[] { 0x00, 0x00, 0x04 }); + store.Put(new byte[] { 0x00, 0x00, 0x00 }, new byte[] { 0x00 }); + store.Put(new byte[] { 0x00, 0x00, 0x01 }, new byte[] { 0x01 }); + store.Put(new byte[] { 0x00, 0x01, 0x02 }, new byte[] { 0x02 }); + + entries = store.Seek(new byte[] { 0x00, 0x00, 0x03 }, SeekDirection.Backward).ToArray(); + Assert.AreEqual(2, entries.Length); + CollectionAssert.AreEqual(new byte[] { 0x00, 0x00, 0x01 }, entries[0].Key); + CollectionAssert.AreEqual(new byte[] { 0x01 }, entries[0].Value); + CollectionAssert.AreEqual(new byte[] { 0x00, 0x00, 0x00 }, entries[1].Key); + CollectionAssert.AreEqual(new byte[] { 0x00 }, entries[1].Value); + } + } + + /// + /// Test Put + /// + /// Store + private void TestPersistenceWrite(IStore store) + { + using (store) + { + store.Put(new byte[] { 0x01, 0x02, 0x03 }, new byte[] { 0x04, 0x05, 0x06 }); + } + } + + /// + /// Test Put + /// + /// Store + private void TestPersistenceDelete(IStore store) + { + using (store) + { + store.Delete(new byte[] { 0x01, 0x02, 0x03 }); + } + } + + /// + /// Test Read + /// + /// Store + /// Should exist + private void TestPersistenceRead(IStore store, bool shouldExist) + { + using (store) + { + var ret = store.TryGet(new byte[] { 0x01, 0x02, 0x03 }); + + if (shouldExist) CollectionAssert.AreEqual(new byte[] { 0x04, 0x05, 0x06 }, ret); + else Assert.IsNull(ret); + } + } + } +} diff --git a/tests/Neo.UnitTests/Neo.UnitTests.csproj b/tests/Neo.UnitTests/Neo.UnitTests.csproj index 6ac259db7b..fb246cbdc3 100644 --- a/tests/Neo.UnitTests/Neo.UnitTests.csproj +++ b/tests/Neo.UnitTests/Neo.UnitTests.csproj @@ -6,8 +6,8 @@ - - + + @@ -24,8 +24,8 @@ - - + + diff --git a/tests/Neo.VM.Tests/Neo.VM.Tests.csproj b/tests/Neo.VM.Tests/Neo.VM.Tests.csproj index 6b3dab4177..53c16ed4af 100644 --- a/tests/Neo.VM.Tests/Neo.VM.Tests.csproj +++ b/tests/Neo.VM.Tests/Neo.VM.Tests.csproj @@ -18,8 +18,8 @@ - - + +